diff --git a/pkg/lib/cockpit.js b/pkg/lib/cockpit.js index b334c615f842..4d12dc939c6a 100644 --- a/pkg/lib/cockpit.js +++ b/pkg/lib/cockpit.js @@ -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"; @@ -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() { diff --git a/pkg/lib/cockpit/_internal/location.ts b/pkg/lib/cockpit/_internal/location-utils.ts similarity index 100% rename from pkg/lib/cockpit/_internal/location.ts rename to pkg/lib/cockpit/_internal/location-utils.ts diff --git a/pkg/lib/cockpit/_internal/parentwebsocket.ts b/pkg/lib/cockpit/_internal/parentwebsocket.ts index 8718d0981fba..beb800d6890b 100644 --- a/pkg/lib/cockpit/_internal/parentwebsocket.ts +++ b/pkg/lib/cockpit/_internal/parentwebsocket.ts @@ -1,4 +1,4 @@ -import { transport_origin } from './location'; +import { transport_origin } from './location-utils'; /* * A WebSocket that connects to parent frame. The mechanism diff --git a/pkg/lib/cockpit/_internal/transport.ts b/pkg/lib/cockpit/_internal/transport.ts index 1e877fbb04aa..4ea0a0c93fb5 100644 --- a/pkg/lib/cockpit/_internal/transport.ts +++ b/pkg/lib/cockpit/_internal/transport.ts @@ -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; diff --git a/pkg/lib/cockpit/location.ts b/pkg/lib/cockpit/location.ts new file mode 100644 index 000000000000..abc360644f38 --- /dev/null +++ b/pkg/lib/cockpit/location.ts @@ -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 . + */ +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]; + last.push(value); + } else { + options[name] = 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; + } +}