diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94b2afb36..39577d411 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: pull_request: branches: - main + - rc/3.0-mv3 env: XDG_CACHE_HOME: ${{ github.workspace }}/.cache @@ -43,9 +44,9 @@ jobs: id: cache with: path: ${{ github.workspace }}/.cache - key: ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }} + key: ${{ runner.os }}-${{ hashFiles('**/package*json', '**/package-lock.json', '**/*config.js', '**/*.patch') }} restore-keys: | - ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }} + ${{ runner.os }}-${{ hashFiles('**/package*json', '**/package-lock.json', '**/*config.js', '**/*.patch') }} ${{ runner.os }}- - name: Restore node_modules @@ -53,7 +54,7 @@ jobs: uses: actions/cache@v3.2.4 with: path: node_modules - key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-${{ hashFiles('**/package*json', '**/package-lock.json', '**/*config.js', '**/*.patch') }} - name: Install dependencies if: steps.yarn-cache.outputs.cache-hit != 'true' @@ -106,7 +107,9 @@ jobs: release-assets: runs-on: ubuntu-latest needs: [test] - if: contains(github.ref, 'refs/tags/') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + if: | + (contains(github.ref, 'refs/tags/') || github.ref == 'refs/heads/rc/3.0-mv3') && + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') steps: - name: Check out Git repository uses: actions/checkout@v3.3.0 @@ -121,9 +124,9 @@ jobs: id: cache with: path: ${{ github.workspace }}/.cache - key: ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }} + key: ${{ runner.os }}-${{ hashFiles('**/package*json', '**/package-lock.json', '**/*config.js', '**/*.patch') }} restore-keys: | - ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }} + ${{ runner.os }}-${{ hashFiles('**/package*json', '**/package-lock.json', '**/*config.js', '**/*.patch') }} ${{ runner.os }}- - name: Restore node_modules @@ -131,7 +134,7 @@ jobs: uses: actions/cache@v3.2.4 with: path: node_modules - key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-${{ hashFiles('**/package*json', '**/package-lock.json', '**/*config.js', '**/*.patch') }} - name: Install dependencies if: steps.yarn-cache.outputs.cache-hit != 'true' diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 000000000..361ae337f --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,17 @@ +{ + "diff": true, + "extensions": [".js", ".ts"], + "package": "./package.json", + "require": [ + "ignore-styles", + "ts-node/register", + "tsconfig-paths/register" + ], + "exit": true, + "recursive": true, + "node-option": [ + "es-module-specifier-resolution=node", + "experimental-specifier-resolution=node", + "loader=ts-node/esm" + ] +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..49991d30c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.14.0 diff --git a/Dockerfile b/Dockerfile index b75b87849..4e386240a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.12.1 +FROM node:18.14.0 ARG USER_ID ARG GROUP_ID diff --git a/README.md b/README.md index 183bd49c2..4682c210b 100644 --- a/README.md +++ b/README.md @@ -63,15 +63,6 @@ You can disable and re-enable local gateway redirects by several means: - Suspend redirects **per site** using the toggle under "Current tab" ([illustrated below](#toggle-gateway-redirects-on-a-per-website-basis)) or in IPFS Companion's preferences - Add `x-ipfs-companion-no-redirect` to the URL itself as a hash ([example](https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR#x-ipfs-companion-no-redirect)) or query parameter ([example](https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?x-ipfs-companion-no-redirect)) - - ### Access frequently-used IPFS actions from your browser bar IPFS Companion enables you to quickly and easily access common actions from your browser bar with just a few clicks: @@ -105,11 +96,6 @@ IPFS Companion ships with a variety of experimental features. Some are disabled - Re-route requests made via the following [experimental protocols](https://github.com/ipfs/ipfs-companion/issues/164) to an HTTP gateway (public or custom): - `ipfs://$cid` - `ipns://$cid_or_fqdn` - - `dweb:/ipfs/$cid` - - `dweb:/ipns/$cid_or_fqdn` - -- Switch between the external HTTP API of your local IPFS node (default setting) and a js-ipfs node embedded in your browser (note that this has some [functionality limitations](https://docs.ipfs.io/how-to/companion-node-types/)) -[![screenshot of node type switch](https://gateway.ipfs.io/ipfs/QmPDxawBTEmH5Dk1anaqpryRyyaNwmqVPt5DsCf21eFWQz)](http://docs.ipfs.io/how-to/companion-node-types/) ## Install IPFS Companion diff --git a/add-on/_locales/ar/messages.json b/add-on/_locales/ar/messages.json index 58f61e83d..1a55095f1 100644 --- a/add-on/_locales/ar/messages.json +++ b/add-on/_locales/ar/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "قم بتبديل كافة تكاملات IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "غير متصل", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "انسخ مسار IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "انسخ مسار IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "استخدم مسار المحتوى هذا مع أدوات وبوابات IPFS للوصول إلى أحدث إصدار تم تحديثه من محتوى علامة التبويب هذه.", @@ -239,19 +239,15 @@ "message": "فشل في إيقاف عقدة IPFS", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "التفضيلات | رفيق IPFS", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "التفضيلات المصاحبة", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "اقرأ المزيد", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "عقدة IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "اضبط على \"خارجي\" للاتصال بالعقدة المحلية باستخدام HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "اضبط على \"Embedded\" لتشغيل عقدة js-ipfs مباشرة في متصفحك. (انقر فوق \"قراءة المزيد\" للتعرف على قيود هذه الميزة التجريبية.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "اضبط على \"المقدمة من Brave\" للاستفادة من دعم IPFS الأصلي لمتصفح Brave.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "تكوين عقدة IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "تكوين إضافي للعقدة JS IPFS المضمنة. يجب أن يكون JSON صالحًا.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "خارجي", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "المضمنة", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "مقدمة من Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/ca/messages.json b/add-on/_locales/ca/messages.json index af84b656d..151c88b3e 100644 --- a/add-on/_locales/ca/messages.json +++ b/add-on/_locales/ca/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Alterna totes les integracions IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "Desconectat", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copy IPNS Path", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copy IPNS Path", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", @@ -239,19 +239,19 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferències | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Preferències de Companion", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { + "option_legend_readMore": { "message": "Llegir més", "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Node IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +267,6 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +275,10 @@ "message": "Configurar Node IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Configuració addicional pel node incrustat JS IPFS. Ha de ser un JSON vàlid.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Extern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Incrustat", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provided by Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/cs/messages.json b/add-on/_locales/cs/messages.json index 9c4b15675..61dfd05b2 100644 --- a/add-on/_locales/cs/messages.json +++ b/add-on/_locales/cs/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Zapnout/vypnout IPFS integraci", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "odpojený", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Kopírovat IPNS cestu", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Kopírovat IPNS cestu", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Pomocí této cesty k obsahu se pomocí nástrojů a bran IPFS dostanete k poslední aktualizované verzi obsahu této karty.", @@ -239,19 +239,15 @@ "message": "Nepodařilo se zastavit uzel IPFS", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Předvolby | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Předvolby Companion", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Více informací", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS uzel", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Nastavením na hodnotu \"Externí\" se připojíte k místnímu uzlu pomocí rozhraní HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Nastavením na \"Embedded\" spustíte uzel js-ipfs přímo v prohlížeči. (Kliknutím na \"Přečtěte si více\" se dozvíte o omezeních této experimentální funkce.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Nastavte možnost \"Provided by Brave\", abyste využili nativní podporu IPFS v prohlížeči Brave.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Konfigurace IPFS uzlu", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Další konfigurace pro embedovaný uzel JS IPFS. Musí to být platný JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Externí", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Integrovaný", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Poskytuje Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/da/messages.json b/add-on/_locales/da/messages.json index 7fb37014f..5a1c619f1 100644 --- a/add-on/_locales/da/messages.json +++ b/add-on/_locales/da/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Skift alle IPFS integrationer", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -231,19 +231,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Læs mere", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS-klient", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -259,34 +255,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Embedded with Chrome Sockets: run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "IPFS-klient opsætning", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Ekstern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Indlejret", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "Indlejret + chrome.sockets", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "Gateways", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/_locales/de/messages.json b/add-on/_locales/de/messages.json index fdbae66bc..0120caa1c 100644 --- a/add-on/_locales/de/messages.json +++ b/add-on/_locales/de/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Alle IPFS-Einbindungen umschalten", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "IPNS-Pfad kopieren", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "IPNS-Pfad kopieren", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Nutze diesen Inhaltspfad mit IPFS-Werkzeugen und -Gateways um an die kürzlich aktualisierte Version des Inhalts dieses Tabs zu gelangen.", @@ -239,19 +239,15 @@ "message": "IPFS-Node konnte nicht beendet werden", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Einstellungen | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Einstellungen", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Mehr Details", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS-Node", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Setze die Option auf \"Extern\", um über die HTTP-API eine Verbindung zu einem lokalen Node herzustellen.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Auf \"Embedded\" stellen, um einen js-ipfs Node direkt in deinem Browser laufen zu lassen. (Klicke \"Lies mehr\" um mehr über die Einschränkungen dieser experimentellen Funktion zu lesen.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Stelle auf \"Provided by Brave\" um die native IPFS-Unterstützung vom Brave Browser zu nutzen.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "IPFS-Node-Konfiguration", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Zusätzliche Konfiguration für den eingebetteten JS-IPFS-Node. Muss gültiges JSON sein.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Extern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Eingebettet", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Bereitgestellt von Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index 22cc7699f..8129f65ef 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Toggle all IPFS integrations", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copy IPNS Path", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copy IPNS Path", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", @@ -223,19 +223,19 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { + "option_legend_readMore": { "message": "Read more", "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS Node", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -248,13 +248,9 @@ "description": "An option title on the Preferences screen (option_ipfsNodeType_title)" }, "option_ipfsNodeType_external_description": { - "message": "Set to \"External\" to connect to a local node using the HTTP API.", + "message": "Set to \"External\" to connect to a local Kubo node using the Kubo RPC API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -263,18 +259,10 @@ "message": "IPFS Node Config", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "External", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Embedded", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provided by Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" @@ -372,7 +360,7 @@ "description": "An option description on the Preferences screen (option_publicSubdomainGatewayUrl_description)" }, "option_header_api": { - "message": "API", + "message": "Kubo RPC API", "description": "A section header on the Preferences screen (option_header_api)" }, "option_ipfsApiUrl_title": { @@ -396,9 +384,13 @@ "description": "An option title on the Preferences screen (option_automaticMode_title)" }, "option_automaticMode_description": { - "message": "Automatically switch from your local gateway to your default public gateway if the Kubo RPC is unavailable.", + "message": "Automatically enables automatic redirection to your local gateway when the Kubo RPC endpoint is accessible, and disable it when the endpoint is unavailable.", "description": "An option description on the Preferences screen (option_automaticMode_description)" }, + "option_automaticMode_description_subtext": { + "message": "Note: disabling this feature will result in static redirects, independent of the Kubo RPC endpoint's availability, and may produce error when your node is offline.", + "description": "An automatic mode option description on the Preferences screen (option_automaticMode_description_subtext)" + }, "option_header_dnslink": { "message": "DNSLink", "description": "A section header on the Preferences screen (option_header_dnslink)" @@ -407,6 +399,10 @@ "message": "Experiments", "description": "A section header on the Preferences screen (option_header_experiments)" }, + "option_header_redirect_rules": { + "message": "Redirect Rules", + "description": "A section header on the Preferences screen (option_header_redirect_rules)" + }, "option_header_reset": { "message": "Reset Preferences", "description": "A section header on the Preferences screen (option_header_reset)" @@ -515,6 +511,18 @@ "message": "Automatically preload assets imported to IPFS via asynchronous HTTP HEAD requests to a public gateway.", "description": "An option description on the Preferences screen (option_preloadAtPublicGateway_description)" }, + "option_redirect_rules_reset_all": { + "message": "Reset All Redirect Rules", + "description": "A button label on the Preferences screen (option_redirect_rules_reset_all)" + }, + "option_redirect_rules_row_origin": { + "message": "Origin", + "description": "A table header on the Preferences screen (option_redirect_rules_row_origin)" + }, + "option_redirect_rules_row_target": { + "message": "Target", + "description": "A table header on the Preferences screen (option_redirect_rules_row_target)" + }, "option_logNamespaces_title": { "message": "Log Namespaces", "description": "An option title for tweaking log level (option_logNamespaces_title)" @@ -751,7 +759,7 @@ "message": "Tracking description", "description": "A description for the 'tracking' grouping of metrics we collect (option_telemetryGroupTracking_description)" }, - "recovery_page_title" : { + "recovery_page_title": { "message": "Problem with your IPFS node | IPFS Companion", "description": "Title of the recovery page (recovery_page_title)" }, @@ -778,5 +786,29 @@ "recovery_page_update_preferences": { "message": "Update your IPFS Companion preferences", "description": "Learn more link on the recovery screen (recovery_page_learn_more)" + }, + "request_permissions_page_title": { + "message": "Grant Host Permissions | IPFS Companion", + "description": "Title of the recovery page (recovery_page_title)" + }, + "request_permissions_page_sub_header": { + "message": "Just one more thing to do before you're all set! :)", + "description": "Sub-Header on the recovery screen (recovery_page_sub_header)" + }, + "request_permissions_page_message_p1": { + "message": "IPFS Companion needs permission to identify IPFS resources on the web.", + "description": "Message Para-1 on the recovery screen (recovery_page_message_p1)" + }, + "request_permissions_page_message_p2": { + "message": "The IPFS Companion requires Host permission to access data on all websites. This allows it to inspect all web requests, identify ones for content-addressed IPFS resources, and load them using your local IPFS node. Please click the button below to grant these permissions.", + "description": "Message Para-2 on the recovery screen (recovery_page_message_p2)" + }, + "request_permissions_page_button": { + "message": "Grant Permission", + "description": "Button on the recovery screen (recovery_page_button)" + }, + "request_permissions_page_learn_more": { + "message": "Learn more about host permissions", + "description": "Learn more link on the recovery screen (recovery_page_learn_more)" } } diff --git a/add-on/_locales/es/messages.json b/add-on/_locales/es/messages.json index 73b8e8428..2f26f83b3 100644 --- a/add-on/_locales/es/messages.json +++ b/add-on/_locales/es/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Alternar todas las integraciones de IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "desconectado", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "copie el sendero del IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "copie el sendero del IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Utilice esta ruta de contenido con las herramientas y las puertas de enlaces del IPFS para llegar a la versión actualizada más reciente del contenido de esta pestaña.", @@ -239,19 +239,15 @@ "message": "Error al detener nodo IPFS", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferencias | IPFS adjunto", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Preferencias adjuntas", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Leer más", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Nodo de IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Configurar como \"Externo\" para conectar a nodo local usando la API HTTP", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Configurar como \"Embebido\" para correr un nodo js-ipfs directamente en el navegador. (Presione en \"Leer más\" para aprender acerca de las limitaciones de esta característica experimental.", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Configurar como \"Provisto por Brave\" para aprovechar el soporte IPFS nativo del navegador Brave.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Configuración de nodo IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Configuración adicional para el nodo JS IPFS incorporado. Debe ser JSON válido.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Externo", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Insertado", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provisto por Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/fi/messages.json b/add-on/_locales/fi/messages.json index dcf646029..97972196d 100644 --- a/add-on/_locales/fi/messages.json +++ b/add-on/_locales/fi/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Kaikkien IPFS-integraatioiden kytkin", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "yhteydetön", @@ -207,15 +207,11 @@ "message": "IPFS-solmun pysäyttäminen epäonnistui", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Lue lisää", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS-solmu", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -231,34 +227,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Sulautettu Chrome Socket:eilla: suorita js-ipfs -solmu selaimessasi pääsyllä chrome.sockets -ohjelmointirajapintaan (lisätietoja alla olevasta linkistä)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "IPFS solmun asetukset", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Lisä-asetukset sulautetulle JS-IPFS-solmulle. Tämän täytyy olla kelvollisessa JSON-muodossa.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Ulkoinen", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Sisäänrakennettu", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "Sisäänrakennettu + chrome.socket:it", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "Yhdyskäytävät", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/_locales/fr/messages.json b/add-on/_locales/fr/messages.json index 265f105b2..54059b9f2 100644 --- a/add-on/_locales/fr/messages.json +++ b/add-on/_locales/fr/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Basculer toutes les intégrations IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "hors ligne", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copier le chemin IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copier le chemin IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Utiliser ce chemin de contenu, les outils IPFS et les passerelles pour obtenir la version la plus récemment mise à jour du contenu de cet onglet.", @@ -239,19 +239,15 @@ "message": "Impossible de stopper le nœud IPFS", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Préférences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Préférences de Companion", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "En savoir plus", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Nœud IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Choisissez \"Externe\" pour vous connecter à un nœud local grâce à l'API HTTP.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Choisissez \"Embarqué\" pour lancer un nœud js-ipfs directement dans votre navigateur. (Cliquez sur \"En savoir plus\" pour connaître les limitations de cette fonctionnalité expérimentale.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Reglez sur \"Embarqué dans Brave\" to utiliser le support natif d'IPFS par le navigateur Brave.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Configuration du nœud IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Configuration supplémentaire pour le nœud JS IPFS embarqué. Doit être un JSON valide.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Externe", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Intégré", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Embarqué dans Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/hu/messages.json b/add-on/_locales/hu/messages.json index 3baf3b6e8..442e962e6 100644 --- a/add-on/_locales/hu/messages.json +++ b/add-on/_locales/hu/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Toggle all IPFS integrations", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -207,15 +207,11 @@ "message": "Failed to stop IPFS node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Olvass tovább", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS csomópont", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -231,34 +227,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Embedded with Chrome Sockets: run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "IPFS csomópont beállítások", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Külső", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Beágyazott", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "Embedded + chrome.sockets", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "Átjárók", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/_locales/id/messages.json b/add-on/_locales/id/messages.json index 217310d47..2d449defa 100644 --- a/add-on/_locales/id/messages.json +++ b/add-on/_locales/id/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Alihkan semua integrasi IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Salin Jalur IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Salin Jalur IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Gunakan jalur konten ini dengan alat dan gerbang IPFS untuk mencapai versi terbaru dari konten tab ini.", @@ -239,19 +239,15 @@ "message": "Gagal Menghentikan Node IPFS", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferensi | Sahabat IPFS", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Preferensi Sahabat", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Baca lebih lanjut", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Node IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Setel ke \"Eksternal\" untuk terhubung ke node lokal menggunakan API HTTP.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Setel ke \"Tertanam\" untuk menjalankan node ipfs js-ipfs langsung di peramban Anda. (Klik \"Baca selengkapnya\" untuk mempelajari tentang batasan fitur eksperimental ini.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Setel ke \"Disediakan oleh Brave\" untuk memanfaatkan dukungan IPFS asli peramban Brave.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Konfigurasi Node IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Konfigurasi tambahan untuk node IPFS JS yang tertanam. Harus JSON yang valid.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Eksternal", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Tertancap", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Disediakan oleh Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/it/messages.json b/add-on/_locales/it/messages.json index 9c6a77e28..58f815735 100644 --- a/add-on/_locales/it/messages.json +++ b/add-on/_locales/it/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Attiva tutte le integrazioni IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copia percorso IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copia percorso IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Utilizza questo percorso contenuti con gli strumenti ed i gateway IPFS per raggiungere la versione più recente dei contenuti di questa tab.", @@ -239,19 +239,15 @@ "message": "Arresto del nodo IPFS fallito", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferenze | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Preferenze Companion", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Leggi di più", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Nodo IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Imposta su \"Esterno\" per connetterti ad un nodo locale utilizzando le API HTTP.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Imposta su \"Integrato\" per avviare un nodo js-ipfs direttamente nel tuo browser (Clicca \"Leggi di più\" per imparare le limitazioni di questa funzionalità sperimentale)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Imposta \"Offerto da Brave\" per sfruttare il supporto IPFS nativo di Brave browser.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Configurazione del Nodo IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Configurazione aggiuntiva per il nodo JS IPFS integrato. Deve essere JSON valido.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Esterno", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Integrato", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Offerto da Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/ja_JP/messages.json b/add-on/_locales/ja_JP/messages.json index e553dff98..17a1d3d9d 100644 --- a/add-on/_locales/ja_JP/messages.json +++ b/add-on/_locales/ja_JP/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "すべてのIPFS統合を切り替えます", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "オフライン", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "IPNSパスをコピー", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "IPNSパスをコピー", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "IPFSツールとゲートウェイでこのコンテンツパスを使用して、このタブのコンテンツの最新版をアクセスできます。", @@ -239,19 +239,15 @@ "message": "IPFSノードの停止に失敗しました", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "設定 | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companionの設定", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "続きを読む", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFSノード", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "HTTP APIを使用してローカルノードに接続する場合は、\"External\"に設定します。", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "ブラウザで直接 js-ipfs ノードを実行するには、\"Embedded\"に設定してください。(実験的な機能の制限について知るには\"続きを読む\"をクリック)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "BraveブラウザでのネイティブIPFS対応を活用するのに、「Braveによって提供された」に設定する。", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "IPFSノード構成", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "組み込みJS IPFSノードの追加構成。有効なJSONである必要があります。", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "外部", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "埋め込み", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Braveによって提供", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/ko_KR/messages.json b/add-on/_locales/ko_KR/messages.json index afa6c9887..4da87debf 100644 --- a/add-on/_locales/ko_KR/messages.json +++ b/add-on/_locales/ko_KR/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Toggle all IPFS integrations", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "오프라인", @@ -207,15 +207,11 @@ "message": "IPFS 노드 중단 실패", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "더 읽기", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS 노드", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -231,34 +227,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Embedded with Chrome Sockets: run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "IPFS 노드 설정", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "외부 노드", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "내장 노드", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "내장 노드 + chrome.sockets", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "게이트웨이", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/_locales/nl/messages.json b/add-on/_locales/nl/messages.json index 68391f704..9951aeadb 100644 --- a/add-on/_locales/nl/messages.json +++ b/add-on/_locales/nl/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Schakel alle IPFS integraties in", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Kopieer IPFS pad", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Kopieer IPFS pad", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", @@ -239,19 +239,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Lees meer", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS node", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "IPFS node configuratie", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Extra configuratie voor de ingebouwde JS IPFS node. Moet geldige JSON zijn.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Extern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Ingebouwd", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Geleverd door Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/no/messages.json b/add-on/_locales/no/messages.json index 591878be3..961948ae3 100644 --- a/add-on/_locales/no/messages.json +++ b/add-on/_locales/no/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Toggle alle IPFS integrasjoner", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "Offline", @@ -207,15 +207,11 @@ "message": "Kunne ikke stoppe IPFS node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Les mer", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS Node", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -231,34 +227,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Embedded with Chrome Sockets: run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "IPFS Node Config", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Ekstern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Embedded", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "Embedded + chrome.sockets", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "Gatewayer", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/_locales/pl/messages.json b/add-on/_locales/pl/messages.json index aaebaf7a2..27a1bf21b 100644 --- a/add-on/_locales/pl/messages.json +++ b/add-on/_locales/pl/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Włącz dostępne integracje IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "niedostępne", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Kopiuj ścieżkę IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Kopiuj ścieżkę IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Użyj tej ścieżki z narzędziami IPFS i bramami, aby dotrzeć do najnowszej wersji tej karty.", @@ -239,19 +239,15 @@ "message": "Nie udało się zatrzymać węzła IPFS", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferencje | Asystent IPFS", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Preferencje Asystenta", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Dowiedz się więcej", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Węzeł IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Ustaw na \"Zewnętrzny\", aby połączyć się z lokalnym węzłem za pomocą HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Ustaw na \"Wbudowany\" aby uruchomić węzeł js-ipfs bezpośrednio w przeglądarce. (Kliknij \"Czytaj więcej\", aby dowiedzieć się o ograniczeniach tej eksperymentalnej funkcji).", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Ustaw na \"Dostarczane przez Brave\", aby wykorzystać natywną obsługę IPFS w przeglądarce Brave.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Konfiguracja węzła IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Konfiguracja dla wbudowanego węzła JS IPFS. Wymagany poprawny JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Zdalny", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Wbudowany", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Dostarczone przez Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/pt/messages.json b/add-on/_locales/pt/messages.json index 583bdc351..684e3b90c 100644 --- a/add-on/_locales/pt/messages.json +++ b/add-on/_locales/pt/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Alternar todas integrações IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -231,19 +231,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Ler mais", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Nó IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -259,34 +255,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Embedded with Chrome Sockets: run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "Configuração do Nó IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Externo", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Embutido", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "Embutido + chrome.sockets", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "Gateways", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/_locales/pt_BR/messages.json b/add-on/_locales/pt_BR/messages.json index 810e0cd57..62c3896fd 100644 --- a/add-on/_locales/pt_BR/messages.json +++ b/add-on/_locales/pt_BR/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Desativar todas as integrações IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copiar a localização do IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copiar a localização do IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Usar esta localização de conteúdo nas ferramentas IPFS e gateways para alcançar a versão mais atualizada do conteúdo desta aba. ", @@ -239,19 +239,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Leia mais", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Nó IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Configuração do nó IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Externo", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Embutido", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provided by Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/ro/messages.json b/add-on/_locales/ro/messages.json index b5702e72d..5c2a5e760 100644 --- a/add-on/_locales/ro/messages.json +++ b/add-on/_locales/ro/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Pornește toate integrările IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copiază calea IPNS", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copiază calea IPNS", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", @@ -239,19 +239,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Preferințe Însoțitor", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Mai multe", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "Nod IPFS", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Configurarea Nodului IPFS", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Configurări suplimentare pentru nodul IPFS JS inclus. Trebuie să fie JSON valid.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Extern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Inclus", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provided by Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/ru/messages.json b/add-on/_locales/ru/messages.json index 70a737ef0..840489515 100644 --- a/add-on/_locales/ru/messages.json +++ b/add-on/_locales/ru/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Переключить все интеграции IPFS", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "офлайн", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copy IPNS Path", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copy IPNS Path", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", @@ -239,19 +239,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Узнать больше", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS узел", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Настройки IPFS узла", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Внешний", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Встроенный", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provided by Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/sv/messages.json b/add-on/_locales/sv/messages.json index 030596ec8..d7e38b125 100644 --- a/add-on/_locales/sv/messages.json +++ b/add-on/_locales/sv/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Toggle all IPFS integrations", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "offline", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "Copy IPNS Path", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "Copy IPNS Path", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", @@ -239,19 +239,15 @@ "message": "Failed to Stop IPFS Node", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Preferences | IPFS Companion", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Companion Preferences", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Läs mer", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IFPS-nod", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "Konfiguration av IPFS-nod", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Extern", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Inbäddad", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Provided by Brave", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/tr/messages.json b/add-on/_locales/tr/messages.json index 7681d54c0..90081a014 100644 --- a/add-on/_locales/tr/messages.json +++ b/add-on/_locales/tr/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Tüm IPFS entegrasyonlarını aç / kapat", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "çevrimdışı", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "IPNS Yolunu Kopyala", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "IPNS Yolunu Kopyala", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "Bu sekme içeriğinin en son güncellenen sürümüne ulaşmak için bu içerik yolunu IPFS araçları ve ağ geçitleriyle kullanın.", @@ -239,19 +239,15 @@ "message": "IPFS Düğümü Durdurulamadı", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "Tercihler | IPFS Refakatçisi", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "Refakatçi Ayarları", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "Daha fazla oku", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS Düğümü", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "HTTP UPA kullanarak yerel bir düğüme bağlanmak için \"Harici\" olarak ayarlayın.", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Tarayıcınızda doğrudan bir js-ipfs düğümünü çalıştırmak için \"Gömülü\" olarak ayarlayın. (Bu deneysel özelliğin sınırlamaları hakkında bilgi edinmek için \"Daha fazlasını okuyun\" 'u tıklayın.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "Brave tarayıcının yerel IPFS desteğinden yararlanmak için \"Brave tarafından sağlandı\" olarak ayarlayın.", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "IPFS Düğüm Yapılandırması", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Gömülü JS IPFS düğümü için ek yapılandırma. Geçerli JSON olmalıdır.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "Harici", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Gömülü", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Brave tarafından sağlandı", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/zh_CN/messages.json b/add-on/_locales/zh_CN/messages.json index 8070bec82..3bd4fbb83 100644 --- a/add-on/_locales/zh_CN/messages.json +++ b/add-on/_locales/zh_CN/messages.json @@ -13,7 +13,7 @@ }, "panel_headerActiveToggleTitle": { "message": "切换所有 IPFS 集成", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "离线", @@ -92,8 +92,8 @@ "description": "A menu item tooltip in Browser Action pop-up (panel_importCurrentIpfsAddressTooltip)" }, "panelCopy_currentIpnsAddress": { - "message": "复制 IPFS 路径", - "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + "message": "复制 IPFS 路径", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" }, "panelCopy_currentIpnsAddressTooltip": { "message": "将此内容路径与IPFS工具和网关一起使用,以访问此选项卡中内容的最近更新版本。", @@ -239,19 +239,15 @@ "message": "停止IPFS 节点失败", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_page_title" : { + "option_page_title": { "message": "首选项 | IPFS 伴侣", "description": "Title of the Preferences page (option_page_title)" }, - "option_page_header" : { + "option_page_header": { "message": "伴侣首选项", "description": "Main header on the Preferences screen (option_page_header)" }, - "option_legend_readMore" : { - "message": "阅读更多", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS 节点", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -267,10 +263,6 @@ "message": "设置为“外部”来通过 HTTP API 连接本地节点", "description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "设置为“嵌入式的”以在你的浏览器里直接运行 js-ipfs 节点(点击“了解更多”来了解该实验性的特性的局限性)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)" - }, "option_ipfsNodeType_brave_description": { "message": "设置为“Brave支持”以利用Brave浏览器的原生支持IPFS的特性。", "description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)" @@ -279,18 +271,10 @@ "message": "IPFS 节点配置", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "嵌入式JS IPFS节点的额外配置。必须是有效的JSON。", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "外部", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "嵌入", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, "option_ipfsNodeType_brave": { "message": "Brave支持", "description": "An option on the Preferences screen (option_ipfsNodeType_brave)" diff --git a/add-on/_locales/zh_TW/messages.json b/add-on/_locales/zh_TW/messages.json index d1803b9e7..f81427eba 100644 --- a/add-on/_locales/zh_TW/messages.json +++ b/add-on/_locales/zh_TW/messages.json @@ -9,7 +9,7 @@ }, "panel_headerActiveToggleTitle": { "message": "Toggle all IPFS integrations", - "description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)" + "description": "A label for an toggling all IPFS integrations (panel_headerActiveToggleTitle)" }, "panel_statusOffline": { "message": "離線", @@ -207,11 +207,7 @@ "message": "停止 IPFS 節點失敗", "description": "System notification title displayed when stopping an IPFS node fails (notify_stopIpfsNodeErrorTitle)" }, - "option_legend_readMore" : { - "message": "了解更多", - "description": "A generic link in option description on the Preferences screen (option_legend_readMore)" - }, - "option_header_nodeType" : { + "option_header_nodeType": { "message": "IPFS 節點", "description": "A section header on the Preferences screen (option_header_nodeType)" }, @@ -223,34 +219,14 @@ "message": "Set to \"External\" to connect to a local node using the HTTP API.", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, - "option_ipfsNodeType_embedded_description": { - "message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, - "option_ipfsNodeType_embedded_chromesockets_description": { - "message": "Embedded with Chrome Sockets: run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", - "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" - }, "option_ipfsNodeConfig_title": { "message": "IPFS 節點設定", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, - "option_ipfsNodeConfig_description": { - "message": "Additional configuration for the embedded JS IPFS node. Must be valid JSON.", - "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" - }, "option_ipfsNodeType_external": { "message": "External", "description": "An option on the Preferences screen (option_ipfsNodeType_external)" }, - "option_ipfsNodeType_embedded": { - "message": "Embedded", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" - }, - "option_ipfsNodeType_embedded_chromesockets": { - "message": "Embedded + chrome.sockets", - "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" - }, "option_header_gateways": { "message": "閘道器群", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/icons/js-ipfs-logo-off.svg b/add-on/icons/js-ipfs-logo-off.svg deleted file mode 100644 index 15fa51653..000000000 --- a/add-on/icons/js-ipfs-logo-off.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - js-ipfs-logo-off - - - - - - - - - - - - - diff --git a/add-on/icons/js-ipfs-logo-on.svg b/add-on/icons/js-ipfs-logo-on.svg deleted file mode 100644 index debe2a3a8..000000000 --- a/add-on/icons/js-ipfs-logo-on.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - js-ipfs-logo-on - - - - - - - - - - - - - diff --git a/add-on/manifest.chromium.json b/add-on/manifest.chromium.json index 9949e6e1d..a7a9833cb 100644 --- a/add-on/manifest.chromium.json +++ b/add-on/manifest.chromium.json @@ -1,17 +1,22 @@ { - "minimum_chrome_version": "72", + "minimum_chrome_version": "111", + "background": { + "service_worker": "dist/bundles/backgroundPage.bundle.js" + }, "permissions": [ - "", + "activeTab", + "clipboardWrite", + "contextMenus", + "declarativeNetRequest", + "declarativeNetRequestFeedback", "idle", - "tabs", "notifications", + "scripting", "storage", + "tabs", "unlimitedStorage", - "contextMenus", - "clipboardWrite", "webNavigation", - "webRequest", - "webRequestBlocking" + "webRequest" ], "incognito": "not_allowed" } diff --git a/add-on/manifest.common.json b/add-on/manifest.common.json index 2b3a331b8..13e199837 100644 --- a/add-on/manifest.common.json +++ b/add-on/manifest.common.json @@ -1,8 +1,8 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "__MSG_manifest_extensionName__", "short_name": "__MSG_manifest_shortExtensionName__", - "version": "2.22.1", + "version": "3.0.0", "description": "__MSG_manifest_extensionDescription__", "homepage_url": "https://github.com/ipfs-shipyard/ipfs-companion", "author": "IPFS Community", @@ -11,10 +11,7 @@ "38": "icons/png/ipfs-logo-on_38.png", "128": "icons/png/ipfs-logo-on_128.png" }, - "background": { - "page": "dist/background/background.html" - }, - "browser_action": { + "action": { "default_icon": { "19": "icons/png/ipfs-logo-off_19.png", "38": "icons/png/ipfs-logo-off_38.png", @@ -28,16 +25,24 @@ "browser_style": false, "page": "dist/options/options.html" }, + "host_permissions": [""], "web_accessible_resources": [ - "icons/png/ipfs-logo-off_19.png", - "icons/png/ipfs-logo-off_38.png", - "icons/png/ipfs-logo-off_128.png", - "icons/ipfs-logo-on.svg", - "icons/ipfs-logo-off.svg", - "dist/recovery/recovery.css", - "dist/recovery/recovery.html", - "dist/recovery/recovery.js" + { + "resources": [ + "icons/png/ipfs-logo-off_19.png", + "icons/png/ipfs-logo-off_38.png", + "icons/png/ipfs-logo-off_128.png", + "icons/ipfs-logo-on.svg", + "icons/ipfs-logo-off.svg", + "dist/recovery/recovery.css", + "dist/recovery/recovery.html", + "dist/recovery/recovery.js" + ], + "matches": [""] + } ], - "content_security_policy": "script-src 'self'; object-src 'self'; frame-src 'self';", + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'; frame-src 'self';" + }, "default_locale": "en" } diff --git a/add-on/manifest.firefox-beta.json b/add-on/manifest.firefox-beta.json index 01d799d86..ae6f3d3c1 100644 --- a/add-on/manifest.firefox-beta.json +++ b/add-on/manifest.firefox-beta.json @@ -1,8 +1,8 @@ { - "applications": { + "browser_specific_settings": { "gecko": { "id": "ipfs-companion-dev-build@ci.ipfs.team", - "update_url": "https://ipfs-shipyard.github.io/ipfs-companion/ci/firefox/update.json" + "strict_min_version": "111.0.0" } } } diff --git a/add-on/manifest.firefox.json b/add-on/manifest.firefox.json index 5e0b44a01..0993471b7 100644 --- a/add-on/manifest.firefox.json +++ b/add-on/manifest.firefox.json @@ -1,18 +1,20 @@ { - "browser_action": { + "action": { "browser_style": false }, "options_ui": { "browser_style": false }, - "applications": { + "background": { + "scripts": ["dist/bundles/backgroundPage.firefox.bundle.js"] + }, + "browser_specific_settings": { "gecko": { "id": "ipfs-firefox-addon@lidel.org", - "strict_min_version": "91.1.0" + "strict_min_version": "111.0.0" } }, "permissions": [ - "", "idle", "tabs", "notifications", diff --git a/add-on/src/background/background.html b/add-on/src/background/background.html deleted file mode 100644 index e3ea3c511..000000000 --- a/add-on/src/background/background.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/add-on/src/background/background.js b/add-on/src/background/background.js index 7b9fddfab..54d4f842e 100644 --- a/add-on/src/background/background.js +++ b/add-on/src/background/background.js @@ -2,25 +2,16 @@ /* eslint-env browser, webextensions */ import browser from 'webextension-polyfill' +import createIpfsCompanion from '../lib/ipfs-companion.js' import { onInstalled } from '../lib/on-installed.js' import { getUninstallURL } from '../lib/on-uninstalled.js' -import { optionDefaults } from '../lib/options.js' -import createIpfsCompanion from '../lib/ipfs-companion.js' // register lifecycle hooks early, otherwise we miss first install event browser.runtime.onInstalled.addListener(onInstalled) browser.runtime.setUninstallURL(getUninstallURL(browser)) -// init add-on after all libs are loaded -document.addEventListener('DOMContentLoaded', async () => { - browser.runtime.sendMessage({ telemetry: { trackView: 'background' } }) - // setting debug namespaces require page reload to get applied - const debugNs = (await browser.storage.local.get({ logNamespaces: optionDefaults.logNamespaces })).logNamespaces - if (debugNs !== localStorage.debug) { - localStorage.debug = debugNs - window.location.reload() - } - // init inlined to read updated localStorage.debug - // @ts-expect-error - TS does not know about window.ipfsCompanion - window.ipfsCompanion = await createIpfsCompanion() -}) +const init = async () => { + await createIpfsCompanion() +} + +init() diff --git a/add-on/src/landing-pages/permissions/request.css b/add-on/src/landing-pages/permissions/request.css new file mode 100644 index 000000000..1a6fdc282 --- /dev/null +++ b/add-on/src/landing-pages/permissions/request.css @@ -0,0 +1,54 @@ +@import url('~tachyons/css/tachyons.css'); +@import url('~ipfs-css/ipfs.css'); + +#left-col { + background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); + background-size: 100%; + background-repeat: repeat; +} + +a:hover { + text-decoration: none; +} + +a:visited { + color: inherit; +} + +/* + https://github.com/tachyons-css/tachyons-queries + Tachyons: $point == large +*/ +@media (min-width: 60em) { + #left-col { + position: fixed; + top: 0; + right: 55%; + width: 45%; + background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); + background-size: 100%; + background-repeat: repeat; + } + + #right-col { + margin-left: 54%; + margin-right: 6%; + } +} + +@media (max-height: 800px) { + #left-col img { + width: 98px !important; + height: 98px !important; + } + + #left-col svg { + width: 60px; + } +} + +.recovery-root { + width: 100%; + height: 100%; + text-align: left; +} diff --git a/add-on/src/landing-pages/permissions/request.html b/add-on/src/landing-pages/permissions/request.html new file mode 100644 index 000000000..965d9e84e --- /dev/null +++ b/add-on/src/landing-pages/permissions/request.html @@ -0,0 +1,20 @@ + + + + IPFS Node is Offline + + + + + + + + +
+
+
+ + +
+ + diff --git a/add-on/src/landing-pages/permissions/request.js b/add-on/src/landing-pages/permissions/request.js new file mode 100644 index 000000000..242a2e920 --- /dev/null +++ b/add-on/src/landing-pages/permissions/request.js @@ -0,0 +1,56 @@ +'use strict' +/* eslint-env browser, webextensions */ + +import choo from 'choo' +import html from 'choo/html/index.js' +import { i18n, runtime, permissions } from 'webextension-polyfill' +import { nodeOffSvg } from '../welcome/page.js' +import createWelcomePageStore from '../welcome/store.js' +import { optionsPage } from '../../lib/constants.js' +import './request.css' + +const app = choo() + +const learnMoreLink = html`${i18n.getMessage('request_permissions_page_learn_more')}` + +const optionsPageLink = html`${i18n.getMessage('recovery_page_update_preferences')}` + +// TODO (whizzzkid): refactor base store to be more generic. +app.use(createWelcomePageStore(i18n, runtime)) +// Register our single route +app.route('*', () => { + runtime.sendMessage({ telemetry: { trackView: 'request-permissions' } }) + const requestPermission = async () => { + await permissions.request({ origins: [''] }) + runtime.reload() + } + + return html`
+
+
+ ${nodeOffSvg(200)} +

${i18n.getMessage('request_permissions_page_sub_header')}

+
+
+ +
+

${i18n.getMessage('request_permissions_page_message_p1')}

+

${i18n.getMessage('request_permissions_page_message_p2')}

+ +

+ ${learnMoreLink} | ${optionsPageLink} + +

+
` +}) + +// Start the application and render it to the given querySelector +app.mount('#root') + +// Set page title and header translation +document.title = i18n.getMessage('request_permissions_page_title') diff --git a/add-on/src/lib/constants.js b/add-on/src/lib/constants.js index ef2f7c61a..179f80943 100644 --- a/add-on/src/lib/constants.js +++ b/add-on/src/lib/constants.js @@ -4,4 +4,5 @@ export const welcomePage = '/dist/landing-pages/welcome/index.html' export const optionsPage = '/dist/options/options.html' export const recoveryPagePath = '/dist/recovery/recovery.html' +export const requestRequiredPermissionsPage = '/dist/landing-pages/permissions/request.html' export const tickMs = 250 // no CPU spike, but still responsive enough diff --git a/add-on/src/lib/context-menus.js b/add-on/src/lib/context-menus.js index b651372fa..cb2167ad4 100644 --- a/add-on/src/lib/context-menus.js +++ b/add-on/src/lib/context-menus.js @@ -3,6 +3,7 @@ import browser from 'webextension-polyfill' import debug from 'debug' +import { ContextMenus } from './context-menus/ContextMenus.js' const log = debug('ipfs-companion:context-menus') log.error = debug('ipfs-companion:context-menus:error') @@ -66,35 +67,32 @@ const apiMenuItemIds = new Set([contextMenuCopyRawCid, contextMenuCopyCanonicalA const apiMenuItems = new Set() // menu items enabled only in IPFS context (dynamic) const ipfsContextItems = new Set() +// listeners for context menu items +const contextMenus = new ContextMenus() export function createContextMenus ( getState, _runtime, ipfsPathValidator, { onAddFromContext, onCopyRawCid, onCopyAddressAtPublicGw }) { try { - const createSubmenu = (id, contextType, menuBuilder) => { - browser.contextMenus.create({ - id, - title: browser.i18n.getMessage(id), - documentUrlPatterns: [''], - contexts: [contextType] - }) - } + const createSubmenu = (id, contextType) => contextMenus.create({ + id, + title: browser.i18n.getMessage(id), + documentUrlPatterns: [''], + contexts: [contextType] + }) + const createImportToIpfsMenuItem = (parentId, id, contextType, ipfsAddOptions) => { const itemId = `${parentId}_${id}` apiMenuItems.add(itemId) - return browser.contextMenus.create({ + contextMenus.create({ id: itemId, parentId, title: browser.i18n.getMessage(id), contexts: [contextType], documentUrlPatterns: [''], - enabled: false, - /* no support for 'icons' in Chrome - icons: { - '48': '/ui-kit/icons/stroke_cube.svg' - }, */ - onclick: (context) => onAddFromContext(context, contextType, ipfsAddOptions) - }) + enabled: false + }, (context) => onAddFromContext(context, contextType, ipfsAddOptions)) } + const createCopierMenuItem = (parentId, id, contextType, handler) => { const itemId = `${parentId}_${id}` ipfsContextItems.add(itemId) @@ -102,7 +100,7 @@ export function createContextMenus ( if (apiMenuItemIds.has(id)) { apiMenuItems.add(itemId) } - return browser.contextMenus.create({ + contextMenus.create({ id: itemId, parentId, title: browser.i18n.getMessage(id), @@ -111,14 +109,10 @@ export function createContextMenus ( '*://*/ipfs/*', '*://*/ipns/*', '*://*.ipfs.dweb.link/*', '*://*.ipns.dweb.link/*', // TODO: add any custom public gateway from Preferences '*://*.ipfs.localhost/*', '*://*.ipns.localhost/*' - ], - /* no support for 'icons' in Chrome - icons: { - '48': '/ui-kit/icons/stroke_copy.svg' - }, */ - onclick: (context) => handler(context, contextType) - }) + ] + }, (context) => handler(context, contextType)) } + const buildSubmenu = (parentId, contextType) => { createSubmenu(parentId, contextType) createImportToIpfsMenuItem(parentId, contextMenuImportToIpfs, contextType, { wrapWithDirectory: true, pin: false }) diff --git a/add-on/src/lib/context-menus/ContextMenus.ts b/add-on/src/lib/context-menus/ContextMenus.ts new file mode 100644 index 000000000..cd4392775 --- /dev/null +++ b/add-on/src/lib/context-menus/ContextMenus.ts @@ -0,0 +1,68 @@ +import browser from 'webextension-polyfill' +import debug from 'debug' + +type listenerCb = (info: browser.Menus.OnClickData, tab: browser.Tabs.Tab | undefined) => void + +/** + * ContextMenus is a wrapper around browser.contextMenus API. + */ +export class ContextMenus { + private readonly contextMenuListeners = new Map() + private readonly log: debug.Debugger & { error?: debug.Debugger } + + constructor () { + this.log = debug('ipfs-companion:contextMenus') + this.log.error = debug('ipfs-companion:contextMenus:error') + this.contextMenuListeners = new Map() + this.init() + } + + /** + * init is called once on extension startup + */ + init (): void { + browser.contextMenus.onClicked.addListener((info, tab) => { + const { menuItemId } = info + if (this.contextMenuListeners.has(menuItemId)) { + this.contextMenuListeners.get(menuItemId)?.forEach(cb => cb(info, tab)) + } + }) + this.log('ContextMenus Listeners ready') + } + + /** + * This method queues the listener function for given menuItemId. + * + * @param menuItemId + * @param cb + */ + queueListener (menuItemId: string, cb: listenerCb): void { + if (this.contextMenuListeners.has(menuItemId)) { + this.contextMenuListeners.get(menuItemId)?.push(cb) + } else { + this.contextMenuListeners.set(menuItemId, [cb]) + } + this.log(`ContextMenus Listener queued for ${menuItemId}`) + } + + /** + * This method creates a context menu item and maps the listener function to it. + * + * @param options + * @param cb + */ + create (options: browser.Menus.CreateCreatePropertiesType, cb?: listenerCb): void { + try { + browser.contextMenus.create(options) + } catch (err) { + this.log.error?.('ContextMenus.create failed', err) + } + if (cb != null) { + if (options?.id != null) { + this.queueListener(options.id, cb) + } else { + throw new Error('ContextMenus.create callback requires options.id') + } + } + } +} diff --git a/add-on/src/lib/copier.js b/add-on/src/lib/copier.js index 7298d5695..4cfc3a277 100644 --- a/add-on/src/lib/copier.js +++ b/add-on/src/lib/copier.js @@ -1,28 +1,75 @@ 'use strict' +import browser from 'webextension-polyfill' import { findValueForContext } from './context-menus.js' +/** + * Writes text to the clipboard. + * + * @param {string} text + */ +async function writeToClipboard (text) { + try { + await navigator.clipboard.writeText(text) + return true + } catch (error) { + // This can happen if the user denies clipboard permissions. + // or the current page is not allowed to access the clipboard. + // no need to log this error, as it is expected in some cases. + return false + } +} + +/** + * Gets the current active tab. + * + * @returns {Promise} + */ +async function getCurrentTab () { + const queryOptions = { active: true, lastFocusedWindow: true } + // `tab` will either be a `tabs.Tab` instance or `undefined`. + const [tab] = await browser.tabs.query(queryOptions) + return tab +} + +/** + * This is the MV3 version of copyTextToClipboard. It uses executeScript to run a function + * in the context of the current tab. This is necessary because the clipboard API is not + * available in the background script. + * + * Manifest Perms: "scripting", "activeTab" + * + * See: + * - https://developer.chrome.com/docs/extensions/reference/scripting/ + * - https://developer.chrome.com/blog/Offscreen-Documents-in-Manifest-v3/ + * + * ServiceWorkers will most likely have access to the clipboard in the future. + * + * @param {string} text + */ +async function copyTextToClipboardFromCurrentTab (text) { + const tab = await getCurrentTab() + if (!tab) { + throw new Error('Unable to get current tab') + } + + const [{ result }] = await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: writeToClipboard, + args: [text] + }) + + if (!result) { + throw new Error('Unable to write to clipboard') + } +} + async function copyTextToClipboard (text, notify) { try { - try { - // Modern API (spotty support, but works in Firefox) - await navigator.clipboard.writeText(text) - // FUN FACT: - // Before this API existed we had no access to cliboard from - // the background page in Firefox and had to inject content script - // into current page to copy there: - // https://github.com/ipfs-shipyard/ipfs-companion/blob/b4a168880df95718e15e57dace6d5006d58e7f30/add-on/src/lib/copier.js#L10-L35 - // :-)) - } catch (e) { - // Fallback to old API (works only in Chromium) - function oncopy (event) { // eslint-disable-line no-inner-declarations - document.removeEventListener('copy', oncopy, true) - event.stopImmediatePropagation() - event.preventDefault() - event.clipboardData.setData('text/plain', text) - } - document.addEventListener('copy', oncopy, true) - document.execCommand('copy') + if (typeof navigator.clipboard !== 'undefined') { // Firefox + await writeToClipboard(text) + } else { + await copyTextToClipboardFromCurrentTab(text) } notify('notify_copiedTitle', text) } catch (error) { @@ -72,7 +119,7 @@ export default function createCopier (notify, ipfsPathValidator) { async copyAddressAtPublicGw (context, contextType) { const url = await findValueForContext(context, contextType) - const publicUrl = ipfsPathValidator.resolveToPublicUrl(url) + const publicUrl = await ipfsPathValidator.resolveToPublicUrl(url) await copyTextToClipboard(publicUrl, notify) }, diff --git a/add-on/src/lib/dnslink.js b/add-on/src/lib/dnslink.js index b648ed2cf..fbda761af 100644 --- a/add-on/src/lib/dnslink.js +++ b/add-on/src/lib/dnslink.js @@ -50,11 +50,11 @@ export default function createDnslinkResolver (getState) { !sameGateway(requestUrl, state.gwURL) }, - dnslinkAtGateway (url, dnslink) { + async dnslinkAtGateway (url, dnslink) { if (typeof url === 'string') { url = new URL(url) } - if (dnslinkResolver.canRedirectToIpns(url, dnslink)) { + if (await dnslinkResolver.canRedirectToIpns(url, dnslink)) { const state = getState() // redirect to IPNS and leave it up to the gateway // to load the correct path from IPFS @@ -65,12 +65,12 @@ export default function createDnslinkResolver (getState) { } }, - readAndCacheDnslink (fqdn) { + async readAndCacheDnslink (fqdn) { let dnslink = dnslinkResolver.cachedDnslink(fqdn) if (typeof dnslink === 'undefined') { try { log(`dnslink cache miss for '${fqdn}', running DNS TXT lookup`) - dnslink = dnslinkResolver.readDnslinkFromTxtRecord(fqdn) + dnslink = await dnslinkResolver.readDnslinkFromTxtRecord(fqdn) if (dnslink) { // TODO: set TTL as maxAge: setDnslink(fqdn, dnslink, maxAge) dnslinkResolver.setDnslink(fqdn, dnslink) @@ -96,6 +96,7 @@ export default function createDnslinkResolver (getState) { const cachedResult = dnslinkResolver.cachedDnslink(fqdn) if (cachedResult) return cachedResult return lookupQueue.add(() => { + // this will resolve eventually. return dnslinkResolver.readAndCacheDnslink(fqdn) }) }, @@ -120,10 +121,10 @@ export default function createDnslinkResolver (getState) { }, // low level lookup without cache - readDnslinkFromTxtRecord (fqdn) { + async readDnslinkFromTxtRecord (fqdn) { const state = getState() let apiProvider - if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) { + if (state.peerCount !== offlinePeerCount) { // Use gw port so it can be a GET: // Chromium does not execute onBeforeSendHeaders for synchronous calls // made from the same extension context as onBeforeSendHeaders @@ -139,29 +140,29 @@ export default function createDnslinkResolver (getState) { // TODO: revisit after https://github.com/ipfs/js-ipfs-api/issues/501 is addressed // TODO: consider worst-case-scenario fallback to https://developers.google.com/speed/public-dns/docs/dns-over-https const apiCall = `${apiProvider}api/v0/name/resolve/${fqdn}?r=false` - const xhr = new XMLHttpRequest() // older XHR API us used because window.fetch appends Origin which causes error 403 in go-ipfs - // synchronous mode with small timeout - // (it is okay, because we do it only once, then it is cached and read via readAndCacheDnslink) - xhr.open('GET', apiCall, false) - xhr.setRequestHeader('Accept', 'application/json') - xhr.send(null) - if (xhr.status === 200) { - const dnslink = JSON.parse(xhr.responseText).Path - // console.log('readDnslinkFromTxtRecord', readDnslinkFromTxtRecord) + const response = await fetch(apiCall, { + method: 'GET', + headers: { + Accept: 'application/json' + } + }) + + if (response.ok) { + const { Path: dnslink } = await response.json() if (!IsIpfs.path(dnslink)) { throw new Error(`dnslink for '${fqdn}' is not a valid IPFS path: '${dnslink}'`) } return dnslink - } else if (xhr.status === 500) { + } else if (response.status === 500) { // go-ipfs returns 500 if host has no dnslink or an error occurred // TODO: find/fill an upstream bug to make this more intuitive return false } else { - throw new Error(xhr.statusText) + throw new Error(response.statusText) } }, - canRedirectToIpns (url, dnslink) { + async canRedirectToIpns (url, dnslink) { if (typeof url === 'string') { url = new URL(url) } @@ -185,7 +186,7 @@ export default function createDnslinkResolver (getState) { // is found in initial response. // More: https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/dnslink.md const foundDnslink = dnslink || - (getState().dnslinkPolicy === 'enabled' + await (getState().dnslinkPolicy === 'enabled' ? dnslinkResolver.readAndCacheDnslink(fqdn) : dnslinkResolver.cachedDnslink(fqdn)) if (foundDnslink) { @@ -205,7 +206,7 @@ export default function createDnslinkResolver (getState) { // Test if URL contains a valid DNSLink FQDN // in url.hostname OR in url.pathname (/ipns/) // and return matching FQDN if present - findDNSLinkHostname (url) { + async findDNSLinkHostname (url) { if (!url) return // Normalize subdomain and path gateways to to /ipns/ const contentPath = ipfsContentPath(url) @@ -214,14 +215,14 @@ export default function createDnslinkResolver (getState) { const ipnsRoot = contentPath.match(/^\/ipns\/([^/]+)/)[1] // console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot) // Ignore PeerIDs, match DNSLink only - if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) { + if (!IsIpfs.cid(ipnsRoot) && await dnslinkResolver.readAndCacheDnslink(ipnsRoot)) { // console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot) return ipnsRoot } } // Check main hostname const { hostname } = new URL(url) - if (dnslinkResolver.readAndCacheDnslink(hostname)) { + if (await dnslinkResolver.readAndCacheDnslink(hostname)) { // console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname) return hostname } diff --git a/add-on/src/lib/ipfs-client/embedded.js b/add-on/src/lib/ipfs-client/embedded.js deleted file mode 100644 index b6ab9ba76..000000000 --- a/add-on/src/lib/ipfs-client/embedded.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -import debug from 'debug' - -import mergeOptions from 'merge-options' -import { create } from 'ipfs-core' -import { optionDefaults } from '../options.js' -const log = debug('ipfs-companion:client:embedded') -log.error = debug('ipfs-companion:client:embedded:error') - -let node = null - -export async function init (browser, opts) { - log('init') - const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig) - const userOpts = JSON.parse(opts.ipfsNodeConfig) - const ipfsOpts = mergeOptions(defaultOpts, userOpts, { start: true }) - const missing = (array) => (!Array.isArray(array) || !array.length) - const { Addresses } = ipfsOpts.config - if (missing(Addresses.Swarm)) { - Addresses.Swarm = [ - '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star', - '/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star' - ] - } - if (missing(ipfsOpts.Delegates)) { - Addresses.Delegates = [ - '/dns4/node0.delegate.ipfs.io/tcp/443/https', - '/dns4/node1.delegate.ipfs.io/tcp/443/https', - '/dns4/node2.delegate.ipfs.io/tcp/443/https', - '/dns4/node3.delegate.ipfs.io/tcp/443/https' - ] - } - node = await create(ipfsOpts) - return node -} - -export async function destroy (browser) { - log('destroy') - if (!node) return - - await node.stop() - node = null -} diff --git a/add-on/src/lib/ipfs-client/index.js b/add-on/src/lib/ipfs-client/index.js index e48c20033..ab4a5dbc4 100644 --- a/add-on/src/lib/ipfs-client/index.js +++ b/add-on/src/lib/ipfs-client/index.js @@ -4,12 +4,14 @@ import debug from 'debug' -import * as external from './external.js' -import * as embedded from './embedded.js' -import * as brave from './brave.js' import { precache } from '../precache.js' +import * as brave from './brave.js' +import * as external from './external.js' import { - prepareReloadExtensions, WebUiReloader, LocalGatewayReloader, InternalTabReloader + InternalTabReloader, + LocalGatewayReloader, + WebUiReloader, + prepareReloadExtensions } from './reloaders/index.js' const log = debug('ipfs-companion:client') log.error = debug('ipfs-companion:client:error') @@ -17,27 +19,11 @@ log.error = debug('ipfs-companion:client:error') // ensure single client at all times, and no overlap between init and destroy let client -export async function initIpfsClient (browser, opts) { +export async function initIpfsClient (browser, opts, inQuickImport) { log('init ipfs client') if (client) return // await destroyIpfsClient() let backend switch (opts.ipfsNodeType) { - case 'embedded:chromesockets': - // TODO: remove this one-time migration after in second half of 2021 - setTimeout(async () => { - log('converting embedded:chromesockets to native external:brave') - opts.ipfsNodeType = 'external:brave' - await browser.storage.local.set({ - ipfsNodeType: 'external:brave', - ipfsNodeConfig: '{}' // remove chrome-apps config - }) - await browser.tabs.create({ url: 'https://docs.ipfs.tech/how-to/companion-node-types/#native' }) - }, 0) - // Halt client init - throw new Error('Embedded + chrome.sockets is deprecated. Switching to Native IPFS in Brave.') - case 'embedded': - backend = embedded - break case 'external:brave': backend = brave break @@ -48,7 +34,9 @@ export async function initIpfsClient (browser, opts) { throw new Error(`Unsupported ipfsNodeType: ${opts.ipfsNodeType}`) } const instance = await backend.init(browser, opts) - _reloadIpfsClientDependents(browser, instance, opts) // async (API is present) + if (!inQuickImport) { + _reloadIpfsClientDependents(browser, instance, opts) // async (API is present) + } client = backend return instance } @@ -67,7 +55,7 @@ export async function destroyIpfsClient (browser) { /** * Reloads pages dependant on ipfs to be online * - * @typedef {embedded|brave|external} Browser + * @typedef {brave|external} Browser * @param {Browser} browser * @param {import('kubo-rpc-client').default} instance * @param {Object} opts @@ -100,7 +88,7 @@ async function _reloadIpfsClientDependents ( /** * Reloads local gateway pages dependant on ipfs to be online * - * @typedef {embedded|brave|external} Browser + * @typedef {brave|external} Browser * @param {Browser} browser * @param {import('kubo-rpc-client').default} instance * @param {Object} opts diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index b055f599e..480975b02 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -3,38 +3,42 @@ import debug from 'debug' -import browser from 'webextension-polyfill' -import toMultiaddr from 'uri-to-multiaddr' -import pMemoize from 'p-memoize' -import LRU from 'lru-cache' import all from 'it-all' -import { optionDefaults, storeMissingOptions, migrateOptions, guiURLString, safeURL } from './options.js' -import { initState, offlinePeerCount } from './state.js' -import { createIpfsPathValidator, dropSlash, sameGateway, safeHostname } from './ipfs-path.js' +import LRU from 'lru-cache' +import pMemoize from 'p-memoize' +import toMultiaddr from 'uri-to-multiaddr' +import browser from 'webextension-polyfill' +import { handleConsentFromState, trackView } from '../lib/telemetry.js' +import { contextMenuCopyAddressAtPublicGw, contextMenuCopyCanonicalAddress, contextMenuCopyCidAddress, contextMenuCopyPermalink, contextMenuCopyRawCid, contextMenuViewOnGateway, createContextMenus, findValueForContext } from './context-menus.js' +import createCopier from './copier.js' import createDnslinkResolver from './dnslink.js' +import { registerSubdomainProxy } from './http-proxy.js' +import createInspector from './inspector.js' +import { braveNodeType, releaseBraveEndpoint, useBraveEndpoint } from './ipfs-client/brave.js' +import { destroyIpfsClient, initIpfsClient, reloadIpfsClientOfflinePages } from './ipfs-client/index.js' +import { browserActionFilesCpImportCurrentTab, createIpfsImportHandler, formatImportDirectory } from './ipfs-import.js' +import { createIpfsPathValidator, dropSlash, safeHostname, sameGateway } from './ipfs-path.js' import { createRequestModifier } from './ipfs-request.js' -import { initIpfsClient, destroyIpfsClient, reloadIpfsClientOfflinePages } from './ipfs-client/index.js' -import { braveNodeType, useBraveEndpoint, releaseBraveEndpoint } from './ipfs-client/brave.js' -import { createIpfsImportHandler, formatImportDirectory, browserActionFilesCpImportCurrentTab } from './ipfs-import.js' import createNotifier from './notifier.js' -import createCopier from './copier.js' -import createInspector from './inspector.js' -import createRuntimeChecks from './runtime-checks.js' -import { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway, contextMenuCopyPermalink, contextMenuCopyCidAddress } from './context-menus.js' -import { registerSubdomainProxy } from './http-proxy.js' import { runPendingOnInstallTasks } from './on-installed.js' -import { handleConsentFromState, startSession, endSession, trackView } from './telemetry.js' +import { guiURLString, migrateOptions, optionDefaults, safeURL, storeMissingOptions } from './options.js' +import { cleanupRules, getExtraInfoSpec } from './redirect-handler/blockOrObserve.js' +import createRuntimeChecks from './runtime-checks.js' +import { initState, offlinePeerCount } from './state.js' + +// this won't work in webworker context. Needs to be enabled manually +// https://github.com/debug-js/debug/issues/916 const log = debug('ipfs-companion:main') log.error = debug('ipfs-companion:main:error') let browserActionPort // reuse instance for status updates between on/off toggles // init happens on addon load in background/background.js -export default async function init () { +export default async function init (inQuickImport = false) { // INIT // =================================================================== let ipfs // ipfs-api instance - /** @type {import('../types.js').CompanionState} */ + /** @type {import('../types/companion.js').CompanionState} */ let state // avoid redundant API reads by utilizing local cache of various states let dnslinkResolver let ipfsPathValidator @@ -55,20 +59,21 @@ export default async function init () { await migrateOptions(browser.storage.local, debug) const options = await browser.storage.local.get(optionDefaults) runtime = await createRuntimeChecks(browser) + state = initState(options) notify = createNotifier(getState) - // ensure consent is set properly on app init - handleConsentFromState(state) if (state.active) { - startSession() // It's ok for this to fail, node might be unavailable or mis-configured try { - ipfs = await initIpfsClient(browser, state) + await handleConsentFromState(state) + ipfs = await initIpfsClient(browser, state, inQuickImport) + trackView('init') } catch (err) { console.error('[ipfs-companion] Failed to init IPFS client', err) notify( 'notify_startIpfsNodeErrorTitle', + err.name === 'ValidationError' ? err.details[0].message : err.message ) } @@ -79,12 +84,14 @@ export default async function init () { copier = createCopier(notify, ipfsPathValidator) ipfsImportHandler = createIpfsImportHandler(getState, getIpfs, ipfsPathValidator, runtime, copier) inspector = createInspector(notify, ipfsPathValidator, getState) - contextMenus = createContextMenus(getState, runtime, ipfsPathValidator, { - onAddFromContext, - onCopyCanonicalAddress: copier.copyCanonicalAddress, - onCopyRawCid: copier.copyRawCid, - onCopyAddressAtPublicGw: copier.copyAddressAtPublicGw - }) + if (!inQuickImport) { + contextMenus = createContextMenus(getState, runtime, ipfsPathValidator, { + onAddFromContext, + onCopyCanonicalAddress: copier.copyCanonicalAddress, + onCopyRawCid: copier.copyRawCid, + onCopyAddressAtPublicGw: copier.copyAddressAtPublicGw + }) + } modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime) log('register all listeners') registerListeners() @@ -109,15 +116,16 @@ export default async function init () { } function registerListeners () { - const onBeforeSendInfoSpec = ['blocking', 'requestHeaders'] + const onBeforeSendInfoSpec = ['requestHeaders'] if (browser.webRequest.OnBeforeSendHeadersOptions && 'EXTRA_HEADERS' in browser.webRequest.OnBeforeSendHeadersOptions) { // Chrome 72+ requires 'extraHeaders' for accessing all headers // Note: we need this for code ensuring kubo-rpc-client can talk to API without setting CORS onBeforeSendInfoSpec.push('extraHeaders') } - browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: [''] }, onBeforeSendInfoSpec) - browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: [''] }, ['blocking']) - browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: [''] }, ['blocking', 'responseHeaders']) + browser.webRequest.onBeforeSendHeaders.addListener( + onBeforeSendHeaders, { urls: [''] }, getExtraInfoSpec(onBeforeSendInfoSpec)) + browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: [''] }, getExtraInfoSpec()) + browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: [''] }, getExtraInfoSpec(['responseHeaders'])) browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: [''], types: ['main_frame'] }) browser.webRequest.onCompleted.addListener(onCompleted, { urls: [''], types: ['main_frame'] }) browser.storage.onChanged.addListener(onStorageChange) @@ -164,20 +172,20 @@ export default async function init () { // =================================================================== // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage - function onRuntimeMessage (request, sender) { + async function onRuntimeMessage (request, sender) { // console.log((sender.tab ? 'Message from a content script:' + sender.tab.url : 'Message from the extension'), request) if (request.pubGwUrlForIpfsOrIpnsPath) { const path = request.pubGwUrlForIpfsOrIpnsPath const { validIpfsOrIpns, resolveToPublicUrl } = ipfsPathValidator - const result = validIpfsOrIpns(path) ? resolveToPublicUrl(path) : null - return Promise.resolve({ pubGwUrlForIpfsOrIpnsPath: result }) + const result = await validIpfsOrIpns(path) ? await resolveToPublicUrl(path) : null + return { pubGwUrlForIpfsOrIpnsPath: result } } if (request.telemetry) { - return Promise.resolve(onTelemetryMessage(request.telemetry, sender)) + return Promise.resolve(onTelemetryMessage(request.telemetry)) } } - function onTelemetryMessage (request, sender) { + function onTelemetryMessage (request) { if (request.trackView) { const { version } = browser.runtime.getManifest() return trackView(request.trackView, { version }) @@ -232,7 +240,7 @@ export default async function init () { peerCount: state.peerCount, gwURLString: dropSlash(state.gwURLString), pubGwURLString: dropSlash(state.pubGwURLString), - webuiRootUrl: dropSlash(state.webuiRootUrl), // TODO: fix js-ipfs - it fails with trailing slash + webuiRootUrl: dropSlash(state.webuiRootUrl), importDir: state.importDir, openViaWebUI: state.openViaWebUI, apiURLString: dropSlash(state.apiURLString), @@ -254,7 +262,7 @@ export default async function init () { const url = info.currentTab.url info.isIpfsContext = ipfsPathValidator.isIpfsPageActionsContext(url) if (info.isIpfsContext) { - info.currentTabPublicUrl = ipfsPathValidator.resolveToPublicUrl(url) + info.currentTabPublicUrl = await ipfsPathValidator.resolveToPublicUrl(url) info.currentTabContentPath = ipfsPathValidator.resolveToIpfsPath(url) if (resolveCache.has(url)) { const [immutableIpfsPath, permalink, cid] = resolveCache.get(url) @@ -273,7 +281,7 @@ export default async function init () { }, 0) } } - info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url) + info.currentDnslinkFqdn = await dnslinkResolver.findDNSLinkHostname(url) info.currentFqdn = info.currentDnslinkFqdn || safeHostname(url) info.currentTabIntegrationsOptOut = !state.activeIntegrations(info.currentFqdn) info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url) @@ -317,6 +325,7 @@ export default async function init () { } // console.log('onAddFromContext.context', context) // console.log('onAddFromContext.fetchOptions', fetchOptions) + const response = await fetch(dataSrc, fetchOptions) const blob = await response.blob() const url = new URL(response.url) @@ -325,6 +334,7 @@ export default async function init () { ? url.hostname : url.pathname.replace(/[\\/]+$/, '').split('/').pop() data = { + path: decodeURIComponent(filename), content: blob } @@ -335,13 +345,14 @@ export default async function init () { preloadFilesAtPublicGateway(results) await copyImportResultsToFiles(results, importDir) - if (!state.openViaWebUI || state.ipfsNodeType.startsWith('embedded')) { + if (!state.openViaWebUI) { await openFilesAtGateway(importDir) } else { await openFilesAtWebUI(importDir) } } catch (error) { console.error('Error in import to IPFS context menu', error) + if (error.message === 'NetworkError when attempting to fetch resource.') { notify('notify_importErrorTitle', 'notify_importTrackingProtectionErrorMsg') console.warn('IPFS import often fails because remote file can not be downloaded due to Tracking Protection. See details at: https://github.com/ipfs/ipfs-companion/issues/227') @@ -362,16 +373,22 @@ export default async function init () { // immediately preceding a switch from one browser window to another. if (windowId !== browser.windows.WINDOW_ID_NONE) { const currentTab = await browser.tabs.query({ active: true, windowId }).then(tabs => tabs[0]) - await contextMenus.update(currentTab.id) + if (!inQuickImport) { + await contextMenus.update(currentTab.id) + } } } async function onActivatedTab (activeInfo) { - await contextMenus.update(activeInfo.tabId) + if (!inQuickImport) { + await contextMenus.update(activeInfo.tabId) + } } async function onNavigationCommitted (details) { - await contextMenus.update(details.tabId) + if (!inQuickImport) { + await contextMenus.update(details.tabId) + } await updatePageActionIndicator(details.tabId, details.url) } @@ -408,7 +425,7 @@ export default async function init () { log.error(`Unable to linkify DOM at '${details.url}' due to`, error) } } - // Ensure embedded js-ipfs in Brave uses correct API + // Ensure Brave uses correct API if (details.url.startsWith(state.webuiRootUrl)) { const apiMultiaddr = toMultiaddr(state.apiURLString) await browser.tabs.executeScript(details.tabId, { @@ -441,8 +458,12 @@ export default async function init () { await Promise.all([ updateAutomaticModeRedirectState(oldPeerCount, state.peerCount), updateBrowserActionBadge(), - contextMenus.update(), - sendStatusUpdateToBrowserAction() + sendStatusUpdateToBrowserAction(), + () => { + if (!inQuickImport) { + contextMenus.update() + } + } ]) } @@ -473,7 +494,7 @@ export default async function init () { // ------------------------------------------------------------------- async function updateBrowserActionBadge () { - if (typeof browser.browserAction.setBadgeBackgroundColor === 'undefined') { + if (typeof browser.action.setBadgeBackgroundColor === 'undefined') { // Firefox for Android does not have this UI, so we just skip it return } @@ -498,13 +519,14 @@ export default async function init () { badgeIcon = '/icons/ipfs-logo-off.svg' } try { - const oldColor = colorArraytoHex(await browser.browserAction.getBadgeBackgroundColor({})) + const oldColor = colorArraytoHex(await browser.action.getBadgeBackgroundColor({})) if (badgeColor !== oldColor) { - await browser.browserAction.setBadgeBackgroundColor({ color: badgeColor }) + await cleanupRules(true) + await browser.action.setBadgeBackgroundColor({ color: badgeColor }) await setBrowserActionIcon(badgeIcon) } - const oldText = await browser.browserAction.getBadgeText({}) - if (oldText !== badgeText) await browser.browserAction.setBadgeText({ text: badgeText }) + const oldText = await browser.action.getBadgeText({}) + if (oldText !== badgeText) await browser.action.setBadgeText({ text: badgeText }) } catch (error) { console.error('Unable to update browserAction badge due to error', error) } @@ -525,14 +547,18 @@ export default async function init () { let iconDefinition = { path: iconPath } try { // Try SVG first -- Firefox supports it natively - await browser.browserAction.setIcon(iconDefinition) + await browser.action.setIcon(iconDefinition) + if (browser.runtime.lastError.message === 'Icon invalid.') { + throw browser.runtime.lastError + } } catch (error) { // Fallback! // Chromium does not support SVG [ticket below is 8 years old, I can't even..] // https://bugs.chromium.org/p/chromium/issues/detail?id=29683 // Still, we want icon, so we precompute rasters of popular sizes and use them instead + iconDefinition = await rasterIconDefinition(iconPath) - await browser.browserAction.setIcon(iconDefinition) + await browser.action.setIcon(iconDefinition) } } @@ -544,6 +570,7 @@ export default async function init () { if (state.automaticMode && state.localGwAvailable) { if (oldPeerCount === offlinePeerCount && newPeerCount > offlinePeerCount && !state.redirect) { await browser.storage.local.set({ useCustomGateway: true }) + reloadIpfsClientOfflinePages(browser, ipfs, state) } else if (newPeerCount === offlinePeerCount && state.redirect) { await browser.storage.local.set({ useCustomGateway: false }) @@ -551,7 +578,7 @@ export default async function init () { } } - async function onStorageChange (changes, area) { + async function onStorageChange (changes) { let shouldReloadExtension = false let shouldRestartIpfsClient = false let shouldStopIpfsClient = false @@ -568,8 +595,6 @@ export default async function init () { await registerSubdomainProxy(getState, runtime) shouldRestartIpfsClient = true shouldStopIpfsClient = !state.active - // Any time the extension switches active state, start or stop the current session. - state.active ? startSession() : endSession() break case 'ipfsNodeType': if (change.oldValue !== braveNodeType && change.newValue === braveNodeType) { @@ -645,6 +670,7 @@ export default async function init () { await destroyIpfsClient(browser) } catch (err) { console.error('[ipfs-companion] Failed to destroy IPFS client', err) + notify('notify_stopIpfsNodeErrorTitle', err.message) } finally { ipfs = null @@ -659,6 +685,7 @@ export default async function init () { console.error('[ipfs-companion] Failed to init IPFS client', err) notify( 'notify_startIpfsNodeErrorTitle', + err.name === 'ValidationError' ? err.details[0].message : err.message ) } @@ -670,6 +697,8 @@ export default async function init () { browser.tabs.reload() // async reload of options page to keep it alive await browser.runtime.reload() } + log('storage change processed') + // Post update to Browser Action (if exists) -- this gives UX a snappy feel await sendStatusUpdateToBrowserAction() } @@ -729,6 +758,7 @@ export default async function init () { const rasterIconDefinition = pMemoize((svgPath) => { const pngPath = (size) => { // point at precomputed PNG file + const baseName = /\/icons\/(.+)\.svg/.exec(svgPath)[1] return `/icons/png/${baseName}_${size}.png` } diff --git a/add-on/src/lib/ipfs-import.js b/add-on/src/lib/ipfs-import.js index 8b4e8664d..1a0fdcd82 100644 --- a/add-on/src/lib/ipfs-import.js +++ b/add-on/src/lib/ipfs-import.js @@ -68,7 +68,7 @@ export function createIpfsImportHandler (getState, getIpfs, ipfsPathValidator, r // share wrapping dir path = `/ipfs/${root.cid}/` } - const url = resolveToPublicUrl(path) + const url = await resolveToPublicUrl(path) await copier.copyTextToClipboard(url) }, @@ -78,7 +78,7 @@ export function createIpfsImportHandler (getState, getIpfs, ipfsPathValidator, r for (const file of files) { if (file && file.cid) { const { path } = ipfsImportHandler.getIpfsPathAndNativeAddress(file.cid) - const preloadUrl = resolveToPublicUrl(`${path}#${redirectOptOutHint}`) + const preloadUrl = await resolveToPublicUrl(`${path}#${redirectOptOutHint}`) try { await fetch(preloadUrl, { method: 'HEAD' }) log('successfully preloaded', file) diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js index 06b012755..5e3fe1ae8 100644 --- a/add-on/src/lib/ipfs-path.js +++ b/add-on/src/lib/ipfs-path.js @@ -163,7 +163,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const ipfsPathValidator = { // Test if URL is a Public IPFS resource // (pass validIpfsOrIpns(url) and not at the local gateway or API) - publicIpfsOrIpnsResource (url) { + async publicIpfsOrIpnsResource (url) { // exclude custom gateway and api, otherwise we have infinite loops const { gwURL, apiURL } = getState() if (!sameGateway(url, gwURL) && !sameGateway(url, apiURL)) { @@ -174,7 +174,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { // Test if URL or a path is a valid IPFS resource // (IPFS needs to be a CID, IPNS can be PeerId or have dnslink entry) - validIpfsOrIpns (urlOrPath) { + async validIpfsOrIpns (urlOrPath) { // normalize input to a content path const path = ipfsContentPath(urlOrPath) if (!path) return false @@ -197,7 +197,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { } // then see if there is an DNSLink entry for 'ipnsRoot' hostname // TODO: use dnslink cache only - if (dnslinkResolver.readAndCacheDnslink(ipnsRoot)) { + if (await dnslinkResolver.readAndCacheDnslink(ipnsRoot)) { // console.log('==> IPNS for FQDN with valid dnslink: ', ipnsRoot) return true } @@ -235,7 +235,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { // The purpose of this resolver is to always return a meaningful, publicly // accessible URL that can be accessed without the need of IPFS client. // TODO: add Local version - resolveToPublicUrl (urlOrPath) { + async resolveToPublicUrl (urlOrPath) { const { pubSubdomainGwURL, pubGwURLString } = getState() const input = urlOrPath @@ -243,7 +243,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { if (input.startsWith('ipns://')) { const dnslinkUrl = new URL(input) dnslinkUrl.protocol = 'https:' - const dnslink = dnslinkResolver.readAndCacheDnslink(dnslinkUrl.hostname) + const dnslink = await dnslinkResolver.readAndCacheDnslink(dnslinkUrl.hostname) if (dnslink) { return dnslinkUrl.toString() } @@ -267,7 +267,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const { id: ipnsId } = subdomainPatternMatch(url) if (!isIPFS.cid(ipnsId)) { // Confirm DNSLink record is present and its not a false-positive - const dnslink = dnslinkResolver.readAndCacheDnslink(ipnsId) + const dnslink = await dnslinkResolver.readAndCacheDnslink(ipnsId) if (dnslink) { // return URL to DNSLink hostname (FQDN without any suffix) url.hostname = ipnsId @@ -382,7 +382,7 @@ export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const fqdn = rawPath.replace(/^.*\/ipns\/([^/]+).*/, '$1') if (err.message === 'resolve non-IPFS names is not implemented' && isFQDN(fqdn)) { // js-ipfs without dnslink support, fallback to the value read from DNSLink - const dnslink = dnslinkResolver.readAndCacheDnslink(fqdn) + const dnslink = await dnslinkResolver.readAndCacheDnslink(fqdn) if (dnslink) { // swap problematic /ipns/{fqdn} with /ipfs/{cid} and retry lookup const safePath = trimDoubleSlashes(rawPath.replace(/^.*(\/ipns\/[^/]+)/, dnslink)) diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js index e20e568ff..15ed5cab5 100644 --- a/add-on/src/lib/ipfs-request.js +++ b/add-on/src/lib/ipfs-request.js @@ -3,13 +3,15 @@ import debug from 'debug' -import LRU from 'lru-cache' -import isIPFS from 'is-ipfs' import isFQDN from 'is-fqdn' +import isIPFS from 'is-ipfs' +import LRU from 'lru-cache' +import { recoveryPagePath } from './constants.js' +import { braveNodeType } from './ipfs-client/brave.js' import { dropSlash, ipfsUri, pathAtHttpGateway, sameGateway } from './ipfs-path.js' import { safeURL } from './options.js' -import { braveNodeType } from './ipfs-client/brave.js' -import { recoveryPagePath } from './constants.js' +import { addRuleToDynamicRuleSetGenerator, isLocalHost, supportsDeclarativeNetRequest } from './redirect-handler/blockOrObserve.js' +import { RequestTracker } from './trackers/requestTracker.js' const log = debug('ipfs-companion:request') log.error = debug('ipfs-companion:request:error') @@ -30,6 +32,9 @@ const recoverableHttpError = (code) => code && code >= 400 // Tracking late redirects for edge cases such as https://github.com/ipfs-shipyard/ipfs-companion/issues/436 const onHeadersReceivedRedirect = new Set() +let addRuleToDynamicRuleSet = null +const observedRequestTracker = new RequestTracker('url-observed') +const resolvedRequestTracker = new RequestTracker('url-resolved') // Request modifier provides event listeners for the various stages of making an HTTP request // API Details: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest @@ -37,6 +42,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida const browser = runtime.browser const runtimeRoot = browser.runtime.getURL('/') const webExtensionOrigin = runtimeRoot ? new URL(runtimeRoot).origin : 'http://companion-origin' // avoid 'null' because it has special meaning + addRuleToDynamicRuleSet = addRuleToDynamicRuleSetGenerator(getState) const isCompanionRequest = (request) => { // We inspect webRequest object (WebExtension API) instead of Origin HTTP // header because the value of the latter changed over the years ad @@ -65,29 +71,29 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // Returns a canonical hostname representing the site from url // Main reason for this is unwrapping DNSLink from local subdomain // .ipns.localhost → - const findSiteFqdn = (url) => { + const findSiteFqdn = async (url) => { if (isIPFS.ipnsSubdomain(url)) { // convert subdomain's .ipns.gateway.tld to - const fqdn = dnslinkResolver.findDNSLinkHostname(url) + const fqdn = await dnslinkResolver.findDNSLinkHostname(url) if (fqdn) return fqdn } return new URL(url).hostname } // Finds canonical hostname of request.url and its parent page (if present) - const findSiteHostnames = (request) => { + const findSiteHostnames = async (request) => { const { url, originUrl, initiator } = request - const fqdn = findSiteFqdn(url) + const fqdn = await findSiteFqdn(url) // FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin) const parentUrl = originUrl || initiator // String value 'null' is explicitly set by Chromium in some contexts const parentFqdn = parentUrl && parentUrl !== 'null' && url !== parentUrl - ? findSiteFqdn(parentUrl) + ? await findSiteFqdn(parentUrl) : null return { fqdn, parentFqdn } } - const preNormalizationSkip = (state, request) => { + const preNormalizationSkip = async (state, request) => { // skip requests to the custom gateway or API (otherwise we have too much recursion) if (sameGateway(request.url, state.gwURL) || sameGateway(request.url, state.apiURL)) { ignore(request.requestId) @@ -97,12 +103,12 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida ignore(request.requestId) } // skip all local requests - if (request.url.startsWith('http://127.0.0.1') || request.url.startsWith('http://localhost') || request.url.startsWith('http://[::1]')) { + if (isLocalHost(request.url)) { ignore(request.requestId) } // skip if a per-site opt-out exists - const { fqdn, parentFqdn } = findSiteHostnames(request) + const { fqdn, parentFqdn } = await findSiteHostnames(request) const triggerOptOut = (optout) => { // Disable optout on canonical public gateway if (fqdn === 'gateway.ipfs.io') return false @@ -138,15 +144,20 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // browser.webRequest.onBeforeRequest // This event is triggered when a request is about to be made, and before headers are available. // This is a good place to listen if you want to cancel or redirect the request. - onBeforeRequest (request) { + async onBeforeRequest (request) { const state = getState() if (!state.active) return + observedRequestTracker.track(request) // When local IPFS node is unreachable , show recovery page where user can redirect // to public gateway. if (!state.nodeActive && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) { - const publicUri = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString) - return { redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}` } + const publicUri = await ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString) + return handleRedirection({ + originUrl: request.url, + redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}`, + request + }) } // When Subdomain Proxy is enabled we normalize address bar requests made @@ -154,21 +165,30 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // take advantage of subdomain redirect provided by go-ipfs >= 0.5 if (state.redirect && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) { const redirectUrl = safeURL(request.url, { useLocalhostName: state.useSubdomains }).toString() - if (redirectUrl !== request.url) return { redirectUrl } + return handleRedirection({ + originUrl: request.url, + redirectUrl, + request + }) } + // For now normalize API to the IP to comply with go-ipfs checks if (state.redirect && request.type === 'main_frame' && sameGateway(request.url, state.apiURL)) { const redirectUrl = safeURL(request.url, { useLocalhostName: false }).toString() - if (redirectUrl !== request.url) return { redirectUrl } + return handleRedirection({ + originUrl: request.url, + redirectUrl, + request + }) } // early sanity checks - if (preNormalizationSkip(state, request)) { + if (await preNormalizationSkip(state, request)) { return } // poor-mans protocol handlers - https://github.com/ipfs/ipfs-companion/issues/164#issuecomment-328374052 if (state.catchUnhandledProtocols && mayContainUnhandledIpfsProtocol(request)) { - const fix = normalizedUnhandledIpfsProtocol(request, state.pubGwURLString) + const fix = await normalizedUnhandledIpfsProtocol(request, state.pubGwURLString) if (fix) { return fix } @@ -188,13 +208,13 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida return } // Detect valid /ipfs/ and /ipns/ on any site - if (ipfsPathValidator.publicIpfsOrIpnsResource(request.url) && isSafeToRedirect(request, runtime)) { + if (await ipfsPathValidator.publicIpfsOrIpnsResource(request.url) && isSafeToRedirect(request, runtime)) { return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime) } // Detect dnslink using heuristics enabled in Preferences if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) { if (state.dnslinkRedirect) { - const dnslinkAtGw = dnslinkResolver.dnslinkAtGateway(request.url) + const dnslinkAtGw = await dnslinkResolver.dnslinkAtGateway(request.url) if (dnslinkAtGw && isSafeToRedirect(request, runtime)) { return redirectToGateway(request, dnslinkAtGw, state, ipfsPathValidator, runtime) } @@ -220,7 +240,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida const { requestHeaders } = request if (isCompanionRequest(request)) { - // '403 - Forbidden' fix for Chrome and Firefox + // '403 - Forbidden' fix for browsers that support blocking webRequest API (e.g. Firefox) // -------------------------------------------- // We update "Origin: *-extension://" HTTP headers in requests made to API // by js-kubo-rpc-client running in the background page of browser @@ -293,7 +313,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // browser.webRequest.onHeadersReceived // Fired when the HTTP response headers associated with a request have been received. // You can use this event to modify HTTP response headers or do a very late redirect. - onHeadersReceived (request) { + async onHeadersReceived (request) { const state = getState() if (!state.active) return @@ -308,7 +328,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida if (runtime.requiresXHRCORSfix && onHeadersReceivedRedirect.has(request.requestId)) { onHeadersReceivedRedirect.delete(request.requestId) if (state.dnslinkPolicy) { - const dnslinkAtGw = dnslinkResolver.dnslinkAtGateway(request.url) + const dnslinkAtGw = await dnslinkResolver.dnslinkAtGateway(request.url) if (dnslinkAtGw) { return redirectToGateway(request, dnslinkAtGw, state, ipfsPathValidator, runtime) } @@ -334,8 +354,8 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // so we force dnslink lookup to pre-populate dnslink cache // in a way that works even when state.dnslinkPolicy !== 'enabled' // All the following requests will be upgraded to IPNS - const cachedDnslink = dnslinkResolver.readAndCacheDnslink(new URL(request.url).hostname) - const dnslinkAtGw = dnslinkResolver.dnslinkAtGateway(request.url, cachedDnslink) + const cachedDnslink = await dnslinkResolver.readAndCacheDnslink(new URL(request.url).hostname) + const dnslinkAtGw = await dnslinkResolver.dnslinkAtGateway(request.url, cachedDnslink) // redirect only if local node is around, as we can't guarantee DNSLink support // at a public subdomain gateway (requires more than 1 level of wildcard TLS certs) if (dnslinkAtGw && state.localGwAvailable) { @@ -374,7 +394,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // Fired when a request could not be processed due to an error on network level. // For example: TCP timeout, DNS lookup failure // NOTE: this is executed only if webRequest.ResourceType='main_frame' - onErrorOccurred (request) { + async onErrorOccurred (request) { const state = getState() if (!state.active) return @@ -403,9 +423,9 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // Check if error can be recovered via DNSLink if (isRecoverableViaDNSLink(request, state, dnslinkResolver)) { const { hostname } = new URL(request.url) - const dnslink = dnslinkResolver.readAndCacheDnslink(hostname) + const dnslink = await dnslinkResolver.readAndCacheDnslink(hostname) if (dnslink) { - const redirectUrl = dnslinkResolver.dnslinkAtGateway(request.url, dnslink) + const redirectUrl = await dnslinkResolver.dnslinkAtGateway(request.url, dnslink) log(`onErrorOccurred: attempting to recover from network error (${request.error}) using dnslink for ${request.url} → ${redirectUrl}`, request) // We are unable to redirect in onErrorOccurred, but we can update the tab return updateTabWithURL(request, redirectUrl, browser) @@ -419,8 +439,8 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // Check if error can be recovered by opening same content-addresed path // using active gateway (public or local, depending on redirect state) - if (isRecoverable(request, state, ipfsPathValidator)) { - const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url) + if (await isRecoverable(request, state, ipfsPathValidator)) { + const redirectUrl = await ipfsPathValidator.resolveToPublicUrl(request.url) log(`onErrorOccurred: attempting to recover from network error (${request.error}) for ${request.url} → ${redirectUrl}`, request) // We are unable to redirect in onErrorOccurred, but we can update the tab return updateTabWithURL(request, redirectUrl, browser) @@ -430,7 +450,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida // browser.webRequest.onCompleted // Fired when HTTP request is completed (successfully or with an error code) // NOTE: this is executed only if webRequest.ResourceType='main_frame' - onCompleted (request) { + async onCompleted (request) { const state = getState() if (!state.active) return if (request.statusCode === 200) return // finish if no error to recover from @@ -450,8 +470,8 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida return browser.tabs.update(request.tabId, { url: fixedUrl }) } - if (isRecoverable(request, state, ipfsPathValidator)) { - const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url) + if (await isRecoverable(request, state, ipfsPathValidator)) { + const redirectUrl = await ipfsPathValidator.resolveToPublicUrl(request.url) log(`onCompleted: attempting to recover from HTTP Error ${request.statusCode} for ${request.url} → ${redirectUrl}`, request) return updateTabWithURL(request, redirectUrl, browser) } @@ -459,10 +479,28 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida } } +/** + * Handles redirection in MV2 and MV3. + * + * @param {object} input contains originUrl and redirectUrl. + * @returns + */ +async function handleRedirection ({ originUrl, redirectUrl, request }) { + if (redirectUrl !== '' && originUrl !== '' && redirectUrl !== originUrl) { + resolvedRequestTracker.track(request) + if (!supportsDeclarativeNetRequest()) { + return { redirectUrl } + } + + // Let browser handle redirection MV3 style. + await addRuleToDynamicRuleSet({ originUrl, redirectUrl }) + } +} + // Returns a string with URL at the active gateway (local or public) -function redirectToGateway (request, url, state, ipfsPathValidator, runtime) { +async function redirectToGateway (request, url, state, ipfsPathValidator, runtime) { const { resolveToPublicUrl, resolveToLocalUrl } = ipfsPathValidator - let redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url) + let redirectUrl = await (state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)) // SUBRESOURCE ON HTTPS PAGE: THE WORKAROUND EXTRAVAGANZA // ------------------------------------------------------ \o/ @@ -484,7 +522,7 @@ function redirectToGateway (request, url, state, ipfsPathValidator, runtime) { // if (state.localGwAvailable) { const { type, originUrl, initiator } = request - // match request types for embedded subdresources, but skip ones coming from local gateway + // match request types for embedded subresources, but skip ones coming from local gateway const parentUrl = originUrl || initiator // FF || Chromium if (type !== 'main_frame' && (parentUrl && !sameGateway(parentUrl, state.gwURL))) { // use raw IP to ensure subresource will be loaded from the path gateway @@ -506,8 +544,11 @@ function redirectToGateway (request, url, state, ipfsPathValidator, runtime) { } } - // return a redirect only if URL changed - if (redirectUrl && request.url !== redirectUrl) return { redirectUrl } + return handleRedirection({ + originUrl: request.url, + redirectUrl, + request + }) } function isSafeToRedirect (request, runtime) { @@ -574,7 +615,11 @@ function normalizedRedirectingProtocolRequest (request, pubGwUrl) { // additional fixups of the final path path = fixupDnslinkPath(path) // /ipfs/example.com → /ipns/example.com if (oldPath !== path && isIPFS.path(path)) { - return { redirectUrl: pathAtHttpGateway(path, pubGwUrl) } + return handleRedirection({ + originUrl: request.url, + redirectUrl: pathAtHttpGateway(path, pubGwUrl), + request + }) } return null } @@ -616,7 +661,12 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) { if (isIPFS.path(path)) { // replace search query with a request to a public gateway // (will be redirected later, if needed) - return { redirectUrl: pathAtHttpGateway(path, pubGwUrl) } + return handleRedirection({ + originUrl: request.url, + redirectUrl: pathAtHttpGateway(path, pubGwUrl), + request + + }) } } @@ -624,7 +674,7 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) { // =================================================================== // Recovery check for onErrorOccurred (request.error) and onCompleted (request.statusCode) -function isRecoverable (request, state, ipfsPathValidator) { +async function isRecoverable (request, state, ipfsPathValidator) { // Note: we are unable to recover default public gateways without a local one const { error, statusCode, url } = request const { redirect, localGwAvailable, pubGwURL, pubSubdomainGwURL } = state @@ -632,7 +682,7 @@ function isRecoverable (request, state, ipfsPathValidator) { request.type === 'main_frame' && (recoverableNetworkErrors.has(error) || recoverableHttpError(statusCode)) && - ipfsPathValidator.publicIpfsOrIpnsResource(url) && + await ipfsPathValidator.publicIpfsOrIpnsResource(url) && ((redirect && localGwAvailable) || (!sameGateway(url, pubGwURL) && !sameGateway(url, pubSubdomainGwURL)))) diff --git a/add-on/src/lib/on-installed.js b/add-on/src/lib/on-installed.js index 11dae33fe..b40d88a35 100644 --- a/add-on/src/lib/on-installed.js +++ b/add-on/src/lib/on-installed.js @@ -3,7 +3,7 @@ import browser from 'webextension-polyfill' import debug from 'debug' -import { welcomePage } from './constants.js' +import { requestRequiredPermissionsPage, welcomePage } from './constants.js' import { brave, braveNodeType } from './ipfs-client/brave.js' const { version } = browser.runtime.getManifest() @@ -21,6 +21,15 @@ export async function onInstalled (details) { export async function runPendingOnInstallTasks () { const { onInstallTasks, displayReleaseNotes } = await browser.storage.local.get(['onInstallTasks', 'displayReleaseNotes']) await browser.storage.local.remove('onInstallTasks') + // this is needed because `permissions.request` cannot be called from a script. If that happens the browser will + // throws: Error: permissions.request may only be called from a user input handler + // To avoid this, we open a new tab with the permissions page and ask the user to grant the permissions. + // That makes the request valid and allows us to gain access to the permissions. + if (!(await browser.permissions.contains({ origins: [''] }))) { + return browser.tabs.create({ + url: requestRequiredPermissionsPage + }) + } switch (onInstallTasks) { case 'onFirstInstall': await useNativeNodeIfFeasible(browser) diff --git a/add-on/src/lib/options.js b/add-on/src/lib/options.js index e9d821631..1bbbfca56 100644 --- a/add-on/src/lib/options.js +++ b/add-on/src/lib/options.js @@ -2,9 +2,10 @@ import isFQDN from 'is-fqdn' import { isIPv4, isIPv6 } from 'is-ip' +import { POSSIBLE_NODE_TYPES } from './state.js' /** - * @type {Readonly} + * @type {Readonly} */ export const optionDefaults = Object.freeze({ active: true, // global ON/OFF switch, overrides everything else @@ -219,5 +220,14 @@ export async function migrateOptions (storage, debug) { } } + { + // -v3.0.0: migrate ipfsNodeType to 'external' (if missing) + const { ipfsNodeType } = await storage.get(['ipfsNodeType']) + if (!POSSIBLE_NODE_TYPES.includes(ipfsNodeType)) { + log('migrating ipfsNodeType to "external"') + await storage.set({ ipfsNodeType: 'external' }) + } + } + // TODO: refactor this, so migrations only run once (like https://github.com/sindresorhus/electron-store#migrations) } diff --git a/add-on/src/lib/redirect-handler/baseRegexFilter.ts b/add-on/src/lib/redirect-handler/baseRegexFilter.ts new file mode 100644 index 000000000..3958ed94b --- /dev/null +++ b/add-on/src/lib/redirect-handler/baseRegexFilter.ts @@ -0,0 +1,108 @@ +import { brave } from '../../lib/ipfs-client/brave.js' + +export interface IRegexFilter { + originUrl: string + redirectUrl: string +} + +export interface IFilter { + regexFilter: string + regexSubstitution: string +} + +/** + * Base class for all regex filters. + */ +export class RegexFilter { + readonly _redirectUrl!: string + readonly _originUrl!: string + readonly originURL: URL + readonly redirectURL: URL + readonly originNS: string + readonly redirectNS: string + readonly isBrave: boolean = brave !== undefined + // by default we cannot handle the request. + private _canHandle = false + regexFilter!: string + regexSubstitution!: string + + constructor ({ originUrl, redirectUrl }: IRegexFilter) { + this._originUrl = originUrl + this._redirectUrl = redirectUrl + this.originURL = new URL(this._originUrl) + this.redirectURL = new URL(this._redirectUrl) + this.redirectNS = this.computeNamespaceFromUrl(this.redirectURL) + this.originNS = this.computeNamespaceFromUrl(this.originURL) + this.computeFilter() + this.normalizeRegexFilter() + } + + /** + * Getter for the originUrl provided at construction. + */ + get originUrl (): string { + return this._originUrl + } + + /** + * Getter for the redirectUrl provided at construction. + */ + get redirectUrl (): string { + return this._redirectUrl + } + + /** + * Getter for the canHandle flag. + */ + get canHandle (): boolean { + return this._canHandle + } + + /** + * Setter for the canHandle flag. + */ + set canHandle (value: boolean) { + this._canHandle = value + } + + /** + * Getter for the filter. This is the regex filter and substitution. + */ + get filter (): IFilter { + if (!this.canHandle) { + throw new Error('Cannot handle this request') + } + + return { + regexFilter: this.regexFilter, + regexSubstitution: this.regexSubstitution + } + } + + /** + * Compute the regex filter and substitution. + * This is the main method that needs to be implemented by subclasses. + * isBraveOverride is used to force the filter to be generated for Brave. For testing purposes only. + */ + computeFilter (isBraveOverride?: boolean): void { + throw new Error('Method not implemented.') + } + + /** + * Normalize the regex filter. This is a helper method that can be used by subclasses. + */ + normalizeRegexFilter (): void { + this.regexFilter = this.regexFilter.replace(/https?\??/ig, 'https?') + } + + /** + * Compute the namespace from the URL. This finds the first path segment. + * e.g. http:////path/to/file/or/cid + * + * @param url URL + */ + computeNamespaceFromUrl ({ pathname }: URL): string { + // regex to match the first path segment. + return (/\/([^/]+)\//i.exec(pathname)?.[1] ?? '').toLowerCase() + } +} diff --git a/add-on/src/lib/redirect-handler/blockOrObserve.ts b/add-on/src/lib/redirect-handler/blockOrObserve.ts new file mode 100644 index 000000000..486346193 --- /dev/null +++ b/add-on/src/lib/redirect-handler/blockOrObserve.ts @@ -0,0 +1,406 @@ +import debug from 'debug' +import { fastHashCode } from 'fast-hash-code' +import browser from 'webextension-polyfill' +import { CompanionState } from '../../types/companion.js' +import { IFilter, IRegexFilter, RegexFilter } from './baseRegexFilter.js' +import { CommonPatternRedirectRegexFilter } from './commonPatternRedirectRegexFilter.js' +import { NamespaceRedirectRegexFilter } from './namespaceRedirectRegexFilter.js' +import { SubdomainRedirectRegexFilter } from './subdomainRedirectRegexFilter.js' + +// this won't work in webworker context. Needs to be enabled manually +// https://github.com/debug-js/debug/issues/916 +const log = debug('ipfs-companion:redirect-handler:blockOrObserve') +log.error = debug('ipfs-companion:redirect-handler:blockOrObserve:error') + +export const DEFAULT_NAMESPACES = new Set(['ipfs', 'ipns']) +export const GLOBAL_STATE_OPTION_CHANGE = 'GLOBAL_STATE_OPTION_CHANGE' +export const DELETE_RULE_REQUEST = 'DELETE_RULE_REQUEST' +export const DELETE_RULE_REQUEST_SUCCESS = 'DELETE_RULE_REQUEST_SUCCESS' + +// We need to match the rest of the URL, so we can use a wildcard. +export const RULE_REGEX_ENDING = '((?:[^\\.]|$).*)$' + +interface regexFilterMap { + id: number + regexSubstitution: string +} + +interface redirectHandlerInput { + originUrl: string + redirectUrl: string + getPort: (state: CompanionState) => string +} + +type messageToSelfType = typeof GLOBAL_STATE_OPTION_CHANGE | typeof DELETE_RULE_REQUEST +interface messageToSelf { + type: messageToSelfType + value?: string | Record +} + +export const defaultNSRegexStr = `(${[...DEFAULT_NAMESPACES].join('|')})` + +// We need to check if the browser supports the declarativeNetRequest API. +// TODO: replace with check for `Blocking` in `chrome.webRequest.OnBeforeRequestOptions` +// which is currently a bug https://bugs.chromium.org/p/chromium/issues/detail?id=1427952 +// this needs to be a function call, because in tests we mock browser.declarativeNetRequest +// the way sinon ends up stubbing it, it's not directly available in the global scope on import +// rather it gets replaced dynamically when the module is imported. Which means, we can't +// just check for the existence of the property, we need to call the browser instance at that point. +export const supportsDeclarativeNetRequest = (): boolean => browser.declarativeNetRequest?.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES > 0 + +/** + * Sends message to self to notify about change. + * + * @param msg + */ +async function sendMessageToSelf (msg: messageToSelfType, value?: any): Promise { + // this check ensures we don't send messages to ourselves if blocking mode is enabled. + if (supportsDeclarativeNetRequest()) { + const message: messageToSelf = { type: msg, value } + // on FF, this call waits for the response from the listener. + // on Chrome, this needs a callback. + await browser.runtime.sendMessage(message) + } +} + +/** + * Notify self about option change. + * + * @returns void + */ +export async function notifyOptionChange (): Promise { + log('notifyOptionChange') + return await sendMessageToSelf(GLOBAL_STATE_OPTION_CHANGE) +} + +/** + * Notify self about rule deletion. + * + * @param id number + * @returns void + */ +export async function notifyDeleteRule (id: number): Promise { + return await sendMessageToSelf(DELETE_RULE_REQUEST, id) +} + +const savedRegexFilters: Map = new Map() +const DEFAULT_LOCAL_RULES: redirectHandlerInput[] = [ + { + originUrl: 'http://127.0.0.1', + redirectUrl: 'http://localhost', + getPort: ({ gwURLString }): string => new URL(gwURLString).port + }, + { + originUrl: 'http://[::1]', + redirectUrl: 'http://localhost', + getPort: ({ gwURLString }): string => new URL(gwURLString).port + }, + { + originUrl: 'http://localhost', + redirectUrl: 'http://127.0.0.1', + getPort: ({ apiURL }): string => new URL(apiURL).port + } +] + +/** + * This function determines if the request is headed to a local IPFS gateway. + * + * @param url + * @returns + */ +export function isLocalHost (url: string): boolean { + return url.startsWith('http://127.0.0.1') || + url.startsWith('http://localhost') || + url.startsWith('http://[::1]') +} + +/** + * Escape the characters that are allowed in the URL, but not in the regex. + * + * @param str URL string to escape + * @returns + */ +export function escapeURLRegex (str: string): string { + // these characters are allowed in the URL, but not in the regex. + // eslint-disable-next-line no-useless-escape + const ALLOWED_CHARS_URL_REGEX = /([:\/\?#\[\]@!$&'\(\ )\*\+,;=\-_\.~])/g + return str.replace(ALLOWED_CHARS_URL_REGEX, '\\$1') +} + +/** + * Construct a regex filter and substitution for a redirect. + * + * @param originUrl + * @param redirectUrl + * @returns + */ +function constructRegexFilter ({ originUrl, redirectUrl }: IRegexFilter): IFilter { + // the order is very important here, because we want to match the best possible filter. + const filtersToTryInOrder: Array = [ + SubdomainRedirectRegexFilter, + NamespaceRedirectRegexFilter, + CommonPatternRedirectRegexFilter + ] + + for (const Filter of filtersToTryInOrder) { + const filter = new Filter({ originUrl, redirectUrl }) + if (filter.canHandle) { + return filter.filter + } + } + + // this is just to satisfy the compiler, this should never happen. Because CommonPatternRedirectRegexFilter can always + // handle. + return new CommonPatternRedirectRegexFilter({ originUrl, redirectUrl }).filter +} + +// If the browser supports the declarativeNetRequest API, we can block the request. +export function getExtraInfoSpec (additionalParams: T[] = []): T[] { + if (!supportsDeclarativeNetRequest()) { + return ['blocking' as T, ...additionalParams] + } + return additionalParams +} + +/** + * Validates if the rule has changed. + * + * @param rule + * @returns {boolean} + */ +function validateIfRuleChanged (rule: browser.DeclarativeNetRequest.Rule): boolean { + if (rule.condition.regexFilter !== undefined) { + const savedRule = savedRegexFilters.get(rule.condition.regexFilter) + if (savedRule !== undefined) { + return savedRule.id !== rule.id || savedRule.regexSubstitution !== rule.action.redirect?.regexSubstitution + } + } + return true +} + +/** + * Clean up all the rules, when extension is disabled. + */ +export async function cleanupRules (resetInMemory: boolean = false): Promise { + if (!supportsDeclarativeNetRequest()) { + return + } + const existingRules = await browser.declarativeNetRequest.getDynamicRules() + const existingRulesIds = existingRules.map(({ id }): number => id) + await browser.declarativeNetRequest.updateDynamicRules({ addRules: [], removeRuleIds: existingRulesIds }) + if (resetInMemory) { + savedRegexFilters.clear() + } +} + +/** + * Clean up a rule by ID. + * + * @param id number + */ +async function cleanupRuleById (id: number): Promise { + const [{ condition: { regexFilter } }] = await browser.declarativeNetRequest.getDynamicRules({ ruleIds: [id] }) + savedRegexFilters.delete(regexFilter as string) + await browser.declarativeNetRequest.updateDynamicRules({ addRules: [], removeRuleIds: [id] }) +} + +/** + * This function sets up the listeners for the extension. + * @param {function} handlerFn + */ +function setupListeners (handlers: Record Promise>): void { + browser.runtime.onMessage.addListener(async (message: messageToSelf): Promise => { + const { type, value } = message + if (type in handlers) { + await handlers[type](value) + } + }) +} + +/** + * Reconciles the rules on fresh start. + * + * @param {CompanionState} state + */ +async function reconcileRulesAndRemoveOld (state: CompanionState): Promise { + const rules = await browser.declarativeNetRequest.getDynamicRules() + const addRules: browser.DeclarativeNetRequest.Rule[] = [] + const removeRuleIds: number[] = [] + + // parse the existing rules and remove the ones that are not needed. + for (const rule of rules) { + if (rule.action.type === 'redirect' && + rule.condition.regexFilter !== undefined && + rule.action.redirect?.regexSubstitution !== undefined) { + if (validateIfRuleChanged(rule)) { + // We need to remove the old rule. + removeRuleIds.push(rule.id) + savedRegexFilters.delete(rule.condition.regexFilter) + } else { + savedRegexFilters.set(rule.condition.regexFilter, { + id: rule.id, + regexSubstitution: rule.action.redirect?.regexSubstitution + }) + } + } + } + + if (!state.active) { + await cleanupRules() + } else { + // add the old rules from memory if state is active. + if (rules.length === 0) { + // we need to populate old rules. + for (const [regexFilter, { regexSubstitution, id }] of savedRegexFilters.entries()) { + addRules.push(generateAddRule(id, regexFilter, regexSubstitution)) + } + } + + // make sure that the default rules are added. + for (const { originUrl, redirectUrl, getPort } of DEFAULT_LOCAL_RULES) { + const port = getPort(state) + const regexFilter = `^${escapeURLRegex(`${originUrl}:${port}`)}\\/${defaultNSRegexStr}\\/${RULE_REGEX_ENDING}` + const regexSubstitution = `${redirectUrl}:${port}/\\1/\\2` + + if (!savedRegexFilters.has(regexFilter)) { + // We need to add the new rule. + addRules.push(saveAndGenerateRule(regexFilter, regexSubstitution)) + } + } + + await browser.declarativeNetRequest.updateDynamicRules({ addRules, removeRuleIds }) + } +} + +/** + * Saves and Generates a rule for the declarativeNetRequest API. + * + * @param regexFilter - The regex filter for the rule. + * @param regexSubstitution - The regex substitution for the rule. + * @param excludedInitiatorDomains - The domains that are excluded from the rule. + * @returns + */ +function saveAndGenerateRule ( + regexFilter: string, + regexSubstitution: string, + excludedInitiatorDomains: string[] = [] +): browser.DeclarativeNetRequest.Rule { + // We need to generate a positive number as an id. + const id = fastHashCode(`${regexFilter}:${regexSubstitution}:${excludedInitiatorDomains.join(':')}`, { + forcePositive: true + }) + // We need to save the regex filter and ID to check if the rule already exists later. + savedRegexFilters.set(regexFilter, { id, regexSubstitution }) + return generateAddRule(id, regexFilter, regexSubstitution, excludedInitiatorDomains) +} + +/** + * Generates a rule for the declarativeNetRequest API. + * + * @param regexFilter - The regex filter for the rule. + * @param regexSubstitution - The regex substitution for the rule. + * @param excludedInitiatorDomains - The domains that are excluded from the rule. + * @returns + */ +export function generateAddRule ( + id: number, + regexFilter: string, + regexSubstitution: string, + excludedInitiatorDomains: string[] = [] +): browser.DeclarativeNetRequest.Rule { + return { + id, + priority: 1, + action: { + type: 'redirect', + redirect: { regexSubstitution } + }, + condition: { + regexFilter, + excludedInitiatorDomains, + resourceTypes: [ + 'csp_report', + 'font', + 'image', + 'main_frame', + 'media', + 'object', + 'other', + 'ping', + 'script', + 'stylesheet', + 'sub_frame', + 'webbundle', + 'xmlhttprequest' + ] + } + } +} + +/** + * Register a redirect rule in the dynamic rule set and update all tabs that match the rule. + * + * @param {redirectHandlerInput} input + * @returns {Promise} + */ +export function addRuleToDynamicRuleSetGenerator ( + getState: () => CompanionState): (input: redirectHandlerInput) => Promise { + // setup listeners for the extension. + setupListeners({ + [GLOBAL_STATE_OPTION_CHANGE]: async (): Promise => { + log('GLOBAL_STATE_OPTION_CHANGE') + await cleanupRules(true) + await reconcileRulesAndRemoveOld(getState()) + }, + [DELETE_RULE_REQUEST]: async (value: number): Promise => { + if (value != null) { + await cleanupRuleById(value) + await browser.runtime.sendMessage({ type: DELETE_RULE_REQUEST_SUCCESS }) + } else { + await cleanupRules(true) + } + } + }) + + // returning a closure to avoid passing `getState` as an argument to `addRuleToDynamicRuleSet`. + return async function ({ originUrl, redirectUrl }: redirectHandlerInput): Promise { + // update the rules so that the next request is handled correctly. + const state = getState() + const redirectIsOrigin = originUrl === redirectUrl + const redirectIsLocal = isLocalHost(originUrl) && isLocalHost(redirectUrl) + const badOriginRedirect = originUrl.includes(state.gwURL.host) && !redirectUrl.includes('recovery') + // We don't want to redirect to the same URL. Or to the gateway. + if (redirectIsOrigin || badOriginRedirect || redirectIsLocal + ) { + return + } + + // first update all the matching tabs to apply the new rule. + const tabs = await browser.tabs.query({ url: `${originUrl}*` }) + await Promise.all(tabs.map(async tab => await browser.tabs.update(tab.id, { url: redirectUrl }))) + + // Then update the rule set for future, we need to construct the regex filter and substitution. + const { regexSubstitution, regexFilter } = constructRegexFilter({ originUrl, redirectUrl }) + + const savedRule = savedRegexFilters.get(regexFilter) + if (savedRule === undefined || savedRule.regexSubstitution !== regexSubstitution) { + const removeRuleIds: number[] = [] + if (savedRule !== undefined) { + // We need to remove the old rule because the substitution has changed. + removeRuleIds.push(savedRule.id) + savedRegexFilters.delete(regexFilter) + } + + await browser.declarativeNetRequest.updateDynamicRules( + { + // We need to add the new rule. + addRules: [saveAndGenerateRule(regexFilter, regexSubstitution)], + // We need to remove the old rules. + removeRuleIds + } + ) + } + // call to reconcile rules and remove old ones. + await reconcileRulesAndRemoveOld(state) + } +} diff --git a/add-on/src/lib/redirect-handler/commonPatternRedirectRegexFilter.ts b/add-on/src/lib/redirect-handler/commonPatternRedirectRegexFilter.ts new file mode 100644 index 000000000..a8eb7567d --- /dev/null +++ b/add-on/src/lib/redirect-handler/commonPatternRedirectRegexFilter.ts @@ -0,0 +1,46 @@ +import { RegexFilter } from './baseRegexFilter.js' +import { RULE_REGEX_ENDING, escapeURLRegex } from './blockOrObserve.js' + +/** + * Handles redirects like: + * origin: '^https?\\:\\/\\/awesome\\.ipfs\\.io\\/(.*)' + * destination: 'http://localhost:8081/ipns/awesome.ipfs.io/$1' + */ +export class CommonPatternRedirectRegexFilter extends RegexFilter { + computeFilter (isBraveOverride: boolean): void { + // this filter is the worst case scenario, we can handle any redirect. + this.canHandle = true + // We can traverse the URL from the end, and find the first character that is different. + let commonIdx = 1 + const leastLength = Math.min(this.originUrl.length, this.redirectUrl.length) + while (commonIdx < leastLength) { + if (this.originUrl[this.originUrl.length - commonIdx] !== this.redirectUrl[this.redirectUrl.length - commonIdx]) { + break + } + commonIdx += 1 + } + + // We can now construct the regex filter and substitution. + this.regexSubstitution = this.redirectUrl.slice(0, this.redirectUrl.length - commonIdx + 1) + '\\1' + // We need to escape the characters that are allowed in the URL, but not in the regex. + const regexFilterFirst = escapeURLRegex(this.originUrl.slice(0, this.originUrl.length - commonIdx + 1)) + this.regexFilter = `^${regexFilterFirst}${RULE_REGEX_ENDING}` + // calling normalize should add the protocol in the regexFilter. + this.normalizeRegexFilter() + + // This method does not parse: + // originUrl: "https://awesome.ipfs.io/" + // redirectUrl: "http://localhost:8081/ipns/awesome.ipfs.io/" + // that ends up with capturing all urls which we do not want. + // This rule can only apply to ipns subdomains. + if (this.regexFilter === `^https?\\:\\/${RULE_REGEX_ENDING}`) { + const subdomain = new URL(this.originUrl).hostname + this.regexFilter = `^https?\\:\\/\\/${escapeURLRegex(subdomain)}${RULE_REGEX_ENDING}` + if (this.isBrave || isBraveOverride) { + this.regexSubstitution = `ipns://${subdomain}\\1` + } else { + this.regexSubstitution = this.regexSubstitution.replace('\\1', `/${subdomain}\\1`) + } + } + } +} diff --git a/add-on/src/lib/redirect-handler/namespaceRedirectRegexFilter.ts b/add-on/src/lib/redirect-handler/namespaceRedirectRegexFilter.ts new file mode 100644 index 000000000..dc29d4831 --- /dev/null +++ b/add-on/src/lib/redirect-handler/namespaceRedirectRegexFilter.ts @@ -0,0 +1,29 @@ +import { RegexFilter } from './baseRegexFilter.js' +import { DEFAULT_NAMESPACES, RULE_REGEX_ENDING, defaultNSRegexStr, escapeURLRegex } from './blockOrObserve.js' + +/** + * Handles namespace redirects like: + * origin: '^https?\\:\\/\\/ipfs\\.io\\/(ipfs|ipns)\\/(.*)' + * destination: 'http://localhost:8080/$1/$2' + */ +export class NamespaceRedirectRegexFilter extends RegexFilter { + computeFilter (isBraveOverride: boolean): void { + this.canHandle = DEFAULT_NAMESPACES.has(this.originNS) && + DEFAULT_NAMESPACES.has(this.redirectNS) && + this.originNS === this.redirectNS && + this.originURL.searchParams.get('uri') == null + // if the namespaces are the same, we can generate simpler regex. + // The only value that needs special handling is the `uri` param. + // A redirect like + // https://ipfs.io/ipfs/QmZMxU -> http://localhost:8080/ipfs/QmZMxU + const [originFirst, originLast] = this.originUrl.split(`/${this.originNS}/`) + this.regexFilter = `^${escapeURLRegex(originFirst)}\\/${defaultNSRegexStr}\\/${RULE_REGEX_ENDING}` + if (this.isBrave || isBraveOverride) { + this.regexSubstitution = '\\1://\\2' + } else { + this.regexSubstitution = this.redirectUrl + .replace(`/${this.redirectNS}/`, '/\\1/') + .replace(originLast, '\\2') + } + } +} diff --git a/add-on/src/lib/redirect-handler/subdomainRedirectRegexFilter.ts b/add-on/src/lib/redirect-handler/subdomainRedirectRegexFilter.ts new file mode 100644 index 000000000..ddb36948d --- /dev/null +++ b/add-on/src/lib/redirect-handler/subdomainRedirectRegexFilter.ts @@ -0,0 +1,74 @@ +import { IRegexFilter, RegexFilter } from './baseRegexFilter.js' +import { DEFAULT_NAMESPACES, RULE_REGEX_ENDING, defaultNSRegexStr, escapeURLRegex } from './blockOrObserve.js' + +/** + * Handles subdomain redirects like: + * origin: '^https?\\:\\/\\/bafybeigfejjsuq5im5c3w3t3krsiytszhfdc4v5myltcg4myv2n2w6jumy\\.ipfs\\.dweb\\.link' + * destination: 'http://localhost:8080/ipfs/bafybeigfejjsuq5im5c3w3t3krsiytszhfdc4v5myltcg4myv2n2w6jumy' + */ +export class SubdomainRedirectRegexFilter extends RegexFilter { + constructor ({ originUrl, redirectUrl }: IRegexFilter) { + super({ originUrl, redirectUrl }) + } + + computeFilter (isBraveOverride: boolean): void { + const isBrave = this.isBrave || isBraveOverride + this.regexSubstitution = this.redirectUrl + this.regexFilter = this.originUrl + if (!DEFAULT_NAMESPACES.has(this.originNS) && DEFAULT_NAMESPACES.has(this.redirectNS)) { + // We'll use this to match the origin URL later. + this.regexFilter = `^${escapeURLRegex(this.regexFilter)}` + this.normalizeRegexFilter() + const origRegexFilter = this.regexFilter + // tld and root are known, we are just interested in the remainder of URL. + const [tld, root, ...urlParts] = this.originURL.hostname.split('.').reverse() + // can use the staticUrlParts to match the origin URL later. + const staticUrlParts = [root, tld] + // regex to match the start of the URL, this remains common. + const commonStaticUrlStart = escapeURLRegex(`^${this.originURL.protocol}//`) + // going though the subdomains to find a namespace or CID. + while (urlParts.length > 0) { + // get the urlPart at the 0th index and remove it from the array. + const subdomainPart = urlParts.shift() as string + // this needs to be computed for every iteration as the staticUrlParts changes + const commonStaticUrlEnd = `\\.${escapeURLRegex(staticUrlParts.join('.'))}\\/${RULE_REGEX_ENDING}` + // this does not work for subdomains where namespace is not provided. + // e.g. https://helia-identify.on.fleek.co/ + // e.g. https://bafybeib3bzis4mejzsnzsb65od3rnv5ffit7vsllratddjkgfgq4wiamqu.on.fleek.co/ + // check if the subdomainPart is a namespace. + if (DEFAULT_NAMESPACES.has(subdomainPart)) { + // We found a namespace, this is going to match group 2, i.e. namespace. + // e.g https://bafybeib3bzis4mejzsnzsb65od3rnv5ffit7vsllratddjkgfgq4wiamqu.ipfs.dweb.link + this.regexFilter = `${commonStaticUrlStart}(.*?)\\.${defaultNSRegexStr}${commonStaticUrlEnd}` + + if (isBrave) { + this.regexSubstitution = '\\2://\\1' + } else { + this.regexSubstitution = this._redirectUrl + .replace(urlParts.reverse().join('.'), '\\1') // replace urlParts or CID. + .replace(`/${subdomainPart}/`, '/\\2/') // replace namespace dynamically. + } + + const pathWithSearch = this.originURL.pathname + this.originURL.search + if (pathWithSearch !== '/' && !isBrave) { + this.regexSubstitution = this.regexSubstitution.replace(pathWithSearch, '/\\3') // replace path + } else { + this.regexSubstitution += '\\3' + } + + // no need to continue, we found a namespace. + break + } + + // till we find a namespace or CID, we keep adding subdomains to the staticUrlParts. + staticUrlParts.unshift(subdomainPart) + } + + if (this.regexFilter !== origRegexFilter) { + // this means we constructed a regexFilter with dynamic parts, instead of the original regexFilter which was + // static. There might be other suited regexFilters in that case. + this.canHandle = true + } + } + } +} diff --git a/add-on/src/lib/state.js b/add-on/src/lib/state.js index 0a99fb2f2..860fb689a 100644 --- a/add-on/src/lib/state.js +++ b/add-on/src/lib/state.js @@ -4,18 +4,19 @@ import { isHostname, safeURL } from './options.js' export const offlinePeerCount = -1 +export const POSSIBLE_NODE_TYPES = ['external', 'external:brave'] /** * - * @param {import('../types.js').CompanionOptions} options - * @param {Partial} [overrides] - * @returns {import('../types.js').CompanionState} + * @param {import('../types/companion.js').CompanionOptions} options + * @param {Partial} [overrides] + * @returns {import('../types/companion.js').CompanionState} */ export function initState (options, overrides) { // we store options and some pregenerated values to avoid async storage // reads and minimize performance impact on overall browsing experience /** - * @type {Partial} + * @type {Partial} */ const state = Object.assign({}, options) // generate some additional values @@ -57,7 +58,7 @@ export function initState (options, overrides) { }) Object.defineProperty(state, 'localGwAvailable', { // TODO: make quick fetch to confirm it works? - get: function () { return this.ipfsNodeType !== 'embedded' } + get: function () { return this.webuiRootUrl != null } }) Object.defineProperty(state, 'webuiRootUrl', { get: function () { @@ -68,5 +69,5 @@ export function initState (options, overrides) { }) // apply optional overrides if (overrides) Object.assign(state, overrides) - return /** @type {import('../types.js').CompanionState} */(state) + return /** @type {import('../types/companion.js').CompanionState} */(state) } diff --git a/add-on/src/lib/storage-provider/WebExtensionStorageProvider.ts b/add-on/src/lib/storage-provider/WebExtensionStorageProvider.ts new file mode 100644 index 000000000..c0d59acc9 --- /dev/null +++ b/add-on/src/lib/storage-provider/WebExtensionStorageProvider.ts @@ -0,0 +1,40 @@ +import type { consentTypes } from '@ipfs-shipyard/ignite-metrics/typings/countly' +import type { StorageProviderInterface } from '@ipfs-shipyard/ignite-metrics/StorageProvider' +import browser from 'webextension-polyfill' + +export class WebExtensionStorageProvider implements StorageProviderInterface { + async setStore (consentArray: consentTypes[]): Promise { + try { + const jsonString = JSON.stringify(consentArray) + if ('localStorage' in globalThis) { + globalThis.localStorage.setItem('@ipfs-shipyard/ignite-metrics:consent', jsonString) + } else { + await browser.storage.local.set({ '@ipfs-shipyard/ignite-metrics:consent': jsonString }) + } + } catch (err) { + // eslint-disable-next-line no-console + console.error(err) + } + } + + async getStore (): Promise { + try { + let jsonString + if ('localStorage' in globalThis) { + jsonString = globalThis.localStorage.getItem('@ipfs-shipyard/ignite-metrics:consent') + } else { + jsonString = (await browser.storage.local.get(['@ipfs-shipyard/ignite-metrics:consent']))['@ipfs-shipyard/ignite-metrics:consent'] + } + if (jsonString != null) { + return JSON.parse(jsonString) + } + } catch (err) { + // eslint-disable-next-line no-console + console.error(err) + } + /** + * Return minimal consent if there is nothing in the store. + */ + return ['minimal'] + } +} diff --git a/add-on/src/lib/telemetry.js b/add-on/src/lib/telemetry.js deleted file mode 100644 index 3d1932cc8..000000000 --- a/add-on/src/lib/telemetry.js +++ /dev/null @@ -1,43 +0,0 @@ -import MetricsProvider from '@ipfs-shipyard/ignite-metrics/vanilla' -import debug from 'debug' - -const log = debug('ipfs-companion:telemetry') - -const metricsProvider = new MetricsProvider({ - appKey: '393f72eb264c28a1b59973da1e0a3938d60dc38a', - autoTrack: false, - storageProvider: null -}) - -/** - * - * @param {import('../types.js').CompanionState} state - * @returns {void} - */ -export function handleConsentFromState (state) { - const telemetryGroups = { - minimal: state?.telemetryGroupMinimal || false, - performance: state?.telemetryGroupPerformance || false, - ux: state?.telemetryGroupUx || false, - feedback: state?.telemetryGroupFeedback || false, - location: state?.telemetryGroupLocation || false - } - for (const [groupName, isEnabled] of Object.entries(telemetryGroups)) { - if (isEnabled) { - log(`Adding consent for '${groupName}'`) - metricsProvider.addConsent(groupName) - } else { - log(`Removing consent for '${groupName}'`) - metricsProvider.removeConsent(groupName) - } - } -} - -const ignoredViewsRegex = [] -export function trackView (view, segments) { - log('trackView called for view: ', view) - metricsProvider.trackView(view, ignoredViewsRegex, segments) -} - -export const startSession = (...args) => metricsProvider.startSession(...args) -export const endSession = (...args) => metricsProvider.endSession(...args) diff --git a/add-on/src/lib/telemetry.ts b/add-on/src/lib/telemetry.ts new file mode 100644 index 000000000..6b3eca5f0 --- /dev/null +++ b/add-on/src/lib/telemetry.ts @@ -0,0 +1,65 @@ +import MetricsProvider from '@ipfs-shipyard/ignite-metrics/browser-vanilla' +import PatchedCountly from 'countly-sdk-web' +import debug from 'debug' +import { WebExtensionStorageProvider } from './storage-provider/WebExtensionStorageProvider.js' +import { CompanionState } from '../types/companion.js' +import { consentTypes } from '@ipfs-shipyard/ignite-metrics' +import type { CountlyEvent } from 'countly-web-sdk' + +const log = debug('ipfs-companion:telemetry') + +const metricsProvider = new MetricsProvider({ + appKey: '393f72eb264c28a1b59973da1e0a3938d60dc38a', + autoTrack: false, + metricsService: PatchedCountly, + storage: 'none', + storageProvider: new WebExtensionStorageProvider() +}) + +/** + * + * @param {import('../types/companion.js').CompanionState} state + * @returns {void} + */ +export async function handleConsentFromState (state: CompanionState): Promise { + const telemetryGroups = { + minimal: state?.telemetryGroupMinimal || false, + performance: state?.telemetryGroupPerformance || false, + ux: state?.telemetryGroupUx || false, + feedback: state?.telemetryGroupFeedback || false, + location: state?.telemetryGroupLocation || false + } + for (const [groupName, isEnabled] of Object.entries(telemetryGroups)) { + if (isEnabled) { + log(`Adding consent for '${groupName}'`) + await metricsProvider.addConsent(groupName as consentTypes) + } else { + log(`Removing consent for '${groupName}'`) + await metricsProvider.removeConsent(groupName as consentTypes) + } + } +} + +const ignoredViewsRegex: RegExp[] = [] + +/** + * TrackView is a wrapper around ignite-metrics trackView + * + * @param view + * @param segments + */ +export function trackView (view: string, segments: Record): void { + log('trackView called for view: ', view) + metricsProvider.trackView(view, ignoredViewsRegex, segments) +} + +/** + * TrackView is a wrapper around ignite-metrics trackView + * + * @param event + * @param segments + */ +export function trackEvent (event: CountlyEvent): void { + log('trackEvent called for event: ', event) + metricsProvider.trackEvent(event) +} diff --git a/add-on/src/lib/trackers/requestTracker.ts b/add-on/src/lib/trackers/requestTracker.ts new file mode 100644 index 000000000..4f3a97009 --- /dev/null +++ b/add-on/src/lib/trackers/requestTracker.ts @@ -0,0 +1,49 @@ +import debug from 'debug' +import type browser from 'webextension-polyfill' +import { trackEvent } from '../telemetry.js' + +export class RequestTracker { + private readonly eventKey: 'url-observed' | 'url-resolved' + private readonly flushInterval: number + private readonly log: debug.Debugger & { error?: debug.Debugger } + private lastSync: number = Date.now() + private requestTypeStore: { [key in browser.WebRequest.ResourceType]?: number } = {} + + constructor (eventKey: 'url-observed' | 'url-resolved', flushInterval = 1000 * 60 * 5) { + this.eventKey = eventKey + this.log = debug(`ipfs-companion:request-tracker:${eventKey}`) + this.log.error = debug(`ipfs-companion:request-tracker:${eventKey}:error`) + this.flushInterval = flushInterval + this.setupFlushScheduler() + } + + track ({ type }: browser.WebRequest.OnBeforeRequestDetailsType): void { + this.log(`track ${type}`, JSON.stringify(this.requestTypeStore)) + this.requestTypeStore[type] = (this.requestTypeStore[type] ?? 0) + 1 + } + + private flushStore (): void { + this.log('flushing') + const count = Object.values(this.requestTypeStore).reduce((a, b): number => a + b, 0) + if (count === 0) { + this.log('nothing to flush') + return + } + trackEvent({ + key: this.eventKey, + count, + dur: Date.now() - this.lastSync, + segmentation: Object.assign({}, this.requestTypeStore) as unknown as Record + }) + // reset + this.lastSync = Date.now() + this.requestTypeStore = {} + } + + private setupFlushScheduler (): void { + setTimeout(() => { + this.flushStore() + this.setupFlushScheduler() + }, this.flushInterval) + } +} diff --git a/add-on/src/options/forms/api-form.js b/add-on/src/options/forms/api-form.js index 94928fc86..83e6094d8 100644 --- a/add-on/src/options/forms/api-form.js +++ b/add-on/src/options/forms/api-form.js @@ -62,6 +62,7 @@ export default function apiForm ({ ipfsNodeType, ipfsApiUrl, ipfsApiPollMs, auto
${browser.i18n.getMessage('option_automaticMode_title')}
${browser.i18n.getMessage('option_automaticMode_description')}
+

${browser.i18n.getMessage('option_automaticMode_description_subtext')}

${switchToggle({ id: 'automaticMode', checked: automaticMode, onchange: onAutomaticModeChange })}
diff --git a/add-on/src/options/forms/gateways-form.js b/add-on/src/options/forms/gateways-form.js index 3b9abbeba..261f3a836 100644 --- a/add-on/src/options/forms/gateways-form.js +++ b/add-on/src/options/forms/gateways-form.js @@ -6,6 +6,7 @@ import html from 'choo/html/index.js' import switchToggle from '../../pages/components/switch-toggle.js' import { guiURLString, hostTextToArray, hostArrayToText } from '../../lib/options.js' import { braveNodeType } from '../../lib/ipfs-client/brave.js' +import { POSSIBLE_NODE_TYPES } from '../../lib/state.js' // Warn about mixed content issues when changing the gateway // to something other than HTTP or localhost @@ -31,7 +32,7 @@ export default function gatewaysForm ({ const onDisabledOnChange = onOptionChange('disabledOn', hostTextToArray) const onEnabledOnChange = onOptionChange('enabledOn', hostTextToArray) const mixedContentWarning = !secureContextUrl.test(customGatewayUrl) - const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded' + const supportRedirectToCustomGateway = POSSIBLE_NODE_TYPES.includes(ipfsNodeType) const allowChangeOfCustomGateway = ipfsNodeType === 'external' const braveClass = ipfsNodeType === braveNodeType ? 'brave' : '' diff --git a/add-on/src/options/forms/ipfs-node-form.js b/add-on/src/options/forms/ipfs-node-form.js index 42e9e47f2..7e817fe76 100644 --- a/add-on/src/options/forms/ipfs-node-form.js +++ b/add-on/src/options/forms/ipfs-node-form.js @@ -5,9 +5,8 @@ import browser from 'webextension-polyfill' import html from 'choo/html/index.js' import { braveNodeType } from '../../lib/ipfs-client/brave.js' -export default function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange, withNodeFromBrave }) { +export default function ipfsNodeForm ({ ipfsNodeType, onOptionChange, withNodeFromBrave }) { const onIpfsNodeTypeChange = onOptionChange('ipfsNodeType') - const onIpfsNodeConfigChange = onOptionChange('ipfsNodeConfig') const braveClass = ipfsNodeType === braveNodeType ? 'brave' : '' return html`
@@ -20,10 +19,6 @@ export default function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionCh

${browser.i18n.getMessage('option_ipfsNodeType_external_description')}

${withNodeFromBrave ? html`

${browser.i18n.getMessage('option_ipfsNodeType_brave_description')}

` : null} -

${browser.i18n.getMessage('option_ipfsNodeType_embedded_description')}

-

- ${browser.i18n.getMessage('option_legend_readMore')} -

@@ -40,29 +35,8 @@ export default function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionCh ${browser.i18n.getMessage('option_ipfsNodeType_brave')} ` : null} - - ${ipfsNodeType.startsWith('embedded') - ? html`
- - -
` - : null}
` diff --git a/add-on/src/options/forms/redirect-rule-form.js b/add-on/src/options/forms/redirect-rule-form.js new file mode 100644 index 000000000..af4093d79 --- /dev/null +++ b/add-on/src/options/forms/redirect-rule-form.js @@ -0,0 +1,79 @@ +'use strict' +/* eslint-env browser, webextensions */ + +import html from 'choo/html/index.js' +import browser from 'webextension-polyfill' + +/** + * + * @param {(event: string, value?: any) => void} emit + * @returns + */ +function ruleItem (emit) { + /** + * Renders Rule Item + * + * @param {{ + * id: string + * origin: string + * target: string + * }} param0 + * @returns + */ + return function ({ id, origin, target }) { + return html` +
+
+
+ ${browser.i18n.getMessage('option_redirect_rules_row_origin')}: ${origin} +
+
+ ${browser.i18n.getMessage('option_redirect_rules_row_target')}: ${target} +
+
+
+ +
+
+ ` + } +} + +/** + * + * @param {{ + * emit: (event: string, value?: any) => void, + * redirectRules: { + * id: string + * origin: string + * target: string + * }[] + * }} param0 + * @returns + */ +export default function redirectRuleForm ({ emit, redirectRules }) { + return html` +
+
+

${browser.i18n.getMessage('option_header_redirect_rules')}

+
+ +
+ +
+
+
+ ${redirectRules ? redirectRules.map(ruleItem(emit)) : html`
Loading...
`} +
+
+
+ ` +} diff --git a/add-on/src/options/options.css b/add-on/src/options/options.css index 662e83cb2..81b096f5c 100644 --- a/add-on/src/options/options.css +++ b/add-on/src/options/options.css @@ -92,4 +92,9 @@ input:invalid { input.brave { background-color: #f7f8fa; } - +div.rule-delete { + display: flex; + flex-direction: row; + justify-content: flex-end; + max-width: 50px; +} diff --git a/add-on/src/options/page.js b/add-on/src/options/page.js index bf656c6e6..e7d79e25f 100644 --- a/add-on/src/options/page.js +++ b/add-on/src/options/page.js @@ -2,15 +2,17 @@ /* eslint-env browser, webextensions */ import html from 'choo/html/index.js' -import globalToggleForm from './forms/global-toggle-form.js' -import ipfsNodeForm from './forms/ipfs-node-form.js' -import fileImportForm from './forms/file-import-form.js' -import dnslinkForm from './forms/dnslink-form.js' -import gatewaysForm from './forms/gateways-form.js' +import { supportsDeclarativeNetRequest } from '../lib/redirect-handler/blockOrObserve.js' import apiForm from './forms/api-form.js' +import dnslinkForm from './forms/dnslink-form.js' import experimentsForm from './forms/experiments-form.js' -import telemetryForm from './forms/telemetry-form.js' +import fileImportForm from './forms/file-import-form.js' +import gatewaysForm from './forms/gateways-form.js' +import globalToggleForm from './forms/global-toggle-form.js' +import ipfsNodeForm from './forms/ipfs-node-form.js' +import redirectRuleForm from './forms/redirect-rule-form.js' import resetForm from './forms/reset-form.js' +import telemetryForm from './forms/telemetry-form.js' // Render the options page: // Passed current app `state` from the store and `emit`, a function to create @@ -41,10 +43,10 @@ export default function optionsPage (state, emit) { // when global toggle is in "suspended" state return html`
- ${globalToggleForm({ - active: state.options.active, - onOptionChange - })} + ${globalToggleForm({ + active: state.options.active, + onOptionChange + })}
` } @@ -112,6 +114,12 @@ export default function optionsPage (state, emit) { })} ${resetForm({ onOptionsReset + })} + ${!supportsDeclarativeNetRequest() + ? '' + : redirectRuleForm({ + redirectRules: state.redirectRules, + emit })} ` diff --git a/add-on/src/options/store.js b/add-on/src/options/store.js index a5a3a0350..961edd72f 100644 --- a/add-on/src/options/store.js +++ b/add-on/src/options/store.js @@ -3,12 +3,23 @@ import browser from 'webextension-polyfill' import { optionDefaults } from '../lib/options.js' +import { DELETE_RULE_REQUEST_SUCCESS, RULE_REGEX_ENDING, notifyDeleteRule, notifyOptionChange } from '../lib/redirect-handler/blockOrObserve.js' import createRuntimeChecks from '../lib/runtime-checks.js' // The store contains and mutates the state for the app export default function optionStore (state, emitter) { state.options = optionDefaults + const fetchRedirectRules = async () => { + const existingRedirectRules = await browser.declarativeNetRequest.getDynamicRules() + state.redirectRules = existingRedirectRules.map(rule => ({ + id: rule.id, + origin: rule.condition.regexFilter?.replace(RULE_REGEX_ENDING, '(.*)').replaceAll('\\', ''), + target: rule.action.redirect?.regexSubstitution?.replace('\\1', '') + })) + emitter.emit('render') + } + const updateStateOptions = async () => { const runtime = await createRuntimeChecks(browser) state.withNodeFromBrave = runtime.brave && await runtime.brave.getIPFSEnabled() @@ -22,16 +33,29 @@ export default function optionStore (state, emitter) { emitter.on('DOMContentLoaded', async () => { browser.runtime.sendMessage({ telemetry: { trackView: 'options' } }) updateStateOptions() + fetchRedirectRules() browser.storage.onChanged.addListener(updateStateOptions) }) - emitter.on('optionChange', ({ key, value }) => ( + emitter.on('redirectRuleDeleteRequest', async (id) => { + console.log('delete rule request', id) + browser.runtime.onMessage.addListener(({ type }) => { + if (type === DELETE_RULE_REQUEST_SUCCESS) { + emitter.emit('render') + } + }) + notifyDeleteRule(id) + }) + + emitter.on('optionChange', async ({ key, value }) => { browser.storage.local.set({ [key]: value }) - )) + await notifyOptionChange() + }) - emitter.on('optionsReset', () => ( + emitter.on('optionsReset', async () => { browser.storage.local.set(optionDefaults) - )) + await notifyOptionChange() + }) } async function getOptions () { diff --git a/add-on/src/pages/components/switch-toggle.js b/add-on/src/pages/components/switch-toggle.js index 962b4abdd..9d8b747a5 100644 --- a/add-on/src/pages/components/switch-toggle.js +++ b/add-on/src/pages/components/switch-toggle.js @@ -4,7 +4,7 @@ import html from 'choo/html/index.js' /** - * @type {import('../../types.js').SwitchToggle} + * @type {import('../../types/companion.js').SwitchToggle} */ export default function switchToggle ({ checked, diff --git a/add-on/src/popup/browser-action/context-actions.js b/add-on/src/popup/browser-action/context-actions.js index df406e2f7..634a811d3 100644 --- a/add-on/src/popup/browser-action/context-actions.js +++ b/add-on/src/popup/browser-action/context-actions.js @@ -15,6 +15,7 @@ import { contextMenuCopyCanonicalAddress, contextMenuCopyCidAddress } from '../../lib/context-menus.js' +import { POSSIBLE_NODE_TYPES } from '../../lib/state.js' const notReady = browser.i18n.getMessage('panelCopy_notReadyHint') @@ -45,7 +46,7 @@ export function contextActions ({ onFilesCpImport }) { const activeCidResolver = active && isIpfsOnline && isApiAvailable && currentTabCid - const activeFilesCpImport = active && isIpfsOnline && isApiAvailable && !ipfsNodeType.startsWith('embedded') + const activeFilesCpImport = active && isIpfsOnline && isApiAvailable && POSSIBLE_NODE_TYPES.includes(ipfsNodeType) && importDir const isMutable = currentTabContentPath.startsWith('/ipns/') const activeViewOnGateway = (currentTab) => { if (!currentTab) return false diff --git a/add-on/src/popup/browser-action/gateway-status.js b/add-on/src/popup/browser-action/gateway-status.js index 4ec95f1c8..b877d27d7 100644 --- a/add-on/src/popup/browser-action/gateway-status.js +++ b/add-on/src/popup/browser-action/gateway-status.js @@ -21,10 +21,9 @@ export default function gatewayStatus ({ gatewayAddress, gatewayVersion, ipfsApiUrl, - ipfsNodeType, swarmPeers }) { - const api = ipfsApiUrl && ipfsNodeType === 'embedded' ? 'js-ipfs' : ipfsApiUrl + const api = ipfsApiUrl return html`