/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "CONTENT_SHARING_ENABLED", "browser.contentsharing.enabled", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "CONTENT_SHARING_SERVER_URL", "browser.contentsharing.server.url", "" ); const SCHEMA_MAP = new Map(); async function loadContentSharingSchema() { if (SCHEMA_MAP.has("CONTENT_SHARING_SCHEMA")) { return SCHEMA_MAP.get("CONTENT_SHARING_SCHEMA"); } const url = "chrome://browser/content/contentsharing/contentsharing.schema.json"; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to load schema: ${response.statusText}`); } const schema = await response.json(); SCHEMA_MAP.set("CONTENT_SHARING_SCHEMA", schema); return schema; } /** * Class for interacting with Content Sharing features, such as sharing bookmarks, tab groups, and tabs. */ class ContentSharingUtilsClass { #validator = null; get isEnabled() { return lazy.CONTENT_SHARING_ENABLED; } get serverURL() { return lazy.CONTENT_SHARING_SERVER_URL; } async getValidator() { if (this.#validator) { return this.#validator; } const schema = await loadContentSharingSchema(); this.#validator = new lazy.JsonSchema.Validator(schema); return this.#validator; } /** * Handles sharing bookmarks by building a share object and sending it to the * content sharing server to get a shareable link, which is then opened in a * new tab. bookmarkFolderGuids can be 1 or more bookmark folder guids. If * more than 1, the first guid will be treated as the parent folder and the * rest will be nested inside it. * * @param {Array} bookmarkFolderGuids An array of bookmark folder guids * @param {Window} window The window to open the share URL in */ async createShareableLinkFromBookmarkFolders(bookmarkFolderGuids, window) { const share = await this.buildShareFromBookmarkFolders(bookmarkFolderGuids); await this.openShareUrlInNewTab(share, window); } /** * Shares the multi-selected tabs * * @param {MozTabbrowserTab[]} tabs */ async handleShareTabs(tabs) { try { const shareObject = { type: "tabs", title: `${tabs.length} tabs`, children: tabs.map(t => ({ uri: t.linkedBrowser.currentURI.spec, title: t.label.slice(0, 100), })), }; const share = this.buildShare(shareObject); await this.openShareUrlInNewTab(share, tabs[0].ownerGlobal); } catch (e) { console.error("ContentSharingUtils: failed to share tabs", e); } } /** * Handles sharing a tab group by building a share object and sending it to the * content sharing server to get a shareable link, which is then opened in a * new tab. * * @param {MozTabbrowserTabGroup} tabGroup The tab group element to share */ async handleShareTabGroup(tabGroup) { let title = tabGroup.label; if (!title) { title = await tabGroup.ownerDocument.l10n.formatValue( "tab-group-name-default" ); } const shareObject = { title, type: "tab_group", children: tabGroup.tabs.map(t => { return { uri: t.linkedBrowser.currentURI.displaySpec, title: t.label }; }), }; const share = this.buildShare(shareObject); await this.openShareUrlInNewTab(share, tabGroup.ownerGlobal); } /** * Builds a share object from bookmark folder guids. It first builds out the * bookmark tree and then builds a share object from that tree, returning an * object to be validated against contentsharing.schema.json and sent to the * content sharing server. bookmarkFolderGuids must be 1 or more folder guids. * If more than 1, the first guid will be treated as the parent folder and * the rest will be nested inside it. * * @param {Array} bookmarkFolderGuids An array of bookmark folder guids * @returns {Promise} The built share object that will be validated against * the contentsharing.schema.json */ async buildShareFromBookmarkFolders(bookmarkFolderGuids) { if (!bookmarkFolderGuids.length) { return null; } let bookmark; if (bookmarkFolderGuids.length === 1) { bookmark = await lazy.PlacesUtils.promiseBookmarksTree( bookmarkFolderGuids[0] ); } else { // More than one folder selected: first folder is the parent, rest are children. bookmark = await lazy.PlacesUtils.promiseBookmarksTree( bookmarkFolderGuids[0] ); bookmark.children = bookmark.children ?? []; for (let guid of bookmarkFolderGuids.slice(1)) { bookmark.children.push( await lazy.PlacesUtils.promiseBookmarksTree(guid) ); } } bookmark.type = "bookmarks"; return this.buildShare(bookmark); } /** * Builds a share object from a given bookmark tree/tab group/selected tabs. * The share object is a simplified version of the bookmark tree that only * includes the necessary information for sharing (e.g. title and url). * For bookmarks, the share object will have a type of "bookmarks" and will * include the title of the bookmark folder and an array of links, where each * link can either be a bookmark (with a url and optional title) or a nested * folder (with its own title and array of links). Nested folders will * recursively call this function to build their share objects. * For tab groups, the share object will have the type "tab_group" and will * take the title of the tab group. * For selected tabs, the share object will have the type "tabs" and the * title will be the number of tabs selected. * Both tab groups and selected tabs will include an array of links, where * each link will have a url and title. * * @param {object} shareObject The bookmark tree to share. * @returns {object} The built share object that will be validated against * the contentsharing.schema.json */ buildShare(shareObject) { const share = { type: shareObject.type ?? "bookmarks", title: shareObject.title, }; let links = []; for (let linkOrNestShare of shareObject.children ?? []) { if (linkOrNestShare.uri) { const link = { url: linkOrNestShare.uri, title: linkOrNestShare.title ?? "", }; links.push(link); } else if (linkOrNestShare.children) { linkOrNestShare.type = "bookmarks"; links.push(this.buildShare(linkOrNestShare)); } } share.links = links; return share; } async openShareUrlInNewTab(share, window) { const shareUrl = await this.createShareableLink(share); window.openWebLinkIn(shareUrl, "tab"); } async createShareableLink(share) { await this.validateSchema(share); if (!this.serverURL) { throw new Error("Content Sharing Server URL is not set"); } let response = await fetch(this.serverURL, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(share), }); let { url } = await response.json(); return url; } countItems(share) { let count = 0; for (let item of share.links) { if (item.links) { count += this.countItems(item); } // Always count the current item count += 1; } return count; } async validateSchema(share) { const validator = await this.getValidator(); const result = validator.validate(share); if (!result.valid) { throw new Error( `ContentSharing Schema Error: ${result.errors.map(e => e.error).join(", ")}` ); } if (this.countItems(share) > 100) { throw new Error( "ContentSharing Schema Error: Share object contains over 100 links" ); } return true; } getCookie() { let hostname; try { let serverURL = new URL(lazy.CONTENT_SHARING_SERVER_URL); // Cookies are port-insensitive, but our test server sets a port number. // Just use the hostname part of the URL for cookie lookup. hostname = serverURL.hostname; } catch (ex) { console.error( `Failed to get cookie because server URL in "browser.contentsharing.server.url" pref is unset or malformed: ` + ex.message ); return null; } const cookies = Services.cookies.getCookiesFromHost(hostname, {}); // Filter on host because parent domain cookies are returned when getting // cookies from a subdomain. let authCookie = cookies.find( cookie => cookie.host == hostname && cookie.name == "auth" && cookie.expiry > Date.now() ); return authCookie?.value; } isSignedIn() { return !!this.getCookie(); } } const ContentSharingUtils = new ContentSharingUtilsClass(); export { ContentSharingUtils };