From 6b5a54fe921a9146bfa7d01770d7d49a6638f53b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 13:58:39 +0000 Subject: [PATCH] Hand the session to the Firefox extension too Firefox doesn't let web pages message an extension (no externally_connectable, Mozilla bug 1319168), so chrome.runtime is undefined here. The ping threw, the download links stayed up, the session never reached the extension, and the ?sessionOnly tab the extension opens never closed. $scope.extension.send() keeps using chrome.runtime.sendMessage() where Chrome exposes it. Otherwise it posts the message to this window, where the extension's siteBridge.js content script (RemoteSlide/RemoteSlide-Chrome, same branch) relays it and posts the answer back. The ?sessionOnly tab now closes once the extension has answered, which the bridge needs before the tab goes away, or after a second at the latest: extension versions up to 1.2.0 never answer the session message, so Chrome users still on one would otherwise keep the tab. Checked in Chromium 141 with both the current and the new extension, and in Firefox 158 and ESR 140 with the new one: extension detected, session stored, ?sessionOnly tab closed. Adds extension_firefox to the injector names in PROTOCOL.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P3g3vcYRESg89oZuTf7eP5 --- PROTOCOL.md | 3 ++- static/js/indexController.js | 13 ++++++----- static/js/mainController.js | 44 ++++++++++++++++++++++++++++-------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 2512564..810388b 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -27,7 +27,8 @@ GET /ws/?as=[&injector=] (WebSocket upgrade) A session has at most one host and one observer; a newer connection replaces the older one, which is closed with code `4001`. Any number of remotes can join. `injector` is free text describing how the host got injected -(`bookmark`, `extension_chrome`) and is shown in the connection info. +(`bookmark`, `extension_chrome`, `extension_firefox`) and is shown in the +connection info. The first frame the server sends is always `init`: diff --git a/static/js/indexController.js b/static/js/indexController.js index 35f3b39..b4cb441 100644 --- a/static/js/indexController.js +++ b/static/js/indexController.js @@ -26,17 +26,18 @@ slideApp.controller("indexController", ["$scope", "$http", "$cookies", "$timeout console.log(data.info) $scope.statusIcon.showMessage("check", "lime", [200], "Connected", 2000); - // Notify extension - try { - chrome.runtime.sendMessage($scope.extension.id.chrome, {session: $scope.session}, function (msg) { - }); + // Notify extension. If it opened this tab just to get a session, close + // the tab once the extension has it - or after a second at the latest, + // as extension versions up to 1.2.0 never answer this message. + var closeSessionOnlyTab = function () { if ($state.params.sessionOnly) { $timeout(function () { window.close(); }, 100); } - } catch (ignoerd) { - } + }; + $scope.extension.send({session: $scope.session}, closeSessionOnlyTab); + $timeout(closeSessionOnlyTab, 900); }); } else if (data.state == "not_found") {// This should be impossible here, since the observer generates the new session ID console.warn("Session not found"); diff --git a/static/js/mainController.js b/static/js/mainController.js index 265307f..41915f2 100644 --- a/static/js/mainController.js +++ b/static/js/mainController.js @@ -94,17 +94,41 @@ slideApp.controller("slideController", ["$scope", "$cookies", "$location", "$htt // chrome: "enmfbjneielhjagdgebkmcdhgbbionek" chrome: "ojmlekgoiolmbfddhkdbnlpndbcipfkp" }, - installed: false - }; - try { - chrome.runtime.sendMessage($scope.extension.id.chrome, {ping: "hello"}, function (msg) { - console.log(msg) - if (msg && msg.pong && msg.pong == 'hello') { - $scope.extension.installed = true; + installed: false, + // Chrome exposes chrome.runtime to this site once the extension is + // installed (externally_connectable). Firefox doesn't let pages message an + // extension at all, so there the extension's siteBridge.js content script + // relays what is posted to this window, and posts the answer back. + send: function (message, callback) { + if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) { + try { + chrome.runtime.sendMessage($scope.extension.id.chrome, message, function (response) { + void chrome.runtime.lastError; + if (callback) callback(response); + }); + } catch (ignored) { + } + return; } - }); - } catch (ignoerd) { - } + var id = ++extensionBridge.lastId; + if (callback) extensionBridge.callbacks[id] = callback; + window.postMessage({channel: "remoteslide", direction: "to-extension", id: id, message: message}, window.location.origin); + } + }; + var extensionBridge = {lastId: 0, callbacks: {}}; + window.addEventListener("message", function (event) { + var data = event.data; + if (event.source !== window || !data || data.channel !== "remoteslide" || data.direction !== "to-page") return; + var callback = extensionBridge.callbacks[data.id]; + delete extensionBridge.callbacks[data.id]; + if (callback) callback(data.response); + }); + $scope.extension.send({ping: "hello"}, function (msg) { + console.log(msg) + if (msg && msg.pong && msg.pong == 'hello') { + $scope.extension.installed = true; + } + }); $scope.isMobile = function () { return window.mobilecheck();