gh-14470: Fixed moz-remote-urls not being synced correctly (gh-15043)

This commit is contained in:
mr. m
2026-08-21 01:24:25 +02:00
committed by GitHub
parent d8a934d2ec
commit 721135f576
4 changed files with 81 additions and 32 deletions

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a9c5bb3f8 100644
index a76699af75252bec31e17b562463c37236cbbfe5..6355a6ee92754846ffcdd123ac49545d520470c7 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -131,6 +131,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
@@ -132,6 +132,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
styleSheets: [
"chrome://browser/skin/preferences/dialog.css",
"chrome://browser/skin/preferences/preferences.css",
@@ -10,7 +10,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
],
resizeCallback: async ({ title, frame }) => {
// Search within main document and highlight matched keyword.
@@ -437,6 +438,8 @@ const CONFIG_PANES = Object.freeze({
@@ -462,6 +463,8 @@ const CONFIG_PANES = Object.freeze({
tabsBrowsing: {
l10nId: "tabs-browsing-section",
groupIds: [
@@ -19,7 +19,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
"browserLayout",
"tabs",
"pageNavigation",
@@ -483,7 +486,7 @@ function register_module(categoryName, categoryObject) {
@@ -509,7 +512,7 @@ function register_module(categoryName, categoryObject) {
}
this._initted = true;
let template = document.getElementById("template-" + categoryName);
@@ -28,7 +28,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
// Replace the template element with the nodes inside of it.
template.replaceWith(template.content);
@@ -527,6 +530,10 @@ function init_all() {
@@ -553,6 +556,10 @@ function init_all() {
register_module("paneHome", gHomePane);
register_module("paneSearch", gSearchPane);
register_module("panePrivacy", gPrivacyPane);
@@ -39,15 +39,3 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
// Restore the cached Firefox Labs nav button visibility so it shows
// immediately when recipes are expected to be available, before
@@ -656,9 +663,9 @@ async function gotoPref(
let redesignEnabled = srdSectionPrefs.all;
let categories = document.getElementById("categories");
const kDefaultCategoryInternalName = redesignEnabled
- ? "paneSync"
+ ? "paneTabsBrowsing"
: "paneGeneral";
- const kDefaultCategory = redesignEnabled ? "sync" : "general";
+ const kDefaultCategory = redesignEnabled ? "tabsBrowsing" : "general";
let hash = document.location.hash;
let category = aCategory || hash.substring(1) || kDefaultCategoryInternalName;

View File

@@ -368,7 +368,7 @@ class nsZenLiveFoldersManager {
},
],
content: {
width: "310px",
width: "312px",
position: "callout",
title_logo: {
imageURL: icon,

View File

@@ -6,6 +6,7 @@ import {
canonicalJSON,
LAYOUT_RECORD_ID,
RECORD_KINDS,
syncableIconUrl,
ZenSpacesSyncModel,
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
@@ -379,6 +380,13 @@ class nsZenSpacesSyncApplier {
folder.label = data.name;
}
if (data.live && !folder.isLiveFolder) {
// The folder synced before its provider config was available and
// materialized as a plain folder; upgrade it so this profile
// projects the config too instead of clobbering it with live: null.
folder.isLiveFolder = true;
}
if ((folder.iconURL || null) !== (data.icon || null)) {
win.gZenFolders.setFolderUserIcon(folder, data.icon || null);
// Window sync picks this up and mirrors the icon everywhere else.
@@ -508,7 +516,7 @@ class nsZenSpacesSyncApplier {
lazy.ZenWindowSync.setPinnedInitialState(
tab,
{ url: data.url, title: data.title || "" },
data.icon || undefined
syncableIconUrl(data.icon) || undefined
);
}
@@ -518,12 +526,15 @@ class nsZenSpacesSyncApplier {
} else {
tab.removeAttribute("zenDefaultUserContextId");
}
// Records from before icon normalization can still carry wrapped or
// remote urls setIcon would refuse.
const icon = syncableIconUrl(data.icon);
tab.zenStaticLabel =
typeof data.staticLabel === "string" ? data.staticLabel : undefined;
tab.zenStaticIcon = data.hasStaticIcon && data.icon ? data.icon : undefined;
if (data.icon) {
tab.zenStaticIcon = data.hasStaticIcon && icon ? icon : undefined;
if (icon) {
try {
win.gBrowser.setIcon(tab, data.icon);
win.gBrowser.setIcon(tab, icon);
// The tab is lazy, so its session state cache keeps the (empty)
// image it was created with: it is only cleared once a restore
// finishes, which never happens for lazy tabs, and it would hide
@@ -547,15 +558,16 @@ class nsZenSpacesSyncApplier {
* @param {object} data
*/
#updateTabIdentity(win, tab, data) {
const icon = syncableIconUrl(data.icon);
const initial = tab._zenPinnedInitialState;
const identityChanged =
initial?.entry?.url !== data.url ||
(initial?.entry?.title || "") !== (data.title || "");
if (identityChanged || (initial?.image || "") !== (data.icon || "")) {
if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) {
lazy.ZenWindowSync.setPinnedInitialState(
tab,
{ url: data.url, title: data.title || "" },
data.icon || undefined
icon || undefined
);
}
if (identityChanged && !tab.linkedPanel) {
@@ -580,10 +592,12 @@ class nsZenSpacesSyncApplier {
}
}
}
tab.zenStaticIcon = data.hasStaticIcon && data.icon ? data.icon : undefined;
if (data.icon && tab.getAttribute("image") !== data.icon) {
tab.zenStaticIcon = data.hasStaticIcon && icon ? icon : undefined;
// Compare normalized: setIcon rewraps svg data: icons into
// moz-remote-image urls when writing the attribute.
if (icon && syncableIconUrl(tab.getAttribute("image")) !== icon) {
try {
win.gBrowser.setIcon(tab, data.icon);
win.gBrowser.setIcon(tab, icon);
} catch (e) {
console.error("ZenSpacesSync: failed to set tab icon", e);
}
@@ -619,7 +633,7 @@ class nsZenSpacesSyncApplier {
},
];
state.index = 1;
state.image = data.icon || undefined;
state.image = syncableIconUrl(data.icon) || undefined;
delete state.scroll;
win.SessionStore.setTabState(tab, state);
} catch (e) {

View File

@@ -36,6 +36,36 @@ const BUILTIN_GUID_PREFIX = "builtin-";
const STORE_FILE_NAME = "zen-spaces-sync.json";
const STORE_VERSION = 1;
// Everything setIcon accepts without a loading principal.
const LOCAL_ICON_PROTOCOLS = ["data:", "chrome:", "about:", "resource:"];
/**
* Normalizes a tab icon for syncing. The image attribute can hold a
* moz-remote-image: wrapper around the actual (svg data:) favicon, carrying
* a per-process contentParentId that means nothing on another device — sync
* the wrapped URL instead. Anything else setIcon would reject as remote is
* dropped.
*
* @param {?string} icon
* @returns {string}
*/
export function syncableIconUrl(icon) {
if (!icon || typeof icon !== "string") {
return "";
}
if (icon.startsWith("moz-remote-image:")) {
try {
const uri = Services.io.newURI(icon);
icon = new URLSearchParams(uri.query).get("url") || "";
} catch (e) {
return "";
}
}
return LOCAL_ICON_PROTOCOLS.some(protocol => icon.startsWith(protocol))
? icon
: "";
}
function sortedClone(value) {
if (Array.isArray(value)) {
return value.map(sortedClone);
@@ -201,7 +231,7 @@ class nsZenSpacesSyncModel {
if (!url || url === "about:blank") {
return null;
}
const icon = initial?.image || tabData.image || "";
const icon = syncableIconUrl(initial?.image || tabData.image || "");
return { url, title: title || "", icon };
}
@@ -357,6 +387,7 @@ class nsZenSpacesSyncModel {
}
const map = new Map();
const pending = new Set();
const ctx = this.#projectionContext(sidebar);
const { tabs, folders, splits, splitIds, splitParents, splitWs } = ctx;
@@ -425,6 +456,10 @@ class nsZenSpacesSyncModel {
} catch (e) {
console.error("ZenSpacesSync: failed to project live folder", e);
}
if (!live) {
pending.add(fid);
continue;
}
}
map.set(fid, {
kind: RECORD_KINDS.FOLDER,
@@ -486,10 +521,20 @@ class nsZenSpacesSyncModel {
});
}
this.#cache = { stamp, map };
this.#cache = { stamp, map, pending };
return map;
}
/**
* Ids that are locally present but deliberately not projected this cycle
* (live folders whose provider config isn't available yet). The diff must
* not read their absence as a deletion.
*/
#pendingIds() {
this.projections();
return this.#cache.pending;
}
#digestCache = null;
/**
@@ -536,6 +581,7 @@ class nsZenSpacesSyncModel {
computeChangedIDs() {
const uploaded = this.#data().uploaded;
const current = this.#digestAll();
const pending = this.#pendingIds();
const now = Date.now() / 1000;
const changes = {};
for (const [id, digest] of current) {
@@ -544,7 +590,7 @@ class nsZenSpacesSyncModel {
}
}
for (const id of Object.keys(uploaded)) {
if (!current.has(id)) {
if (!current.has(id) && !pending.has(id)) {
changes[id] = now;
}
}
@@ -554,13 +600,14 @@ class nsZenSpacesSyncModel {
hasPendingChanges() {
const uploaded = this.#data().uploaded;
const current = this.#digestAll();
const pending = this.#pendingIds();
for (const [id, digest] of current) {
if (uploaded[id] !== digest) {
return true;
}
}
for (const id of Object.keys(uploaded)) {
if (!current.has(id)) {
if (!current.has(id) && !pending.has(id)) {
return true;
}
}