tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 6ed756f28dbb3035487ca748dc0a6dd6b65de5d2
parent 5a6f5460decaed17de35d490e3377396f163b98f
Author: Stephen Thompson <sthompson@mozilla.com>
Date:   Fri, 14 Nov 2025 18:38:47 +0000

Bug 1994530 - canonical URL actor for tab notes r=tabbrowser-reviewers,jswinarton

Standalone new JSWindowActor called CanonicalURL that notifies the parent process when the canonical URL in page content changes. This is intended for use by the in-development Tab Notes feature. That feature requires that we assign users' notes to specific web pages, and we are experimenting with a proof of concept where we use a canonical URL from the page content to determine whether a note pertains to that page.

This implementation has many shortfalls, but it's just an extension point right now. For example, this does not detect back/forward button navigation nor single-page application navigation.

Creates a new `tabnotes` folder under browser/components scoped to the Tabbed Browser component in Bugzilla. Again, this is liable to change in the future. It is likely that this code will move to join some other existing JSWindowActor if we decide that it's a good fit.

Differential Revision: https://phabricator.services.mozilla.com/D272561

Diffstat:
Mbrowser/components/DesktopActorRegistry.sys.mjs | 15+++++++++++++++
Mbrowser/components/moz.build | 1+
Abrowser/components/tabnotes/CanonicalURL.sys.mjs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/tabnotes/CanonicalURLChild.sys.mjs | 42++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/tabnotes/CanonicalURLParent.sys.mjs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/tabnotes/moz.build | 17+++++++++++++++++
Abrowser/components/tabnotes/tsconfig.json | 16++++++++++++++++
Abrowser/components/tabnotes/types/tabnotes.ts | 8++++++++
8 files changed, 263 insertions(+), 0 deletions(-)

diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs @@ -273,6 +273,21 @@ let JSWINDOWACTORS = { messageManagerGroups: ["browsers"], }, + CanonicalURL: { + parent: { + esModuleURI: "resource:///actors/CanonicalURLParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/CanonicalURLChild.sys.mjs", + events: { + DOMContentLoaded: {}, + }, + }, + enablePreference: "browser.tabs.notes.enabled", + matches: ["http://*/*", "https://*/*"], + messageManagerGroups: ["browsers"], + }, + ClickHandler: { parent: { esModuleURI: "resource:///actors/ClickHandlerParent.sys.mjs", diff --git a/browser/components/moz.build b/browser/components/moz.build @@ -66,6 +66,7 @@ DIRS += [ "sidebar", "syncedtabs", "tabbrowser", + "tabnotes", "tabunloader", "taskbartabs", "textrecognition", diff --git a/browser/components/tabnotes/CanonicalURL.sys.mjs b/browser/components/tabnotes/CanonicalURL.sys.mjs @@ -0,0 +1,94 @@ +/* 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/. */ + +/** + * Given a web page content document, finds candidates for an explicitly + * declared canonical URL. Includes a fallback URL to use in case the content + * did not declare a canonical URL. + * + * @param {Document} document + * @returns {CanonicalURLSourceResults} + */ +export function findCandidates(document) { + return { + link: getLinkRelCanonical(document), + opengraph: getOpenGraphUrl(document), + jsonLd: getJSONLDUrl(document), + fallback: getFallbackCanonicalUrl(document), + }; +} + +/** + * Given a set of canonical URL candidates from `CanonicalURL.findCandidates`, + * returns the best value to use as the canonical URL. + * + * @param {CanonicalURLSourceResults} sources + * @returns {string} + */ +export function pickCanonicalUrl(sources) { + return ( + sources.link ?? sources.opengraph ?? sources.jsonLd ?? sources.fallback + ); +} + +/** + * TODO: resolve relative URLs + * TODO: can be a different hostname or domain; does that need special handling? + * + * @see https://www.rfc-editor.org/rfc/rfc6596 + * + * @param {Document} document + * @returns {string|null} + */ +function getLinkRelCanonical(document) { + return document.querySelector('link[rel="canonical"]')?.getAttribute("href"); +} + +/** + * @see https://ogp.me/#url + * + * @param {Document} document + * @returns {string|null} + */ +function getOpenGraphUrl(document) { + return document + .querySelector('meta[property="og:url"]') + ?.getAttribute("content"); +} + +/** + * Naïvely returns the first JSON-LD entity's URL, if found. + * TODO: make sure it's a web page-like/content schema? + * + * @see https://schema.org/url + * + * @param {Document} document + * @returns {string|null} + */ +function getJSONLDUrl(document) { + return Array.from( + document.querySelectorAll('script[type="application/ld+json"]') + ) + .map(script => { + try { + return JSON.parse(script.textContent); + } catch { + return null; + } + }) + .find(obj => obj?.url)?.url; +} + +/** + * @param {Document} document + * @returns {string|null} + */ +function getFallbackCanonicalUrl(document) { + const fallbackUrl = URL.parse(document.documentURI); + if (fallbackUrl) { + fallbackUrl.hash = ""; + return fallbackUrl.toString(); + } + return null; +} diff --git a/browser/components/tabnotes/CanonicalURLChild.sys.mjs b/browser/components/tabnotes/CanonicalURLChild.sys.mjs @@ -0,0 +1,42 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + findCandidates: "moz-src:///browser/components/tabnotes/CanonicalURL.sys.mjs", + pickCanonicalUrl: + "moz-src:///browser/components/tabnotes/CanonicalURL.sys.mjs", +}); + +/** + * Identifies the canonical URL in a top-level content frame, if possible, + * and notifies the parent process about it. + */ +export class CanonicalURLChild extends JSWindowActorChild { + /** + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "DOMContentLoaded": + this.#discoverCanonicalUrl(); + } + } + + /** + * Find a canonical URL in the document and tell the parent about it. + */ + #discoverCanonicalUrl() { + const candidates = lazy.findCandidates(this.document); + const canonicalUrl = lazy.pickCanonicalUrl(candidates); + const canonicalUrlSources = Object.keys(candidates).filter( + candidate => candidates[candidate] + ); + this.sendAsyncMessage("CanonicalURL:Identified", { + canonicalUrl, + canonicalUrlSources, + }); + } +} diff --git a/browser/components/tabnotes/CanonicalURLParent.sys.mjs b/browser/components/tabnotes/CanonicalURLParent.sys.mjs @@ -0,0 +1,70 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { + return console.createInstance({ + prefix: "CanonicalURL", + maxLogLevel: Services.prefs.getBoolPref("browser.tabs.notes.debug", false) + ? "Debug" + : "Warn", + }); +}); + +/** + * Receives canonical URL identifications from CanonicalURLChild and dispatches + * event notifications on the <browser>. + */ +export class CanonicalURLParent extends JSWindowActorParent { + /** + * Called when a message is received from the content process. + * + * @param {ReceiveMessageArgument} msg + */ + receiveMessage(msg) { + switch (msg.name) { + case "CanonicalURL:Identified": + { + const browser = this.browsingContext?.embedderElement; + + // If we don't have a browser then it went away before we could record, + // so we don't know where the data came from. + if (!browser) { + lazy.logConsole.debug( + "CanonicalURL:Identified: reject due to missing browser" + ); + return; + } + + if (!browser.ownerGlobal.gBrowser?.getTabForBrowser(browser)) { + lazy.logConsole.debug( + "CanonicalURL:Identified: reject due to the browser not being a tab browser" + ); + return; + } + + const { canonicalUrl, canonicalUrlSources } = msg.data; + + let event = new browser.ownerGlobal.CustomEvent( + "CanonicalURL:Identified", + { + bubbles: true, + cancelable: false, + detail: { + canonicalUrl, + canonicalUrlSources, + }, + } + ); + browser.dispatchEvent(event); + lazy.logConsole.info("CanonicalURL:Identified", { + canonicalUrl, + canonicalUrlSources, + }); + } + break; + } + } +} diff --git a/browser/components/tabnotes/moz.build b/browser/components/tabnotes/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Tabbed Browser") + +MOZ_SRC_FILES += [ + "CanonicalURL.sys.mjs", +] + +FINAL_TARGET_FILES.actors += [ + "CanonicalURLChild.sys.mjs", + "CanonicalURLParent.sys.mjs", +] diff --git a/browser/components/tabnotes/tsconfig.json b/browser/components/tabnotes/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["**/*.mjs", "types/*.ts"], + "exclude": [], + "extends": "../../../tools/@types/tsconfig.json", + + "compilerOptions": { + "checkJs": true, + + "plugins": [ + { + "transform": "../../../tools/ts/plugins/checkRootOnly.js", + "transformProgram": true + } + ] + } +} diff --git a/browser/components/tabnotes/types/tabnotes.ts b/browser/components/tabnotes/types/tabnotes.ts @@ -0,0 +1,8 @@ +/* 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/. */ + +type CanonicalURLSource = "link" | "opengraph" | "jsonLd" | "fallback"; +type CanonicalURLSourceResults = { + [source in CanonicalURLSource]: string | null; +};