diff --git a/Chromium Web Store.crx b/Chromium Web Store.crx index 6c7311c..63e317f 100644 Binary files a/Chromium Web Store.crx and b/Chromium Web Store.crx differ diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 7868c33..c8ebaaa 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -69,6 +69,14 @@ "message": "Always download CRX files", "description": "" }, + "options_webstoreIntegrationTooltip": { + "message": "Enable Install/Uninstall buttons on the Chrome Web Store. (Could expose information about which extensions you have installed to Google.)", + "description": "" + }, + "options_webstoreIntegration": { + "message": "Enable Chrome Web Store Integration", + "description": "" + }, "options_importExportInstallAllButton": { "message": "Install All", "description": "" diff --git a/src/manifest.json b/src/manifest.json index ff7f171..3a39054 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -4,7 +4,7 @@ "name": "__MSG_extension_Name__", "description": "__MSG_extension_Description__", "default_locale": "en", - "version": "1.5.4", + "version": "1.5.4.1", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqF/d41Q7agjkUzYq8ZGbQr8XW8mmEIMXOnR1uCTnYLL+Dm9Z+LO50xZukOISNy6zFxpI8ts/OGLsm+I2x9+UprUU4/EVdmxuwegFE6NBoEhHoRNYY0gbXZkaU8YY/XwzjVY/k18DDhl5NYPEnF6uq4Oyidg+xtd3W4+iGYczuOLER1Tp5y614zOTphcvFYhvUkCijQ6HT1TtRq/34SlFoRQqo4SFiLriK451xWIcfwiMLIekWrdoQa1v8dqIlMA3r6CKc0QykJpSYbiyormWiZ0hl2HLpkZ85mD9V0eDQ5RCtb6vkybK7INcq4yKQV4YkXhr9NpX9U4re4dlFQjEJQIDAQAB", "permissions": [ "management", @@ -53,5 +53,14 @@ }, "storage": { "managed_schema": "managed_storage.json" + }, + "web_accessible_resources": [ + { + "resources": ["scripts/chromeApi.js"], + "matches": ["https://chromewebstore.google.com/*"] + } + ], + "externally_connectable": { + "matches": ["https://chromewebstore.google.com/*"] } } diff --git a/src/options.html b/src/options.html index 1df7121..7ff13cf 100644 --- a/src/options.html +++ b/src/options.html @@ -6,37 +6,75 @@
-

Update Options

+

+ Update Options +

-

Ignored Extensions

-
+

+ Ignored Extensions +

+

Advanced


+
-

Import / Export

+

+ Import / Export +

- +
diff --git a/src/scripts/background.js b/src/scripts/background.js index c3822ec..9777ba6 100644 --- a/src/scripts/background.js +++ b/src/scripts/background.js @@ -202,3 +202,112 @@ chrome.downloads.onChanged.addListener((d) => { } }); chrome.contextMenus.onClicked.addListener(handleContextClick); + +// below functions are for the new chrome web store: +// port is used for functions with callbacks: +chrome.runtime.onConnectExternal.addListener((port) => { + var port_disconnected = false; + port.onDisconnect.addListener(() => { + port_disconnected = true; + }); + port.onMessage.addListener(function (msg, port) { + // console.log("recieved msg through port:", msg); + switch (msg.func) { + case "onInstalled": + var [callback, ..._] = msg.args; + chrome.management.onInstalled.addListener(function cb() { + if (port_disconnected) { + chrome.management.onInstalled.removeListener(cb); + return; + } + port.postMessage({ + args: arguments, + callbackIndex: callback, + err: chrome.runtime.lastError, + }); + }); + break; + case "onUninstalled": + var [callback, ..._] = msg.args; + chrome.management.onUninstalled.addListener(function cb() { + if (port_disconnected) { + chrome.management.onUninstalled.removeListener(cb); + return; + } + port.postMessage({ + args: arguments, + callbackIndex: callback, + err: chrome.runtime.lastError, + }); + }); + break; + case "uninstall": + var [ext_id, options, callback, ..._] = msg.args; + chrome.management.uninstall(ext_id, options, function () { + port.postMessage({ + args: arguments, + callbackIndex: callback, + err: chrome.runtime.lastError, + }); + }); + break; + case "setEnabled": + var [ext_id, enabled, callback, ..._] = msg.args; + chrome.management.setEnabled(ext_id, enabled, function () { + port.postMessage({ + args: arguments, + callbackIndex: callback, + err: chrome.runtime.lastError, + }); + }); + break; + } + }); +}); +chrome.runtime.onMessageExternal.addListener(function ( + request, + sender, + sendResponse, +) { + // console.log("recieved msg through runtime:", request); + switch (request.func) { + case "getExtensionStatus": + var [id, ext, ..._] = request.args; + chrome.management.getAll((exts) => { + const this_ext = exts.filter((extInfo) => extInfo.id === id); + if (!this_ext.length) { + sendResponse({ args: ["installable"] }); + return; + } + sendResponse({ + args: [this_ext[0].enabled ? "enabled" : "disabled"], + }); + }); + break; + case "getAll": + var [id, ..._] = request.args; + chrome.management.getAll((exts) => { + // instead of exposing entire extension list, filter to only the relevant extension. + sendResponse({ + args: [exts.filter((extInfo) => extInfo.id === id)], + }); + }); + break; + case "beginInstallWithManifest3": + var [extInfo, href, ..._] = request.args; + + promptInstall( + buildExtensionUrl(href), + true, + WEBSTORE.chrome, + msgHandler, + ); + sendResponse({ + // because a "cancel" in the context of chromium-web-store won't be detected, + // we must throw user_cancelled here to ensure the button doesn't get stuck on loading spinner. + // The behaviour of this is similar to sending success ("") so this should be fine. + args: ["user_cancelled"], + }); + break; + } +}); diff --git a/src/scripts/chromeApi.js b/src/scripts/chromeApi.js new file mode 100644 index 0000000..3d834a0 --- /dev/null +++ b/src/scripts/chromeApi.js @@ -0,0 +1,97 @@ +// https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/extensions/api/webstore_private.json +const ncws_re = /.*detail(?:\/[^\/]+)?\/([a-z]{32})/i; // copied from util.js since it's out of context +const THIS_EXT_ID = ncws_re.exec(window.location.href)[1]; +const EXT_ID = "ocaahdebbfolfmndjeplogmgcagdmblk"; // need to hardcode this so it's available immediately. (chrome.runtime.id will be undefined in this context) + +const port = chrome.runtime.connect(EXT_ID, { name: "windowchromeport" }); +const CALLBACKS = []; +port.onMessage.addListener((msg, port) => { + if (msg.callbackIndex !== undefined) { + if (msg.err) { + // "throw" error + chrome.runtime.lastError = msg.err; + } + CALLBACKS[msg.callbackIndex].apply(...Array.from(msg.args)); + } +}); +window.chrome.webstorePrivate = { + getExtensionStatus: function (id, manifest, cb) { + chrome.runtime.sendMessage( + EXT_ID, + { func: "getExtensionStatus", args: [id, manifest] }, + (resp) => { + resp.args && cb(...resp.args); + }, + ); + }, + beginInstallWithManifest3: function (extinfo, cb) { + chrome.runtime.sendMessage( + EXT_ID, + { + func: "beginInstallWithManifest3", + args: [extinfo, window.location.href], + }, + (resp) => { + resp.args && cb(...resp.args); + }, + ); + }, + isInIncognitoMode: (cb) => cb(false), // just return false since it's likely this extension isn't running in incognito + getReferrerChain: (cb) => cb("EgIIAA=="), + completeInstall: function (id, cb) { + // will never be called since we cancel all install attempts with "user_cancelled" + // instead we rely on the onInstalled listener to continue the flow correctly (behaviour might be slightly different, + // for example an extension installed tooltip will not show.) + cb(true); + }, +}; +window.chrome.management = { + setEnabled: function (extId, enabled, callback) { + port.postMessage({ + func: "setEnabled", + args: [extId, enabled, CALLBACKS.length], + }); + CALLBACKS.push(callback); + }, + install: function () { + console.log( + "chrome.management.install not implemented, but called with args:", + arguments, + ); + }, + uninstall: function (extId, options, callback) { + port.postMessage({ + func: "uninstall", + args: [extId, options, CALLBACKS.length], + }); + CALLBACKS.push(callback); + }, + getAll: function (cb) { + chrome.runtime.sendMessage( + EXT_ID, + { func: "getAll", args: [THIS_EXT_ID] }, + (resp) => { + resp.args && cb(...resp.args); + }, + ); + }, + onInstalled: { + addListener: function (callback) { + port.postMessage({ + func: "onInstalled", + args: [CALLBACKS.length], + }); + CALLBACKS.push(callback); + }, + }, + onUninstalled: { + addListener: function (callback) { + port.postMessage({ + func: "onUninstalled", + args: [CALLBACKS.length], + }); + CALLBACKS.push(callback); + }, + }, +}; +// window.chrome.runtime.getManifest = () => true; // this is referenced in the CWS source but omitting it seems to have no effect diff --git a/src/scripts/inject.js b/src/scripts/inject.js index 8c701fc..b4c3796 100644 --- a/src/scripts/inject.js +++ b/src/scripts/inject.js @@ -133,29 +133,6 @@ attachMainObserver = new MutationObserver(function (mutations, observer) { }); observer.disconnect(); }); -var modifyButtonObserverNew = new MutationObserver(function ( - mutations, - observer, -) { - mutations.forEach(function (mutation) { - const btn = mutation.target.querySelector( - "button.UywwFc-LgbsSe-OWXEXe-dgl2Hf", - ); - const header = mutation.target.querySelector(".KgGEHd"); - header && header.setAttribute("target", "_top"); - if (btn && !btn.hasAttribute("isInstallBtn")) { - btn.setAttribute("isInstallBtn", "true"); - chrome.runtime.sendMessage( - { - checkExtInstalledId: getExtensionId(window.location.href), - }, - (resp) => { - modifyNewCWSButton(btn, !resp.installed); - }, - ); - } - }); -}); if (is_ews.test(window.location.href)) { new MutationObserver(function (mutations, observer) { mutations.forEach(function (mutation) { @@ -194,21 +171,14 @@ if (is_cws.test(window.location.href)) { childList: true, }); } -function injectCSS(cssCode) { - var style = document.createElement("style"); - style.textContent = cssCode; - document.head.appendChild(style); -} -if (is_ncws.test(window.location.href)) { - injectCSS(` - div[jscontroller="o2G9me"].gSrP5d#c2[jsaction="rcuQ6b:npT2md;KKVPLd:KrIKWc;JIbuQc:M4KNod"] { - display: none; - } - `); - modifyButtonObserverNew.observe(document.body, { - childList: true, - }); +function injectScript(file_path, tag) { + var node = document.getElementsByTagName(tag)[0]; + var script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", file_path); + node.appendChild(script); } + if ( is_ows.test(window.location.href) && document.body.querySelector("#feedback-container") //built-ins don't have a feedback section @@ -241,3 +211,16 @@ window.onload = () => { } }); }; +if (is_ncws.test(window.location.href)) { + chrome.storage.sync.get( + { webstore_integration: true }, + function (stored_values) { + if (stored_values.webstore_integration) { + injectScript( + chrome.runtime.getURL("scripts/chromeApi.js"), + "head", + ); + } + }, + ); +} diff --git a/src/scripts/util.js b/src/scripts/util.js index 23d8517..32ed678 100644 --- a/src/scripts/util.js +++ b/src/scripts/util.js @@ -9,6 +9,7 @@ const DEFAULT_MANAGEMENT_OPTIONS = { update_period_in_minutes: 60, removed_extensions: {}, manually_install: false, + webstore_integration: true, }; var fromXML; // prettier-ignore @@ -39,7 +40,7 @@ const is_ncws = /chromewebstore.google.com\//i; const is_ows = /addons.opera.com\/.*extensions/i; const is_ews = /microsoftedge\.microsoft\.com\/addons\//i; const cws_re = /.*detail\/[^\/]*\/([a-z]{32})/i; -const ncws_re = /.*detail\/[^\/]*\/([a-z]{32})/i; +const ncws_re = /.*detail(?:\/[^\/]+)?\/([a-z]{32})/i; const ows_re = /.*details\/([^\/?#]+)/i; const ews_re = /.*addons\/.+?\/([a-z]{32})/i;