diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa36ed5..8b03f38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,12 +15,19 @@ on: jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + include: + - build_script: build_mv2 + - build_script: build_mv3_firefox + - build_script: build_mv3_chrome + - build_script: build_mv3_safari steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - name: Install dependencies run: npm ci @@ -29,7 +36,7 @@ jobs: run: npm run lint - name: Build - run: npm run build + run: npm run ${{ matrix.build_script }} - name: Get version from package.json if: startsWith(github.ref, 'refs/tags/v') @@ -41,34 +48,72 @@ jobs: - name: Save metadata run: | mkdir -p ./build/.metadata + ORIGINAL_FILE_NAME=$(ls artifacts/*.zip) + FILE_NAME="${ORIGINAL_FILE_NAME##*/}" + FILE_NAME="${FILE_NAME%.zip}" + if [ "$GITHUB_EVENT_NAME" = "push" ]; then if [[ "$GITHUB_REF" =~ ^refs/tags/ ]]; then - mv artifacts/*.zip ./build/iitc-button-${{steps.get-version-key-from-json.outputs.version}}.zip + # Release build echo "release" > ./build/.metadata/build_type else # Beta build from master branch - mv artifacts/*.zip ./build/iitc-button-${{github.sha}}.zip + FILE_NAME="${FILE_NAME}-$(echo ${{ github.sha }} | cut -c 1-6)" echo "beta" > ./build/.metadata/build_type fi echo ${{ github.sha }} > ./build/.metadata/commit elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - mv artifacts/*.zip ./build/iitc-button-${{github.event.pull_request.head.sha}}.zip + # Build artifacts for PR + FILE_NAME="${FILE_NAME}-$(echo ${{ github.event.pull_request.head.sha }} | cut -c 1-6)" echo "PR" > ./build/.metadata/build_type echo ${{ github.event.pull_request.head.sha }} > ./build/.metadata/commit fi - echo $( ls ./build/ | grep '.zip' ) > ./build/.metadata/zip_filename + + mv $ORIGINAL_FILE_NAME "./build/$FILE_NAME".zip echo "GITHUB_SHA_SHORT=$(cat ./build/.metadata/commit | cut -c 1-6)" >> $GITHUB_ENV + echo "FILE_NAME=$FILE_NAME" >> $GITHUB_ENV - uses: ncipollo/release-action@v1 if: startsWith(github.ref, 'refs/tags/v') + env: + FILE_PATH: ./build/${{ env.FILE_NAME }} with: allowUpdates: true artifactErrorsFailBuild: true - artifacts: "./build/iitc-button-${{ steps.get-version-key-from-json.outputs.version }}.zip" + artifacts: ${{ env.FILE_PATH }} token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: - name: iitc-button-${{ env.GITHUB_SHA_SHORT }}-artifact + name: ${{ env.FILE_NAME }} path: | ./build/ + + collect: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: collected_artifacts + + - name: Move artifacts to final directory + run: | + mkdir -p final_artifacts + echo "Moving contents from artifact directories to final_artifacts directory:" + for dir in collected_artifacts/*; do + if [ -d "$dir" ]; then + cp -rf "$dir"/* "$dir"/.metadata final_artifacts/ + fi + done + echo "Contents of final_artifacts directory:" + ls -la final_artifacts/ + + - name: Get metadata + run: echo "GITHUB_SHA_SHORT=$(cat ./final_artifacts/.metadata/commit | cut -c 1-6)" >> $GITHUB_ENV + + - uses: actions/upload-artifact@v4 + with: + name: iitc-button-${{ env.GITHUB_SHA_SHORT }}-artifacts + path: final_artifacts/* diff --git a/.github/workflows/comment-pr.yml b/.github/workflows/comment-pr.yml index 659a241..8e52146 100644 --- a/.github/workflows/comment-pr.yml +++ b/.github/workflows/comment-pr.yml @@ -1,23 +1,49 @@ name: Add artifact links to pull request + on: workflow_run: - workflows: ['Build IITC Button'] + workflows: ["Build IITC Button"] types: [completed] - - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: - artifacts-url-comments: - if: ${{ github.event.workflow_run.event == 'pull_request' }} + comment-links: runs-on: ubuntu-latest steps: - - name: add artifact links to pull request and related issues step - uses: tonyhallett/artifacts-url-comments@v1.1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get artifact list from API + id: get-artifacts + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const runId = ${{ github.event.workflow_run.id }}; + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + artifacts.data.artifacts.forEach((artifact, index) => { + const nameVar = `artifact${index + 1}`; + const urlVar = `url${index + 1}`; + let displayName = artifact.name.endsWith('-artifacts') ? `**${artifact.name}**` : artifact.name; + core.exportVariable(nameVar, displayName); + core.exportVariable(urlVar, `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}`); + }); + + - name: Comment with artifact links for PR + uses: marocchino/sticky-pull-request-comment@v2 with: - prefix: Here are the build results - suffix: Artifacts will only be retained for 90 days. - format: url - addTo: pull + header: pr_artifacts + number: ${{ github.event.workflow_run.pull_requests[0].number }} + message: | + Build completed successfully. Below are the download links for the build artifacts: + + | Artifact Name | Download Link | + | ------------- | ------------- | + | ${{ env.artifact1 }} | [Download](${{ env.url1 }}) | + | ${{ env.artifact2 }} | [Download](${{ env.url2 }}) | + | ${{ env.artifact3 }} | [Download](${{ env.url3 }}) | + | ${{ env.artifact4 }} | [Download](${{ env.url4 }}) | + | ${{ env.artifact5 }} | [Download](${{ env.url5 }}) | + + Artifacts will only be retained for 90 days. diff --git a/README.md b/README.md index 5f3d807..a555a23 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,18 @@ Theoretically, you violates ToS, but as you totally relies on intel.ingress.com npm install ``` -### Compiles and hot-reloads for development +### Compiles and minifies for production with Manifest V2 ``` -npm run serve +npm run build_mv2 ``` -### Compiles and minifies for production +### Compiles and minifies for production with Manifest V3 ``` -npm run build +npm run build_mv3_firefox +# or +npm run build_mv3_chrome +# or +npm run build_mv3_safari ``` ### Lints and fixes files @@ -59,7 +63,7 @@ See [Configuration Reference](https://cli.vuejs.org/config/). ## Build for Safari (MacOS and iOS) 1. Follow the [general build instructions](#project-setup). -To build for iOS, set the _BROWSER="safari-ios"_ environment variable (example: `BROWSER="safari-ios" npm run build`) +To build for iOS, set the _BROWSER="safari-ios"_ environment variable (example: `BROWSER="safari-ios" npm run build`). Tested only on Manifest V2. 2. Open the Xcode project in the `safari` folder @@ -77,4 +81,3 @@ To develop without a certificate, tell Safari to load unsigned extensions using * Select the Extensions tab. This tab shows the localized description, display name, and version number for the selected Safari App Extension. It also provides more information about the permissions claimed by the extension. * Find your new extension in the list on the left, and enable it by selecting the checkbox. - diff --git a/package-lock.json b/package-lock.json index 1af2cc6..f5226a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "core-js": "^3.8.3", "highlight.js": "^10.7.3", "jszip": "^3.10.1", - "lib-iitc-manager": "^1.8.3", + "lib-iitc-manager": "^1.9.0", "scored-fuzzysearch": "^1.0.5", "vue": "^2.6.14" }, @@ -2933,6 +2933,48 @@ } } }, + "node_modules/@vue/cli-service/node_modules/copy-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/@vue/cli-service/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@vue/cli-shared-utils": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", @@ -5307,48 +5349,6 @@ "node": ">=0.10.0" } }, - "node_modules/copy-webpack-plugin": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", - "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.7", - "glob-parent": "^6.0.1", - "globby": "^11.0.3", - "normalize-path": "^3.0.0", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/core-js": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", @@ -9460,9 +9460,9 @@ } }, "node_modules/lib-iitc-manager": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/lib-iitc-manager/-/lib-iitc-manager-1.8.4.tgz", - "integrity": "sha512-1Mk58EgTbUDR7DOZAqx8oEWaFvu8kuhyZPyyuAIYYDf91jAdr61Ht3/rtQYOSXPIQGTj+sbgN5musk40puverQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/lib-iitc-manager/-/lib-iitc-manager-1.9.0.tgz", + "integrity": "sha512-8baF0kySzDQIqNv8+eSLNhjHN+jQIG8TLKvENDNVSaE4dWmdyF0IjWaYqxxE06+uwQktQG3XeY49QLYelmBscw==", "dependencies": { "@bundled-es-modules/deepmerge": "^4.3.1", "xhr2": "^0.2.1" diff --git a/package.json b/package.json index aabc2ec..ad04a2d 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "license": "GPLv3", "private": true, "scripts": { - "serve": "vue-cli-service build --mode development --watch", - "build": "vue-cli-service build", + "build_mv2": "export MANIFEST_VERSION=2 && vue-cli-service build", + "build_mv3_firefox": "export MANIFEST_VERSION=3 && export BROWSER=firefox && vue-cli-service build", + "build_mv3_chrome": "export MANIFEST_VERSION=3 && export BROWSER=chrome && vue-cli-service build", + "build_mv3_safari": "export MANIFEST_VERSION=3 && export BROWSER=safari && vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { @@ -14,7 +16,7 @@ "core-js": "^3.8.3", "highlight.js": "^10.7.3", "jszip": "^3.10.1", - "lib-iitc-manager": "^1.8.4", + "lib-iitc-manager": "^1.9.0", "scored-fuzzysearch": "^1.0.5", "vue": "^2.6.14" }, diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index e54a9bb..fdcbc84 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -262,5 +262,20 @@ }, "saveToJson": { "message": "Save to json" + }, + "alertChromeRequiresDevModeTitle": { + "message": "Our extension requires Developer Mode to be enabled to manage UserScripts, due to Manifest V3 limitations in Chrome. It's easy to enable!" + }, + "alertChromeRequiresDevModeButton": { + "message": "Please follow this guide" + }, + "alertHostPermissionsRequiredTitle": { + "message": "Provide access to URLs for userscripts to work. You will be able to change your choice in your browser settings." + }, + "alertHostPermissionsRequiredButtonIntel": { + "message": "Ingress Intel only" + }, + "alertHostPermissionsRequiredButtonAllUrls": { + "message": "All URLs to ensure full support" } } diff --git a/src/background/background.js b/src/background/background.js index fda284f..d8299fc 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -1,8 +1,12 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 import { Manager } from "lib-iitc-manager"; import browser from "webextension-polyfill"; +import { + IS_LEGACY_API, + IS_SCRIPTING_API, + IS_USERSCRIPTS_API, +} from "@/userscripts/env"; import { _ } from "@/i18n"; -import { inject_plugin } from "./injector"; import { onUpdatedListener, onRemovedListener, @@ -11,43 +15,60 @@ import { } from "./intel"; import "./requests"; import { strToBase64 } from "@/strToBase64"; +import { + init_userscripts_api, + is_iitc_enabled, + is_userscripts_api_available, +} from "@/userscripts/utils"; +import { + inject_plugin_via_content_scripts, + manage_userscripts_api, +} from "@/background/injector"; const manager = new Manager({ storage: browser.storage.local, message: (message, args) => { - try { - browser.runtime - .sendMessage({ - type: "showMessage", - message: _(message, args), - }) - .then(); - } catch { - // If popup is closed, message goes nowhere and an error occurs. Ignore. - } + browser.runtime + .sendMessage({ + type: "showMessage", + message: _(message, args), + }) + .then() + .catch(() => {}); // If popup is closed, message goes nowhere and an error occurs. Ignore. }, progressbar: (is_show) => { - try { - browser.runtime - .sendMessage({ - type: "showProgressbar", - value: is_show, - }) - .then(); - } catch { - // If popup is closed, message goes nowhere and an error occurs. Ignore. - } + browser.runtime + .sendMessage({ + type: "showProgressbar", + value: is_show, + }) + .then() + .catch(() => {}); // If popup is closed, message goes nowhere and an error occurs. Ignore. + }, + inject_plugin: async (plugin) => { + if (IS_USERSCRIPTS_API) return; + + const iitc_status = await is_iitc_enabled(); + if (iitc_status === false) return; + + await inject_plugin_via_content_scripts(plugin, true); }, - inject_plugin: (plugin) => inject_plugin(plugin).then(), + plugin_event: async (data) => { + if (IS_SCRIPTING_API) return; + await manage_userscripts_api(data); + }, + is_daemon: true, }); manager.run().then(); -const { onUpdated, onRemoved } = browser.tabs; -onUpdated.addListener((tabId, status, tab) => - onUpdatedListener(tabId, status, tab, manager) -); -onRemoved.addListener(onRemovedListener); +if (IS_LEGACY_API || IS_SCRIPTING_API) { + const { onUpdated, onRemoved } = browser.tabs; + onUpdated.addListener((tabId, status, tab) => + onUpdatedListener(tabId, status, tab, manager) + ); + onRemoved.addListener(onRemovedListener); +} browser.runtime.onMessage.addListener(async (request) => { switch (request.type) { @@ -56,15 +77,18 @@ browser.runtime.onMessage.addListener(async (request) => { break; case "toggleIITC": await onToggleIITC(request.value); + if (IS_USERSCRIPTS_API && request.value === true) { + await initUserscriptsApi(); + } + break; + case "popupWasOpened": + if (IS_USERSCRIPTS_API) { + await initUserscriptsApi(); + } break; case "xmlHttpRequestHandler": await xmlHttpRequestHandler(request.value); break; - } -}); - -browser.runtime.onMessage.addListener(async function (request) { - switch (request.type) { case "managePlugin": await manager.managePlugin(request.uid, request.action); break; @@ -80,61 +104,61 @@ browser.runtime.onMessage.addListener(async function (request) { case "addUserScripts": // TODO: The onMessage method should be able to return a value, but does not do so because of a bug. // More info: https://github.com/mozilla/webextension-polyfill/issues/172 - try { - browser.runtime - .sendMessage({ - type: "resolveAddUserScripts", - scripts: await manager.addUserScripts(request.scripts), - }) - .then(); - } catch { - // If tab is closed, message goes nowhere and an error occurs. Ignore. - } + browser.runtime + .sendMessage({ + type: "resolveAddUserScripts", + scripts: await manager.addUserScripts(request.scripts), + }) + .then() + .catch(() => {}); // If tab is closed, message goes nowhere and an error occurs. Ignore. break; case "getPluginInfo": - try { - browser.runtime - .sendMessage({ - type: "resolveGetPluginInfo", - info: await manager.getPluginInfo(request.uid), - }) - .then(); - } catch { - // If tab is closed, message goes nowhere and an error occurs. Ignore. - } + browser.runtime + .sendMessage({ + type: "resolveGetPluginInfo", + info: await manager.getPluginInfo(request.uid), + }) + .then() + .catch(() => {}); // If tab is closed, message goes nowhere and an error occurs. Ignore. break; case "getBackupData": - try { - browser.runtime - .sendMessage({ - type: "resolveGetBackupData", - data: await manager.getBackupData(request.params), - }) - .then(); - } catch { - // If tab is closed, message goes nowhere and an error occurs. Ignore. - } + browser.runtime + .sendMessage({ + type: "resolveGetBackupData", + data: await manager.getBackupData(request.params), + }) + .then() + .catch(() => {}); // If tab is closed, message goes nowhere and an error occurs. Ignore. break; case "setBackupData": - try { - browser.runtime - .sendMessage({ - type: "resolveSetBackupData", - data: await manager.setBackupData( - request.params, - request.backup_data - ), - }) - .then(); - } catch { - // If tab is closed, message goes nowhere and an error occurs. Ignore. - } + browser.runtime + .sendMessage({ + type: "resolveSetBackupData", + data: await manager.setBackupData( + request.params, + request.backup_data + ), + }) + .then() + .catch(() => {}); // If tab is closed, message goes nowhere and an error occurs. Ignore. + break; + case "checkUserScriptsApiAvailable": + browser.runtime + .sendMessage({ + type: "resolveCheckUserScriptsApiAvailable", + data: is_userscripts_api_available(), + }) + .then() + .catch(() => {}); // If tab is closed, message goes nowhere and an error occurs. Ignore. break; case "setCustomChannelUrl": await manager.setCustomChannelUrl(request.value); break; case "setUpdateCheckInterval": await manager.setUpdateCheckInterval(request.interval, request.channel); + if (IS_USERSCRIPTS_API) { + await createCheckUpdateAlarm(); + } break; } }); @@ -148,36 +172,103 @@ async function xmlHttpRequestHandler(data) { response: JSON.stringify(response), }); - const injectedCode = ` - document.dispatchEvent(new CustomEvent('bridgeResponse', { - detail: "${strToBase64(String(detail_stringify))}" - })); - `; + const bridge_data = strToBase64(String(detail_stringify)); - try { - await browser.tabs.executeScript(data.tab_id, { - code: injectedCode, + let allTabs = [ + { + id: data.tab_id, + }, + ]; + if (IS_USERSCRIPTS_API) { + allTabs = await browser.tabs.query({ active: true }); + } + + for (const tab of allTabs) { + await browser.tabs.sendMessage(tab.id, { + type: "xmlHttpRequestToCS", + value: bridge_data, }); - } catch (error) { - console.error(`An error occurred while execute script: ${error.message}`); } } - const req = new XMLHttpRequest(); - req.onload = function () { - const response = { - readyState: this.readyState, - responseHeaders: this.responseHeaders, - responseText: this.responseText, - status: this.status, - statusText: this.statusText, + try { + const response = await fetch(data.url, { + mode: "no-cors", + method: data.method, + headers: data.headers, + body: data.method !== "GET" ? data.data : undefined, + credentials: data.user && data.password ? "include" : "same-origin", + }); + + const text = await response.text(); + + // Create a response object similar to the one in XMLHttpRequest + const responseObject = { + readyState: 4, + responseHeaders: "Not directly accessible with fetch", + responseText: text, + status: response.status, + statusText: response.statusText, }; - xmlResponse(data.tab_id, data.onload, response); + + await xmlResponse(data.tab_id, data.onload, responseObject); + } catch (error) { + console.error("Fetch error:", error); + } +} + +async function initUserscriptsApi() { + if (IS_SCRIPTING_API) return; + + let scripts = []; + try { + scripts = await chrome.userScripts.getScripts(); + // eslint-disable-next-line no-empty + } catch {} + + const is_gm_api_plugin_exist = scripts.some( + (script) => script.id === "gm_api" + ); + if (is_gm_api_plugin_exist) return; + + init_userscripts_api(); + const plugins_event = { + event: "add", + plugins: await manager.getEnabledPlugins(), }; - req.open(data.method, data.url, true, data.user, data.password); - for (let [header_name, header_value] of Object.entries(data.headers)) { - req.setRequestHeader(header_name, header_value); + await manage_userscripts_api(plugins_event); +} + +async function createCheckUpdateAlarm() { + if (IS_SCRIPTING_API) return; + + const storage_intervals = await browser.storage.local.get([ + "channel", + "release_update_check_interval", + "beta_update_check_interval", + "custom_update_check_interval", + "external_update_check_interval", + ]); + const channel_interval = + storage_intervals[storage_intervals["channel"]] | 604800; + const external_interval = storage_intervals["external"] | 604800; + + let interval_seconds = Math.min(channel_interval, external_interval); + if (interval_seconds < 30) { + interval_seconds = 30; } + await chrome.alarms.create("check-update-alarm", { + periodInMinutes: interval_seconds / 60, + }); +} - req.send(data.data); +if (IS_USERSCRIPTS_API) { + browser.alarms.onAlarm.addListener(async () => { + await manager.checkUpdates(false); + }); } + +self.addEventListener("activate", () => { + initUserscriptsApi().then(); + createCheckUpdateAlarm().then(); +}); diff --git a/src/background/injector.js b/src/background/injector.js index be9ec30..ce46987 100644 --- a/src/background/injector.js +++ b/src/background/injector.js @@ -1,45 +1,94 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 import browser from "webextension-polyfill"; -import { check_matching } from "lib-iitc-manager"; - -export async function inject_plugin(plugin) { - const tabs = await getTabsToInject(); - - const is_ingress_tab = (url) => { - return /https:\/\/(intel|missions).ingress.com\/*/.test(url); - }; +import { gm_api_for_plugin } from "@/userscripts/wrapper"; +import { getNiaTabsToInject, getPluginMatches } from "@/background/utils"; +import { is_userscripts_api_available } from "@/userscripts/utils"; +import { IS_LEGACY_API } from "@/userscripts/env"; +export async function inject_plugin_via_content_scripts(plugin, use_gm_api) { + const tabs = await getNiaTabsToInject(plugin); for (let tab of Object.values(tabs)) { - if ( - (!is_ingress_tab(tab.url) || !check_matching(plugin, "")) && - !check_matching(plugin, tab.url) - ) { - continue; + const pluginTab = { ...plugin }; + if (use_gm_api) { + pluginTab.code = await gm_api_for_plugin(pluginTab, tab.id); } - const inject = ` - document.dispatchEvent(new CustomEvent('IITCButtonInitJS', { - detail: ${JSON.stringify({ plugin: plugin, tab_id: tab.id })} - })); - `; - try { - await browser.tabs.executeScript(tab.id, { - code: inject, - runAt: "document_end", - }); + if (IS_LEGACY_API) { + const inject = ` + document.dispatchEvent(new CustomEvent('IITCButtonInitJS', { + detail: ${JSON.stringify({ plugin: pluginTab })} + })); + `; + await browser.tabs.executeScript(tab.id, { + code: inject, + runAt: "document_end", + }); + } else { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: (pluginDetail) => { + document.dispatchEvent( + new CustomEvent("IITCButtonInitJS", { + detail: pluginDetail, + }) + ); + }, + args: [{ plugin: pluginTab }], + injectImmediately: true, + }); + } } catch (error) { - console.error(`An error occurred while reloading tabs: ${error.message}`); + console.error( + `An error occurred while injecting script: ${error.message}` + ); } } } -// Fetch all completly loaded Ingress Intel tabs -export async function getTabsToInject() { - let allTabs = await browser.tabs.query({ status: "complete" }); +export async function manage_userscripts_api(plugins_event) { + if (!is_userscripts_api_available()) return; + + const event = plugins_event.event; + const plugins = plugins_event.plugins; + const use_gm_api = plugins_event.use_gm_api !== false; + + if (event === "remove") { + const remove_ids = Object.keys(plugins); + try { + await browser.userScripts.unregister({ ids: remove_ids }); + } catch (e) { + console.log("an error occurred while unregistering the plugin", e); + } + } + + let plugins_obj = []; + for (let plugin of Object.values(plugins)) { + if (use_gm_api) { + plugin.code = await gm_api_for_plugin(plugin, 0); + } + plugins_obj.push({ + id: plugin.uid, + matches: + plugin.uid === "gm_api" ? ["https://*/*"] : getPluginMatches(plugin), + js: [{ code: plugin.code }], + runAt: plugin.uid === "gm_api" ? "document_start" : "document_end", + world: "MAIN", + }); + } - return allTabs.filter(function (tab) { - return tab.status === "complete" && tab.url; - }); + if (event === "add") { + try { + await browser.userScripts.register(plugins_obj); + } catch (e) { + console.log("an error occurred while registering the plugin", e); + } + } else if (event === "update") { + try { + await browser.userScripts.update(plugins_obj); + } catch (e) { + console.log("an error occurred while updating the plugin", e); + } + } } diff --git a/src/background/intel.js b/src/background/intel.js index 49a6fd9..89be169 100644 --- a/src/background/intel.js +++ b/src/background/intel.js @@ -1,13 +1,15 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 import browser from "webextension-polyfill"; -import { getTabsToInject } from "./injector"; +import { getTabsToInject } from "@/background/utils"; +import { is_iitc_enabled } from "@/userscripts/utils"; +import { IS_USERSCRIPTS_API } from "@/userscripts/env"; let lastIITCTab = null; export async function onRequestOpenIntel() { if (lastIITCTab) { const tabInfo = await getTabInfo(lastIITCTab); - if (isIngressIntelUrl(tabInfo.url)) { + if (tabInfo && isIngressIntelUrl(tabInfo.url)) { return await setTabActive(lastIITCTab); } } @@ -23,15 +25,24 @@ export async function onRequestOpenIntel() { } } -export async function onToggleIITC(value) { - await browser.storage.local.set({ IITC_is_enabled: value }); +export async function onToggleIITC(status) { + await browser.storage.local.set({ IITC_is_enabled: status }); - // Fetch all completly loaded Ingress Intel tabs - const tabs = await getTabsToInject(); + if (IS_USERSCRIPTS_API) { + if (status === false) { + try { + await browser.userScripts.unregister(); + // eslint-disable-next-line no-empty + } catch {} + } + } else { + // Fetch all completly loaded Ingress Intel tabs + const tabs = await getTabsToInject(); - for (let tab of Object.values(tabs)) { - if (isIngressIntelUrl(tab.url)) { - await browser.tabs.reload(tab.id); + for (let tab of Object.values(tabs)) { + if (isIngressIntelUrl(tab.url)) { + await browser.tabs.reload(tab.id); + } } } } @@ -53,10 +64,8 @@ export function onRemovedListener(tabId) { } async function initialize(manager) { - const storage = await browser.storage.local.get(["IITC_is_enabled"]); - const status = storage["IITC_is_enabled"]; - - if (status !== false) { + const status = await is_iitc_enabled(); + if (status) { await manager.inject(); } } @@ -75,7 +84,11 @@ async function setTabActive(tabId) { } async function getTabInfo(tabId) { - return await browser.tabs.get(tabId); + try { + return await browser.tabs.get(tabId); + } catch { + return null; + } } function isIngressIntelUrl(url) { diff --git a/src/background/requests.js b/src/background/requests.js index d2ccb64..34448f5 100644 --- a/src/background/requests.js +++ b/src/background/requests.js @@ -1,6 +1,7 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 import browser from "webextension-polyfill"; +import { IS_USERSCRIPTS_API } from "@/userscripts/env"; import { parseMeta, ajaxGet, getUniqId } from "lib-iitc-manager"; const IS_CHROME = !!global.chrome.app; @@ -16,31 +17,135 @@ const blacklist = ["//(?:(?:gist.|)github.com|gitlab.com)/"].map( const cache = {}; /** + * webRequest: + * If the URL looks like an IITC plugin, saving the URL to the cache + * + * webRequestBlocking: * If the URL looks like an IITC plugin and the bypass flag is not set, * it stops the download and triggers an in-depth check of the plugin. * * @param {Object} req - webRequest.onBeforeRequest object. - * @return {Object} + * @return {Object|void} - Returns an object if webRequestBlocking mode */ export function onBeforeRequest(req) { const { method, tabId, url } = req; - if (tabId in cache && cache[tabId] === "bypass") { - delete cache[tabId]; - return {}; - } - if (method !== "GET") { - return; - } + if (IS_USERSCRIPTS_API) { + const local_cache = { + last_userscript_request: { + tabId: tabId, + url: url, + }, + }; + browser.storage.local.set(local_cache).then(); + } else { + if (tabId in cache && cache[tabId] === "bypass") { + delete cache[tabId]; + return {}; + } + if (method !== "GET") { + return; + } - if (!blacklist.some(matches, url) || whitelist.some(matches, url)) { - maybeInstallUserJs(tabId, url).then(); - return IS_CHROME - ? { redirectUrl: "javascript:void 0" } // eslint-disable-line no-script-url - : { cancel: true }; // for sites with strict CSP in FF + if (!blacklist.some(matches, url) || whitelist.some(matches, url)) { + maybeInstallUserJs(tabId, url).then(); + return IS_CHROME + ? { redirectUrl: "javascript:void 0" } // eslint-disable-line no-script-url + : { cancel: true }; // for sites with strict CSP in FF + } } } +if (browser.webRequest) { + const extraInfoSpec = !IS_USERSCRIPTS_API ? ["blocking"] : []; + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, + { + urls: [ + // 1. *:// comprises only http/https + // 2. the API ignores #hash part + // 3. Firefox: onBeforeRequest does not work with file:// or moz-extension:// + "*://*/*.user.js", + "*://*/*.user.js?*", + "file://*/*.user.js", + "file://*/*.user.js?*", + ], + types: ["main_frame"], + }, + extraInfoSpec + ); +} + +if (browser.declarativeNetRequest) { + browser.runtime.onInstalled.addListener(async function () { + // restore the default rule if the extension is installed or updated + const existingRules = await browser.declarativeNetRequest.getDynamicRules(); + + const jsview_url = await browser.runtime.getURL(`/jsview.html`); + browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: existingRules.map((rule) => rule.id), + addRules: [ + { + id: 1, + priority: 10, + action: { + type: "allow", + }, + condition: { + urlFilter: "|*.user.js*#pass|", + resourceTypes: ["main_frame"], + }, + }, + { + id: 2, + priority: 2, + action: { + type: "redirect", + redirect: { + url: jsview_url, + }, + }, + condition: { + urlFilter: "||github.com/*/*/raw/*/*.user.js", + requestDomains: ["github.com"], + resourceTypes: ["main_frame"], + }, + }, + { + id: 3, + priority: 2, + action: { + type: "redirect", + redirect: { + url: jsview_url, + }, + }, + condition: { + urlFilter: "||gitlab.com/*/*/-/raw/*/*.user.js", + requestDomains: ["gitlab.com"], + resourceTypes: ["main_frame"], + }, + }, + { + id: 4, + priority: 1, + action: { + type: "redirect", + redirect: { + url: jsview_url, + }, + }, + condition: { + urlFilter: "|*.user.js^", + excludedRequestDomains: ["github.com", "gitlab.com"], + resourceTypes: ["main_frame"], + }, + }, + ], + }); + }); +} + /** * Writes the tab ID into the cache so that it does not interact with the tab later on and restarts the request. * @@ -137,23 +242,3 @@ browser.tabs.onCreated.addListener((tab) => { browser.tabs.onRemoved.addListener((tabId) => { delete cache[tabId]; }); - -// Seems unable to access browser.webRequest in Safari in non-persistent background -if (browser.webRequest) { - browser.webRequest.onBeforeRequest.addListener( - onBeforeRequest, - { - urls: [ - // 1. *:// comprises only http/https - // 2. the API ignores #hash part - // 3. Firefox: onBeforeRequest does not work with file:// or moz-extension:// - "*://*/*.user.js", - "*://*/*.user.js?*", - "file://*/*.user.js", - "file://*/*.user.js?*", - ], - types: ["main_frame"], - }, - ["blocking"] - ); -} diff --git a/src/background/utils.js b/src/background/utils.js new file mode 100644 index 0000000..36b6478 --- /dev/null +++ b/src/background/utils.js @@ -0,0 +1,38 @@ +//@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import browser from "webextension-polyfill"; +import { check_matching } from "lib-iitc-manager"; + +const is_ingress_tab = (url) => { + return /https:\/\/(intel|missions).ingress.com\/*/.test(url); +}; + +// Fetch all completly loaded tabs +export async function getTabsToInject() { + let allTabs = await browser.tabs.query({ status: "complete" }); + return allTabs.filter(function (tab) { + return tab.status === "complete" && tab.url; + }); +} + +// Filter all completly loaded Ingress Intel tabs +export async function getNiaTabsToInject(plugin) { + const tabs = await getTabsToInject(); + return Object.values(tabs).filter( + (tab) => + (is_ingress_tab(tab.url) && check_matching(plugin, "")) || + check_matching(plugin, tab.url) + ); +} + +export function getPluginMatches(plugin) { + let matches = []; + if (check_matching(plugin, "")) { + matches.push("https://intel.ingress.com/*"); + matches.push("https://missions.ingress.com/*"); + } + if (plugin.match) { + matches = matches.concat(plugin.match); + } + return matches; +} diff --git a/src/content-scripts/bridge.js b/src/content-scripts/bridge.js index 8d2e5dc..089d191 100644 --- a/src/content-scripts/bridge.js +++ b/src/content-scripts/bridge.js @@ -1,8 +1,8 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 import browser from "webextension-polyfill"; -import { inject } from "@/content-scripts/utils"; import { strToBase64 } from "@/strToBase64"; +import { isRunContentScript } from "@/content-scripts/utils"; export async function bridgeAction(e) { const task = e.detail; @@ -34,6 +34,16 @@ const xmlResponseBridge = async (data) => { .then(); }; +if (isRunContentScript) { + browser.runtime.onMessage.addListener(async (request) => { + switch (request.type) { + case "xmlHttpRequestToCS": + bridgeResponse(request.value); + break; + } + }); +} + // Sends the entire plugins scoped storage to the page context const getStorageBridge = async (req) => { const all_storage = await browser.storage.local.get(null); @@ -48,12 +58,8 @@ const getStorageBridge = async (req) => { response: JSON.stringify(plugins_storage), }); - const injectedCode = ` - document.dispatchEvent(new CustomEvent('bridgeResponse', { - detail: "${strToBase64(String(detail_stringify))}" - })); - `; - inject(injectedCode); + const bridge_base64_data = strToBase64(String(detail_stringify)); + bridgeResponse(bridge_base64_data); }; // Saves the value in the persistent storage in order to synchronize the data with the storage in the page context @@ -67,3 +73,11 @@ const setValueBridge = async (req) => { const delValueBridge = async (req) => { await browser.storage.local.remove(req.key); }; + +const bridgeResponse = (bridge_base64_data) => { + dispatchEvent( + new CustomEvent("bridgeResponse", { + detail: bridge_base64_data, + }) + ); +}; diff --git a/src/content-scripts/loader.js b/src/content-scripts/loader.js index 67c75f4..ccec32f 100644 --- a/src/content-scripts/loader.js +++ b/src/content-scripts/loader.js @@ -1,29 +1,24 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 -import browser from "webextension-polyfill"; -import { GM } from "./gm-api"; -import { inject, IITCButtonInitJS } from "./utils"; +import { IITCButtonInitJS, isRunContentScript } from "./utils"; import { bridgeAction } from "@/content-scripts/bridge"; +import { inject_gm_api } from "@/userscripts/wrapper"; +import { IS_USERSCRIPTS_API } from "@/userscripts/env"; +import { is_iitc_enabled } from "@/userscripts/utils"; function preparePage() { - document.addEventListener("DOMContentLoaded", function () { - if (window.location.hostname === "intel.ingress.com") { - window.onload = function () {}; - document.body.onload = function () {}; - } - }); - - inject( - `((${GM.toString()}))()\n//# sourceURL=${browser.runtime.getURL( - "js/GM_api.js" - )}` - ); document.addEventListener("bridgeRequest", bridgeAction); + if (IS_USERSCRIPTS_API) return; + + inject_gm_api(); document.addEventListener("IITCButtonInitJS", IITCButtonInitJS); } -browser.storage.local.get(["IITC_is_enabled"]).then((data) => { - if (data["IITC_is_enabled"] !== false) { - preparePage(); - } -}); +if (isRunContentScript) { + window.iitcbutton = true; + is_iitc_enabled().then((status) => { + if (status) { + preparePage(); + } + }); +} diff --git a/src/content-scripts/utils.js b/src/content-scripts/utils.js index bbdb6e6..0fd45b6 100644 --- a/src/content-scripts/utils.js +++ b/src/content-scripts/utils.js @@ -1,11 +1,11 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 -import browser from "webextension-polyfill"; import { getUID } from "lib-iitc-manager"; -import { strToBase64 } from "@/strToBase64"; const LOADED_PLUGINS = []; +export const isRunContentScript = !window.iitcbutton; + export function inject(code) { const script = document.createElement("script"); script.appendChild(document.createTextNode(code)); @@ -15,46 +15,15 @@ export function inject(code) { script.parentElement.removeChild(script); } -function getPluginHash(uid) { - return "VMin" + strToBase64(uid); -} - export async function IITCButtonInitJS(e) { - const tab_id = e.detail.tab_id; const plugin = e.detail.plugin; - const meta = { ...plugin }; - delete meta.code; - const uid = plugin.uid ? plugin.uid : getUID(plugin); - let data_key = getPluginHash(uid); if (LOADED_PLUGINS.includes(uid)) { console.debug(`Plugin ${uid} is already loaded. Skip`); } else { LOADED_PLUGINS.push(uid); console.debug(`Plugin ${uid} loaded`); - - const name = encodeURIComponent(plugin.name); - const injectedCode = [ - "((GM)=>{", - // an implementation of GM API v3 based on GM API v4 - "const GM_info = GM.info; const unsafeWindow = window;", - "const exportFunction = GM.exportFunction; const createObjectIn = GM.createObjectIn; const cloneInto = GM.cloneInto;", - "const GM_getValue = (key, value) => GM._getValueSync(key, value);", - "const GM_setValue = (key, value) => GM._setValueSync(key, value);", - "const GM_xmlhttpRequest = (details) => GM.xmlHttpRequest(details);", - - plugin.code, - // adding a new line in case the code ends with a line comment - plugin.code.endsWith("\n") ? "" : "\n", - `})(GM("${data_key}", ${tab_id}, ${JSON.stringify(meta)}))`, - - // Firefox lists .user.js among our own content scripts so a space at start will group them - `\n//# sourceURL=${browser.runtime.getURL( - "plugins/%20" + name + ".user.js" - )}`, - ].join(""); - - inject(injectedCode); + inject(plugin.code); } } diff --git a/src/jsview/App.vue b/src/jsview/App.vue index c64f30c..8ace71c 100644 --- a/src/jsview/App.vue +++ b/src/jsview/App.vue @@ -7,9 +7,11 @@ + + diff --git a/src/userscripts/env.js b/src/userscripts/env.js new file mode 100644 index 0000000..a3e10df --- /dev/null +++ b/src/userscripts/env.js @@ -0,0 +1,10 @@ +//@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import browser from "webextension-polyfill"; + +export const IS_CHROME = !!browser.runtime.OnInstalledReason?.CHROME_UPDATE; +export const MANIFEST = browser.runtime.getManifest(); + +export const IS_USERSCRIPTS_API = IS_CHROME && MANIFEST.manifest_version === 3; +export const IS_SCRIPTING_API = !!browser.scripting; +export const IS_LEGACY_API = !IS_USERSCRIPTS_API && !IS_SCRIPTING_API; diff --git a/src/content-scripts/gm-api.js b/src/userscripts/gm-api.js similarity index 95% rename from src/content-scripts/gm-api.js rename to src/userscripts/gm-api.js index 8b09d43..ab03f57 100644 --- a/src/content-scripts/gm-api.js +++ b/src/userscripts/gm-api.js @@ -1,5 +1,12 @@ //@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 export const GM = function () { + document.addEventListener("DOMContentLoaded", function () { + if (window.location.hostname === "intel.ingress.com") { + window.onload = function () {}; + document.body.onload = function () {}; + } + }); + const cache = {}; const defineProperty = Object.defineProperty; @@ -178,7 +185,7 @@ export const GM = function () { cloneInto: makeFunc((obj) => obj), }; }; - document.addEventListener("bridgeResponse", function (e) { + addEventListener("bridgeResponse", function (e) { const detail = JSON.parse(base64ToStr(e.detail)); const uuid = detail.task_uuid; diff --git a/src/userscripts/utils.js b/src/userscripts/utils.js new file mode 100644 index 0000000..3b755c8 --- /dev/null +++ b/src/userscripts/utils.js @@ -0,0 +1,33 @@ +//@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import browser from "webextension-polyfill"; +import { inject_gm_api } from "@/userscripts/wrapper"; + +export async function is_iitc_enabled() { + const status = await browser.storage.local + .get(["IITC_is_enabled"]) + .then((data) => data["IITC_is_enabled"]); + return status !== false; +} + +export function is_userscripts_api_available() { + try { + // Property access which throws if developer mode is not enabled. + return browser.userScripts !== undefined; + } catch { + // Not available. + return false; + } +} + +export const init_userscripts_api = () => { + if (!is_userscripts_api_available()) return; + try { + browser.userScripts.configureWorld({ + csp: "script-src 'self' 'unsafe-inline'", + messaging: true, + }); + inject_gm_api(); + // eslint-disable-next-line no-empty + } catch {} +}; diff --git a/src/userscripts/wrapper.js b/src/userscripts/wrapper.js new file mode 100644 index 0000000..f4a088d --- /dev/null +++ b/src/userscripts/wrapper.js @@ -0,0 +1,70 @@ +//@license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import browser from "webextension-polyfill"; +import { IS_USERSCRIPTS_API } from "@/userscripts/env"; +import { manage_userscripts_api } from "@/background/injector"; +import { strToBase64 } from "@/strToBase64"; +import { getUID } from "lib-iitc-manager"; +import { GM } from "@/userscripts/gm-api"; + +function getPluginHash(uid) { + return "VMin" + strToBase64(uid); +} + +function inject(code) { + const script = document.createElement("script"); + script.appendChild(document.createTextNode(code)); + (document.body || document.head || document.documentElement).appendChild( + script + ); + script.parentElement.removeChild(script); +} + +export function inject_gm_api() { + const plugin = { + uid: "gm_api", + code: `((${GM.toString()}))()\n//# sourceURL=${browser.runtime.getURL( + "js/GM_api.js" + )}`, + }; + + if (IS_USERSCRIPTS_API) { + const plugins_event = { + event: "add", + use_gm_api: false, + plugins: [plugin], + }; + manage_userscripts_api(plugins_event).then(); + } else { + inject(plugin.code); + } +} + +export async function gm_api_for_plugin(plugin, tab_id) { + const uid = plugin.uid ? plugin.uid : getUID(plugin); + let data_key = getPluginHash(uid); + const name = encodeURIComponent(plugin.name); + + const meta = { ...plugin }; + delete meta.code; + + return [ + "((GM)=>{", + // an implementation of GM API v3 based on GM API v4 + "const GM_info = GM.info; const unsafeWindow = window;", + "const exportFunction = GM.exportFunction; const createObjectIn = GM.createObjectIn; const cloneInto = GM.cloneInto;", + "const GM_getValue = (key, value) => GM._getValueSync(key, value);", + "const GM_setValue = (key, value) => GM._setValueSync(key, value);", + "const GM_xmlhttpRequest = (details) => GM.xmlHttpRequest(details);", + + plugin.code, + // adding a new line in case the code ends with a line comment + plugin.code.endsWith("\n") ? "" : "\n", + `})(GM("${data_key}", ${tab_id}, ${JSON.stringify(meta)}))`, + + // Firefox lists .user.js among our own content scripts so a space at start will group them + `\n//# sourceURL=${browser.runtime.getURL( + "plugins/%20" + name + ".user.js" + )}`, + ].join(""); +} diff --git a/vue.config.js b/vue.config.js index d6fec2e..53668d7 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,3 +1,84 @@ +const manifest_transformer = (manifest) => { + const browser = process.env.BROWSER; + const manifest_version = process.env.MANIFEST_VERSION; + + if (manifest_version === "2") { + manifest_v2_transformer(manifest, browser); + } else if (manifest_version === "3") { + if (browser === undefined) { + throw Error("BROWSER environment variable is not set"); + } + manifest_v3_transformer(manifest, browser); + } else { + throw Error("MANIFEST_VERSION environment variable is not set"); + } + manifest.manifest_version = parseInt(manifest_version); +}; + +const manifest_v2_transformer = (manifest, browser) => { + manifest.content_scripts = [ + { + matches: [""], + run_at: "document_start", + js: ["js/content-script.js"], + }, + ]; + manifest.permissions.push(""); + manifest.permissions.push("webRequest"); + manifest.permissions.push("webRequestBlocking"); + manifest.background.page = "background.html"; + + if (browser === "safari-ios") { + manifest.background.persistent = false; + } +}; + +const manifest_v3_transformer = (manifest, browser) => { + manifest.host_permissions = [ + "https://intel.ingress.com/*", + "http://*/*", + "https://*/*", + ]; + manifest.content_security_policy = { + extension_pages: + "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ws://localhost:9090 http://localhost:8000 https://*; img-src 'self' https://iitc.app", + }; + manifest.content_scripts = [ + { + matches: [""], + run_at: "document_start", + js: ["js/content-script.js"], + }, + ]; + manifest.permissions.push("webRequest"); + + manifest.action = manifest.browser_action; + delete manifest.browser_action; + + if (browser === "chrome") { + manifest.minimum_chrome_version = "120"; + manifest.permissions.push("userScripts"); + manifest.permissions.push("alarms"); + manifest.permissions.push("declarativeNetRequest"); + manifest.background.service_worker = "js/background.js"; + + manifest.web_accessible_resources = [ + { + resources: ["jsview.html"], + matches: [""], + }, + ]; + } + if (browser === "firefox" || browser === "safari") { + manifest.permissions.push("webRequestBlocking"); + manifest.permissions.push("scripting"); + manifest.background.page = "background.html"; + } + if (browser === "safari") { + delete manifest.content_security_policy; + } +}; + module.exports = { filenameHashing: false, productionSourceMap: false, @@ -35,11 +116,31 @@ module.exports = { }, }, manifestTransformer: (manifest) => { - if (process.env.BROWSER === "safari-ios") { - manifest.background.persistent = false; - } + manifest_transformer(manifest); return manifest; }, + artifactFilename: ({ name, version, mode }) => { + const browser = + process.env.MANIFEST_VERSION === "3" ? process.env.BROWSER : "all"; + if (mode === "production") { + return `${name}-v${version}-${browser}-MV${process.env.MANIFEST_VERSION}.zip`; + } + return `${name}-v${version}-${browser}-MV${process.env.MANIFEST_VERSION}-${mode}.zip`; + }, + }, + }, + chainWebpack: (config) => { + config.optimization.delete("splitChunks"); + }, + configureWebpack: { + devtool: "cheap-module-source-map", + optimization: { + splitChunks: { + cacheGroups: { + default: false, + vendors: false, + }, + }, }, }, };