Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cockpit location ts #21276

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 2 additions & 179 deletions pkg/lib/cockpit.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
} from './cockpit/_internal/common';
import { Deferred, later_invoke } from './cockpit/_internal/deferred';
import { event_mixin } from './cockpit/_internal/event-mixin';
import { url_root, transport_origin, calculate_application, calculate_url } from './cockpit/_internal/location';
import { transport_origin, calculate_application, calculate_url } from './cockpit/_internal/location-utils';
import { get_window_location_hash, Location } from 'cockpit/location';
import { ensure_transport, transport_globals } from './cockpit/_internal/transport';
import { FsInfoClient } from "./cockpit/fsinfo";

Expand Down Expand Up @@ -1117,184 +1118,6 @@ function factory() {

let last_loc = null;

function get_window_location_hash() {
return (window.location.href.split('#')[1] || '');
}

function Location() {
const self = this;
const application = cockpit.transport.application();
self.url_root = url_root || "";

if (window.mock?.url_root)
self.url_root = window.mock.url_root;

if (application.indexOf("cockpit+=") === 0) {
if (self.url_root)
self.url_root += '/';
self.url_root = self.url_root + application.replace("cockpit+", '');
}

const href = get_window_location_hash();
const options = { };
self.path = decode(href, options);

/* Resolve dots and double dots */
function resolve_path_dots(parts) {
const out = [];
const length = parts.length;
for (let i = 0; i < length; i++) {
const part = parts[i];
if (part === "" || part == ".") {
continue;
} else if (part == "..") {
if (out.length === 0)
return null;
out.pop();
} else {
out.push(part);
}
}
return out;
}

function decode_path(input) {
const parts = input.split('/').map(decodeURIComponent);
let result, i;
let pre_parts = [];

if (self.url_root)
pre_parts = self.url_root.split('/').map(decodeURIComponent);

if (input && input[0] !== "/") {
result = [].concat(self.path);
result.pop();
result = result.concat(parts);
} else {
result = parts;
}

result = resolve_path_dots(result);
for (i = 0; i < pre_parts.length; i++) {
if (pre_parts[i] !== result[i])
break;
}
if (i == pre_parts.length)
result.splice(0, pre_parts.length);

return result;
}

function encode(path, options, with_root) {
if (typeof path == "string")
path = decode_path(path);

let href = "/" + path.map(encodeURIComponent).join("/");
if (with_root && self.url_root && href.indexOf("/" + self.url_root + "/") !== 0)
href = "/" + self.url_root + href;

/* Undo unnecessary encoding of these */
href = href.replaceAll("%40", "@");
href = href.replaceAll("%3D", "=");
href = href.replaceAll("%2B", "+");
href = href.replaceAll("%23", "#");

let opt;
const query = [];
function push_option(v) {
query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
}

if (options) {
for (opt in options) {
let value = options[opt];
if (!Array.isArray(value))
value = [value];
value.forEach(push_option);
}
if (query.length > 0)
href += "?" + query.join("&");
}
return href;
}

function decode(href, options) {
if (href[0] == '#')
href = href.substr(1);

const pos = href.indexOf('?');
const first = (pos === -1) ? href : href.substr(0, pos);
const path = decode_path(first);
if (pos !== -1 && options) {
href.substring(pos + 1).split("&")
.forEach(function(opt) {
const parts = opt.split('=');
const name = decodeURIComponent(parts[0]);
const value = decodeURIComponent(parts[1]);
if (options[name]) {
let last = options[name];
if (!Array.isArray(value))
last = options[name] = [last];
last.push(value);
} else {
options[name] = value;
}
});
}

return path;
}

function href_for_go_or_replace(/* ... */) {
let href;
if (arguments.length == 1 && arguments[0] instanceof Location) {
href = String(arguments[0]);
} else if (typeof arguments[0] == "string") {
const options = arguments[1] || { };
href = encode(decode(arguments[0], options), options);
} else {
href = encode.apply(self, arguments);
}
return href;
}

function replace(/* ... */) {
if (self !== last_loc)
return;
const href = href_for_go_or_replace.apply(self, arguments);
window.location.replace(window.location.pathname + '#' + href);
}

function go(/* ... */) {
if (self !== last_loc)
return;
const href = href_for_go_or_replace.apply(self, arguments);
window.location.hash = '#' + href;
}

Object.defineProperties(self, {
path: {
enumerable: true,
writable: false,
value: self.path
},
options: {
enumerable: true,
writable: false,
value: options
},
href: {
enumerable: true,
value: href
},
go: { value: go },
replace: { value: replace },
encode: { value: encode },
decode: { value: decode },
toString: { value: function() { return href } }
});
}

Object.defineProperty(cockpit, "location", {
enumerable: true,
get: function() {
Expand Down
2 changes: 1 addition & 1 deletion pkg/lib/cockpit/_internal/parentwebsocket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { transport_origin } from './location';
import { transport_origin } from './location-utils';

/*
* A WebSocket that connects to parent frame. The mechanism
Expand Down
2 changes: 1 addition & 1 deletion pkg/lib/cockpit/_internal/transport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EventEmitter } from '../event';

import type { JsonObject } from './common';
import { calculate_application, calculate_url } from './location';
import { calculate_application, calculate_url } from './location-utils';
import { ParentWebSocket } from './parentwebsocket';

type ControlCallback = (message: JsonObject) => void;
Expand Down
179 changes: 179 additions & 0 deletions pkg/lib/cockpit/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2024 Red Hat, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { url_root, calculate_application } from './_internal/location-utils';

/* HACK: Mozilla will unescape 'window.location.hash' before returning
* it, which is broken.
*
* https://bugzilla.mozilla.org/show_bug.cgi?id=135309
*/
export function get_window_location_hash() {
return (window.location.href.split('#')[1] || '');
}

export class Location {
path: string[];
href: string;
url_root: string;
options: any;

constructor() {
const application = calculate_application();
this.url_root = url_root || "";

if (window.mock?.url_root)
this.url_root = window.mock.url_root;

if (application.indexOf("cockpit+=") === 0) {
if (this.url_root)
this.url_root += '/';
this.url_root = this.url_root + application.replace("cockpit+", '');
}

this.href = get_window_location_hash();
this.options = {}; // untyped
this.path = this.decode(this.href, this.options);
}

#resolve_path_dots(parts) {
const out = [];
const length = parts.length;
for (let i = 0; i < length; i++) {
const part = parts[i];
if (part === "" || part == ".") {
continue;
} else if (part == "..") {
if (out.length === 0)
return null;
out.pop();
} else {
out.push(part);
}
}
return out;
}

href_for_go_or_replace(/* ... */) {
console.log("thisthing", this);
let href;
if (arguments.length == 1 && arguments[0] instanceof Location) {
href = String(arguments[0]);
} else if (typeof arguments[0] == "string") {
const options = arguments[1] || { };
href = this.encode(this.decode(arguments[0], options), options);
} else {
href = this.encode.apply(this, arguments);
}
return href;
}

decode_path(input) {
const parts = input.split('/').map(decodeURIComponent);
let result, i;
let pre_parts = [];

if (this.url_root)
pre_parts = this.url_root.split('/').map(decodeURIComponent);

if (input && input[0] !== "/") {
result = [].concat(this.path);
result.pop();
result = result.concat(parts);
} else {
result = parts;
}

result = this.#resolve_path_dots(result);
for (i = 0; i < pre_parts.length; i++) {
if (pre_parts[i] !== result[i])
break;
}
if (i == pre_parts.length)
result.splice(0, pre_parts.length);

return result;
}

encode(path: string, options, with_root: boolean = false) {
if (typeof path == "string")
path = this.decode_path(path);

let href = "/" + path.map(encodeURIComponent).join("/");
if (with_root && this.url_root && href.indexOf("/" + this.url_root + "/") !== 0)
href = "/" + this.url_root + href;

/* Undo unnecessary encoding of these */
href = href.replaceAll("%40", "@");
href = href.replaceAll("%3D", "=");
href = href.replaceAll("%2B", "+");
href = href.replaceAll("%23", "#");

const query: string[] = [];
if (options) {
for (const opt in options) {
let value = options[opt];
if (!Array.isArray(value))
value = [value];
value.forEach(function(v: string) {
query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
});
}
if (query.length > 0)
href += "?" + query.join("&");
}
return href;
}

decode(href: string, options) {
if (href[0] == '#')
href = href.substr(1);

const pos = href.indexOf('?');
const first = (pos === -1) ? href : href.substr(0, pos);
const path = this.decode_path(first);
if (pos !== -1 && options) {
href.substring(pos + 1).split("&")
.forEach(function(opt) {
const parts = opt.split('=');
const name = decodeURIComponent(parts[0]);
const value = decodeURIComponent(parts[1]);
if (options[name]) {
let last = options[name];
if (!Array.isArray(value))
last = options[name] = [last];

Check failure

Code scanning / CodeQL

Remote property injection High

A property name to write to depends on a
user-provided value
.
last.push(value);
} else {
options[name] = value;

Check failure

Code scanning / CodeQL

Remote property injection High

A property name to write to depends on a
user-provided value
.
}
});
}

return path;
}

replace(/* ... */) {
const href = this.href_for_go_or_replace.apply(this, arguments);
window.location.replace(window.location.pathname + '#' + href);
}

go(/* ... */) {
const href = this.href_for_go_or_replace.apply(this, arguments);
window.location.hash = '#' + href;
}
}
Loading