├── .babelrc ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── config.yml └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── generated-pr.yml │ └── stale.yml ├── .gitignore ├── .mocharc.json ├── .nvmrc ├── .release-please-manifest.json ├── .tx └── config ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PRIVACY-POLICY.md ├── README.md ├── SECURITY.md ├── add-on ├── _locales │ ├── ar │ │ └── messages.json │ ├── ca │ │ └── messages.json │ ├── cs │ │ └── messages.json │ ├── da │ │ └── messages.json │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fi │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── hu │ │ └── messages.json │ ├── id │ │ └── messages.json │ ├── it │ │ └── messages.json │ ├── ja_JP │ │ └── messages.json │ ├── ko_KR │ │ └── messages.json │ ├── nl │ │ └── messages.json │ ├── no │ │ └── messages.json │ ├── pl │ │ └── messages.json │ ├── pt │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── ro │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── sv │ │ └── messages.json │ ├── tr │ │ └── messages.json │ ├── zh_CN │ │ └── messages.json │ └── zh_TW │ │ └── messages.json ├── icons │ ├── brave-ipfs-logo-off.svg │ ├── brave-ipfs-logo-on.svg │ ├── ipfs-logo-off.svg │ ├── ipfs-logo-on.svg │ └── png │ │ ├── ipfs-logo-off_128.png │ │ ├── ipfs-logo-off_19.png │ │ ├── ipfs-logo-off_38.png │ │ ├── ipfs-logo-on_128.png │ │ ├── ipfs-logo-on_19.png │ │ └── ipfs-logo-on_38.png ├── images │ ├── ipld.svg │ ├── libp2p.svg │ ├── multiformats.svg │ └── stars.png ├── manifest.brave-beta.json ├── manifest.brave.json ├── manifest.chromium.json ├── manifest.common.json ├── manifest.firefox-beta.json ├── manifest.firefox.json └── src │ ├── background │ └── background.js │ ├── contentScripts │ └── linkifyDOM.js │ ├── landing-pages │ ├── permissions │ │ ├── request.css │ │ ├── request.html │ │ └── request.js │ └── welcome │ │ ├── index.html │ │ ├── index.js │ │ ├── page.js │ │ ├── store.js │ │ └── welcome.css │ ├── lib │ ├── constants.js │ ├── context-menus.js │ ├── context-menus │ │ └── ContextMenus.ts │ ├── copier.js │ ├── dnslink.js │ ├── http-proxy.js │ ├── inspector.js │ ├── ipfs-client │ │ ├── brave.js │ │ ├── external.js │ │ ├── index.js │ │ └── reloaders │ │ │ ├── index.js │ │ │ ├── internalTabReloader.js │ │ │ ├── localGatewayReloader.js │ │ │ ├── reloaderBase.js │ │ │ └── webUiReloader.js │ ├── ipfs-companion.js │ ├── ipfs-import.js │ ├── ipfs-path.js │ ├── ipfs-request.js │ ├── notifier.js │ ├── on-installed.js │ ├── on-uninstalled.js │ ├── options.js │ ├── precache.js │ ├── redirect-handler │ │ ├── baseRegexFilter.ts │ │ ├── blockOrObserve.ts │ │ ├── commonPatternRedirectRegexFilter.ts │ │ ├── namespaceRedirectRegexFilter.ts │ │ └── subdomainRedirectRegexFilter.ts │ ├── runtime-checks.js │ ├── state.js │ ├── telemetry.ts │ └── trackers │ │ └── requestTracker.ts │ ├── options │ ├── forms │ │ ├── api-form.js │ │ ├── dnslink-form.js │ │ ├── experiments-form.js │ │ ├── file-import-form.js │ │ ├── gateways-form.js │ │ ├── global-toggle-form.js │ │ ├── ipfs-node-form.js │ │ ├── redirect-rule-form.js │ │ ├── reset-form.js │ │ └── telemetry-form.js │ ├── options.css │ ├── options.html │ ├── options.js │ ├── page.js │ └── store.js │ ├── pages │ └── components │ │ ├── switch-toggle.css │ │ └── switch-toggle.js │ ├── popup │ ├── browser-action │ │ ├── browser-action.css │ │ ├── context-actions.js │ │ ├── gateway-status.js │ │ ├── header.js │ │ ├── icon.js │ │ ├── index.html │ │ ├── index.js │ │ ├── ipfs-version.js │ │ ├── nav-header.js │ │ ├── nav-item.js │ │ ├── options-icon.js │ │ ├── page.js │ │ ├── power-icon.js │ │ ├── redirect-icon.js │ │ ├── store.js │ │ ├── tools-button.js │ │ ├── tools.js │ │ └── version-update-icon.js │ ├── heartbeat.css │ ├── logo.js │ ├── quick-import.css │ ├── quick-import.html │ └── quick-import.js │ ├── recovery │ ├── recovery.css │ ├── recovery.html │ └── recovery.js │ ├── types │ ├── companion.d.ts │ └── global.d.ts │ └── utils │ └── i18n.js ├── ci ├── access-control-allow-all.sh ├── download-release-artifacts.sh └── update-manifest.sh ├── docker-compose.e2e.yml ├── docs ├── CONTRIBUTING.md ├── DEVELOPER-NOTES.md ├── LOCALIZATION-NOTES.md ├── MV3.md ├── README.md ├── RELEASE-PROCESS.md ├── dnslink.md ├── node-types.md ├── telemetry │ └── COLLECTED_DATA.md └── x-ipfs-path-header.md ├── package-lock.json ├── package.json ├── patches ├── @multiformats+multiaddr+11.0.7.patch ├── @protobufjs+inquire+1.1.0.patch └── multiaddr+10.0.1.patch ├── release-please-config.json ├── scripts ├── fetch-unbranded.sh ├── fetch-webui-from-gateway.js ├── generate-png-icons.sh ├── new-locale-key.sh ├── prom-client-stub │ └── index.js └── rename-artifacts.js ├── test ├── data │ └── linkify-demo.html ├── e2e │ └── ipfs-companion.test.js ├── functional │ └── lib │ │ ├── context-menus │ │ └── ContextMenus.test.ts │ │ ├── dnslink.test.js │ │ ├── ipfs-client │ │ └── reloaders │ │ │ └── reloaders.test.js │ │ ├── ipfs-companion.test.js │ │ ├── ipfs-import.test.js │ │ ├── ipfs-path.test.js │ │ ├── ipfs-request-dnslink.test.js │ │ ├── ipfs-request-gateway-recover.test.js │ │ ├── ipfs-request-gateway-redirect.test.js │ │ ├── ipfs-request-protocol-handlers.test.js │ │ ├── ipfs-request-workarounds.test.js │ │ ├── options.test.js │ │ ├── redirect-handler │ │ ├── blockOrObserve.test.ts │ │ ├── commonPatternRedirectRegexFilter.test.ts │ │ ├── declarativeNetRequest.mock.ts │ │ ├── namespaceRedirectRegexFilter.test.ts │ │ └── subdomainRedirectRegexFilter.test.ts │ │ ├── runtime-checks.test.js │ │ └── state.test.js ├── helpers │ ├── is-mv3-testing-enabled.js │ ├── mock-i18n.js │ └── mv3-test-helper.ts └── setup │ └── mocha-setup.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "syntax-async-generators" 4 | ], 5 | "presets": [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | "targets": { 10 | "firefox": 68, 11 | "chrome": 72 12 | } 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /build 3 | /coverage/ 4 | /node_modules 5 | /add-on/dist 6 | /firefox 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json linguist-generated=true 2 | yarn.json linguist-generated=true 3 | ci/firefox/update.json linguist-generated=true 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lidel @whizzzkid @ipfs/gui-dev 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: '' 5 | labels: need/triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser: [e.g. chrome, safari] 29 | - Version: [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser: [e.g. stock browser, safari] 35 | - Version: [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Getting Help on IPFS 4 | url: https://ipfs.io/help 5 | about: All information about how and where to get help on IPFS. 6 | - name: IPFS Official Forum 7 | url: https://discuss.ipfs.io 8 | about: Please post general questions, support requests, and discussions here. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: need/triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for welcome - https://github.com/behaviorbot/welcome 2 | 3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 4 | # Comment to be posted to on first time issues 5 | newIssueWelcomeComment: > 6 | Thank you for submitting your first issue to this repository! A maintainer 7 | will be here shortly to triage and review. 8 | 9 | In the meantime, please double-check that you have provided all the 10 | necessary information to make this process easy! Any information that can 11 | help save additional round trips is useful! We currently aim to give 12 | initial feedback within **seven business days**. If this does not happen, feel 13 | free to leave a comment. 14 | 15 | Please keep an eye on how this issue will be labeled, as labels give an 16 | overview of priorities, assignments and additional actions requested by the 17 | maintainers: 18 | 19 | - "Priority" labels will show how urgent this is for the team. 20 | - "Status" labels will show if this is ready to be worked on, blocked, or in progress. 21 | - "Need" labels will indicate if additional input or analysis is required. 22 | 23 | Finally, remember to use https://discuss.ipfs.io if you just need general 24 | support. 25 | 26 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 27 | # Comment to be posted to on PRs from first time contributors in your repository 28 | newPRWelcomeComment: > 29 | Thank you for submitting this PR! 30 | 31 | A maintainer will be here shortly to review it. 32 | 33 | We are super grateful, but we are also overloaded! Help us by making sure 34 | that: 35 | 36 | * The context for this PR is clear, with relevant discussion, decisions 37 | and stakeholders linked/mentioned. 38 | 39 | * Your contribution itself is clear (code comments, self-review for the 40 | rest) and in its best form. Follow the [code contribution 41 | guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) 42 | if they apply. 43 | 44 | Getting other community members to do a review would be great help too on 45 | complex PRs (you can ask in the chats/forums). If you are unsure about 46 | something, just leave us a comment. 47 | 48 | Next steps: 49 | 50 | * A maintainer will triage and assign priority to this PR, commenting on 51 | any missing things and potentially assigning a reviewer for high 52 | priority items. 53 | 54 | * The PR gets reviews, discussed and approvals as needed. 55 | 56 | * The PR is merged by maintainers when it has been approved and comments addressed. 57 | 58 | We currently aim to provide initial feedback/triaging within **seven business 59 | days**. Please keep an eye on any labelling actions, as these will indicate 60 | priorities and status of your contribution. 61 | 62 | We are very grateful for your contribution! 63 | 64 | 65 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 66 | # Comment to be posted to on pull requests merged by a first time user 67 | # Currently disabled 68 | #firstPRMergeComment: "" 69 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | firefox-version: 6 | description: The version of selenium/standalone-firefox image to use 7 | default: latest 8 | required: true 9 | chromium-version: 10 | description: The version of selenium/standalone-chrome image to use 11 | default: latest 12 | required: true 13 | kubo-version: 14 | description: The version of ipfs/kubo image to use 15 | default: latest 16 | required: true 17 | ipfs-companion-version: 18 | description: The version of ipfs-companion extension to use (defaults to building the extension from source) 19 | default: '' 20 | required: false 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Check out repo 27 | uses: actions/checkout@v3 28 | - name: Set up node 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: 18 32 | - name: Download ipfs-companion 33 | if: inputs.ipfs-companion-version != '' 34 | env: 35 | IPFS_COMPANION_VERSION: ${{ inputs.ipfs-companion-version }} 36 | run: ./ci/download-release-artifacts.sh 37 | - name: Build ipfs-companion 38 | if: inputs.ipfs-companion-version == '' 39 | run: npm run release-build 40 | - name: Prepare E2E env 41 | run: npm run compose:e2e:prepare 42 | env: 43 | FIREFOX_VERSION: ${{ inputs.firefox-version }} 44 | CHROMIUM_VERSION: ${{ inputs.chromium-version }} 45 | KUBO_VERSION: ${{ inputs.kubo-version }} 46 | - name: Start E2E env 47 | run: npm run compose:e2e:up 48 | env: 49 | FIREFOX_VERSION: ${{ inputs.firefox-version }} 50 | CHROMIUM_VERSION: ${{ inputs.chromium-version }} 51 | KUBO_VERSION: ${{ inputs.kubo-version }} 52 | - name: Wait for E2E env set up to complete 53 | run: sleep 60 54 | - name: Run E2E tests 55 | run: npm run compose:e2e:test 56 | env: 57 | IPFS_COMPANION_VERSION: ${{ inputs.ipfs-companion-version }} 58 | - name: Stop E2E env 59 | run: npm run compose:e2e:down 60 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /node_modules 3 | /yarn.lock 4 | /firefox 5 | /cache 6 | /build 7 | /npm-debug.log 8 | /yarn-error.log 9 | /crowdin.yml 10 | .connect-deps* 11 | .*~ 12 | /add-on/dist 13 | /add-on/webui/ 14 | /add-on/ui-kit/ 15 | /coverage 16 | /.nyc_output 17 | /add-on/manifest.json 18 | 19 | .DS_Store 20 | .vscode 21 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "diff": true, 3 | "extensions": [".js", ".ts"], 4 | "package": "./package.json", 5 | "require": [ 6 | "ignore-styles", 7 | "ts-node/register", 8 | "tsconfig-paths/register" 9 | ], 10 | "exit": true, 11 | "recursive": true, 12 | "node-option": [ 13 | "es-module-specifier-resolution=node", 14 | "experimental-specifier-resolution=node", 15 | "loader=ts-node/esm" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.14.0 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.1.0" 3 | } 4 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:ipfs:p:ipfs-companion:r:messages-json] 5 | file_filter = add-on/_locales//messages.json 6 | source_file = add-on/_locales/en/messages.json 7 | source_lang = en 8 | type = CHROME 9 | minimum_perc = 40 10 | 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.14.0 2 | 3 | ARG USER_ID 4 | ARG GROUP_ID 5 | 6 | RUN curl -s https://ipfs.io/ipfs/QmbukYcmtyU6ZEKt6fepnvrTNa9F6VqsUPMUgNxQjEmphH > /usr/local/bin/jq && chmod +x /usr/local/bin/jq 7 | 8 | RUN mkdir -p /home/node/app 9 | RUN if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \ 10 | userdel -f node && \ 11 | if getent group node ; then groupdel node; fi && \ 12 | if getent passwd ${USER_ID} ; then userdel -f $(getent passwd ${USER_ID} | cut -d: -f1); fi && \ 13 | if getent group ${GROUP_ID} ; then groupdel $(getent group ${GROUP_ID} | cut -d: -f1); fi && \ 14 | groupadd -g ${GROUP_ID} node && \ 15 | useradd -l -u ${USER_ID} -g node node; fi 16 | RUN chown -fhR node:node /home/node 17 | 18 | WORKDIR /home/node/app 19 | 20 | COPY --chown=node:node ./package.json ./package-lock.json /home/node/app/ 21 | 22 | USER node 23 | 24 | RUN npm run ci:install 25 | 26 | COPY --chown=node:node . /home/node/app 27 | 28 | ENV PATH="/home/node/app/node_modules/.bin:${PATH}" 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Licensed under MIT. 2 | # Copyright (2016) by Kevin van Zonneveld https://twitter.com/kvz 3 | # 4 | # https://www.npmjs.com/package/fakefile 5 | # 6 | # Please do not edit this file directly, but propose changed upstream instead: 7 | # https://github.com/kvz/fakefile/blob/master/Makefile 8 | # 9 | # This Makefile offers convience shortcuts into any Node.js project that utilizes npm scripts. 10 | # It functions as a wrapper around the actual listed in `package.json` 11 | # So instead of typing: 12 | # 13 | # $ npm script build:assets 14 | # 15 | # you could also type: 16 | # 17 | # $ make build-assets 18 | # 19 | # Notice that colons (:) are replaced by dashes for Makefile compatibility. 20 | # 21 | # The benefits of this wrapper are: 22 | # 23 | # - You get to keep the the scripts package.json, which is more portable 24 | # (Makefiles & Windows are harder to mix) 25 | # - Offer a polite way into the project for developers coming from different 26 | # languages (npm scripts is obviously very Node centric) 27 | # - Profit from better autocomplete (make ) than npm currently offers. 28 | # OSX users will have to install bash-completion 29 | # (http://davidalger.com/development/bash-completion-on-os-x-with-brew/) 30 | 31 | ifeq ($(shell test -e ./yarn.lock && echo -n yes),yes) 32 | RUNNER=yarn 33 | INSTALLER=yarn install 34 | else 35 | RUNNER=npm run 36 | INSTALLER=npm install 37 | endif 38 | 39 | define npm_script_targets 40 | TARGETS := $(shell \ 41 | node -e 'for (var k in require("./package.json").scripts) {console.log(k.replace(/:/g, "-"));}' 42 | | grep -v -E "^install$$" 43 | ) 44 | $$(TARGETS): 45 | $(RUNNER) $(shell \ 46 | node -e 'for (var k in require("./package.json").scripts) {console.log(k.replace(/:/g, "-"), k);}' 47 | | grep -E "^$(MAKECMDGOALS)\s" 48 | | head -n1 49 | | awk '{print $$2}' 50 | ) 51 | 52 | .PHONY: $$(TARGETS) 53 | endef 54 | 55 | $(eval $(call npm_script_targets)) 56 | 57 | # These npm run scripts are available, without needing to be mentioned in `package.json` 58 | install: 59 | $(INSTALLER) 60 | -------------------------------------------------------------------------------- /PRIVACY-POLICY.md: -------------------------------------------------------------------------------- 1 | # **IPFS Companion Privacy Policy** 2 | 3 | First Posted: 2019-02-15
4 | Last Update: 2023-01-27
([change history](https://github.com/ipfs-shipyard/ipfs-companion/commits/main/PRIVACY-POLICY.md)) 5 | 6 | This Privacy Policy governs the use of the IPFS Companion browser extension 7 | offered by Protocol Labs, Inc. (“**IPFS Companion**” or the “**Service**”). 8 | 9 | The Service is offered subject to your acceptance without modification of all 10 | the terms and conditions herein, and all other other operating rules, policies 11 | and procedures that may be updated from time to time. By accessing or using any 12 | part of the Service, you agree to be bound by the terms and conditions of the 13 | Privacy Policy. If you do not agree to all of the terms and conditions of this 14 | Privacy Policy, then you may not access the Service. 15 | 16 | **Personal Information** 17 | 18 | We do not collect personal information from the users of the Service. 19 | 20 | **Metrics** 21 | 22 | We collect non-user-specific metrics via the Service. For more information on 23 | how to change your preferences with respect to the metrics, please contact us 24 | via methods mentioned below. 25 | 26 | **Additional Privacy Considerations** 27 | 28 | If you add files to the IPFS Network using the IPFS Companion extension, they 29 | will be stored on your local IPFS Network node. Those files are also then cached 30 | by anyone who retrieves those files from the IPFS network and co-hosted on that 31 | user’s local IPFS Network node. Generally, cached files will eventually expire, 32 | but it’s possible for a user with whom you have shared access to such files (by 33 | sharing the relevant Content Identifier or CID) to pin that data, which means 34 | the cached files then will not expire and will remain stored on such user’s 35 | local IPFS Network node. All content shared with the IPFS Network is public by 36 | default. This means your files and data that you’ve added will be accessible to 37 | everyone who knows the CID or queries the data on the IPFS Network. If you want 38 | to share certain materials or data privately, you must encrypt such data before 39 | adding it to the IPFS Network. 40 | 41 | 42 | If you are using “Linkify IPFS Addresses” or “Catch Unhandled IPFS Protocols” 43 | experiments, websites will be able to detect you are running IPFS Companion. 44 | This behavior can be changed on the Preferences screen by disabling mentioned experiments. 45 | 46 | 47 | If you are using DNSLink (its lookup is enabled by default), then the IPFS node 48 | will be executing DNS queries for all domain names visited during browsing, and 49 | those queries will use a DNS resolver configured in your operating system. To 50 | disable this behavior, set "DNSLink lookup" to "Off" in Preferences. 51 | 52 | 53 | **Contact Us** 54 | 55 | Questions about our Privacy Policy? Please contact us at 56 | . For general information, please reach out via a [new issue on our GitHub repo](https://github.com/ipfs/ipfs-companion/issues/new/choose). 57 | 58 | **Changes to our Privacy Policy** 59 | 60 | If we decide to change our Privacy Policy, we will post those changes on this 61 | page and also at https://ipfs.tech/companion-privacy. 62 | 63 | This document is CC-BY-SA. It was last updated January 24th, 2023. 64 | 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security notes 2 | 3 | The IPFS protocol and its implementations are still in heavy development. This means that there may be problems in our protocols, or there may be mistakes in our implementations. And — though IPFS is not production-ready yet — many people are already running nodes on their machines, so we take security vulnerabilities very seriously. 4 | 5 | ## Reporting security issues 6 | 7 | If you discover a security issue in ipfs-companion, please bring it to our attention right away! 8 | 9 | If you find a vulnerability that may affect live deployments — for example, by exposing a remote execution exploit — please send your report privately to security@ipfs.io **Please do not file a public issue.** 10 | 11 | If the issue is a protocol weakness that cannot be immediately exploited, or something not yet deployed, just discuss it openly. 12 | 13 | ## Local build and source verification 14 | 15 | Security-conscious users can confirm that downloaded package does match source code. 16 | 17 | Required steps: 18 | 19 | 1. Download package version that is to be verified 20 | 2. Check out sources of the [same tag](https://github.com/ipfs/ipfs-companion/tags) 21 | 2. Build package from sources using `yarn dev-build` command. 22 | As a result, you will have freshly built packages in `build/` directory 23 | 3. Unzip contents and compare manually or use handy one-liners below. 24 | 25 | Convention used in examples below: 26 | - `ipfs-firefox-addon1.xpi` and `ipfs-firefox-addon2.xpi` are equal and match Git sources 27 | - `ipfs-firefox-addon3.xpi` contains changes not present in current sources 28 | 29 | ### Compare SHA-256 30 | 31 | Note that XPIs are unzipped before being piped to `sha256sum`: 32 | ```bash 33 | $ diff -q <(unzip -p ipfs-firefox-addon1.xpi|sha256sum|cut -f1 -d' ') <(unzip -p ipfs-firefox-addon3.xpi|sha256sum|cut -f1 -d' ') && echo same || echo not_same 34 | ``` 35 | 36 | Sample output for files with matching contents: 37 | ```bash 38 | $ diff <(unzip -v -l ipfs-firefox-addon1.xpi | cut -c 1-9,59-,49-57 | sort -k3) <(unzip -v -l ipfs-firefox-addon2.xpi | cut -c 1-9,59-,49-57 | sort -k3) && echo same || echo not_same 39 | same 40 | 41 | ``` 42 | 43 | Sample output for files with different contents: 44 | ```bash 45 | $ diff -q <(unzip -p ipfs-firefox-addon1.xpi|sha256sum|cut -f1 -d' ') <(unzip -p ipfs-firefox-addon3.xpi|sha256sum|cut -f1 -d' ') && echo same || echo not_same 46 | Files /proc/self/fd/11 and /proc/self/fd/12 differ 47 | not_same 48 | ``` 49 | 50 | ### Inspect differences 51 | 52 | It is possible to list package contents with CRC-32 checksums provided by ZIP container: 53 | ```bash 54 | $ unzip -v -l ipfs-firefox-addon1.xpi | cut -c 1-9,59-,49-57 | sort -k3 55 | ``` 56 | To compare two packages in one command: 57 | ```bash 58 | $ diff <(unzip -v -l ipfs-firefox-addon1.xpi | cut -c 1-9,59-,49-57 | sort -k3) <(unzip -v -l ipfs-firefox-addon3.xpi | cut -c 1-9,59-,49-57 | sort -k3) && echo same || echo not_same 59 | ``` 60 | 61 | Sample output for two different versions of XPI: 62 | ```diff 63 | $ diff <(unzip -v -l ipfs-firefox-addon1.xpi | cut -c 1-9,59-,49-57 | sort -k3) <(unzip -v -l ipfs-firefox-addon3.xpi | cut -c 1-9,59-,49-57 | sort -k3) && echo same || echo not_same 64 | 5,7c5,7 65 | < 174 d991ba90 defaults/preferences/prefs.js 66 | < 37615 30 files 67 | < 2270 06d511c2 harness-options.json 68 | --- 69 | > 225 5b287a38 defaults/preferences/prefs.js 70 | > 38998 30 files 71 | > 2315 6291e716 harness-options.json 72 | 13c13 73 | < 385 c2c13711 options.xul 74 | --- 75 | > 513 4f3e0732 options.xul 76 | 33,34c33,34 77 | < 6376 45297f28 resources/ipfs-firefox-addon/lib/index.js 78 | < 966 e48f0540 resources/ipfs-firefox-addon/lib/package.json 79 | --- 80 | > 7517 7565d9af resources/ipfs-firefox-addon/lib/index.js 81 | > 984 83079aef resources/ipfs-firefox-addon/lib/package.json 82 | not_same 83 | ``` 84 | -------------------------------------------------------------------------------- /add-on/icons/brave-ipfs-logo-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /add-on/icons/brave-ipfs-logo-on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /add-on/icons/ipfs-logo-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ipfs-logo-off 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /add-on/icons/ipfs-logo-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ipfs-logo-on 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /add-on/icons/png/ipfs-logo-off_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/icons/png/ipfs-logo-off_128.png -------------------------------------------------------------------------------- /add-on/icons/png/ipfs-logo-off_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/icons/png/ipfs-logo-off_19.png -------------------------------------------------------------------------------- /add-on/icons/png/ipfs-logo-off_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/icons/png/ipfs-logo-off_38.png -------------------------------------------------------------------------------- /add-on/icons/png/ipfs-logo-on_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/icons/png/ipfs-logo-on_128.png -------------------------------------------------------------------------------- /add-on/icons/png/ipfs-logo-on_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/icons/png/ipfs-logo-on_19.png -------------------------------------------------------------------------------- /add-on/icons/png/ipfs-logo-on_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/icons/png/ipfs-logo-on_38.png -------------------------------------------------------------------------------- /add-on/images/ipld.svg: -------------------------------------------------------------------------------- 1 | cenas_color -------------------------------------------------------------------------------- /add-on/images/multiformats.svg: -------------------------------------------------------------------------------- 1 | ipfs-icons-size-corrected -------------------------------------------------------------------------------- /add-on/images/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/ipfs-companion/7d29eff9f6ba496b356f5cd917bc8502d5b7a885/add-on/images/stars.png -------------------------------------------------------------------------------- /add-on/manifest.brave-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAshQhe/y/j0adZqOFes0nqgRgjDNx4eX6oNrESKeKbpUjH9eSN+lCYqBM3PT+BhPxo+sj/aVgCYsddiIbO43Bq/LsQLBFd+kD1I4qZSN4pJAX9AdsbMmXR9XV0W/O9zlyqkXAfxV13Hwmy+e6IH3p59ytQbpcuLnyipspQ4VXZprLkiWdvPMdifT9wgf5gmD30S1n7uaNrKCu8yZk/Lz5Z+KjoxRdk7X7FJYW+hoUGKb6Ld3Q99iLeKPIvcTjK6/xNHXTbaZfRYbfI8i/mSaxetGxSo7/XkMB8VvAiUkZ4gVSp786oMciQVwK2UyVFXw9pJhGD+O4ozcNk0PSq8aE7QIDAQAB" 3 | } 4 | -------------------------------------------------------------------------------- /add-on/manifest.brave.json: -------------------------------------------------------------------------------- 1 | { 2 | "sockets": { 3 | "udp": { 4 | "send": "*", 5 | "bind": "*" 6 | }, 7 | "tcp": { 8 | "connect": "*" 9 | }, 10 | "tcpServer": { 11 | "listen": "*:*" 12 | } 13 | }, 14 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi+PCLMME8x15VZQqUn+vBUJH8oqGqNiUX6lYLBi5w3HwSGcknEyCF5LDJv7tR3yeSxD8PhohQVJd2+5WB4BJkvzVR3F6uHS7hrgZ0lcq+xa+q4c1At35332C//ahX+ZvK3/n9v20jzJ8xdesQmfG8coFIyOZfOQ/owTno+QoPrUO9syXeG6nbYQnyfDip+UXe663zfBiNmwuVPo8R58zAOmpz7yAlCH+yEmj1YjQYpqbtYHwJwvN4elGF9wthgFNxoIZiqbe0wTUZiNjC1bZPiAed3+WftK0/P6czFpIP4SzjXszVps93l+yI15OB7VoeFu6oQk5G0d1/38W7GotUwIDAQAB" 15 | } 16 | -------------------------------------------------------------------------------- /add-on/manifest.chromium.json: -------------------------------------------------------------------------------- 1 | { 2 | "minimum_chrome_version": "111", 3 | "background": { 4 | "service_worker": "dist/bundles/backgroundPage.bundle.js" 5 | }, 6 | "permissions": [ 7 | "activeTab", 8 | "clipboardWrite", 9 | "contextMenus", 10 | "declarativeNetRequest", 11 | "declarativeNetRequestFeedback", 12 | "idle", 13 | "notifications", 14 | "scripting", 15 | "storage", 16 | "tabs", 17 | "unlimitedStorage", 18 | "webNavigation", 19 | "webRequest" 20 | ], 21 | "incognito": "not_allowed" 22 | } 23 | -------------------------------------------------------------------------------- /add-on/manifest.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_manifest_extensionName__", 4 | "short_name": "__MSG_manifest_shortExtensionName__", 5 | "version": "3.2.0", 6 | "description": "__MSG_manifest_extensionDescription__", 7 | "homepage_url": "https://github.com/ipfs-shipyard/ipfs-companion", 8 | "author": "IPFS Community", 9 | "icons": { 10 | "19": "icons/png/ipfs-logo-on_19.png", 11 | "38": "icons/png/ipfs-logo-on_38.png", 12 | "128": "icons/png/ipfs-logo-on_128.png" 13 | }, 14 | "action": { 15 | "default_icon": { 16 | "19": "icons/png/ipfs-logo-off_19.png", 17 | "38": "icons/png/ipfs-logo-off_38.png", 18 | "128": "icons/png/ipfs-logo-off_128.png" 19 | }, 20 | "default_title": "__MSG_browserAction_title__", 21 | "default_popup": "dist/popup/browser-action/index.html" 22 | }, 23 | "options_ui": { 24 | "open_in_tab": true, 25 | "browser_style": false, 26 | "page": "dist/options/options.html" 27 | }, 28 | "host_permissions": [ 29 | "" 30 | ], 31 | "web_accessible_resources": [ 32 | { 33 | "resources": [ 34 | "icons/png/ipfs-logo-off_19.png", 35 | "icons/png/ipfs-logo-off_38.png", 36 | "icons/png/ipfs-logo-off_128.png", 37 | "icons/ipfs-logo-on.svg", 38 | "icons/ipfs-logo-off.svg", 39 | "dist/recovery/recovery.css", 40 | "dist/recovery/recovery.html", 41 | "dist/recovery/recovery.js" 42 | ], 43 | "matches": [ 44 | "" 45 | ] 46 | } 47 | ], 48 | "content_security_policy": { 49 | "extension_pages": "script-src 'self'; object-src 'self'; frame-src 'self';" 50 | }, 51 | "default_locale": "en" 52 | } 53 | -------------------------------------------------------------------------------- /add-on/manifest.firefox-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser_specific_settings": { 3 | "gecko": { 4 | "id": "ipfs-companion-dev-build@ci.ipfs.team", 5 | "strict_min_version": "111.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /add-on/manifest.firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "browser_style": false 4 | }, 5 | "options_ui": { 6 | "browser_style": false 7 | }, 8 | "background": { 9 | "scripts": ["dist/bundles/backgroundPage.firefox.bundle.js"] 10 | }, 11 | "browser_specific_settings": { 12 | "gecko": { 13 | "id": "ipfs-firefox-addon@lidel.org", 14 | "strict_min_version": "111.0" 15 | } 16 | }, 17 | "permissions": [ 18 | "idle", 19 | "tabs", 20 | "notifications", 21 | "proxy", 22 | "storage", 23 | "unlimitedStorage", 24 | "contextMenus", 25 | "clipboardWrite", 26 | "webNavigation", 27 | "webRequest", 28 | "webRequestBlocking" 29 | ], 30 | "content_scripts": [ ], 31 | "protocol_handlers": [ 32 | { 33 | "protocol": "web+dweb", 34 | "name": "IPFS Companion: DWEB Protocol Handler", 35 | "uriTemplate": "https://dweb.link/ipfs/?uri=%s" 36 | }, 37 | { 38 | "protocol": "web+ipns", 39 | "name": "IPFS Companion: IPNS Protocol Handler", 40 | "uriTemplate": "https://dweb.link/ipns/?uri=%s" 41 | }, 42 | { 43 | "protocol": "web+ipfs", 44 | "name": "IPFS Companion: IPFS Protocol Handler", 45 | "uriTemplate": "https://dweb.link/ipfs/?uri=%s" 46 | }, 47 | { 48 | "protocol": "dweb", 49 | "name": "IPFS Companion: DWEB Protocol Handler", 50 | "uriTemplate": "https://dweb.link/ipfs/?uri=%s" 51 | }, 52 | { 53 | "protocol": "ipns", 54 | "name": "IPFS Companion: IPNS Protocol Handler", 55 | "uriTemplate": "https://dweb.link/ipns/?uri=%s" 56 | }, 57 | { 58 | "protocol": "ipfs", 59 | "name": "IPFS Companion: IPFS Protocol Handler", 60 | "uriTemplate": "https://dweb.link/ipfs/?uri=%s" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /add-on/src/background/background.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import createIpfsCompanion from '../lib/ipfs-companion.js' 6 | import { onInstalled } from '../lib/on-installed.js' 7 | import { getUninstallURL } from '../lib/on-uninstalled.js' 8 | 9 | // register lifecycle hooks early, otherwise we miss first install event 10 | browser.runtime.onInstalled.addListener(onInstalled) 11 | browser.runtime.setUninstallURL(getUninstallURL(browser)) 12 | 13 | const init = async () => { 14 | await createIpfsCompanion() 15 | } 16 | 17 | init() 18 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/permissions/request.css: -------------------------------------------------------------------------------- 1 | @import url('~tachyons/css/tachyons.css'); 2 | @import url('~ipfs-css/ipfs.css'); 3 | 4 | #left-col { 5 | background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); 6 | background-size: 100%; 7 | background-repeat: repeat; 8 | } 9 | 10 | a:hover { 11 | text-decoration: none; 12 | } 13 | 14 | a:visited { 15 | color: inherit; 16 | } 17 | 18 | /* 19 | https://github.com/tachyons-css/tachyons-queries 20 | Tachyons: $point == large 21 | */ 22 | @media (min-width: 60em) { 23 | #left-col { 24 | position: fixed; 25 | top: 0; 26 | right: 55%; 27 | width: 45%; 28 | background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); 29 | background-size: 100%; 30 | background-repeat: repeat; 31 | } 32 | 33 | #right-col { 34 | margin-left: 54%; 35 | margin-right: 6%; 36 | } 37 | } 38 | 39 | @media (max-height: 800px) { 40 | #left-col img { 41 | width: 98px !important; 42 | height: 98px !important; 43 | } 44 | 45 | #left-col svg { 46 | width: 60px; 47 | } 48 | } 49 | 50 | .recovery-root { 51 | width: 100%; 52 | height: 100%; 53 | text-align: left; 54 | } 55 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/permissions/request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IPFS Node is Offline 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/permissions/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import choo from 'choo' 5 | import html from 'choo/html/index.js' 6 | import { i18n, runtime, permissions } from 'webextension-polyfill' 7 | import { nodeOffSvg } from '../welcome/page.js' 8 | import createWelcomePageStore from '../welcome/store.js' 9 | import { optionsPage } from '../../lib/constants.js' 10 | import './request.css' 11 | 12 | const app = choo() 13 | 14 | const learnMoreLink = html`${i18n.getMessage('request_permissions_page_learn_more')}` 15 | 16 | const optionsPageLink = html`${i18n.getMessage('recovery_page_update_preferences')}` 17 | 18 | // TODO (whizzzkid): refactor base store to be more generic. 19 | app.use(createWelcomePageStore(i18n, runtime)) 20 | // Register our single route 21 | app.route('*', () => { 22 | runtime.sendMessage({ telemetry: { trackView: 'request-permissions' } }) 23 | const requestPermission = async () => { 24 | await permissions.request({ origins: [''] }) 25 | runtime.reload() 26 | } 27 | 28 | return html`
29 |
30 |
31 | ${nodeOffSvg(200)} 32 |

${i18n.getMessage('request_permissions_page_sub_header')}

33 |
34 |
35 | 36 |
37 |

${i18n.getMessage('request_permissions_page_message_p1')}

38 |

${i18n.getMessage('request_permissions_page_message_p2')}

39 | 45 |

46 | ${learnMoreLink} | ${optionsPageLink} 47 | 48 |

49 |
` 50 | }) 51 | 52 | // Start the application and render it to the given querySelector 53 | app.mount('#root') 54 | 55 | // Set page title and header translation 56 | document.title = i18n.getMessage('request_permissions_page_title') 57 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/welcome/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/welcome/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import './welcome.css' 5 | 6 | import browser from 'webextension-polyfill' 7 | import choo from 'choo' 8 | import createWelcomePageStore from './store.js' 9 | import createWelcomePage from './page.js' 10 | 11 | const app = choo() 12 | 13 | app.use(createWelcomePageStore(browser.i18n, browser.runtime)) 14 | app.route('*', createWelcomePage(browser.i18n)) 15 | app.mount('#root') 16 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/welcome/store.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | import browser from 'webextension-polyfill' 4 | 5 | export default function createWelcomePageStore (i18n, runtime) { 6 | return function welcomePageStore (state, emitter) { 7 | state.isIpfsOnline = null 8 | state.peerCount = null 9 | state.webuiRootUrl = null 10 | let port 11 | emitter.on('DOMContentLoaded', async () => { 12 | browser.runtime.sendMessage({ telemetry: { trackView: 'welcome' } }) 13 | emitter.emit('render') 14 | port = runtime.connect({ name: 'browser-action-port' }) 15 | port.onMessage.addListener(async (message) => { 16 | if (message.statusUpdate) { 17 | const webuiRootUrl = message.statusUpdate.webuiRootUrl 18 | const peerCount = message.statusUpdate.peerCount 19 | const isIpfsOnline = peerCount > -1 20 | if (isIpfsOnline !== state.isIpfsOnline || peerCount !== state.peerCount || webuiRootUrl !== state.webuiRootUrl) { 21 | state.webuiRootUrl = webuiRootUrl 22 | state.isIpfsOnline = isIpfsOnline 23 | state.peerCount = peerCount 24 | emitter.emit('render') 25 | } 26 | } 27 | }) 28 | }) 29 | 30 | emitter.on('openWebUi', async (page = '/') => { 31 | const url = `${state.webuiRootUrl}#${page}` 32 | try { 33 | await browser.tabs.create({ url }) 34 | } catch (error) { 35 | console.error(`Unable Open Web UI (${url})`, error) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /add-on/src/landing-pages/welcome/welcome.css: -------------------------------------------------------------------------------- 1 | @import url('~tachyons/css/tachyons.css'); 2 | @import url('~ipfs-css/ipfs.css'); 3 | @import url('../../popup/heartbeat.css'); 4 | 5 | #left-col { 6 | background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); 7 | background-size: 100%; 8 | background-repeat: repeat; 9 | } 10 | 11 | .underline-under { 12 | text-decoration: underline; 13 | text-underline-position: under; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | .state-unknown { 21 | opacity: 0; 22 | filter: blur( .15em ); 23 | } 24 | 25 | /* 26 | https://github.com/tachyons-css/tachyons-queries 27 | Tachyons: $point == large 28 | */ 29 | @media (min-width: 64em) { 30 | #left-col { 31 | position: fixed; 32 | top: 0; 33 | right: 50%; 34 | width: 50%; 35 | background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); 36 | background-size: 100%; 37 | background-repeat: repeat; 38 | } 39 | 40 | #right-col { 41 | margin-left: 50%; 42 | } 43 | } 44 | 45 | @media (max-height: 800px) { 46 | #left-col img { 47 | width: 98px !important; 48 | height: 98px !important; 49 | } 50 | 51 | #left-col svg { 52 | width: 60px; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /add-on/src/lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | export const welcomePage = '/dist/landing-pages/welcome/index.html' 5 | export const optionsPage = '/dist/options/options.html' 6 | export const recoveryPagePath = '/dist/recovery/recovery.html' 7 | export const requestRequiredPermissionsPage = '/dist/landing-pages/permissions/request.html' 8 | export const tickMs = 250 // no CPU spike, but still responsive enough 9 | -------------------------------------------------------------------------------- /add-on/src/lib/context-menus/ContextMenus.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import debug from 'debug' 3 | 4 | type listenerCb = (info: browser.Menus.OnClickData, tab: browser.Tabs.Tab | undefined) => void 5 | 6 | /** 7 | * ContextMenus is a wrapper around browser.contextMenus API. 8 | */ 9 | export class ContextMenus { 10 | private readonly contextMenuListeners = new Map() 11 | private readonly log: debug.Debugger & { error?: debug.Debugger } 12 | 13 | constructor () { 14 | this.log = debug('ipfs-companion:contextMenus') 15 | this.log.error = debug('ipfs-companion:contextMenus:error') 16 | this.contextMenuListeners = new Map() 17 | this.init() 18 | } 19 | 20 | /** 21 | * init is called once on extension startup 22 | */ 23 | init (): void { 24 | browser.contextMenus.onClicked.addListener((info, tab) => { 25 | const { menuItemId } = info 26 | if (this.contextMenuListeners.has(menuItemId)) { 27 | this.contextMenuListeners.get(menuItemId)?.forEach(cb => cb(info, tab)) 28 | } 29 | }) 30 | this.log('ContextMenus Listeners ready') 31 | } 32 | 33 | /** 34 | * This method queues the listener function for given menuItemId. 35 | * 36 | * @param menuItemId 37 | * @param cb 38 | */ 39 | queueListener (menuItemId: string, cb: listenerCb): void { 40 | if (this.contextMenuListeners.has(menuItemId)) { 41 | this.contextMenuListeners.get(menuItemId)?.push(cb) 42 | } else { 43 | this.contextMenuListeners.set(menuItemId, [cb]) 44 | } 45 | this.log(`ContextMenus Listener queued for ${menuItemId}`) 46 | } 47 | 48 | /** 49 | * This method creates a context menu item and maps the listener function to it. 50 | * 51 | * @param options 52 | * @param cb 53 | */ 54 | create (options: browser.Menus.CreateCreatePropertiesType, cb?: listenerCb): void { 55 | try { 56 | browser.contextMenus.create(options) 57 | } catch (err) { 58 | this.log.error?.('ContextMenus.create failed', err) 59 | } 60 | if (cb != null) { 61 | if (options?.id != null) { 62 | this.queueListener(options.id, cb) 63 | } else { 64 | throw new Error('ContextMenus.create callback requires options.id') 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /add-on/src/lib/copier.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import browser from 'webextension-polyfill' 4 | import { findValueForContext } from './context-menus.js' 5 | 6 | /** 7 | * Writes text to the clipboard. 8 | * 9 | * @param {string} text 10 | */ 11 | async function writeToClipboard (text) { 12 | try { 13 | await navigator.clipboard.writeText(text) 14 | return true 15 | } catch (error) { 16 | // This can happen if the user denies clipboard permissions. 17 | // or the current page is not allowed to access the clipboard. 18 | // no need to log this error, as it is expected in some cases. 19 | return false 20 | } 21 | } 22 | 23 | /** 24 | * Gets the current active tab. 25 | * 26 | * @returns {Promise} 27 | */ 28 | async function getCurrentTab () { 29 | const queryOptions = { active: true, lastFocusedWindow: true } 30 | // `tab` will either be a `tabs.Tab` instance or `undefined`. 31 | const [tab] = await browser.tabs.query(queryOptions) 32 | return tab 33 | } 34 | 35 | /** 36 | * This is the MV3 version of copyTextToClipboard. It uses executeScript to run a function 37 | * in the context of the current tab. This is necessary because the clipboard API is not 38 | * available in the background script. 39 | * 40 | * Manifest Perms: "scripting", "activeTab" 41 | * 42 | * See: 43 | * - https://developer.chrome.com/docs/extensions/reference/scripting/ 44 | * - https://developer.chrome.com/blog/Offscreen-Documents-in-Manifest-v3/ 45 | * 46 | * ServiceWorkers will most likely have access to the clipboard in the future. 47 | * 48 | * @param {string} text 49 | */ 50 | async function copyTextToClipboardFromCurrentTab (text) { 51 | const tab = await getCurrentTab() 52 | if (!tab) { 53 | throw new Error('Unable to get current tab') 54 | } 55 | 56 | const [{ result }] = await browser.scripting.executeScript({ 57 | target: { tabId: tab.id }, 58 | func: writeToClipboard, 59 | args: [text] 60 | }) 61 | 62 | if (!result) { 63 | throw new Error('Unable to write to clipboard') 64 | } 65 | } 66 | 67 | async function copyTextToClipboard (text, notify) { 68 | try { 69 | if (typeof navigator.clipboard !== 'undefined') { // Firefox 70 | await writeToClipboard(text) 71 | } else { 72 | await copyTextToClipboardFromCurrentTab(text) 73 | } 74 | notify('notify_copiedTitle', text) 75 | } catch (error) { 76 | console.error('[ipfs-companion] Failed to copy text', error) 77 | notify('notify_addonIssueTitle', 'Unable to copy') 78 | } 79 | } 80 | 81 | export default function createCopier (notify, ipfsPathValidator) { 82 | return { 83 | async copyTextToClipboard (text) { 84 | await copyTextToClipboard(text, notify) 85 | }, 86 | 87 | async copyCanonicalAddress (context, contextType) { 88 | const url = await findValueForContext(context, contextType) 89 | const ipfsPath = ipfsPathValidator.resolveToIpfsPath(url) 90 | await copyTextToClipboard(ipfsPath, notify) 91 | }, 92 | 93 | async copyCidAddress (context, contextType) { 94 | const url = await findValueForContext(context, contextType) 95 | const ipfsPath = await ipfsPathValidator.resolveToImmutableIpfsPath(url) 96 | await copyTextToClipboard(ipfsPath, notify) 97 | }, 98 | 99 | async copyRawCid (context, contextType) { 100 | const url = await findValueForContext(context, contextType) 101 | try { 102 | const cid = await ipfsPathValidator.resolveToCid(url) 103 | await copyTextToClipboard(cid, notify) 104 | } catch (error) { 105 | console.error('Unable to resolve/copy direct CID:', error.message) 106 | if (notify) { 107 | const errMsg = error.toString() 108 | if (errMsg.startsWith('Error: no link')) { 109 | // Sharding support is limited: 110 | // - https://github.com/ipfs/js-ipfs/issues/1279 111 | // - https://github.com/ipfs/go-ipfs/issues/5270 112 | notify('notify_addonIssueTitle', 'Unable to resolve CID within HAMT-sharded directory, sorry! Will be fixed soon.') 113 | } else { 114 | notify('notify_addonIssueTitle', 'notify_inlineErrorMsg', error.message) 115 | } 116 | } 117 | } 118 | }, 119 | 120 | async copyAddressAtPublicGw (context, contextType) { 121 | const url = await findValueForContext(context, contextType) 122 | const publicUrl = await ipfsPathValidator.resolveToPublicUrl(url) 123 | await copyTextToClipboard(publicUrl, notify) 124 | }, 125 | 126 | async copyPermalink (context, contextType) { 127 | const url = await findValueForContext(context, contextType) 128 | const permalink = await ipfsPathValidator.resolveToPermalink(url) 129 | await copyTextToClipboard(permalink, notify) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /add-on/src/lib/inspector.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import browser from 'webextension-polyfill' 4 | import { findValueForContext } from './context-menus.js' 5 | import { pathAtHttpGateway } from './ipfs-path.js' 6 | 7 | export default function createInspector (notify, ipfsPathValidator, getState) { 8 | return { 9 | async viewOnGateway (context, contextType) { 10 | const url = await findValueForContext(context, contextType) 11 | const ipfsPath = ipfsPathValidator.resolveToIpfsPath(url) 12 | const gateway = getState().pubGwURLString 13 | const gatewayUrl = pathAtHttpGateway(ipfsPath, gateway) 14 | await browser.tabs.create({ url: gatewayUrl }) 15 | } 16 | // TODO: view in WebUI's Files 17 | // TODO: view in WebUI's IPLD Explorer 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/external.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser */ 3 | 4 | import debug from 'debug' 5 | 6 | import { create } from 'kubo-rpc-client' 7 | const log = debug('ipfs-companion:client:external') 8 | log.error = debug('ipfs-companion:client:external:error') 9 | 10 | export async function init (browser, opts) { 11 | log(`init with IPFS API at ${opts.apiURLString}`) 12 | const clientConfig = opts.apiURLString 13 | const api = await create(clientConfig) 14 | return api 15 | } 16 | 17 | export async function destroy (browser) { 18 | log('destroy') 19 | } 20 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env browser, webextensions */ 4 | 5 | import debug from 'debug' 6 | 7 | import { precache } from '../precache.js' 8 | import * as brave from './brave.js' 9 | import * as external from './external.js' 10 | import { 11 | InternalTabReloader, 12 | LocalGatewayReloader, 13 | WebUiReloader, 14 | prepareReloadExtensions 15 | } from './reloaders/index.js' 16 | const log = debug('ipfs-companion:client') 17 | log.error = debug('ipfs-companion:client:error') 18 | 19 | // ensure single client at all times, and no overlap between init and destroy 20 | let client 21 | 22 | export async function initIpfsClient (browser, opts, inQuickImport) { 23 | log('init ipfs client') 24 | if (client) return // await destroyIpfsClient() 25 | let backend 26 | switch (opts.ipfsNodeType) { 27 | case 'external:brave': 28 | backend = brave 29 | break 30 | case 'external': 31 | backend = external 32 | break 33 | default: 34 | throw new Error(`Unsupported ipfsNodeType: ${opts.ipfsNodeType}`) 35 | } 36 | const instance = await backend.init(browser, opts) 37 | if (!inQuickImport) { 38 | _reloadIpfsClientDependents(browser, instance, opts) // async (API is present) 39 | } 40 | client = backend 41 | return instance 42 | } 43 | 44 | export async function destroyIpfsClient (browser) { 45 | log('destroy ipfs client') 46 | if (!client) return 47 | try { 48 | await client.destroy(browser) 49 | await _reloadIpfsClientDependents(browser) // sync (API stopped working) 50 | } finally { 51 | client = null 52 | } 53 | } 54 | 55 | /** 56 | * Reloads pages dependant on ipfs to be online 57 | * 58 | * @typedef {brave|external} Browser 59 | * @param {Browser} browser 60 | * @param {import('kubo-rpc-client').default} instance 61 | * @param {Object} opts 62 | * @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]=} reloadExtensions 63 | * @returns {void} 64 | */ 65 | async function _reloadIpfsClientDependents ( 66 | browser, instance, opts, reloadExtensions = [WebUiReloader, LocalGatewayReloader, InternalTabReloader]) { 67 | // online || offline 68 | if (browser.tabs && browser.tabs.query) { 69 | const tabs = await browser.tabs.query({}) 70 | if (tabs) { 71 | try { 72 | const reloadExtensionInstances = await prepareReloadExtensions(reloadExtensions, browser, log) 73 | // the reload process is async, fire and forget. 74 | reloadExtensionInstances.forEach(ext => ext.reload(tabs)) 75 | } catch (e) { 76 | log('Failed to trigger reloaders') 77 | } 78 | } 79 | } 80 | 81 | // online only 82 | if (client && instance && opts) { 83 | // add important data to local ipfs repo for instant load 84 | setTimeout(() => precache(instance, opts), 5000) 85 | } 86 | } 87 | 88 | /** 89 | * Reloads local gateway pages dependant on ipfs to be online 90 | * 91 | * @typedef {brave|external} Browser 92 | * @param {Browser} browser 93 | * @param {import('kubo-rpc-client').default} instance 94 | * @param {Object} opts 95 | * @returns {void} 96 | */ 97 | export function reloadIpfsClientOfflinePages (browser, instance, opts) { 98 | _reloadIpfsClientDependents(browser, instance, opts, [LocalGatewayReloader]) 99 | } 100 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/reloaders/index.js: -------------------------------------------------------------------------------- 1 | import InternalTabReloader from './internalTabReloader.js' 2 | import LocalGatewayReloader from './localGatewayReloader.js' 3 | import WebUiReloader from './webUiReloader.js' 4 | 5 | /** 6 | * Prepares extension by creating an instance and awaiting for init. 7 | * 8 | * @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]} extensions 9 | * @param {Browser} browserInstance 10 | * @param {Logger} loggerInstance 11 | * @returns {Promise} 12 | */ 13 | function prepareReloadExtensions (extensions, browserInstance, loggerInstance) { 14 | const reloadExtensions = Array.isArray(extensions) ? extensions : [extensions] 15 | return Promise.all(reloadExtensions 16 | .map(async Ext => { 17 | try { 18 | const ext = new Ext(browserInstance, loggerInstance) 19 | await ext.init() 20 | return ext 21 | } catch (e) { 22 | loggerInstance(`Extension Instance Failed to Initialize with error: ${e}. Extension: ${Ext}`) 23 | } 24 | }) 25 | ) 26 | } 27 | 28 | export { 29 | InternalTabReloader, 30 | LocalGatewayReloader, 31 | WebUiReloader, 32 | prepareReloadExtensions 33 | } 34 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/reloaders/internalTabReloader.js: -------------------------------------------------------------------------------- 1 | import ReloaderBase from './reloaderBase.js' 2 | 3 | export default class InternalTabReloader extends ReloaderBase { 4 | /** 5 | * Setting up the extension origin. 6 | */ 7 | init () { 8 | this.extensionOrigin = this._browserInstance.runtime.getURL('/') 9 | this._log('InternalTabReloader Ready for use.') 10 | } 11 | 12 | /** 13 | * Performs url validation for the tab. If tab is a WebUI tab. 14 | * 15 | * @param {Object} tab 16 | * @param {string} tab.url 17 | * @returns {boolean} 18 | */ 19 | validation ({ url }) { 20 | return url.startsWith(this.extensionOrigin) 21 | } 22 | 23 | /** 24 | * Returns message when reloading the tab. 25 | * 26 | * @param {Object} tab 27 | * @param {string} tab.url 28 | * @returns {string} message. 29 | */ 30 | message ({ url }) { 31 | return `reloading internal extension page at ${url}` 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/reloaders/localGatewayReloader.js: -------------------------------------------------------------------------------- 1 | import ReloaderBase from './reloaderBase.js' 2 | import isIPFS from 'is-ipfs' 3 | 4 | export default class LocalGatewayReloader extends ReloaderBase { 5 | /** 6 | * Performs url validation for the tab. If tab is loaded via local gateway. 7 | * 8 | * @param {Object} tab 9 | * @param {string} tab.url 10 | * @param {string} tab.url 11 | * @returns {boolean} 12 | */ 13 | validation ({ url, title }) { 14 | /** 15 | * Check if the url is the local gateway url and if the title is contained within the url then it was not loaded. 16 | * - This assumes that the title of most pages on the web will be set and hence when not reachable, the browser 17 | * will set title to the url/host (both chrome and brave) and 'problem loading page' for firefox. 18 | * - There is probability that this might be true in case the tag is omitted, but worst case it only reloads 19 | * those pages. 20 | * - The benefit we get from this approach is the static nature of just observing the tabs in their current state 21 | * which reduces the overhead of injecting content scripts to track urls that were loaded after the connection 22 | * was offline, it may also need extra permissions to inject code on error pages. 23 | */ 24 | return (isIPFS.url(url) || isIPFS.subdomain(url)) && 25 | (url.includes(title) || title.toLowerCase() === 'problem loading page') 26 | } 27 | 28 | /** 29 | * Returns message when reloading the tab. 30 | * 31 | * @param {Object} tab 32 | * @param {string} tab.url 33 | * @returns {string} message. 34 | */ 35 | message ({ url }) { 36 | return `reloading local gateway at ${url}` 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/reloaders/reloaderBase.js: -------------------------------------------------------------------------------- 1 | export default class ReloaderBase { 2 | /** 3 | * Constructor for reloader base class. 4 | * 5 | * @param {Browser} browser 6 | * @param {Logger} log 7 | */ 8 | constructor (browser, log) { 9 | if (!browser || !log) { 10 | throw new Error('Instances of browser and logger are needed!') 11 | } 12 | this._browserInstance = browser 13 | this._log = log 14 | }; 15 | 16 | /** 17 | * Initializes the instance. 18 | */ 19 | init () { 20 | this._log('Initialized without additional config.') 21 | } 22 | 23 | /** 24 | * To be implemented in child class. 25 | */ 26 | validation () { 27 | throw new Error('Validation: Method Not Implemented') 28 | } 29 | 30 | /** 31 | * To be implemented in child class. 32 | */ 33 | message () { 34 | throw new Error('Message: Method Not Implemented') 35 | } 36 | 37 | /** 38 | * Handles reload for all tabs. 39 | * params {Array<tabs>} 40 | */ 41 | reload (tabs) { 42 | tabs 43 | .filter(tab => this.validation(tab)) 44 | .forEach(tab => { 45 | this._log(this.message(tab)) 46 | this._browserInstance.tabs.reload(tab.id) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /add-on/src/lib/ipfs-client/reloaders/webUiReloader.js: -------------------------------------------------------------------------------- 1 | import ReloaderBase from './reloaderBase.js' 2 | 3 | export default class WebUiReloader extends ReloaderBase { 4 | /** 5 | * Performs url validation for the tab. If tab is a WebUI tab. 6 | * 7 | * @param {Object} tab 8 | * @returns {boolean} 9 | */ 10 | validation ({ url }) { 11 | const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/') 12 | const ipns = url.includes('/webui.ipfs.io/#/') 13 | return bundled || ipns 14 | } 15 | 16 | /** 17 | * Returns message when reloading the tab. 18 | * 19 | * @param {Object} tab 20 | * @param {string} tab.url 21 | * @returns {string} message. 22 | */ 23 | message ({ url }) { 24 | return `reloading webui at ${url}` 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /add-on/src/lib/notifier.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import browser from 'webextension-polyfill' 4 | import debug from 'debug' 5 | const log = debug('ipfs-companion:notifier') 6 | log.error = debug('ipfs-companion:notifier:error') 7 | 8 | export default function createNotifier (getState) { 9 | const { getMessage } = browser.i18n 10 | return async (titleKey, messageKey, messageParam) => { 11 | const title = browser.i18n.getMessage(titleKey) || titleKey 12 | let message 13 | if (messageKey.startsWith('notify_')) { 14 | message = messageParam ? getMessage(messageKey, messageParam) : getMessage(messageKey) 15 | } else { 16 | message = messageKey 17 | } 18 | log(`${title}: ${message}`) 19 | if (getState().displayNotifications && browser && browser.notifications.create) { 20 | try { 21 | return await browser.notifications.create({ 22 | type: 'basic', 23 | iconUrl: browser.runtime.getURL('icons/ipfs-logo-on.svg'), 24 | title, 25 | message 26 | }) 27 | } catch (err) { 28 | log.error('failed to create a notification', err) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /add-on/src/lib/on-installed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import debug from 'debug' 6 | import { requestRequiredPermissionsPage, welcomePage } from './constants.js' 7 | import { brave, braveNodeType } from './ipfs-client/brave.js' 8 | 9 | const { version } = browser.runtime.getManifest() 10 | export const updatePage = 'https://github.com/ipfs-shipyard/ipfs-companion/releases/tag/v' 11 | 12 | export async function onInstalled (details) { 13 | // details.temporary === run via `npm run firefox` 14 | if (details.reason === 'install' || details.temporary) { 15 | await browser.storage.local.set({ onInstallTasks: 'onFirstInstall' }) 16 | } else if (details.reason === 'update' || details.temporary) { 17 | await browser.storage.local.set({ onInstallTasks: 'onVersionUpdate' }) 18 | } 19 | } 20 | 21 | export async function runPendingOnInstallTasks () { 22 | const { onInstallTasks, displayReleaseNotes } = await browser.storage.local.get(['onInstallTasks', 'displayReleaseNotes']) 23 | await browser.storage.local.remove('onInstallTasks') 24 | // this is needed because `permissions.request` cannot be called from a script. If that happens the browser will 25 | // throws: Error: permissions.request may only be called from a user input handler 26 | // To avoid this, we open a new tab with the permissions page and ask the user to grant the permissions. 27 | // That makes the request valid and allows us to gain access to the permissions. 28 | if (!(await browser.permissions.contains({ origins: ['<all_urls>'] }))) { 29 | return browser.tabs.create({ 30 | url: requestRequiredPermissionsPage 31 | }) 32 | } 33 | switch (onInstallTasks) { 34 | case 'onFirstInstall': 35 | await useNativeNodeIfFeasible(browser) 36 | return browser.tabs.create({ 37 | url: welcomePage 38 | }) 39 | case 'onVersionUpdate': 40 | if (!displayReleaseNotes) return 41 | await browser.storage.local.set({ dismissedUpdate: version }) 42 | return browser.tabs.create({ url: updatePage + version }) 43 | } 44 | } 45 | 46 | async function useNativeNodeIfFeasible (browser) { 47 | // lazy-loaded dependencies due to debug package 48 | // depending on the value of localStorage.debug, which is set later 49 | const log = debug('ipfs-companion:on-installed') 50 | log.error = debug('ipfs-companion:on-installed:error') 51 | const { ipfsNodeType, ipfsApiUrl } = await browser.storage.local.get(['ipfsNodeType', 'ipfsApiUrl']) 52 | 53 | // Brave >= v1.19 (https://brave.com/ipfs-support/) 54 | if (typeof brave !== 'undefined' && ipfsNodeType !== braveNodeType) { 55 | try { 56 | log(`brave detected, but node type is ${ipfsNodeType}. testing external endpoint at ${ipfsApiUrl}`) 57 | const response = await (await fetch(`${ipfsApiUrl}/api/v0/id`, { method: 'post' })).json() 58 | if (typeof response.ID === 'undefined') throw new Error(`unable to read PeerID from API at ${ipfsApiUrl}`) 59 | log(`endpoint is online, PeerID is ${response.ID}, nothing to do`) 60 | } catch (e) { 61 | log.error(`endpoint ${ipfsApiUrl} does not work`, e) 62 | log('switching node type to one provided by brave') 63 | await browser.storage.local.set({ ipfsNodeType: braveNodeType }) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /add-on/src/lib/on-uninstalled.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser */ 3 | 4 | const stableChannels = new Set([ 5 | 'ipfs-firefox-addon@lidel.org', // firefox (for legacy reasons) 6 | 'nibjojkomfdiaoajekhjakgkdhaomnch' // chromium (chrome web store) 7 | ]) 8 | 9 | const stableChannelFormUrl = 'https://docs.google.com/forms/d/e/1FAIpQLSfLF7uzaxRKiF4XpPL9_DvkdaQHoRnDihRTZ1uVL6ceQwIrtg/viewform' 10 | 11 | export function getUninstallURL (browser) { 12 | // on uninstall feedback form shown only on stable channel 13 | return stableChannels.has(browser.runtime.id) ? stableChannelFormUrl : '' 14 | } 15 | -------------------------------------------------------------------------------- /add-on/src/lib/precache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import debug from 'debug' 5 | const log = debug('ipfs-companion:precache') 6 | log.error = debug('ipfs-companion:precache:error') 7 | 8 | /** 9 | * Adds important assets such as Web UI to the local js-ipfs-repo. 10 | * This ensures they load instantly, even in offline environments. 11 | */ 12 | export async function precache (ipfs, state) { 13 | const roots = [] 14 | // find out the content path of webui, and add it to precache list 15 | try { 16 | let cid, name 17 | if (state.useLatestWebUI) { // resolve DNSLink 18 | cid = await ipfs.resolve('/ipns/webui.ipfs.io', { recursive: true }) 19 | name = 'latest webui from DNSLink at webui.ipfs.io' 20 | } else { // find out safelisted path behind <api-port>/webui 21 | cid = new URL((await fetch(`${state.apiURLString}webui`)).url).pathname 22 | name = `stable webui hardcoded at ${state.apiURLString}webui` 23 | } 24 | roots.push({ 25 | nodeType: 'external', 26 | name, 27 | cid 28 | }) 29 | } catch (e) { 30 | log.error('unable to find webui content path for precache', e) 31 | } 32 | 33 | // precache each root 34 | for (const { name, cid, nodeType } of roots) { 35 | if (state.ipfsNodeType !== nodeType) continue 36 | if (await inRepo(ipfs, cid)) { 37 | log(`${name} (${cid}) already in local repo, skipping import`) 38 | continue 39 | } 40 | log(`importing ${name} (${cid}) to local ipfs repo`) 41 | 42 | // prefetch over IPFS 43 | try { 44 | for await (const ref of ipfs.refs(cid, { recursive: true })) { 45 | if (ref.err) { 46 | log.error(`error while preloading ${name} (${cid})`, ref.err) 47 | continue 48 | } 49 | } 50 | log(`${name} successfully cached under CID ${cid}`) 51 | } catch (err) { 52 | log.error(`error while processing ${name}`, err) 53 | } 54 | } 55 | } 56 | 57 | async function inRepo (ipfs, cid) { 58 | // dag.get in offline mode will throw block is not present in local repo 59 | // (we also have timeout as a failsafe) 60 | try { 61 | await ipfs.dag.get(cid, { offline: true, timeout: 5000 }) 62 | return true 63 | } catch (_) { 64 | return false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /add-on/src/lib/redirect-handler/baseRegexFilter.ts: -------------------------------------------------------------------------------- 1 | import { brave } from '../../lib/ipfs-client/brave.js' 2 | 3 | export interface IRegexFilter { 4 | originUrl: string 5 | redirectUrl: string 6 | } 7 | 8 | export interface IFilter { 9 | regexFilter: string 10 | regexSubstitution: string 11 | } 12 | 13 | /** 14 | * Base class for all regex filters. 15 | */ 16 | export class RegexFilter { 17 | readonly _redirectUrl!: string 18 | readonly _originUrl!: string 19 | readonly originURL: URL 20 | readonly redirectURL: URL 21 | readonly originNS: string 22 | readonly redirectNS: string 23 | readonly isBrave: boolean = brave !== undefined 24 | // by default we cannot handle the request. 25 | private _canHandle = false 26 | regexFilter!: string 27 | regexSubstitution!: string 28 | 29 | constructor ({ originUrl, redirectUrl }: IRegexFilter) { 30 | this._originUrl = originUrl 31 | this._redirectUrl = redirectUrl 32 | this.originURL = new URL(this._originUrl) 33 | this.redirectURL = new URL(this._redirectUrl) 34 | this.redirectNS = this.computeNamespaceFromUrl(this.redirectURL) 35 | this.originNS = this.computeNamespaceFromUrl(this.originURL) 36 | this.computeFilter() 37 | this.normalizeRegexFilter() 38 | } 39 | 40 | /** 41 | * Getter for the originUrl provided at construction. 42 | */ 43 | get originUrl (): string { 44 | return this._originUrl 45 | } 46 | 47 | /** 48 | * Getter for the redirectUrl provided at construction. 49 | */ 50 | get redirectUrl (): string { 51 | return this._redirectUrl 52 | } 53 | 54 | /** 55 | * Getter for the canHandle flag. 56 | */ 57 | get canHandle (): boolean { 58 | return this._canHandle 59 | } 60 | 61 | /** 62 | * Setter for the canHandle flag. 63 | */ 64 | set canHandle (value: boolean) { 65 | this._canHandle = value 66 | } 67 | 68 | /** 69 | * Getter for the filter. This is the regex filter and substitution. 70 | */ 71 | get filter (): IFilter { 72 | if (!this.canHandle) { 73 | throw new Error('Cannot handle this request') 74 | } 75 | 76 | return { 77 | regexFilter: this.regexFilter, 78 | regexSubstitution: this.regexSubstitution 79 | } 80 | } 81 | 82 | /** 83 | * Compute the regex filter and substitution. 84 | * This is the main method that needs to be implemented by subclasses. 85 | * isBraveOverride is used to force the filter to be generated for Brave. For testing purposes only. 86 | */ 87 | computeFilter (isBraveOverride?: boolean): void { 88 | throw new Error('Method not implemented.') 89 | } 90 | 91 | /** 92 | * Normalize the regex filter. This is a helper method that can be used by subclasses. 93 | */ 94 | normalizeRegexFilter (): void { 95 | this.regexFilter = this.regexFilter.replace(/https?\??/ig, 'https?') 96 | } 97 | 98 | /** 99 | * Compute the namespace from the URL. This finds the first path segment. 100 | * e.g. http://<gateway>/<namespace>/path/to/file/or/cid 101 | * 102 | * @param url URL 103 | */ 104 | computeNamespaceFromUrl ({ pathname }: URL): string { 105 | // regex to match the first path segment. 106 | return (/\/([^/]+)\//i.exec(pathname)?.[1] ?? '').toLowerCase() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /add-on/src/lib/redirect-handler/commonPatternRedirectRegexFilter.ts: -------------------------------------------------------------------------------- 1 | import { RegexFilter } from './baseRegexFilter.js' 2 | import { RULE_REGEX_ENDING, escapeURLRegex } from './blockOrObserve.js' 3 | 4 | /** 5 | * Handles redirects like: 6 | * origin: '^https?\\:\\/\\/awesome\\.ipfs\\.io\\/(.*)' 7 | * destination: 'http://localhost:8081/ipns/awesome.ipfs.io/$1' 8 | */ 9 | export class CommonPatternRedirectRegexFilter extends RegexFilter { 10 | computeFilter (isBraveOverride: boolean): void { 11 | // this filter is the worst case scenario, we can handle any redirect. 12 | this.canHandle = true 13 | // We can traverse the URL from the end, and find the first character that is different. 14 | let commonIdx = 1 15 | const leastLength = Math.min(this.originUrl.length, this.redirectUrl.length) 16 | while (commonIdx < leastLength) { 17 | if (this.originUrl[this.originUrl.length - commonIdx] !== this.redirectUrl[this.redirectUrl.length - commonIdx]) { 18 | break 19 | } 20 | commonIdx += 1 21 | } 22 | 23 | // We can now construct the regex filter and substitution. 24 | this.regexSubstitution = this.redirectUrl.slice(0, this.redirectUrl.length - commonIdx + 1) + '\\1' 25 | // We need to escape the characters that are allowed in the URL, but not in the regex. 26 | const regexFilterFirst = escapeURLRegex(this.originUrl.slice(0, this.originUrl.length - commonIdx + 1)) 27 | this.regexFilter = `^${regexFilterFirst}${RULE_REGEX_ENDING}` 28 | // calling normalize should add the protocol in the regexFilter. 29 | this.normalizeRegexFilter() 30 | 31 | // This method does not parse: 32 | // originUrl: "https://awesome.ipfs.io/" 33 | // redirectUrl: "http://localhost:8081/ipns/awesome.ipfs.io/" 34 | // that ends up with capturing all urls which we do not want. 35 | // This rule can only apply to ipns subdomains. 36 | if (this.regexFilter === `^https?\\:\\/${RULE_REGEX_ENDING}`) { 37 | const subdomain = new URL(this.originUrl).hostname 38 | this.regexFilter = `^https?\\:\\/\\/${escapeURLRegex(subdomain)}${RULE_REGEX_ENDING}` 39 | if (this.isBrave || isBraveOverride) { 40 | this.regexSubstitution = `ipns://${subdomain}\\1` 41 | } else { 42 | this.regexSubstitution = this.regexSubstitution.replace('\\1', `/${subdomain}\\1`) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /add-on/src/lib/redirect-handler/namespaceRedirectRegexFilter.ts: -------------------------------------------------------------------------------- 1 | import { RegexFilter } from './baseRegexFilter.js' 2 | import { DEFAULT_NAMESPACES, RULE_REGEX_ENDING, defaultNSRegexStr, escapeURLRegex } from './blockOrObserve.js' 3 | 4 | /** 5 | * Handles namespace redirects like: 6 | * origin: '^https?\\:\\/\\/ipfs\\.io\\/(ipfs|ipns)\\/(.*)' 7 | * destination: 'http://localhost:8080/$1/$2' 8 | */ 9 | export class NamespaceRedirectRegexFilter extends RegexFilter { 10 | computeFilter (isBraveOverride: boolean): void { 11 | this.canHandle = DEFAULT_NAMESPACES.has(this.originNS) && 12 | DEFAULT_NAMESPACES.has(this.redirectNS) && 13 | this.originNS === this.redirectNS && 14 | this.originURL.searchParams.get('uri') == null 15 | // if the namespaces are the same, we can generate simpler regex. 16 | // The only value that needs special handling is the `uri` param. 17 | // A redirect like 18 | // https://ipfs.io/ipfs/QmZMxU -> http://localhost:8080/ipfs/QmZMxU 19 | const [originFirst, originLast] = this.originUrl.split(`/${this.originNS}/`) 20 | this.regexFilter = `^${escapeURLRegex(originFirst)}\\/${defaultNSRegexStr}\\/${RULE_REGEX_ENDING}` 21 | if (this.isBrave || isBraveOverride) { 22 | this.regexSubstitution = '\\1://\\2' 23 | } else { 24 | this.regexSubstitution = this.redirectUrl 25 | .replace(`/${this.redirectNS}/`, '/\\1/') 26 | .replace(originLast, '\\2') 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /add-on/src/lib/redirect-handler/subdomainRedirectRegexFilter.ts: -------------------------------------------------------------------------------- 1 | import { IRegexFilter, RegexFilter } from './baseRegexFilter.js' 2 | import { DEFAULT_NAMESPACES, RULE_REGEX_ENDING, defaultNSRegexStr, escapeURLRegex } from './blockOrObserve.js' 3 | 4 | /** 5 | * Handles subdomain redirects like: 6 | * origin: '^https?\\:\\/\\/bafybeigfejjsuq5im5c3w3t3krsiytszhfdc4v5myltcg4myv2n2w6jumy\\.ipfs\\.dweb\\.link' 7 | * destination: 'http://localhost:8080/ipfs/bafybeigfejjsuq5im5c3w3t3krsiytszhfdc4v5myltcg4myv2n2w6jumy' 8 | */ 9 | export class SubdomainRedirectRegexFilter extends RegexFilter { 10 | constructor ({ originUrl, redirectUrl }: IRegexFilter) { 11 | super({ originUrl, redirectUrl }) 12 | } 13 | 14 | computeFilter (isBraveOverride: boolean): void { 15 | const isBrave = this.isBrave || isBraveOverride 16 | this.regexSubstitution = this.redirectUrl 17 | this.regexFilter = this.originUrl 18 | if (!DEFAULT_NAMESPACES.has(this.originNS) && DEFAULT_NAMESPACES.has(this.redirectNS)) { 19 | // We'll use this to match the origin URL later. 20 | this.regexFilter = `^${escapeURLRegex(this.regexFilter)}` 21 | this.normalizeRegexFilter() 22 | const origRegexFilter = this.regexFilter 23 | // tld and root are known, we are just interested in the remainder of URL. 24 | const [tld, root, ...urlParts] = this.originURL.hostname.split('.').reverse() 25 | // can use the staticUrlParts to match the origin URL later. 26 | const staticUrlParts = [root, tld] 27 | // regex to match the start of the URL, this remains common. 28 | const commonStaticUrlStart = escapeURLRegex(`^${this.originURL.protocol}//`) 29 | // going though the subdomains to find a namespace or CID. 30 | while (urlParts.length > 0) { 31 | // get the urlPart at the 0th index and remove it from the array. 32 | const subdomainPart = urlParts.shift() as string 33 | // this needs to be computed for every iteration as the staticUrlParts changes 34 | const commonStaticUrlEnd = `\\.${escapeURLRegex(staticUrlParts.join('.'))}\\/${RULE_REGEX_ENDING}` 35 | // this does not work for subdomains where namespace is not provided. 36 | // e.g. https://helia-identify.on.fleek.co/ 37 | // e.g. https://bafybeib3bzis4mejzsnzsb65od3rnv5ffit7vsllratddjkgfgq4wiamqu.on.fleek.co/ 38 | // check if the subdomainPart is a namespace. 39 | if (DEFAULT_NAMESPACES.has(subdomainPart)) { 40 | // We found a namespace, this is going to match group 2, i.e. namespace. 41 | // e.g https://bafybeib3bzis4mejzsnzsb65od3rnv5ffit7vsllratddjkgfgq4wiamqu.ipfs.dweb.link 42 | this.regexFilter = `${commonStaticUrlStart}(.*?)\\.${defaultNSRegexStr}${commonStaticUrlEnd}` 43 | 44 | if (isBrave) { 45 | this.regexSubstitution = '\\2://\\1' 46 | } else { 47 | this.regexSubstitution = this._redirectUrl 48 | .replace(urlParts.reverse().join('.'), '\\1') // replace urlParts or CID. 49 | .replace(`/${subdomainPart}/`, '/\\2/') // replace namespace dynamically. 50 | } 51 | 52 | const pathWithSearch = this.originURL.pathname + this.originURL.search 53 | if (pathWithSearch !== '/' && !isBrave) { 54 | this.regexSubstitution = this.regexSubstitution.replace(pathWithSearch, '/\\3') // replace path 55 | } else { 56 | this.regexSubstitution += '\\3' 57 | } 58 | 59 | // no need to continue, we found a namespace. 60 | break 61 | } 62 | 63 | // till we find a namespace or CID, we keep adding subdomains to the staticUrlParts. 64 | staticUrlParts.unshift(subdomainPart) 65 | } 66 | 67 | if (this.regexFilter !== origRegexFilter) { 68 | // this means we constructed a regexFilter with dynamic parts, instead of the original regexFilter which was 69 | // static. There might be other suited regexFilters in that case. 70 | this.canHandle = true 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /add-on/src/lib/runtime-checks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import { brave } from './ipfs-client/brave.js' 5 | 6 | // this is our kitchen sink for runtime detection 7 | 8 | function getBrowserInfo (browser) { 9 | // browser.runtime.getBrowserInfo is not available in Chromium-based browsers 10 | if (browser && browser.runtime && browser.runtime.getBrowserInfo) { 11 | return browser.runtime.getBrowserInfo() 12 | } 13 | return Promise.resolve({}) 14 | } 15 | 16 | function getPlatformInfo (browser) { 17 | if (browser && browser.runtime && browser.runtime.getPlatformInfo) { 18 | return browser.runtime.getPlatformInfo() 19 | } 20 | return Promise.resolve() 21 | } 22 | 23 | export default async function createRuntimeChecks (browser) { 24 | // browser 25 | const { name, version } = await getBrowserInfo(browser) 26 | const isFirefox = name && (name.includes('Firefox') || name.includes('Fennec')) 27 | const hasNativeProtocolHandler = !!(browser && browser.protocol && browser.protocol.registerStringProtocol) // TODO: chrome.ipfs support 28 | // platform 29 | const platformInfo = await getPlatformInfo(browser) 30 | const isAndroid = platformInfo ? platformInfo.os === 'android' : false 31 | return Object.freeze({ 32 | browser, 33 | brave, // easy Boolean(runtime.brave) 34 | isFirefox, 35 | isAndroid, 36 | requiresXHRCORSfix: !!(isFirefox && version && version.startsWith('68')), 37 | hasNativeProtocolHandler 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /add-on/src/lib/state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | // @ts-check 4 | import { isHostname, safeURL } from './options.js' 5 | 6 | export const offlinePeerCount = -1 7 | export const POSSIBLE_NODE_TYPES = ['external', 'external:brave'] 8 | 9 | /** 10 | * 11 | * @param {import('../types/companion.js').CompanionOptions} options 12 | * @param {Partial<import('../types/companion.js').CompanionOptions>} [overrides] 13 | * @returns {import('../types/companion.js').CompanionState} 14 | */ 15 | export function initState (options, overrides) { 16 | // we store options and some pregenerated values to avoid async storage 17 | // reads and minimize performance impact on overall browsing experience 18 | /** 19 | * @type {Partial<import('../types/companion.js').CompanionState & import('../types/companion.js').CompanionOptions>} 20 | */ 21 | const state = Object.assign({}, options) 22 | // generate some additional values 23 | state.peerCount = offlinePeerCount 24 | state.pubGwURL = safeURL(options.publicGatewayUrl) 25 | state.pubGwURLString = state.pubGwURL?.toString() 26 | delete state.publicGatewayUrl 27 | state.pubSubdomainGwURL = safeURL(options.publicSubdomainGatewayUrl) 28 | state.pubSubdomainGwURLString = state.pubSubdomainGwURL?.toString() 29 | delete state.publicSubdomainGatewayUrl 30 | state.redirect = options.useCustomGateway 31 | delete state.useCustomGateway 32 | state.apiURL = safeURL(options.ipfsApiUrl, { useLocalhostName: false }) // go-ipfs returns 403 if IP is beautified to 'localhost' 33 | state.apiURLString = state.apiURL?.toString() 34 | delete state.ipfsApiUrl 35 | state.gwURL = safeURL(options.customGatewayUrl, { useLocalhostName: state.useSubdomains }) 36 | state.gwURLString = state.gwURL?.toString() 37 | delete state.customGatewayUrl 38 | state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy 39 | 40 | // attach helper functions 41 | state.activeIntegrations = (url) => { 42 | if (!state.active) return false 43 | try { 44 | const hostname = isHostname(url) ? url : new URL(url).hostname 45 | // opt-out has more weight, we also match parent domains 46 | const disabledDirectlyOrIndirectly = state.disabledOn?.some(optout => hostname.endsWith(optout)) 47 | // ..however direct opt-in should overwrite parent's opt-out 48 | const enabledDirectly = state.enabledOn?.some(optin => optin === hostname) 49 | return !(disabledDirectlyOrIndirectly && !enabledDirectly) 50 | } catch (_) { 51 | return false 52 | } 53 | } 54 | // TODO refactor this into a class. It's getting too big and messy. 55 | Object.defineProperty(state, 'nodeActive', { 56 | // TODO: make quick fetch to confirm it works? 57 | get: function () { return this.peerCount !== offlinePeerCount } 58 | }) 59 | Object.defineProperty(state, 'localGwAvailable', { 60 | // TODO: make quick fetch to confirm it works? 61 | get: function () { return this.webuiRootUrl != null } 62 | }) 63 | Object.defineProperty(state, 'webuiRootUrl', { 64 | get: function () { 65 | // Did user opt-in for rolling release published on DNSLink? 66 | if (state.useLatestWebUI) return `${state.gwURLString}ipns/webui.ipfs.io/` 67 | return `${state.apiURLString}webui` 68 | } 69 | }) 70 | // apply optional overrides 71 | if (overrides) Object.assign(state, overrides) 72 | return /** @type {import('../types/companion.js').CompanionState} */(state) 73 | } 74 | -------------------------------------------------------------------------------- /add-on/src/lib/telemetry.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { CompanionState } from '../types/companion.js' 3 | 4 | const log = debug('ipfs-companion:telemetry') 5 | 6 | /** 7 | * 8 | * @param {import('../types/companion.js').CompanionState} state 9 | * @returns {void} 10 | */ 11 | export async function handleConsentFromState (state: CompanionState): Promise<void> { 12 | const telemetryGroups = { 13 | minimal: state?.telemetryGroupMinimal || false, 14 | performance: state?.telemetryGroupPerformance || false, 15 | ux: state?.telemetryGroupUx || false, 16 | feedback: state?.telemetryGroupFeedback || false, 17 | location: state?.telemetryGroupLocation || false 18 | } 19 | for (const [groupName, isEnabled] of Object.entries(telemetryGroups)) { 20 | if (isEnabled) { 21 | log(`Telemetry consent for '${groupName}' would be enabled, but tracking has been removed`) 22 | } else { 23 | log(`Telemetry consent for '${groupName}' is disabled`) 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * TrackView is a no-op function that only logs debug messages 30 | * Tracking functionality has been removed 31 | * 32 | * @param view 33 | * @param segments 34 | */ 35 | export function trackView (view: string, segments: Record<string, string>): void { 36 | log('trackView called for view (no-op): ', view) 37 | } 38 | 39 | /** 40 | * TrackEvent is a no-op function that only logs debug messages 41 | * Tracking functionality has been removed 42 | * 43 | * @param event 44 | */ 45 | export function trackEvent (event: object): void { 46 | log('trackEvent called for event (no-op): ', event) 47 | } 48 | -------------------------------------------------------------------------------- /add-on/src/lib/trackers/requestTracker.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import type browser from 'webextension-polyfill' 3 | 4 | export const DEFAULT_REQUEST_TRACKER_FLUSH_INTERVAL = 1000 * 60 * 60 5 | 6 | export class RequestTracker { 7 | private readonly eventKey: 'url-observed' | 'url-resolved' 8 | private readonly flushInterval: number 9 | private readonly log: debug.Debugger & { error?: debug.Debugger } 10 | private lastSync: number = Date.now() 11 | private requestTypeStore: { [key in browser.WebRequest.ResourceType]?: number } = {} 12 | 13 | constructor (eventKey: 'url-observed' | 'url-resolved', flushInterval = DEFAULT_REQUEST_TRACKER_FLUSH_INTERVAL) { 14 | this.eventKey = eventKey 15 | this.log = debug(`ipfs-companion:request-tracker:${eventKey}`) 16 | this.log.error = debug(`ipfs-companion:request-tracker:${eventKey}:error`) 17 | this.flushInterval = flushInterval 18 | this.setupFlushScheduler() 19 | } 20 | 21 | track ({ type }: browser.WebRequest.OnBeforeRequestDetailsType): void { 22 | this.log(`track ${type}`, JSON.stringify(this.requestTypeStore)) 23 | this.requestTypeStore[type] = (this.requestTypeStore[type] ?? 0) + 1 24 | } 25 | 26 | private flushStore (): void { 27 | this.log('flushing') 28 | const count = Object.values(this.requestTypeStore).reduce((a, b): number => a + b, 0) 29 | if (count === 0) { 30 | this.log('nothing to flush') 31 | return 32 | } 33 | // TODO: implement tracking 34 | // reset 35 | this.lastSync = Date.now() 36 | this.requestTypeStore = {} 37 | } 38 | 39 | private setupFlushScheduler (): void { 40 | setTimeout(() => { 41 | this.flushStore() 42 | this.setupFlushScheduler() 43 | }, this.flushInterval) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /add-on/src/options/forms/api-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | import { guiURLString } from '../../lib/options.js' 7 | import { braveNodeType } from '../../lib/ipfs-client/brave.js' 8 | import switchToggle from '../../pages/components/switch-toggle.js' 9 | 10 | export default function apiForm ({ ipfsNodeType, ipfsApiUrl, ipfsApiPollMs, automaticMode, onOptionChange }) { 11 | const onIpfsApiUrlChange = onOptionChange('ipfsApiUrl', (url) => guiURLString(url, { useLocalhostName: false })) 12 | const onIpfsApiPollMsChange = onOptionChange('ipfsApiPollMs') 13 | const onAutomaticModeChange = onOptionChange('automaticMode') 14 | const apiAddresEditable = ipfsNodeType === 'external' 15 | const braveClass = ipfsNodeType === braveNodeType ? 'brave' : '' 16 | 17 | return html` 18 | <form> 19 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 20 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_api')}</h2> 21 | <div class="flex-row-ns pb0-ns"> 22 | <label for="ipfsApiUrl"> 23 | <dl> 24 | <dt>${browser.i18n.getMessage('option_ipfsApiUrl_title')}</dt> 25 | <dd>${browser.i18n.getMessage('option_ipfsApiUrl_description')}</dd> 26 | </dl> 27 | </label> 28 | <input 29 | class="bg-white navy self-center-ns ${braveClass}" 30 | id="ipfsApiUrl" 31 | type="url" 32 | inputmode="url" 33 | required 34 | pattern="^https?://[^/]+/?$" 35 | spellcheck="false" 36 | title="${browser.i18n.getMessage(apiAddresEditable ? 'option_hint_url' : 'option_hint_readonly')}" 37 | onchange=${onIpfsApiUrlChange} 38 | ${apiAddresEditable ? '' : 'disabled'} 39 | value=${ipfsApiUrl} /> 40 | </div> 41 | <div class="flex-row-ns pb0-ns"> 42 | <label for="ipfsApiPollMs"> 43 | <dl> 44 | <dt>${browser.i18n.getMessage('option_ipfsApiPollMs_title')}</dt> 45 | <dd>${browser.i18n.getMessage('option_ipfsApiPollMs_description')}</dd> 46 | </dl> 47 | </label> 48 | <input 49 | class="bg-white navy self-center-ns" 50 | id="ipfsApiPollMs" 51 | type="number" 52 | inputmode="numeric" 53 | min="1000" 54 | max="60000" 55 | step="1000" 56 | required 57 | onchange=${onIpfsApiPollMsChange} 58 | value=${ipfsApiPollMs} /> 59 | </div> 60 | <div class="flex-row-ns pb0-ns"> 61 | <label for="automaticMode"> 62 | <dl> 63 | <dt>${browser.i18n.getMessage('option_automaticMode_title')}</dt> 64 | <dd>${browser.i18n.getMessage('option_automaticMode_description')}</dd> 65 | <p class="i">${browser.i18n.getMessage('option_automaticMode_description_subtext')}</p> 66 | </dl> 67 | </label> 68 | <div class="self-center-ns">${switchToggle({ id: 'automaticMode', checked: automaticMode, onchange: onAutomaticModeChange })}</div> 69 | </div> 70 | </fieldset> 71 | </form> 72 | ` 73 | } 74 | -------------------------------------------------------------------------------- /add-on/src/options/forms/dnslink-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | import switchToggle from '../../pages/components/switch-toggle.js' 7 | 8 | export default function dnslinkForm ({ 9 | dnslinkPolicy, 10 | dnslinkDataPreload, 11 | dnslinkRedirect, 12 | onOptionChange 13 | }) { 14 | const onDnslinkPolicyChange = onOptionChange('dnslinkPolicy') 15 | const onDnslinkRedirectChange = onOptionChange('dnslinkRedirect') 16 | const onDnslinkDataPreloadChange = onOptionChange('dnslinkDataPreload') 17 | 18 | return html` 19 | <form> 20 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 21 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_dnslink')}</h2> 22 | <div class="flex-row-ns pb0-ns"> 23 | <label for="dnslinkPolicy"> 24 | <dl> 25 | <dt>${browser.i18n.getMessage('option_dnslinkPolicy_title')}</dt> 26 | <dd> 27 | ${browser.i18n.getMessage('option_dnslinkPolicy_description')} 28 | <p><a class="link underline hover-aqua" href="https://docs.ipfs.tech/how-to/dnslink-companion/" target="_blank"> 29 | ${browser.i18n.getMessage('option_legend_readMore')} 30 | </a></p> 31 | </dd> 32 | </dl> 33 | </label> 34 | <select id="dnslinkPolicy" name='dnslinkPolicy' class="self-center-ns bg-white navy" onchange=${onDnslinkPolicyChange}> 35 | <option 36 | value='false' 37 | selected=${String(dnslinkPolicy) === 'false'}> 38 | ${browser.i18n.getMessage('option_dnslinkPolicy_disabled')} 39 | </option> 40 | <option 41 | value='best-effort' 42 | selected=${dnslinkPolicy === 'best-effort'}> 43 | ${browser.i18n.getMessage('option_dnslinkPolicy_bestEffort')} 44 | </option> 45 | <option 46 | value='enabled' 47 | selected=${dnslinkPolicy === 'enabled'}> 48 | ${browser.i18n.getMessage('option_dnslinkPolicy_enabled')} 49 | </option> 50 | </select> 51 | </div> 52 | <div class="flex-row-ns pb0-ns"> 53 | <label for="dnslinkDataPreload"> 54 | <dl> 55 | <dt>${browser.i18n.getMessage('option_dnslinkDataPreload_title')}</dt> 56 | <dd>${browser.i18n.getMessage('option_dnslinkDataPreload_description')}</dd> 57 | </dl> 58 | </label> 59 | <div class="self-center-ns">${switchToggle({ id: 'dnslinkDataPreload', checked: dnslinkDataPreload, disabled: dnslinkRedirect, onchange: onDnslinkDataPreloadChange })}</div> 60 | </div> 61 | <div class="flex-row-ns pb0-ns"> 62 | <label for="dnslinkRedirect"> 63 | <dl> 64 | <dt>${browser.i18n.getMessage('option_dnslinkRedirect_title')}</dt> 65 | <dd> 66 | ${browser.i18n.getMessage('option_dnslinkRedirect_description')} 67 | ${dnslinkRedirect ? html`<p class="red i">${browser.i18n.getMessage('option_dnslinkRedirect_warning')}</p>` : null} 68 | <p><a class="link underline hover-aqua" href="https://docs.ipfs.tech/how-to/address-ipfs-on-web/#subdomain-gateway" target="_blank"> 69 | ${browser.i18n.getMessage('option_legend_readMore')} 70 | </a></p> 71 | </dd> 72 | </dl> 73 | </label> 74 | <div class="self-center-ns">${switchToggle({ id: 'dnslinkRedirect', checked: dnslinkRedirect, onchange: onDnslinkRedirectChange })}</div> 75 | </div> 76 | </fieldset> 77 | </form> 78 | ` 79 | } 80 | -------------------------------------------------------------------------------- /add-on/src/options/forms/file-import-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | import switchToggle from '../../pages/components/switch-toggle.js' 7 | 8 | export default function fileImportForm ({ 9 | importDir, openViaWebUI, preloadAtPublicGateway, onOptionChange 10 | }) { 11 | const onImportDirChange = onOptionChange('importDir') 12 | const onOpenViaWebUIChange = onOptionChange('openViaWebUI') 13 | const onPreloadAtPublicGatewayChange = onOptionChange('preloadAtPublicGateway') 14 | return html` 15 | <form> 16 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 17 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_fileImport')}</h2> 18 | <div class="flex-row-ns pb0-ns"> 19 | <label for="importDir"> 20 | <dl> 21 | <dt>${browser.i18n.getMessage('option_importDir_title')}</dt> 22 | <dd> 23 | ${browser.i18n.getMessage('option_importDir_description')} 24 | <p><a class="link underline hover-aqua" href="https://docs.ipfs.tech/concepts/file-systems/#mutable-file-system-mfs" target="_blank"> 25 | ${browser.i18n.getMessage('option_legend_readMore')} 26 | </a></p> 27 | </dd> 28 | </dl> 29 | </label> 30 | <input 31 | class="bg-white navy self-center-ns" 32 | id="importDir" 33 | type="text" 34 | pattern="^\/(.*)" 35 | required 36 | onchange=${onImportDirChange} 37 | value=${importDir} /> 38 | </div> 39 | <div class="flex-row-ns pb0-ns"> 40 | <label for="openViaWebUI"> 41 | <dl> 42 | <dt>${browser.i18n.getMessage('option_openViaWebUI_title')}</dt> 43 | <dd>${browser.i18n.getMessage('option_openViaWebUI_description')}</dd> 44 | </dl> 45 | </label> 46 | <div class="self-center-ns">${switchToggle({ id: 'openViaWebUI', checked: openViaWebUI, onchange: onOpenViaWebUIChange })}</div> 47 | </div> 48 | <div class="flex-row-ns pb0-ns"> 49 | <label for="preloadAtPublicGateway"> 50 | <dl> 51 | <dt>${browser.i18n.getMessage('option_preloadAtPublicGateway_title')}</dt> 52 | <dd>${browser.i18n.getMessage('option_preloadAtPublicGateway_description')}</dd> 53 | </dl> 54 | </label> 55 | <div class="self-center-ns">${switchToggle({ id: 'preloadAtPublicGateway', checked: preloadAtPublicGateway, onchange: onPreloadAtPublicGatewayChange })}</div> 56 | </div> 57 | </fieldset> 58 | </form> 59 | ` 60 | } 61 | -------------------------------------------------------------------------------- /add-on/src/options/forms/global-toggle-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | import switchToggle from '../../pages/components/switch-toggle.js' 7 | 8 | export default function globalToggleForm ({ active, onOptionChange }) { 9 | const toggle = onOptionChange('active') 10 | return html` 11 | <form class="db b mb3 bg-aqua-muted charcoal"> 12 | <label for="active" class="dib pa3 flex items-center pointer ${!active ? 'charcoal bg-gray-muted br2' : ''}"> 13 | ${switchToggle({ id: 'active', checked: active, onchange: toggle, style: 'mr3' })} 14 | ${browser.i18n.getMessage('panel_headerActiveToggleTitle')} 15 | </label> 16 | </form> 17 | ` 18 | } 19 | -------------------------------------------------------------------------------- /add-on/src/options/forms/ipfs-node-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | import { braveNodeType } from '../../lib/ipfs-client/brave.js' 7 | 8 | export default function ipfsNodeForm ({ ipfsNodeType, onOptionChange, withNodeFromBrave }) { 9 | const onIpfsNodeTypeChange = onOptionChange('ipfsNodeType') 10 | const braveClass = ipfsNodeType === braveNodeType ? 'brave' : '' 11 | return html` 12 | <form> 13 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 14 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_nodeType')}</h2> 15 | <div class="flex-row-ns pb0-ns"> 16 | <label for="ipfsNodeType"> 17 | <dl> 18 | <dt>${browser.i18n.getMessage('option_ipfsNodeType_title')}</dt> 19 | <dd> 20 | <p>${browser.i18n.getMessage('option_ipfsNodeType_external_description')}</p> 21 | ${withNodeFromBrave ? html`<p>${browser.i18n.getMessage('option_ipfsNodeType_brave_description')}</p>` : null} 22 | </dd> 23 | </dl> 24 | </label> 25 | <select id="ipfsNodeType" name='ipfsNodeType' class="self-center-ns bg-white navy ${braveClass}" onchange=${onIpfsNodeTypeChange}> 26 | <option 27 | value='external' 28 | selected=${ipfsNodeType === 'external'}> 29 | ${browser.i18n.getMessage('option_ipfsNodeType_external')} 30 | </option> 31 | ${withNodeFromBrave 32 | ? html`<option 33 | value='external:brave' 34 | selected=${ipfsNodeType === 'external:brave'}> 35 | ${browser.i18n.getMessage('option_ipfsNodeType_brave')} 36 | </option>` 37 | : null} 38 | </select> 39 | </div> 40 | </fieldset> 41 | </form> 42 | ` 43 | } 44 | -------------------------------------------------------------------------------- /add-on/src/options/forms/redirect-rule-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import browser from 'webextension-polyfill' 6 | 7 | /** 8 | * 9 | * @param {(event: string, value?: any) => void} emit 10 | * @returns 11 | */ 12 | function ruleItem (emit) { 13 | /** 14 | * Renders Rule Item 15 | * 16 | * @param {{ 17 | * id: string 18 | * origin: string 19 | * target: string 20 | * }} param0 21 | * @returns 22 | */ 23 | return function ({ id, origin, target }) { 24 | return html` 25 | <div class="flex flex-row-ns pb0-ns"> 26 | <dl class="flex-grow-1"> 27 | <dt> 28 | <span class="b">${browser.i18n.getMessage('option_redirect_rules_row_origin')}:</span> ${origin} 29 | </dt> 30 | <dt> 31 | <span class="b">${browser.i18n.getMessage('option_redirect_rules_row_target')}:</span> ${target} 32 | </dt> 33 | </dl> 34 | <div class="rule-delete"> 35 | <button class="f6 ph3 pv2 mt0 mb0 bg-transparent b--none red" onclick=${() => emit('redirectRuleDeleteRequest', id)}>X</button> 36 | </div> 37 | </div> 38 | ` 39 | } 40 | } 41 | 42 | /** 43 | * 44 | * @param {{ 45 | * emit: (event: string, value?: any) => void, 46 | * redirectRules: { 47 | * id: string 48 | * origin: string 49 | * target: string 50 | * }[] 51 | * }} param0 52 | * @returns 53 | */ 54 | export default function redirectRuleForm ({ emit, redirectRules }) { 55 | return html` 56 | <form> 57 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 58 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_redirect_rules')}</h2> 59 | <div class="flex-row-ns pb0-ns"> 60 | <label for="deleteAllRules"> 61 | <dl> 62 | <dt> 63 | <div class="self-right-ns"> 64 | Found ${redirectRules?.length ?? 0} rules 65 | </div> 66 | </dt> 67 | </dl> 68 | </label> 69 | <div class="self-center-ns"> 70 | <button id="deleteAllRules" class="Button transition-all sans-serif v-mid fw5 nowrap lh-copy bn br1 pa2 pointer focus-outline white bg-red white" onclick=${() => emit('redirectRuleDeleteRequest')}>${browser.i18n.getMessage('option_redirect_rules_reset_all')}</button> 71 | </div> 72 | </div> 73 | <div style="max-height: 250px; overflow-y: auto"> 74 | ${redirectRules ? redirectRules.map(ruleItem(emit)) : html`<div>Loading...</div>`} 75 | </div> 76 | </fieldset> 77 | </form> 78 | ` 79 | } 80 | -------------------------------------------------------------------------------- /add-on/src/options/forms/reset-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | 7 | export default function resetForm ({ 8 | onOptionsReset 9 | }) { 10 | return html` 11 | <form> 12 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 13 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_reset')}</h2> 14 | <div class="flex-row-ns pb0-ns"> 15 | <label for="resetAllOptions"> 16 | <dl> 17 | <dt>${browser.i18n.getMessage('option_resetAllOptions_title')}</dt> 18 | <dd>${browser.i18n.getMessage('option_resetAllOptions_description')}</dd> 19 | </dl> 20 | </label> 21 | <div class="self-center-ns"><button id="resetAllOptions" class="Button transition-all sans-serif v-mid fw5 nowrap lh-copy bn br1 pa2 pointer focus-outline white bg-red white" onclick=${onOptionsReset}>${browser.i18n.getMessage('option_resetAllOptions_title')}</button></div> 22 | </div> 23 | </fieldset> 24 | </form> 25 | ` 26 | } 27 | -------------------------------------------------------------------------------- /add-on/src/options/forms/telemetry-form.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import browser from 'webextension-polyfill' 6 | import switchToggle from '../../pages/components/switch-toggle.js' 7 | 8 | export default function telemetryForm ({ 9 | onOptionChange, 10 | ...stateOptions 11 | }) { 12 | // @ts-expect-error - TS doesn't like the `html` template tag 13 | return html` 14 | <form> 15 | <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal"> 16 | <h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_telemetry')}</h2> 17 | <div class="mb2"> 18 | <p>${browser.i18n.getMessage('option_telemetry_disclaimer')}</p> 19 | <p> 20 | <a class="link underline hover-aqua" href="https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/COLLECTION_POLICY.md" target="_blank"> 21 | ${browser.i18n.getMessage('option_legend_readMore')} 22 | </a> 23 | </p> 24 | </div> 25 | <div class="flex-row-ns pb0-ns"> 26 | <label for="telemetryGroupMinimal"> 27 | <dl> 28 | <dt>${browser.i18n.getMessage('option_telemetryGroupMinimal_title')}</dt> 29 | <dd> 30 | <p>${browser.i18n.getMessage('option_telemetryGroupMinimal_description')}</p> 31 | </dd> 32 | </dl> 33 | </label> 34 | <div class="self-center-ns">${switchToggle({ 35 | id: 'telemetryGroupMinimal', 36 | checked: stateOptions.telemetryGroupMinimal, 37 | onchange: onOptionChange('telemetryGroupMinimal') 38 | })}</div> 39 | </div> 40 | </fieldset> 41 | </form> 42 | ` 43 | } 44 | -------------------------------------------------------------------------------- /add-on/src/options/options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import './options.css' 5 | 6 | import { i18n } from 'webextension-polyfill' 7 | import choo from 'choo' 8 | import optionsPage from './page.js' 9 | import optionsStore from './store.js' 10 | 11 | const app = choo() 12 | 13 | // Use the store to setup state defaults and event listeners for mutations 14 | app.use(optionsStore) 15 | 16 | // Register our single route 17 | app.route('*', optionsPage) 18 | 19 | // Start the application and render it to the given querySelector 20 | app.mount('#root') 21 | 22 | // Set page title and header translation 23 | document.getElementById('header-text').innerText = i18n.getMessage('option_page_header') 24 | document.title = i18n.getMessage('option_page_title') 25 | -------------------------------------------------------------------------------- /add-on/src/options/page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import { supportsDeclarativeNetRequest } from '../lib/redirect-handler/blockOrObserve.js' 6 | import apiForm from './forms/api-form.js' 7 | import dnslinkForm from './forms/dnslink-form.js' 8 | import experimentsForm from './forms/experiments-form.js' 9 | import fileImportForm from './forms/file-import-form.js' 10 | import gatewaysForm from './forms/gateways-form.js' 11 | import globalToggleForm from './forms/global-toggle-form.js' 12 | import ipfsNodeForm from './forms/ipfs-node-form.js' 13 | import redirectRuleForm from './forms/redirect-rule-form.js' 14 | import resetForm from './forms/reset-form.js' 15 | import telemetryForm from './forms/telemetry-form.js' 16 | 17 | // Render the options page: 18 | // Passed current app `state` from the store and `emit`, a function to create 19 | // events, allowing views to signal back to the store that something happened. 20 | export default function optionsPage (state, emit) { 21 | const onOptionChange = (key, modifyValue) => (e) => { 22 | e.preventDefault() 23 | 24 | const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value 25 | 26 | if (!e.target.reportValidity()) { 27 | return console.warn(`[ipfs-companion] Invalid value for ${key}: ${value}`) 28 | } 29 | 30 | emit('optionChange', { key, value: modifyValue ? modifyValue(value) : value }) 31 | if (modifyValue) { 32 | emit('render') 33 | } 34 | } 35 | 36 | const onOptionsReset = (e) => { 37 | e.preventDefault() 38 | emit('optionsReset') 39 | } 40 | 41 | if (!state.options.active) { 42 | // we don't want to confuse users by showing "active" checkboxes 43 | // when global toggle is in "suspended" state 44 | return html` 45 | <div class="sans-serif"> 46 | ${globalToggleForm({ 47 | active: state.options.active, 48 | onOptionChange 49 | })} 50 | </div> 51 | ` 52 | } 53 | return html` 54 | <div class="sans-serif"> 55 | ${globalToggleForm({ 56 | active: state.options.active, 57 | onOptionChange 58 | })} 59 | ${ipfsNodeForm({ 60 | ipfsNodeType: state.options.ipfsNodeType, 61 | ipfsNodeConfig: state.options.ipfsNodeConfig, 62 | withNodeFromBrave: state.withNodeFromBrave, 63 | onOptionChange 64 | })} 65 | ${state.options.ipfsNodeType.startsWith('external') 66 | ? apiForm({ 67 | ipfsNodeType: state.options.ipfsNodeType, 68 | ipfsApiUrl: state.options.ipfsApiUrl, 69 | ipfsApiPollMs: state.options.ipfsApiPollMs, 70 | automaticMode: state.options.automaticMode, 71 | onOptionChange 72 | }) 73 | : null} 74 | ${gatewaysForm({ 75 | ipfsNodeType: state.options.ipfsNodeType, 76 | customGatewayUrl: state.options.customGatewayUrl, 77 | useCustomGateway: state.options.useCustomGateway, 78 | useSubdomains: state.options.useSubdomains, 79 | publicGatewayUrl: state.options.publicGatewayUrl, 80 | publicSubdomainGatewayUrl: state.options.publicSubdomainGatewayUrl, 81 | disabledOn: state.options.disabledOn, 82 | enabledOn: state.options.enabledOn, 83 | onOptionChange 84 | })} 85 | ${fileImportForm({ 86 | importDir: state.options.importDir, 87 | openViaWebUI: state.options.openViaWebUI, 88 | preloadAtPublicGateway: state.options.preloadAtPublicGateway, 89 | onOptionChange 90 | })} 91 | ${dnslinkForm({ 92 | dnslinkPolicy: state.options.dnslinkPolicy, 93 | dnslinkDataPreload: state.options.dnslinkDataPreload, 94 | dnslinkRedirect: state.options.dnslinkRedirect, 95 | onOptionChange 96 | })} 97 | ${experimentsForm({ 98 | useLatestWebUI: state.options.useLatestWebUI, 99 | displayNotifications: state.options.displayNotifications, 100 | displayReleaseNotes: state.options.displayReleaseNotes, 101 | catchUnhandledProtocols: state.options.catchUnhandledProtocols, 102 | linkify: state.options.linkify, 103 | recoverFailedHttpRequests: state.options.recoverFailedHttpRequests, 104 | detectIpfsPathHeader: state.options.detectIpfsPathHeader, 105 | logNamespaces: state.options.logNamespaces, 106 | onOptionChange 107 | })} 108 | <!-- we gather no telemetry (https://github.com/ipfs/ipfs-companion/issues/1315), hiding UI for now 109 | ${telemetryForm({ 110 | telemetryGroupMinimal: state.options.telemetryGroupMinimal, 111 | telemetryGroupMarketing: state.options.telemetryGroupMarketing, 112 | telemetryGroupPerformance: state.options.telemetryGroupPerformance, 113 | telemetryGroupTracking: state.options.telemetryGroupTracking, 114 | onOptionChange 115 | })} 116 | --> 117 | ${resetForm({ 118 | onOptionsReset 119 | })} 120 | ${!supportsDeclarativeNetRequest() 121 | ? '' 122 | : redirectRuleForm({ 123 | redirectRules: state.redirectRules, 124 | emit 125 | })} 126 | </div> 127 | ` 128 | } 129 | -------------------------------------------------------------------------------- /add-on/src/options/store.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import { optionDefaults } from '../lib/options.js' 6 | import { DELETE_RULE_REQUEST_SUCCESS, RULE_REGEX_ENDING, notifyDeleteRule, notifyOptionChange } from '../lib/redirect-handler/blockOrObserve.js' 7 | import createRuntimeChecks from '../lib/runtime-checks.js' 8 | 9 | // The store contains and mutates the state for the app 10 | export default function optionStore (state, emitter) { 11 | state.options = optionDefaults 12 | 13 | const fetchRedirectRules = async () => { 14 | const existingRedirectRules = await browser.declarativeNetRequest.getDynamicRules() 15 | state.redirectRules = existingRedirectRules.map(rule => ({ 16 | id: rule.id, 17 | origin: rule.condition.regexFilter?.replace(RULE_REGEX_ENDING, '(.*)').replaceAll('\\', ''), 18 | target: rule.action.redirect?.regexSubstitution 19 | })) 20 | emitter.emit('render') 21 | } 22 | 23 | const updateStateOptions = async () => { 24 | const runtime = await createRuntimeChecks(browser) 25 | state.withNodeFromBrave = runtime.brave && await runtime.brave.getIPFSEnabled() 26 | /** 27 | * FIXME: Why are we setting `state.options` when state is supposed to extend options? 28 | */ 29 | state.options = await getOptions() 30 | emitter.emit('render') 31 | } 32 | 33 | emitter.on('DOMContentLoaded', async () => { 34 | browser.runtime.sendMessage({ telemetry: { trackView: 'options' } }) 35 | updateStateOptions() 36 | fetchRedirectRules() 37 | browser.storage.onChanged.addListener(updateStateOptions) 38 | }) 39 | 40 | emitter.on('redirectRuleDeleteRequest', async (id) => { 41 | console.log('delete rule request', id) 42 | browser.runtime.onMessage.addListener(({ type }) => { 43 | if (type === DELETE_RULE_REQUEST_SUCCESS) { 44 | emitter.emit('render') 45 | } 46 | }) 47 | notifyDeleteRule(id) 48 | }) 49 | 50 | emitter.on('optionChange', async ({ key, value }) => { 51 | browser.storage.local.set({ [key]: value }) 52 | await notifyOptionChange() 53 | }) 54 | 55 | emitter.on('optionsReset', async () => { 56 | browser.storage.local.set(optionDefaults) 57 | await notifyOptionChange() 58 | }) 59 | } 60 | 61 | async function getOptions () { 62 | const storedOpts = await browser.storage.local.get() 63 | return Object.keys(optionDefaults).reduce((opts, key) => { 64 | opts[key] = storedOpts[key] == null ? optionDefaults[key] : storedOpts[key] 65 | return opts 66 | }, {}) 67 | } 68 | -------------------------------------------------------------------------------- /add-on/src/pages/components/switch-toggle.css: -------------------------------------------------------------------------------- 1 | @import url('~@material/switch/dist/mdc.switch.css'); 2 | 3 | .mdc-switch { 4 | --mdc-theme-secondary: #3e9096 /* teal */ 5 | } 6 | 7 | .mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb, 8 | .mdc-switch:not(.mdc-switch--checked) .mdc-switch__track { 9 | background-color: #7f8491; /* charcoal-muted */ 10 | border-color: #7f8491; 11 | } 12 | -------------------------------------------------------------------------------- /add-on/src/pages/components/switch-toggle.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | 6 | /** 7 | * @type {import('../../types/companion.js').SwitchToggle} 8 | */ 9 | export default function switchToggle ({ 10 | checked, 11 | disabled, 12 | id, 13 | onchange, 14 | style 15 | }) { 16 | if (typeof checked === 'undefined') return 17 | // @ts-expect-error - TS doesn't like the `html` template tag 18 | return html` 19 | <div class="mdc-switch ${style || ''} ${checked ? 'mdc-switch--checked' : ''} ${disabled ? 'mdc-switch--disabled' : ''}"> 20 | <div class="mdc-switch__track"></div> 21 | <div class="mdc-switch__thumb-underlay"> 22 | <div class="mdc-switch__thumb"> 23 | <input type="checkbox" id="${id}" onchange=${onchange} class="mdc-switch__native-control" role="switch" 24 | ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''}> 25 | </div> 26 | </div> 27 | </div> 28 | ` 29 | } 30 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/browser-action.css: -------------------------------------------------------------------------------- 1 | @import url('~tachyons/css/tachyons.css'); 2 | @import url('~ipfs-css/ipfs.css'); 3 | @import url('../heartbeat.css'); 4 | @import url('../../pages/components/switch-toggle.css'); 5 | 6 | .bg-near-white--hover:hover { 7 | background-color: #F4F4F4; 8 | } 9 | 10 | .header-icon:active { 11 | color: #edf0f4; 12 | transform: translateY(2px); 13 | } 14 | .header-icon[disabled], 15 | .header-icon[disabled]:active { 16 | cursor: not-allowed; 17 | pointer-events: none; 18 | transform: none; 19 | } 20 | 21 | .outline-0--focus:focus { 22 | outline: 0; 23 | } 24 | 25 | .no-user-select { 26 | -webkit-user-select: none; /* Old Chrome, Safari */ 27 | -moz-user-select: none; /* Firefox */ 28 | -ms-user-select: none; /* Internet Explorer/Edge */ 29 | user-select: none; /* Non-prefixed version, Chrome and Opera */ 30 | } 31 | 32 | .force-select-all { 33 | -webkit-user-select: all; /* Chrome 49+ */ 34 | -moz-user-select: all; /* Firefox 43+ */ 35 | -ms-user-select: all; /* No support yet */ 36 | user-select: all; /* Likely future */ 37 | } 38 | 39 | .fade-in { 40 | animation: fade-in 600ms; 41 | } 42 | @keyframes fade-in { 43 | from { opacity: 0; } 44 | to { opacity: 1; } 45 | } 46 | 47 | .blink { 48 | animation: blink 1s linear infinite; 49 | } 50 | 51 | @keyframes blink { 52 | 50% { 53 | opacity: 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/gateway-status.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | 7 | function statusEntry ({ label, labelLegend, value, check, itemClass = '', valueClass = '' }) { 8 | const offline = browser.i18n.getMessage('panel_statusOffline') 9 | label = label ? browser.i18n.getMessage(label) : null 10 | labelLegend = labelLegend ? browser.i18n.getMessage(labelLegend) : label 11 | value = value || value === 0 ? value : offline 12 | return html` 13 | <div class="flex mb1 ${check ? '' : 'o-60'} ${itemClass}" title="${labelLegend}"> 14 | <span class="w-40 f7 ttu no-user-select">${label}</span> 15 | <span class="w-60 f7 tr monospace truncate force-select-all ${valueClass}" title="${value}">${value}</span> 16 | </div> 17 | ` 18 | } 19 | 20 | export default function gatewayStatus ({ 21 | gatewayAddress, 22 | kuboRpcBackendVersion, 23 | ipfsApiUrl, 24 | swarmPeers 25 | }) { 26 | const api = ipfsApiUrl 27 | return html` 28 | <ul class="fade-in list mv0 pt2 ph3 white"> 29 | ${statusEntry({ 30 | label: 'panel_statusSwarmPeers', 31 | labelLegend: 'panel_statusSwarmPeersTitle', 32 | value: swarmPeers, 33 | check: swarmPeers 34 | })} 35 | ${statusEntry({ 36 | label: 'panel_statusGatewayAddress', 37 | labelLegend: 'panel_statusGatewayAddressTitle', 38 | value: gatewayAddress, 39 | check: gatewayAddress 40 | })} 41 | ${statusEntry({ 42 | label: 'panel_statusApiAddress', 43 | labelLegend: 'panel_statusApiAddressTitle', 44 | value: api, 45 | check: kuboRpcBackendVersion 46 | })} 47 | </ul> 48 | ` 49 | } 50 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/header.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import logo from '../logo.js' 6 | import versionUpdateIcon from './version-update-icon.js' 7 | import powerIcon from './power-icon.js' 8 | import optionsIcon from './options-icon.js' 9 | import ipfsVersion from './ipfs-version.js' 10 | import gatewayStatus from './gateway-status.js' 11 | 12 | export default function header (props) { 13 | const { ipfsNodeType, active, onToggleActive, onOpenPrefs, onOpenReleaseNotes, isIpfsOnline, onOpenWelcomePage, newVersion } = props 14 | return html` 15 | <div> 16 | <div class="pt3 pr3 pb2 pl3 no-user-select flex justify-between items-center"> 17 | <div class="inline-flex items-center"> 18 | <div 19 | onclick=${onOpenWelcomePage} 20 | class="transition-all pointer ${active ? '' : 'o-40'}" 21 | style="${active ? '' : 'filter: blur( .15em )'}"> 22 | ${logo({ 23 | size: 54, 24 | path: '../../../icons', 25 | ipfsNodeType, 26 | isIpfsOnline: (active && isIpfsOnline) 27 | })} 28 | </div> 29 | <div class="flex flex-column ml2 white ${active ? '' : 'o-40'}"> 30 | <div> 31 | <h1 class="inter fw6 f2 ttu ma0 pa0"> 32 | IPFS 33 | </h1> 34 | </div> 35 | <span class="${active ? '' : 'o-0'}">${ipfsVersion(props)}</span> 36 | </div> 37 | </div> 38 | <div class="tr ma0 pb1"> 39 | ${newVersion 40 | ? versionUpdateIcon({ 41 | newVersion, 42 | active, 43 | title: 'panel_headerNewVersionTitle', 44 | action: onOpenReleaseNotes 45 | }) 46 | : null} 47 | ${powerIcon({ 48 | active, 49 | title: 'panel_headerActiveToggleTitle', 50 | action: onToggleActive 51 | })} 52 | ${optionsIcon({ 53 | active, 54 | title: 'panel_openPreferences', 55 | action: onOpenPrefs 56 | })} 57 | </div> 58 | </div> 59 | <div class="pb1 ${active ? '' : 'o-40'}"> 60 | ${gatewayStatus(props)} 61 | </div> 62 | </div> 63 | ` 64 | } 65 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/icon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import browser from 'webextension-polyfill' 6 | 7 | export default function icon ({ 8 | svg, title, active, action, className 9 | }) { 10 | return html` 11 | <button class="header-icon pa0 ma0 dib bn bg-transparent transition-all ${className} ${action ? 'pointer' : null} ${active ? 'aqua' : 'gray'}" 12 | style="outline:none;" 13 | title="${browser.i18n.getMessage(title) || title}" 14 | onclick=${action}> 15 | ${svg} 16 | </button> 17 | ` 18 | } 19 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta name="viewport" content="width=device-width"> 6 | <link rel="stylesheet" href="/dist/bundles/uiCommons.css"> 7 | <link rel="stylesheet" href="/dist/bundles/browserAction.css"> 8 | </head> 9 | <body style="width: 320px; overflow: hidden; background: white;"> 10 | <div id="root"></div> 11 | <script src="/dist/bundles/uiCommons.bundle.js"></script> 12 | <script src="/dist/bundles/browserAction.bundle.js"></script> 13 | </body> 14 | </html> 15 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import './browser-action.css' 5 | 6 | import choo from 'choo' 7 | import browserActionPage from './page.js' 8 | import browserActionStore from './store.js' 9 | 10 | const app = choo() 11 | 12 | // Use the store to setup state defaults and event listeners for mutations 13 | app.use(browserActionStore) 14 | 15 | // Register our single route 16 | app.route('*', browserActionPage) 17 | 18 | // Start the application and render it to the given querySelector 19 | app.mount('#root') 20 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/ipfs-version.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | 7 | function statusEntry ({ label, labelLegend, title, value, check, valueClass = '' }) { 8 | const offline = browser.i18n.getMessage('panel_statusOffline') 9 | label = label ? browser.i18n.getMessage(label) : null 10 | labelLegend = labelLegend ? browser.i18n.getMessage(labelLegend) : label 11 | value = value || value === 0 ? value : offline 12 | return html` 13 | <div title="${labelLegend}" class="ma0 pa0" style="line-height: 0.25"> 14 | <span class="f7 tr monospace force-select-all ${valueClass}" title="${title}">${value.substring(0, 20)}</span> 15 | </div> 16 | ` 17 | } 18 | 19 | export default function ipfsVersion ({ 20 | kuboRpcBackendVersion 21 | }) { 22 | return html` 23 | ${statusEntry({ 24 | title: browser.i18n.getMessage('panel_kuboRpcBackendVersionTitle'), 25 | value: kuboRpcBackendVersion, 26 | check: kuboRpcBackendVersion 27 | })} 28 | ` 29 | } 30 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/nav-header.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | 7 | export default function navHeader (label) { 8 | return html` 9 | <div class="no-select w-100 outline-0--focus tl ph3 pt2 mt1 pb1 o-40 f6"> 10 | ${browser.i18n.getMessage(label)} 11 | </div> 12 | ` 13 | } 14 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/nav-item.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import switchToggle from '../../pages/components/switch-toggle.js' 6 | 7 | export default function navItem ({ 8 | disabled, 9 | helperText, 10 | onClick, 11 | style, 12 | switchValue, 13 | text, 14 | title 15 | }) { 16 | let buttonStyle = 'black button-reset db w-100 bg-white b--none outline-0--focus pt2 ph3 f6 tl' 17 | if (disabled) { 18 | buttonStyle += ' o-40' 19 | } else { 20 | buttonStyle += ' pointer bg-near-white--hover' 21 | } 22 | if (style) { 23 | buttonStyle += ` ${style}` 24 | } 25 | if (disabled) { 26 | title = '' 27 | } 28 | 29 | return html` 30 | 31 | <button class="${buttonStyle}" 32 | onclick=${disabled ? null : onClick} title="${title || ''}" ${disabled ? 'disabled' : ''}> 33 | <div class="flex flex-row items-center justify-between"><div class="truncate">${text}</div>${switchToggle({ checked: switchValue, disabled, style: 'fr ml2' })}</div> 34 | <div class="f7 o-40 w-80 truncate mv1">${helperText}</div> 35 | </button> 36 | ` 37 | } 38 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/options-icon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import icon from './icon.js' 6 | 7 | export default function optionsIcon ({ 8 | action, 9 | active, 10 | size = '1.8rem', 11 | title 12 | }) { 13 | const svg = html` 14 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86" 15 | class="fill-current-color" 16 | style="width:${size}; height:${size}"> 17 | <path d="M74.05 50.23c-.07-3.58 1.86-5.85 5.11-7.1-.2-2-2.48-7.45-3.63-8.76-3.11 1.46-6.06 1.23-8.54-1.22s-2.72-5.46-1.26-8.64a29.24 29.24 0 0 0-8.8-3.63c-1.06 3.08-3.12 5-6.35 5.25-3.82.29-6.29-1.69-7.61-5.22a30.11 30.11 0 0 0-8.77 3.67c1.5 3.16 1.3 6.1-1.15 8.6s-5.45 2.76-8.64 1.29a29.33 29.33 0 0 0-3.58 8.79C24 44.43 25.94 46.62 26 50s-1.82 5.84-5.1 7.12a29.21 29.21 0 0 0 3.68 8.71c3.09-1.38 6-1.15 8.42 1.22s2.79 5.33 1.41 8.49a29.72 29.72 0 0 0 8.76 3.57 1.46 1.46 0 0 0 .11-.21 7.19 7.19 0 0 1 13.53-.16c.13.33.28.32.55.25a29.64 29.64 0 0 0 8-3.3 4 4 0 0 0 .37-.25c-1.27-2.86-1.15-5.57.88-7.94 2.44-2.84 5.5-3.26 8.91-1.8a29.23 29.23 0 0 0 3.65-8.7c-3.17-1.22-5.05-3.38-5.12-6.77zM50 59.54a8.57 8.57 0 1 1 8.59-8.31A8.58 8.58 0 0 1 50 59.54z"/> 18 | </svg> 19 | ` 20 | return icon({ svg, title, active, action }) 21 | } 22 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import header from './header.js' 6 | import { activeTabActions } from './context-actions.js' 7 | import tools from './tools.js' 8 | 9 | // Render the browser action page: 10 | // Passed current app `state` from the store and `emit`, a function to create 11 | // events, allowing views to signal back to the store that something happened. 12 | export default function browserActionPage (state, emit) { 13 | const onViewOnGateway = () => emit('viewOnGateway') 14 | const onCopy = (copyAction) => emit('copy', copyAction) 15 | const onFilesCpImport = () => emit('filesCpImport') 16 | 17 | const onQuickImport = () => emit('quickImport') 18 | const onOpenWebUi = () => emit('openWebUi', '/') 19 | const onOpenWelcomePage = () => emit('openWelcomePage') 20 | const onOpenPrefs = () => emit('openPrefs') 21 | const onOpenReleaseNotes = () => emit('openReleaseNotes') 22 | const onToggleGlobalRedirect = () => emit('toggleGlobalRedirect') 23 | const onToggleSiteIntegrations = () => emit('toggleSiteIntegrations') 24 | const onToggleActive = () => emit('toggleActive') 25 | 26 | const headerProps = Object.assign({ onToggleActive, onOpenPrefs, onOpenReleaseNotes, onOpenWelcomePage }, state) 27 | const activeTabActionsProps = Object.assign({ onViewOnGateway, onToggleSiteIntegrations, onCopy, onFilesCpImport }, state) 28 | const opsProps = Object.assign({ onQuickImport, onOpenWebUi, onToggleGlobalRedirect }, state) 29 | 30 | return html` 31 | <div class="sans-serif" style="text-rendering: optimizeLegibility;"> 32 | <div class="ba bw1 b--white ipfs-gradient-0"> 33 | ${header(headerProps)} 34 | ${tools(opsProps)} 35 | </div> 36 | ${activeTabActions(activeTabActionsProps)} 37 | </div> 38 | ` 39 | } 40 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/power-icon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import icon from './icon.js' 6 | 7 | export default function powerIcon ({ 8 | action, 9 | active, 10 | size = '1.8rem', 11 | title 12 | }) { 13 | const svg = html` 14 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86" 15 | class="fill-current-color mr1" 16 | style="width:${size}; height:${size}"> 17 | <path d="M50 20.11A29.89 29.89 0 1 0 79.89 50 29.89 29.89 0 0 0 50 20.11zm-3.22 17a3.22 3.22 0 0 1 6.44 0v6.43a3.22 3.22 0 0 1-6.44 0zM50 66.08a16.14 16.14 0 0 1-11.41-27.49 3.28 3.28 0 0 1 1.76-.65 2.48 2.48 0 0 1 2.42 2.41 2.58 2.58 0 0 1-.77 1.77A10.81 10.81 0 0 0 38.59 50a11.25 11.25 0 0 0 22.5 0 10.93 10.93 0 0 0-3.21-7.88 3.37 3.37 0 0 1-.65-1.77 2.48 2.48 0 0 1 2.42-2.41 2.16 2.16 0 0 1 1.76.65A16.14 16.14 0 0 1 50 66.08z"/> 18 | </svg> 19 | ` 20 | return icon({ svg, title, active, action }) 21 | } 22 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/redirect-icon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import icon from './icon.js' 6 | 7 | export default function redirectIcon ({ 8 | action, 9 | active, 10 | size = '2rem', 11 | title 12 | }) { 13 | const svg = html` 14 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" 15 | class="fill-current-color" 16 | style="width:${size}; height:${size}"> 17 | <path d="M75.4 41.6l-7.3-11.2c-.1-.2-.4-.2-.5 0l-7.3 11.3c-.1.2 0 .4.2.4h4.3l-.2 20.6c0 3.2-2.6 5.8-5.8 5.8h-.2a5.8 5.8 0 0 1-5.8-5.8V37.3a11.8 11.8 0 0 0-23.6 0L29 58.7h-4.3c-.2 0-.4.3-.3.5l7.3 11.6c.1.2.4.2.6 0l7.3-11.6c.1-.2 0-.5-.3-.5h-4.2l.2-21.5a5.8 5.8 0 0 1 11.6 0v25.3c0 6.5 5.3 11.8 11.8 11.8h.2c6.5 0 11.8-5.3 11.8-11.8l.2-20.4h4.4c.4 0 .2-.4.1-.5z"/> 18 | </svg> 19 | ` 20 | return icon({ 21 | svg, 22 | title, 23 | active, 24 | action 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/tools-button.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | 6 | export default function toolsButton ({ 7 | disabled, 8 | iconD, 9 | iconSize, 10 | onClick, 11 | style, 12 | text, 13 | title 14 | }) { 15 | let buttonStyle = 'header-icon fade-in w-50 ba bw1 snow b--snow bg-transparent f7 ph1 pv0 br4 ma1 flex justify-center items-center truncate' 16 | if (disabled) { 17 | buttonStyle += ' o-60' 18 | } else { 19 | buttonStyle += ' pointer' 20 | } 21 | if (style) { 22 | buttonStyle += ` ${style}` 23 | } 24 | if (disabled) { 25 | title = '' 26 | } 27 | 28 | return html` 29 | 30 | <div class="${buttonStyle}" onclick=${disabled ? null : onClick} title="${title || ''}" ${disabled ? 'disabled' : ''}> 31 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="mr1" width="${iconSize}" height="${iconSize}"><path fill="currentColor" d="${iconD}"/></svg> 32 | <div class="flex flex-row items-center justify-between"><div class="truncate">${text}</div></div> 33 | </div> 34 | ` 35 | } 36 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/tools.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | import html from 'choo/html/index.js' 6 | import toolsButton from './tools-button.js' 7 | import { POSSIBLE_NODE_TYPES } from '../../lib/state.js' 8 | 9 | export default function tools ({ 10 | active, 11 | ipfsNodeType, 12 | isApiAvailable, 13 | isIpfsOnline, 14 | onOpenWebUi, 15 | onQuickImport 16 | }) { 17 | const activeQuickImport = active && isIpfsOnline && isApiAvailable 18 | const activeWebUI = active && isIpfsOnline && POSSIBLE_NODE_TYPES.includes(ipfsNodeType) 19 | 20 | return html` 21 | <div class="flex pb2 ph2 justify-between"> 22 | ${toolsButton({ 23 | text: browser.i18n.getMessage('panel_quickImport'), 24 | title: browser.i18n.getMessage('panel_quickImportTooltip'), 25 | disabled: !activeQuickImport, 26 | onClick: onQuickImport, 27 | iconSize: 20, 28 | iconD: 'M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V52.74H33.53v-5.48h13.73V33.53h5.48v13.73h13.73v5.48H52.74z' 29 | })} 30 | ${toolsButton({ 31 | text: browser.i18n.getMessage('panel_openWebui'), 32 | title: browser.i18n.getMessage('panel_openWebuiTooltip'), 33 | disabled: !activeWebUI, 34 | onClick: onOpenWebUi, 35 | iconSize: 18, 36 | iconD: 'M69.69 20.57c-.51-.51-1.06-1-1.62-1.47l-.16-.1c-.56-.46-1.15-.9-1.76-1.32l-.5-.35c-.25-.17-.52-.32-.79-.48A28.27 28.27 0 0050 12.23h-.69a28.33 28.33 0 00-27.52 28.36c0 13.54 19.06 37.68 26 46a3.21 3.21 0 005 0c6.82-8.32 25.46-32.25 25.46-45.84a28.13 28.13 0 00-8.56-20.18zM51.07 49.51a9.12 9.12 0 119.13-9.12 9.12 9.12 0 01-9.13 9.12z' 37 | })} 38 | </div> 39 | ` 40 | } 41 | -------------------------------------------------------------------------------- /add-on/src/popup/browser-action/version-update-icon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import icon from './icon.js' 6 | 7 | export default function versionUpdateIcon ({ 8 | action, 9 | active, 10 | className, 11 | newVersion, 12 | size = '1.8rem', 13 | title 14 | }) { 15 | let svg = html` 16 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86" 17 | class="fill-yellow-muted mr1" 18 | style="width:${size}; height:${size}"> 19 | <path xmlns="http://www.w3.org/2000/svg" d="M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V44.71h5.48zm0-26.53h-5.48v-5.49h5.48z"/> 20 | </svg> 21 | ` 22 | // special handling for beta and dev builds 23 | // TODO: remove when we have no users on beta channel anymore 24 | if (newVersion.match(/\./g).length > 2) { 25 | svg = html` 26 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86" 27 | class="fill-red-muted mr1" 28 | style="width:${size}; height:${size}"> 29 | <path d="M82.84 71.14L55.06 23a5.84 5.84 0 00-10.12 0L17.16 71.14a5.85 5.85 0 005.06 8.77h55.56a5.85 5.85 0 005.06-8.77zm-30.1-.66h-5.48V65h5.48zm0-10.26h-5.48V38.46h5.48z"/> 30 | </svg> 31 | ` 32 | title = 'Beta channel is deprecated, please switch to regular releases' 33 | className = `${className} blink` 34 | } 35 | return icon({ svg, title, active, action, className }) 36 | } 37 | -------------------------------------------------------------------------------- /add-on/src/popup/heartbeat.css: -------------------------------------------------------------------------------- 1 | @keyframes heartbeat { 2 | 0% { 3 | transform: scale(1); 4 | } 5 | 5% { 6 | transform: scale(1.05); 7 | filter: drop-shadow(0 0 1.05em rgba(95, 203, 207, 0.5)); 8 | } 9 | 10% { 10 | transform: scale(1.025); 11 | filter: drop-shadow(0 0 1.025em rgba(95, 203, 207, 0.25)); 12 | } 13 | 15% { 14 | transform: scale(1.075); 15 | filter: drop-shadow(0 0 1.075em rgba(95, 203, 207, 0.5)); 16 | } 17 | 50% { 18 | transform: scale(1); 19 | } 20 | 100% { 21 | transform: scale(1); 22 | } 23 | } 24 | 25 | .heartbeat { 26 | animation-name: heartbeat; 27 | animation-iteration-count: infinite; 28 | animation-duration: 2.5s; 29 | } -------------------------------------------------------------------------------- /add-on/src/popup/logo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import html from 'choo/html/index.js' 5 | import { braveNodeType } from '../lib/ipfs-client/brave.js' 6 | 7 | function logoFileName (nodeType, isIpfsOnline) { 8 | let prefix 9 | if (nodeType === braveNodeType) prefix = 'brave-' 10 | return `${prefix || ''}ipfs-logo-${isIpfsOnline ? 'on' : 'off'}.svg` 11 | } 12 | 13 | export default function logo ({ 14 | heartbeat = true, 15 | ipfsNodeType = 'external', 16 | isIpfsOnline = true, 17 | path, 18 | size = 52 19 | }) { 20 | return html` 21 | <img 22 | alt="IPFS" 23 | src="${path}/${logoFileName(ipfsNodeType, isIpfsOnline)}" 24 | class="v-mid ${isIpfsOnline ? '' : 'o-40'} ${isIpfsOnline && heartbeat ? 'heartbeat' : ''}" 25 | style="width:${size}px; height:${size}px" /> 26 | ` 27 | } 28 | -------------------------------------------------------------------------------- /add-on/src/popup/quick-import.css: -------------------------------------------------------------------------------- 1 | @import url('~tachyons/css/tachyons.css'); 2 | @import url('~ipfs-css/ipfs.css'); 3 | @import url('heartbeat.css'); 4 | 5 | html, body, #root { 6 | height: 100%; 7 | } 8 | 9 | .hover-inner-shadow { 10 | transition: box-shadow 0.2s ease-in-out; 11 | } 12 | .hover-inner-shadow:hover { 13 | box-shadow: inset 0 0 10px 5px rgba(211, 235, 237, 0.2); 14 | } 15 | 16 | .no-user-select { 17 | user-select: none; 18 | -moz-user-select: none; 19 | -webkit-user-select: none; 20 | } 21 | 22 | /* Temporary CSS for custom checkbox (TODO: move/replace with ipfs-css */ 23 | input[type='checkbox'] { 24 | display:none; 25 | } 26 | 27 | input[type=checkbox] + .mark { 28 | background: transparent; 29 | height: 16px; 30 | width: 16px; 31 | display: inline-block; 32 | padding: 0; 33 | border: 1px #6ACAD1 solid; 34 | position: relative; 35 | } 36 | 37 | input[type=checkbox]:checked + .mark::after { 38 | color: #6ACAD1; 39 | position: absolute; 40 | left: 3px; 41 | top: 3px; 42 | content: ""; 43 | height: 5px; 44 | width: 8px; 45 | border-left: 2px solid; 46 | border-bottom: 2px solid; 47 | transform: rotate(-45deg); 48 | } 49 | -------------------------------------------------------------------------------- /add-on/src/popup/quick-import.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | 4 | <head> 5 | <meta charset="utf-8"> 6 | <meta name="viewport" content="width=device-width"> 7 | <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" /> 8 | <link rel="stylesheet" href="/dist/bundles/uiCommons.css"> 9 | <link rel="stylesheet" href="/dist/bundles/importPage.css"> 10 | </head> 11 | 12 | <body> 13 | <div id="root"></div> 14 | <script src="/dist/bundles/uiCommons.bundle.js"></script> 15 | <script src="/dist/bundles/importPage.bundle.js"></script> 16 | </body> 17 | 18 | </html> 19 | -------------------------------------------------------------------------------- /add-on/src/recovery/recovery.css: -------------------------------------------------------------------------------- 1 | @import url('~tachyons/css/tachyons.css'); 2 | @import url('~ipfs-css/ipfs.css'); 3 | 4 | #left-col { 5 | background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); 6 | background-size: 100%; 7 | background-repeat: repeat; 8 | } 9 | 10 | a:hover { 11 | text-decoration: none; 12 | } 13 | 14 | a:visited { 15 | color: inherit; 16 | } 17 | 18 | /* 19 | https://github.com/tachyons-css/tachyons-queries 20 | Tachyons: $point == large 21 | */ 22 | @media (min-width: 60em) { 23 | #left-col { 24 | position: fixed; 25 | top: 0; 26 | right: 55%; 27 | width: 45%; 28 | background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%); 29 | background-size: 100%; 30 | background-repeat: repeat; 31 | } 32 | 33 | #right-col { 34 | margin-left: 54%; 35 | margin-right: 6%; 36 | } 37 | } 38 | 39 | @media (max-height: 800px) { 40 | #left-col img { 41 | width: 98px !important; 42 | height: 98px !important; 43 | } 44 | 45 | #left-col svg { 46 | width: 60px; 47 | } 48 | } 49 | 50 | .recovery-root { 51 | width: 100%; 52 | height: 100%; 53 | text-align: left; 54 | } 55 | -------------------------------------------------------------------------------- /add-on/src/recovery/recovery.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>IPFS Node is Offline 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /add-on/src/recovery/recovery.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import choo from 'choo' 5 | import html from 'choo/html/index.js' 6 | import { i18n, runtime } from 'webextension-polyfill' 7 | import { nodeOffSvg } from '../landing-pages/welcome/page.js' 8 | import createWelcomePageStore from '../landing-pages/welcome/store.js' 9 | import { optionsPage } from '../lib/constants.js' 10 | import './recovery.css' 11 | 12 | const app = choo() 13 | 14 | const learnMoreLink = html`${i18n.getMessage('recovery_page_learn_more')}` 15 | 16 | const optionsPageLink = html`${i18n.getMessage('recovery_page_update_preferences')}` 17 | 18 | // TODO (whizzzkid): refactor base store to be more generic. 19 | app.use(createWelcomePageStore(i18n, runtime)) 20 | // Register our single route 21 | app.route('*', (state) => { 22 | runtime.sendMessage({ telemetry: { trackView: 'recovery' } }) 23 | const { hash } = window.location 24 | const { href: publicURI } = new URL(decodeURIComponent(hash.slice(1))) 25 | 26 | if (!publicURI) { 27 | return 28 | } 29 | 30 | const openURLFromHash = () => { 31 | try { 32 | console.log('Opening URL from hash:', publicURI) 33 | window.location.replace(publicURI) 34 | } catch (err) { 35 | console.error('Failed to open URL from hash:', err) 36 | } 37 | } 38 | 39 | // if the IPFS node is online, open the URL from the hash, this will redirect to the local node. 40 | if (state.isIpfsOnline) { 41 | openURLFromHash() 42 | return 43 | } 44 | 45 | return html`
46 |
47 |
48 | ${nodeOffSvg(200)} 49 |

${i18n.getMessage('recovery_page_sub_header')}

50 |
51 |
52 | 53 |
54 |

${i18n.getMessage('recovery_page_message_p1')}

55 |

${i18n.getMessage('recovery_page_message_p2')}

56 |

Public URL: ${publicURI}

57 | 64 |

65 | ${learnMoreLink} | ${optionsPageLink} 66 | 67 |

68 |
` 69 | }) 70 | 71 | // Start the application and render it to the given querySelector 72 | app.mount('#root') 73 | 74 | // Set page title and header translation 75 | document.title = i18n.getMessage('recovery_page_title') 76 | -------------------------------------------------------------------------------- /add-on/src/types/companion.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface CompanionOptions { 3 | active: boolean 4 | ipfsNodeType: string 5 | ipfsNodeConfig: string 6 | publicGatewayUrl: string 7 | publicSubdomainGatewayUrl: string 8 | useCustomGateway: boolean 9 | useSubdomains: boolean 10 | enabledOn: string[] 11 | disabledOn: string[] 12 | automaticMode: boolean 13 | linkify: boolean 14 | dnslinkPolicy: boolean | string 15 | dnslinkDataPreload: boolean 16 | dnslinkRedirect: boolean 17 | recoverFailedHttpRequests: boolean 18 | detectIpfsPathHeader: boolean 19 | preloadAtPublicGateway: boolean 20 | catchUnhandledProtocols: boolean 21 | displayNotifications: boolean 22 | displayReleaseNotes: boolean 23 | customGatewayUrl: string 24 | ipfsApiUrl: string 25 | ipfsApiPollMs: number 26 | logNamespaces: string 27 | importDir: string 28 | useLatestWebUI: boolean 29 | dismissedUpdate: null | string 30 | openViaWebUI: boolean 31 | telemetryGroupMinimal: boolean 32 | telemetryGroupPerformance: boolean 33 | telemetryGroupUx: boolean 34 | telemetryGroupFeedback: boolean 35 | telemetryGroupLocation: boolean 36 | } 37 | 38 | export interface CompanionState extends Omit { 39 | peerCount: number 40 | pubGwURL: URL 41 | pubGwURLString: string 42 | pubSubdomainGwURL: URL 43 | pubSubdomainGwURLString: string 44 | redirect: boolean 45 | apiURL: URL 46 | apiURLString: string 47 | gwURL: URL 48 | gwURLString: string 49 | activeIntegrations: (url: string) => boolean 50 | localGwAvailable: boolean 51 | webuiRootUrl: string 52 | } 53 | 54 | interface SwitchToggleArguments { 55 | id: string 56 | onchange: () => void 57 | checked?: boolean 58 | disabled?: boolean 59 | style?: string 60 | } 61 | export function SwitchToggle (args: SwitchToggleArguments): undefined | HTMLElement 62 | -------------------------------------------------------------------------------- /add-on/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'is-ipfs' { 2 | function cid (value: string): boolean 3 | } 4 | -------------------------------------------------------------------------------- /add-on/src/utils/i18n.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import browser from 'webextension-polyfill' 5 | 6 | /** 7 | * Renders a translated string with html anchors. 8 | * @param {String} message The message to translate. 9 | * @param {Array} links The array of hrefs. 10 | * @param {Object} attributes HTML attributes to put in the anchor. 11 | * @return {html} An HTML node with the translated string with anchors. 12 | */ 13 | export const renderTranslatedLinks = (message, links, attributes) => { 14 | const regexLink = /<\d+>(.+?)<\/\d+>/mg 15 | const regexIndex = /<(\d+)>/mg 16 | const str = browser.i18n.getMessage(message) 17 | let output = str 18 | 19 | let matchLink = regexLink.exec(str) 20 | while (matchLink !== null) { 21 | let matchIndex = regexIndex.exec(matchLink[0]) 22 | while (matchIndex !== null) { 23 | output = output.replace(matchLink[0], `${matchLink[1]}`) 24 | matchIndex = regexIndex.exec(str) 25 | } 26 | matchLink = regexLink.exec(str) 27 | } 28 | 29 | const template = document.createElement('template') 30 | template.innerHTML = output 31 | 32 | return template.content 33 | } 34 | 35 | /** 36 | * Renders a translated string with html spans. 37 | * @param {String} message - The message to translate. 38 | * @param {Array} values - The array of dynamic values ($1, $2 etc) in the translation. 39 | * @param {Object} attributes - HTML attributes to put in the span around each of them. 40 | * @return {html} An HTML node with the translated string with spans. 41 | */ 42 | export const renderTranslatedSpans = (message, values, attributes) => { 43 | const regexSpan = /<\d+>(.+?)<\/\d+>/mg 44 | const regexIndex = /<(\d+)>/mg 45 | const str = browser.i18n.getMessage(message, values) 46 | let output = str 47 | 48 | let matchSpan = regexSpan.exec(str) 49 | while (matchSpan !== null) { 50 | let matchIndex = regexIndex.exec(matchSpan[0]) 51 | while (matchIndex !== null) { 52 | output = output.replace(matchSpan[0], `${matchSpan[1]}`) 53 | matchIndex = regexIndex.exec(str) 54 | } 55 | matchSpan = regexSpan.exec(str) 56 | } 57 | 58 | const template = document.createElement('template') 59 | template.innerHTML = output 60 | 61 | return template.content 62 | } 63 | -------------------------------------------------------------------------------- /ci/access-control-allow-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "${ACCESS_CONTROL_ALLOW_ORIGIN:-[\"*\"]}" 5 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "${ACCESS_CONTROL_ALLOW_METHODS:-[\"*\"]}" 6 | -------------------------------------------------------------------------------- /ci/download-release-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | IPFS_COMPANION_VERSION=${IPFS_COMPANION_VERSION:-$(jq -r '.version' ./add-on/manifest.common.json)} 5 | 6 | id="$(curl --retry 5 --no-progress-meter "https://api.github.com/repos/ipfs/ipfs-companion/releases/tags/v$IPFS_COMPANION_VERSION" | jq '.id')" 7 | assets="$(curl --retry 5 --no-progress-meter --location "https://api.github.com/repos/ipfs/ipfs-companion/releases/$id/assets" | jq -r '.[].name')" 8 | 9 | if [ ! -d build ]; then 10 | mkdir build 11 | fi 12 | 13 | for asset in $assets; do 14 | curl --retry 5 --no-progress-meter --location --output "build/$asset" "https://github.com/ipfs/ipfs-companion/releases/download/v$IPFS_COMPANION_VERSION/$asset" 15 | done 16 | -------------------------------------------------------------------------------- /ci/update-manifest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used by CI setup to update add-on name in manifest 3 | # to explicitly state that produced artifact is a dev build. 4 | set -e 5 | 6 | MANIFEST=add-on/manifest.common.json 7 | 8 | # restore original in case it was modified manually 9 | test -d .git && git checkout -- $MANIFEST 10 | 11 | # skip all manifest mutations when building for stable channel 12 | if [ "$RELEASE_CHANNEL" = "stable" ]; then 13 | echo "Skipping manifest modification (RELEASE_CHANNEL=${RELEASE_CHANNEL})" 14 | exit 0 15 | fi 16 | 17 | # Use jq for JSON operations 18 | function set-manifest { 19 | jq -M --indent 2 "$1" < $MANIFEST > tmp.json && mv tmp.json $MANIFEST 20 | } 21 | 22 | ## Set NAME 23 | # Name includes git revision to make QA and bug reporting easier for users :-) 24 | REVISION=$(git show-ref --head HEAD | head -c 7) 25 | if [ "$RELEASE_CHANNEL" = "beta" ]; then 26 | set-manifest ".name = \"IPFS Companion (Beta @ $REVISION)\"" 27 | else 28 | set-manifest ".name = \"IPFS Companion (Dev Build @ $REVISION)\"" 29 | fi 30 | grep $REVISION $MANIFEST 31 | 32 | ## Set VERSION 33 | # Browsers do not accept non-numeric values in version string 34 | # so we calculate some sub-versions based on number of commits in master and current branch 35 | # mozilla/addons-linter: Version string must be a string comprising one to four dot-separated integers (0-65535). E.g: 1.2.3.4" 36 | BUILD_VERSION=$(git rev-list --count --first-parent HEAD) 37 | set-manifest ".version = (.version + \".${BUILD_VERSION}\")" 38 | grep \"version\" $MANIFEST 39 | -------------------------------------------------------------------------------- /docker-compose.e2e.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | firefox: 4 | image: ${FIREFOX_IMAGE:-selenium/standalone-firefox}:${FIREFOX_VERSION:-latest} 5 | platform: linux/amd64 6 | shm_size: 2g 7 | ports: 8 | - 4444 9 | - 7900 10 | chromium: 11 | # WARN: `standalone-chrome` does NOT work on ARM-based machines; 12 | # see https://github.com/SeleniumHQ/docker-selenium#experimental-mult-arch-aarch64armhfamd64-images; 13 | # try using `seleniarm/standalone-chromium` instead 14 | # export CHROMIUM_IMAGE=seleniarm/standalone-chromium 15 | image: ${CHROMIUM_IMAGE:-selenium/standalone-chrome}:${CHROMIUM_VERSION:-latest} 16 | platform: linux/amd64 17 | shm_size: 2g 18 | ports: 19 | - 4444 20 | - 7900 21 | kubo: 22 | image: ipfs/kubo:${KUBO_VERSION:-latest} 23 | ports: 24 | - 4001 25 | - 5001 26 | - 8080 27 | volumes: 28 | - ./ci/access-control-allow-all.sh:/container-init.d/001-access-control-allow-all.sh 29 | e2e: 30 | build: 31 | dockerfile: ./Dockerfile 32 | context: . 33 | environment: 34 | - SELENIUM_REMOTE_CHROMIUM_URL=http://chromium:4444 35 | - SELENIUM_REMOTE_FIREFOX_URL=http://firefox:4444 36 | - IPFS_API_URL=http://kubo:5001 37 | - CUSTOM_GATEWAY_URL=http://kubo:8080 38 | - TEST_E2E=true 39 | - TEST_HEADLESS=${TEST_HEADLESS:-false} 40 | - IPFS_COMPANION_VERSION=${IPFS_COMPANION_VERSION} 41 | volumes: 42 | - ./build:/home/node/app/build 43 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 4 | > Contributions are always welcome! 5 | 6 | This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 7 | 8 | 9 | ## Issues 10 | 11 | ### Creating New Issues 12 | 13 | Do not hesitate and [create a new Issue](https://github.com/ipfs/ipfs-companion/issues/new/choose) if you see a bug, room for improvement or simply have a question. 14 | 15 | ### Working on existing Issues 16 | 17 | Feel free to work on issues that are [not assigned yet](https://github.com/ipfs/ipfs-companion/issues?utf8=✓&q=is%3Aissue+is%3Aopen+no%3Aassignee), especially ones marked with [help wanted](https://github.com/ipfs/ipfs-companion/issues?q=is%3Aopen+label%3A%22help+wanted%22+no%3Aassignee) tag. 18 | As a courtesy, please add a comment informing about your intent. That way we will not duplicate effort. 19 | 20 | ### Submitting Pull Requests 21 | 22 | Just make sure your PR comes with its own tests and does pass CI tests. 23 | See the [GitHub Flow Guide](https://guides.github.com/introduction/flow/) for details. 24 | 25 | 26 | ## Translations 27 | 28 | 29 | Go to Transifex and join [IPFS Companion Translation Project](https://www.transifex.com/ipfs/ipfs-companion/) :sparkles: 30 | 31 | If you want to download translations from Transifex and run them locally, make sure to read [Localization Notes](./LOCALIZATION-NOTES.md) first. 32 | 33 | ## Writing Code 34 | 35 | **If you plan to write code make sure to read [Developer Notes](DEVELOPER-NOTES.md) to get familiar with tools and commands that will make your work easier.** 36 | 37 | ## How to Help with Things Beyond Browser Extension? 38 | 39 | - https://github.com/ipfs/in-web-browsers 40 | - https://github.com/ipfs/community/blob/master/CONTRIBUTING.md 41 | [![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) 42 | -------------------------------------------------------------------------------- /docs/LOCALIZATION-NOTES.md: -------------------------------------------------------------------------------- 1 | # Localization in IPFS Companion 2 | 3 | ### Table of contents 4 | 5 | - [Localization in IPFS Companion](#localization-in-ipfs-companion) 6 | - [Table of contents](#table-of-contents) 7 | - [Running Chrome with a specific locale](#running-chrome-with-a-specific-locale) 8 | - [Further resources](#further-resources) 9 | - [Running Firefox with a specific locale](#running-firefox-with-a-specific-locale) 10 | - [Further resources](#further-resources-1) 11 | - [Contributing translations](#contributing-translations) 12 | 13 | IPFS Companion supports running in specific locales, with translations provided by the community via Transifex. 14 | 15 | ## Running Chrome with a specific locale 16 | 17 | Chrome comes with locales out of the box, so it is enough to set the proper env: 18 | 19 | ```go 20 | LANGUAGE=pl chromium --user-data-dir=`mktemp -d` 21 | ``` 22 | 23 | ### Further resources 24 | 25 | - [Language Codes in Chromium Project](https://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc) 26 | 27 | ## Running Firefox with a specific locale 28 | 29 | Unless you've installed a locale-specific build, Firefox will have English only. If your build already has the locale you are interested in, skip step #2. 30 | 31 | 1. Set `intl.locale.requested` in `about:config` or the command line via: 32 | 33 | ```bash 34 | web-ext run --pref intl.locale.requested=pl 35 | ``` 36 | 37 | 2. Install your language pack from https://addons.mozilla.org/firefox/language-tools/ 38 | 3. Reload the browser extension; it should detect your new locale 39 | 40 | ### Further resources 41 | 42 | - [Mozilla: Use Firefox in another language](https://support.mozilla.org/en-US/kb/use-firefox-interface-other-languages-language-pack#w_how-to-change-the-language-of-the-user-interface) 43 | - [Mozilla: Locale Codes](https://wiki.mozilla.org/L10n:Locale_Codes) 44 | 45 | ## Contributing translations 46 | 47 | Internationalization in IPFS Companion (and all IPFS-related projects) depends on the contributions of the community. You can give back by contributing translations in your language(s)! Go to the [IPFS Companion Transifex page](https://www.transifex.com/ipfs/ipfs-companion/), send a request to join a specific language team, and start translating. You can also download raw files from Transifex, translate them in your own editor/tool, and then upload them back there, but many people prefer using the simple and friendly Transifex GUI. 48 | 49 | If your language is not present in `add-on/_locales` yet, but is supported by mainstream browsers, please create a [new issue](https://github.com/ipfs/ipfs-companion/issues/new) requesting it. 50 | 51 | Don't worry if GitHub does not immediately reflect translations added at Transifex: New translations are merged manually before every release. Locale files at GitHub are often behind what is already translated at Transifex. It is a good idea to keep Transifex email notifications enabled to be notified about new strings to translate. 52 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Content present in this directory was moved to [IPFS Docs](https://docs.ipfs.io). 2 | -------------------------------------------------------------------------------- /docs/RELEASE-PROCESS.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | --- 3 | 4 | This process can be used to push a manual release to the [Firefox Add-Ons](https://addons.mozilla.org/) and [Chrome Web Store](https://chrome.google.com/webstore/category/extensions). Before you can do that you need to: 5 | 6 | - [Release Process](#release-process) 7 | - [Tag a Release](#tag-a-release) 8 | - [Build Release Artifacts](#build-release-artifacts) 9 | - [Publish on Chrome Web Store](#publish-on-chrome-web-store) 10 | - [Publish on Firefox Add-Ons Store](#publish-on-firefox-add-ons-store) 11 | - [Release on Github.](#release-on-github) 12 | 13 | 14 | ## Tag a Release 15 | 16 | - Bump `` version in `add-on/manifest.common.json` 17 | - Create a `chore(main): Release v` PR. 18 | - Create a PR for Release 19 | - Generate and push the tag to github 20 | ```sh 21 | git tag v && git push && git push origin v # Don't forget the 'v' prefix 22 | ``` 23 | - Create draft release from https://github.com/ipfs/ipfs-companion/releases using the `v` tag 24 | - Use github's auto release notes feature to fill release notes. 25 | 26 | ## Build Release Artifacts 27 | 28 | - run `npm run release-build` 29 | - `build` folder would contain three artifacts, one each for: 30 | - brave 31 | - chromium 32 | - firefox 33 | - ~~Attach these assets to the draft release.~~ The built assets should be attached to the draft release automatically if all the above steps are followed. 34 | 35 | ## Publish on Chrome Web Store 36 | 37 | All Chromium-based browsers support install from Chrome Web Store. 38 | Brave, Opera, and Edge do not require additional publishing step. 39 | 40 | - IPFS Companion Chrome Webstore: https://chrome.google.com/webstore/detail/ipfs-companion/nibjojkomfdiaoajekhjakgkdhaomnch 41 | - Publishing requires your Google Account to belong to the IPFS Companion Maintainers Google Group (ask IPFS Stewards to be added). 42 | - Go to Developer Dashboard and select publisher in the top right: `IPFS Shipyard` 43 | - You should see `IPFS Companion` and `IPFS Companion (Beta @ xxxxxx). If not, select `Items` on the left menu. 44 | - Select the correct extension you want to publish, usually `IPFS Companion`. 45 | - Select `Package` on the left menu (Under the Build category). 46 | - Upload the newly built package for Chromium [earlier](#build-release-artifacts). Use the same `ipfs_companion-_chromium.zip` file on the draft release. 47 | - The new package goes to draft state automatically. 48 | - Go to `Store listing` section from the left sub-menu. (This should happen automatically after uploading a new package). 49 | - Click `Save Draft` and then `Submit for Review`. 50 | - "Review" may take from a few hours to a few days. Google sends no email informing when new version is approved. 51 | - Only way to check when it is published is to inspect the version number on the store listing or Dev Dashboard. 52 | 53 | ## Publish on [Firefox Add-Ons Store](https://addons.mozilla.org/) 54 | 55 | - IPFS Companion Firefox Add-On: https://addons.mozilla.org/en-US/firefox/addon/ipfs-companion/ 56 | - Publishing requires logging into a [Developer Account](https://addons.mozilla.org/developers/) at Firefox Add-Ons site. The account needs to be marked as one of developers of IPFS Companion (ask IPFS Stewards to be added). 57 | - Goto `Manage My Submissions` from the drop-down menu from your username. 58 | - Select `IPFS Companion` item. 59 | - On the left menu select `Manage Status & Versions` 60 | - Open the latest one in a new tab because you'll need this for reference for copying listing details. 61 | - Click `Upload a New Version`. 62 | - Select the Firefox-specific asset generated [earlier](#build-release-artifacts). 63 | - For `Compatibility` select both : 64 | - Firefox 65 | - Firefox for Android 66 | - Optionally quickly check the validation report to see there are no blocking validations. 67 | - Click continue. 68 | - When asked: `Do you need to submit source code` select `yes` 69 | - Go to Github Release and download .zip with Source Code for the release tag. 70 | - Upload it to Mozilla. 71 | - In the field asking for release notes just link to the release at Github 72 | - Add Instructions/Notes for the reviewer (*Warning: if you forget, the release may be rejected during human review*): 73 | - Reuse the explanation from the previous release (ask reviewer to run the `npm run release-build` so they can run the build in a Docker container and get reproducible result across platforms) 74 | - The new build is published immediately, but it will be reviewed in a week by a real human (unlike Chrome Web Store), and can be taken down if it doesn’t meet the guidelines around privacy and reproducibility. 75 | 76 | ## Release on Github. 77 | 78 | Release the draft release just created. You don't need to wait for the Chrome or Firefox stores to finish the extension review; you can just verify that the review is in progress. 79 | 80 | That's it! 81 | -------------------------------------------------------------------------------- /docs/dnslink.md: -------------------------------------------------------------------------------- 1 | Moved [here](https://docs.ipfs.io/how-to/dnslink-companion/) 2 | -------------------------------------------------------------------------------- /docs/node-types.md: -------------------------------------------------------------------------------- 1 | Moved [here](https://docs.ipfs.io/how-to/companion-node-types/) 2 | -------------------------------------------------------------------------------- /docs/telemetry/COLLECTED_DATA.md: -------------------------------------------------------------------------------- 1 | # Telemetry Data Collection 2 | 3 | ## Telemetry has been removed 4 | 5 | All telemetry collection has been removed from this project. No data is being collected or sent to any analytics services. 6 | 7 | The previous implementation used Countly for telemetry collection, but this functionality has been completely removed. 8 | 9 | ## Privacy 10 | 11 | Currently, there is no telemetry collection implemented in this project. 12 | As a general rule, we collect only application data; no user data. 13 | 14 | ## Historical Information 15 | 16 | Previously, the project collected application data (not user data) through Countly. The following information is kept for historical reference only: 17 | 18 | ### What metrics data were collected 19 | 20 | | Metric data name | Metric feature name | Metric trigger | Analytics use | 21 | | :-----------------: | ------------------- | -------------------------------------------- | ------------- | 22 | | view:welcome | views | When the welcome view is shown | View count | 23 | | view:options | views | When the options view is shown | View count | 24 | | view:quick-import | views | When the quick-import view is shown | View count | 25 | | view:browser-action | views | When the browser-action view is shown | View count | 26 | | event:url-resolved | event | Number of URLs resolved by companion | Metrics | 27 | | event:url-observed | event | Number of URLs observed (including resolved) | Metrics | 28 | 29 | - "Metric data name" - The app-specific metric/event name we're using for this metric data. (e.g. APP_BOOTSTRAP_START) 30 | - "Metric feature name" - The metric feature the event/metric data correlates to. The group the metric feature belongs to is defined in our [COLLECTION_POLICY](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/COLLECTION_POLICY.md#metric-features-and-their-groupings). (e.g. Minimal) 31 | - "Metric trigger" - An explanation covering when this metric data is triggered. (e.g. On Application init) 32 | - "Analytics use" - An explanation about how this metric data is used for analytics. (e.g. Input to load time calculations) 33 | - "Notes" - Any additional notes. (e.g. Used as a timestamp identifier for when an application is first loaded) 34 | 35 | ## Other related documents 36 | 37 | - [COLLECTION_POLICY](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/COLLECTION_POLICY.md) 38 | - [PRIVACY_POLICY](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/PRIVACY_POLICY.md) 39 | - [FAQs](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/FAQs.md) 40 | -------------------------------------------------------------------------------- /docs/x-ipfs-path-header.md: -------------------------------------------------------------------------------- 1 | Moved [here](https://docs.ipfs.io/how-to/companion-x-ipfs-path-header/) 2 | -------------------------------------------------------------------------------- /patches/@multiformats+multiaddr+11.0.7.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js b/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js 2 | index 1ee89f9..ed10f8e 100644 3 | --- a/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js 4 | +++ b/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js 5 | @@ -32,6 +32,7 @@ export const table = [ 6 | [445, 296, 'onion3'], 7 | [446, V, 'garlic64'], 8 | [460, 0, 'quic'], 9 | + [461, 0, 'quic-v1'], 10 | [465, 0, 'webtransport'], 11 | [466, V, 'certhash'], 12 | [477, 0, 'ws'], 13 | diff --git a/node_modules/@multiformats/multiaddr/src/protocols-table.ts b/node_modules/@multiformats/multiaddr/src/protocols-table.ts 14 | index 8d864cd..2e352ea 100644 15 | --- a/node_modules/@multiformats/multiaddr/src/protocols-table.ts 16 | +++ b/node_modules/@multiformats/multiaddr/src/protocols-table.ts 17 | @@ -41,6 +41,7 @@ export const table: Array<[number, number, string, boolean?, boolean?]> = [ 18 | [445, 296, 'onion3'], 19 | [446, V, 'garlic64'], 20 | [460, 0, 'quic'], 21 | + [461, 0, 'quic-v1'], 22 | [465, 0, 'webtransport'], 23 | [466, V, 'certhash'], 24 | [477, 0, 'ws'], 25 | -------------------------------------------------------------------------------- /patches/@protobufjs+inquire+1.1.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@protobufjs/inquire/index.js b/node_modules/@protobufjs/inquire/index.js 2 | index 33778b5..3115fa7 100644 3 | --- a/node_modules/@protobufjs/inquire/index.js 4 | +++ b/node_modules/@protobufjs/inquire/index.js 5 | @@ -9,7 +9,7 @@ module.exports = inquire; 6 | */ 7 | function inquire(moduleName) { 8 | try { 9 | - var mod = eval("quire".replace(/^/,"re"))(moduleName); // eslint-disable-line no-eval 10 | + var mod = require(moduleName); // eslint-disable-line no-eval 11 | if (mod && (mod.length || Object.keys(mod).length)) 12 | return mod; 13 | } catch (e) {} // eslint-disable-line no-empty 14 | -------------------------------------------------------------------------------- /patches/multiaddr+10.0.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/multiaddr/src/convert.js b/node_modules/multiaddr/src/convert.js 2 | index c315201..80170c7 100644 3 | --- a/node_modules/multiaddr/src/convert.js 4 | +++ b/node_modules/multiaddr/src/convert.js 5 | @@ -51,6 +51,7 @@ Convert.toString = function convertToString (proto, buf) { 6 | case 55: // dns6 7 | case 56: // dnsaddr 8 | case 400: // unix 9 | + case 466: // certhash 10 | case 777: // memory 11 | return bytes2str(buf) 12 | 13 | @@ -84,6 +85,7 @@ Convert.toBytes = function convertToBytes (/** @type {string | number } */ proto 14 | case 55: // dns6 15 | case 56: // dnsaddr 16 | case 400: // unix 17 | + case 466: // certhash 18 | case 777: // memory 19 | return str2bytes(str) 20 | 21 | diff --git a/node_modules/multiaddr/src/protocols-table.js b/node_modules/multiaddr/src/protocols-table.js 22 | index 3431af5..8939fb1 100644 23 | --- a/node_modules/multiaddr/src/protocols-table.js 24 | +++ b/node_modules/multiaddr/src/protocols-table.js 25 | @@ -60,6 +60,9 @@ Protocols.table = [ 26 | [445, 296, 'onion3'], 27 | [446, V, 'garlic64'], 28 | [460, 0, 'quic'], 29 | + [461, 0, 'quic-v1'], 30 | + [465, 0, 'webtransport'], 31 | + [466, V, 'certhash'], 32 | [477, 0, 'ws'], 33 | [478, 0, 'wss'], 34 | [479, 0, 'p2p-websocket-star'], 35 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "include-component-in-tag": false, 3 | "group-pull-request-title-pattern": "chore${scope}: release${component} v${version}", 4 | "extra-files": [ 5 | { 6 | "type": "json", 7 | "path": "add-on/manifest.common.json", 8 | "jsonpath": "$.version" 9 | } 10 | ], 11 | "packages": { 12 | ".": { 13 | "release-type": "node", 14 | "plugins": ["node-workspace"], 15 | "package-name": "ipfs-companion" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/fetch-unbranded.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BUILD_TYPE=${1:-$FIREFOX_RELEASE} 3 | echo "Looking up latest URL for $BUILD_TYPE" 4 | BUILD_ROOT="/pub/firefox/tinderbox-builds/mozilla-${BUILD_TYPE}/" 5 | ROOT="https://archive.mozilla.org" 6 | LATEST=$(curl -s "$ROOT$BUILD_ROOT" | grep $BUILD_TYPE | grep -Po '\K[[:digit:]]+' | sort -n | tail -1) 7 | FILE=$(curl -s "$ROOT$BUILD_ROOT$LATEST/" | grep '.tar.' | grep -Po ' console.log(`progress: ${Math.round(state.percent)} %, transferred: ${state.size.transferred}`, state)) 19 | .on('response', (response) => console.log('Status Code', response.statusCode)) 20 | .on('error', (error) => console.log('Download Error', error)) 21 | .on('close', () => console.log('Done! webui extracted to: ' + destination)) 22 | .pipe( 23 | tar.extract({ 24 | strip: 1, 25 | C: destination 26 | }) 27 | ) 28 | -------------------------------------------------------------------------------- /scripts/generate-png-icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | dir=$(dirname $0) 3 | for svg in $dir/../add-on/icons/*.svg; do 4 | for size in 19 38 128; do 5 | png=$dir/../add-on/icons/png/$(basename -s .svg ${svg})_${size}.png 6 | inkscape -z -e $png -w $size -h $size $svg 7 | optipng -o7 -i0 $png 8 | done 9 | done 10 | -------------------------------------------------------------------------------- /scripts/new-locale-key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read -p "Key: " key 4 | read -p "Message: " message 5 | 6 | dir=$(dirname $0) 7 | 8 | cat $dir/../add-on/_locales/en/messages.json | \ 9 | jq --arg foo bar ". + {\"${key}\": {\"message\": \"${message}\"}}" | \ 10 | sponge $dir/../add-on/_locales/en/messages.json 11 | 12 | tail -10 $dir/../add-on/_locales/en/messages.json 13 | -------------------------------------------------------------------------------- /scripts/prom-client-stub/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This is a basic stub to build node-like js-ipfs 4 | // for use in Brave, but without prometheus libs which are not compatible 5 | // with browser context 6 | function Gauge () {} 7 | module.exports = { 8 | Gauge, 9 | register: { 10 | clear () { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/rename-artifacts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import glob from 'glob' 5 | 6 | const files = glob.sync('build/*/*.zip') 7 | 8 | files.map(async file => { 9 | const path = file.split('/') 10 | const name = path.pop().split('.zip').shift() 11 | const target = path.pop() 12 | const newFile = `build/${name}_${target}.zip` 13 | // remove old artifact, if exists 14 | if (fs.existsSync(newFile)) fs.unlinkSync(newFile) 15 | // rename artifact 16 | console.log(`${file} → ${newFile}`) 17 | fs.renameSync(file, newFile) 18 | // remove empty dir 19 | fs.rmdirSync(`build/${target}`, { recursive: true }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/data/linkify-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | Make sure the Linkify experiment is enabled in IPFS Companion before you load this page:
9 | Linkify enabled on Preferences screen 10 |

11 |

Should be linkified

13 | /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w 14 |
/ipfs/bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy 15 |
ipfs://QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w (cidv0b58) 16 |
ipfs://bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy (cidv1b32) 17 |
ipns://en.wikipedia-on-ipfs.org 18 |
dweb:/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w 19 |

Should NOT be linkified

20 |
ipfs:/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w 21 |
ipns:/en.wikipedia-on-ipfs.org 22 |
dweb://ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w 23 |
24 |

25 | dweb:/ipfs/Qm.. link 26 | relative /ipfs/Qm link 27 | gateway link 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/functional/lib/ipfs-client/reloaders/reloaders.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { describe, it, before, beforeEach } from 'mocha' 3 | import browser from 'sinon-chrome' 4 | import sinon from 'sinon' 5 | 6 | // Units to be tested. 7 | import { 8 | prepareReloadExtensions, 9 | InternalTabReloader, 10 | LocalGatewayReloader, 11 | WebUiReloader 12 | } from '../../../../../add-on/src/lib/ipfs-client/reloaders/index.js' 13 | 14 | const CUSTOM_GATEWAY_URL = { 15 | customGatewayUrl: 'http://127.0.0.1:8080' 16 | } 17 | 18 | const logger = sinon.spy() 19 | 20 | describe('Reloader Helper Method: prepareReloadExtensions', function () { 21 | it('Prepares the reloader extensions', async function () { 22 | browser.runtime.id = 'testid' 23 | browser.storage.local.get.resolves(CUSTOM_GATEWAY_URL) 24 | browser.runtime.getURL.returns('chrome-extension://') 25 | await prepareReloadExtensions([WebUiReloader, InternalTabReloader, LocalGatewayReloader], browser, logger) 26 | sinon.assert.callCount(logger, 3) 27 | }) 28 | }) 29 | 30 | describe('Reloaders', function () { 31 | before(() => { 32 | global.browser = browser 33 | browser.runtime.id = 'testid' 34 | global.chrome = {} 35 | }) 36 | 37 | beforeEach(function () { 38 | browser.flush() 39 | logger.resetHistory() 40 | }) 41 | 42 | describe('WebUiReloader', function () { 43 | let webUiReloader 44 | beforeEach(async function () { 45 | webUiReloader = new WebUiReloader(browser, logger) 46 | await webUiReloader.init() 47 | }) 48 | 49 | it('should initialize', function () { 50 | sinon.assert.calledWith(logger, 'Initialized without additional config.') 51 | }) 52 | 53 | it('should handle webUi reloading', function () { 54 | const tabs = [{ 55 | url: 'http://some-url.com', 56 | id: 1 57 | }, { 58 | url: 'ipfs://some-ipfs-server.tld/webui/index.html#/', 59 | id: 2 60 | }] 61 | 62 | webUiReloader.reload(tabs) 63 | sinon.assert.calledWith(browser.tabs.reload, 2) 64 | sinon.assert.calledWith(logger, 'reloading webui at ipfs://some-ipfs-server.tld/webui/index.html#/') 65 | }) 66 | }) 67 | 68 | describe('InternalTabReloader', function () { 69 | let internalTabReloader 70 | beforeEach(async function () { 71 | browser.runtime.getURL.returns('chrome-extension://') 72 | internalTabReloader = new InternalTabReloader(browser, logger) 73 | await internalTabReloader.init() 74 | }) 75 | 76 | it('should initialize', function () { 77 | sinon.assert.calledWith(logger, 'InternalTabReloader Ready for use.') 78 | }) 79 | 80 | it('should handle internal tab reloading', function () { 81 | const tabs = [{ 82 | url: 'http://127.0.0.1:8080/ipfs/cid/wiki1', 83 | id: 1 84 | }, { 85 | url: 'chrome-extension:///index.html', 86 | id: 2 87 | }] 88 | 89 | internalTabReloader.reload(tabs) 90 | sinon.assert.calledWith(browser.tabs.reload, 2) 91 | sinon.assert.calledWith(logger, 'reloading internal extension page at chrome-extension:///index.html') 92 | }) 93 | }) 94 | 95 | describe('LocalGatewayReloader', function () { 96 | let localGatewayReloader 97 | beforeEach(async function () { 98 | browser.storage.local.get.resolves(CUSTOM_GATEWAY_URL) 99 | localGatewayReloader = new LocalGatewayReloader(browser, logger) 100 | await localGatewayReloader.init() 101 | }) 102 | 103 | it('should initialize', function () { 104 | sinon.assert.calledWith(logger, 'Initialized without additional config.') 105 | }) 106 | 107 | it('should handle local gateway tab reloading', function () { 108 | const tabs = [{ 109 | title: 'Main Page', 110 | url: 'http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki1', 111 | id: 1 112 | }, { 113 | title: '127.0.0.1:8080', 114 | url: 'http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki2', 115 | id: 2 116 | }] 117 | 118 | localGatewayReloader.reload(tabs) 119 | sinon.assert.calledWith(browser.tabs.reload, 2) 120 | sinon.assert.calledWith(logger, 'reloading local gateway at http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki2') 121 | }) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /test/functional/lib/ipfs-companion.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { after, before, describe, it } from 'mocha' 3 | import browser from 'sinon-chrome' 4 | import { URL } from 'url' 5 | import { optionDefaults } from '../../../add-on/src/lib/options.js' 6 | 7 | // We need to do this because global is not mapped otherwise, we need to stub browser and chrome runtime 8 | // so that the webextension-polyfill does not complain about the test runner not being a browser instance. 9 | const init = async () => (await import('../../../add-on/src/lib/ipfs-companion.js')).default() 10 | 11 | describe('lib/ipfs-companion.js', function () { 12 | describe('init', function () { 13 | before(function () { 14 | global.URL = global.URL || URL 15 | global.screen = { width: 1024, height: 720 } 16 | global.addEventListener = () => { } 17 | global.location = { hostname: 'test' } 18 | 19 | browser.runtime.getManifest.returns({ version: '0.0.0' }) // on-installed.js 20 | }) 21 | 22 | it('should query local storage for options with hardcoded defaults for fallback', async function () { 23 | this.timeout(10000) 24 | browser.storage.local.get.resolves(optionDefaults) 25 | browser.storage.local.set.resolves() 26 | const ipfsCompanion = await init() 27 | expect(browser.storage.local.get.calledWith(optionDefaults)).to.equal(true) 28 | return await ipfsCompanion.destroy() 29 | }) 30 | 31 | after(function () { 32 | browser.flush() 33 | }) 34 | }) 35 | 36 | describe.skip('onStorageChange()', function () { 37 | it('should update ipfs API instance on IPFS API URL change', async function () { 38 | browser.storage.local.get.resolves(optionDefaults) 39 | browser.storage.local.set.resolves() 40 | browser.action.setBadgeBackgroundColor.resolves() 41 | browser.action.setBadgeText.resolves() 42 | browser.action.setIcon.resolves() 43 | browser.tabs.query.resolves([{ id: 'TEST' }]) 44 | browser.contextMenus.update.resolves() 45 | browser.idle.queryState.resolves('active') 46 | 47 | const ipfsCompanion = await init() 48 | 49 | const oldIpfsApiUrl = 'http://127.0.0.1:5001' 50 | const newIpfsApiUrl = 'http://1.2.3.4:8080' 51 | const changes = { ipfsApiUrl: { oldValue: oldIpfsApiUrl, newValue: newIpfsApiUrl } } 52 | const area = 'local' 53 | const ipfs = global.window.ipfs 54 | browser.storage.onChanged.dispatch(changes, area) 55 | expect(ipfs).to.not.equal(window.ipfs) 56 | return ipfsCompanion.destroy() 57 | }) 58 | 59 | after(function () { 60 | browser.flush() 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /test/functional/lib/redirect-handler/commonPatternRedirectRegexFilter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import browserMock from 'sinon-chrome' 3 | import isManifestV3 from '../../../helpers/is-mv3-testing-enabled.js' 4 | import { CommonPatternRedirectRegexFilter } from '../../../../add-on/src/lib/redirect-handler/commonPatternRedirectRegexFilter.js' 5 | 6 | describe('lib/redirect-handler/commonPatternRedirectRegexFilter', () => { 7 | before(function () { 8 | if (!isManifestV3) { 9 | return this.skip() 10 | } 11 | browserMock.runtime.id = 'testid' 12 | }) 13 | 14 | describe('isBrave', () => { 15 | it('should create filter for brave', () => { 16 | const filter = new CommonPatternRedirectRegexFilter({ 17 | originUrl: 'https://awesome.ipfs.io/', 18 | redirectUrl: 'http://localhost:8080/ipns/awesome.ipfs.io/' 19 | }) 20 | filter.computeFilter(true) 21 | filter.normalizeRegexFilter() 22 | expect(filter.regexFilter).to.equal('^https?\\:\\/\\/awesome\\.ipfs\\.io((?:[^\\.]|$).*)$') 23 | expect(filter.regexSubstitution).to.equal('ipns://awesome.ipfs.io\\1') 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/functional/lib/redirect-handler/declarativeNetRequest.mock.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | 3 | /** 4 | * https://github.com/acvetkov/sinon-chrome/issues/110 5 | * 6 | * Since this is not implemented in sinon-chrome, this is a bare-bones mock implementation. 7 | * This still needs to be instrumented in sinon, to be able to assert on calls. 8 | */ 9 | class DeclarativeNetRequestMock implements browser.DeclarativeNetRequest.Static { 10 | private dynamicRules: Map; 11 | private sessionRules: Map; 12 | 13 | constructor() { 14 | this.dynamicRules = new Map() 15 | this.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES = 5000 16 | } 17 | 18 | async getDynamicRules(): Promise { 19 | return [...this.dynamicRules.values()] 20 | } 21 | 22 | async updateDynamicRules({ 23 | addRules, 24 | removeRuleIds 25 | }: browser.DeclarativeNetRequest.UpdateDynamicRulesOptionsType): Promise { 26 | if (removeRuleIds && addRules) { 27 | removeRuleIds.forEach(id => this.dynamicRules.delete(id)) 28 | addRules.forEach(rule => this.dynamicRules.set(rule.id, rule)) 29 | } 30 | } 31 | 32 | async updateSessionRules({ 33 | addRules, 34 | removeRuleIds 35 | }: browser.DeclarativeNetRequest.UpdateSessionRulesOptionsType): Promise { 36 | if (removeRuleIds && addRules) { 37 | removeRuleIds.forEach(id => this.sessionRules.delete(id)) 38 | addRules.forEach(rule => this.sessionRules.set(rule.id, rule)) 39 | } 40 | } 41 | 42 | async getSessionRules (): Promise { 43 | return [...this.sessionRules.values()] 44 | } 45 | 46 | async getEnabledRulesets(): Promise { 47 | throw new Error('Method not implemented.') 48 | } 49 | 50 | async getDisabledRulesets (): Promise { 51 | throw new Error('Method not implemented.') 52 | } 53 | 54 | async updateEnabledRulesets (options: browser.DeclarativeNetRequest.UpdateEnabledRulesetsUpdateRulesetOptionsType): Promise { 55 | throw new Error('Method not implemented.') 56 | } 57 | 58 | async getAvailableStaticRuleCount (): Promise { 59 | throw new Error('Method not implemented.') 60 | } 61 | 62 | async testMatchOutcome ( 63 | request: browser.DeclarativeNetRequest.TestMatchOutcomeRequestType, 64 | options: browser.DeclarativeNetRequest.TestMatchOutcomeOptionsType 65 | ): Promise { 66 | throw new Error('Method not implemented.') 67 | } 68 | } 69 | 70 | export default DeclarativeNetRequestMock 71 | -------------------------------------------------------------------------------- /test/functional/lib/redirect-handler/namespaceRedirectRegexFilter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import browserMock from 'sinon-chrome' 3 | import { NamespaceRedirectRegexFilter } from '../../../../add-on/src/lib/redirect-handler/namespaceRedirectRegexFilter' 4 | import isManifestV3 from '../../../helpers/is-mv3-testing-enabled' 5 | 6 | describe('lib/redirect-handler/namespaceRedirectRegexFilter', () => { 7 | before(function () { 8 | if (!isManifestV3) { 9 | return this.skip() 10 | } 11 | browserMock.runtime.id = 'testid' 12 | }) 13 | 14 | describe('isBrave', () => { 15 | it('should create filter for brave', () => { 16 | const filter = new NamespaceRedirectRegexFilter({ 17 | originUrl: 'https://ipfs.io/ipfs/QmZMxU', 18 | redirectUrl: 'http://localhost:8080/ipfs/QmZMxU' 19 | }) 20 | filter.computeFilter(true) 21 | filter.normalizeRegexFilter() 22 | expect(filter.regexFilter).to.equal('^https?\\:\\/\\/ipfs\\.io\\/(ipfs|ipns)\\/((?:[^\\.]|$).*)$') 23 | expect(filter.regexSubstitution).to.equal('\\1://\\2') 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/functional/lib/redirect-handler/subdomainRedirectRegexFilter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import browserMock from 'sinon-chrome' 3 | import { SubdomainRedirectRegexFilter } from '../../../../add-on/src/lib/redirect-handler/subdomainRedirectRegexFilter.js' 4 | import isManifestV3 from '../../../helpers/is-mv3-testing-enabled' 5 | 6 | describe('lib/redirect-handler/subdomainRedirectRegexFilter', () => { 7 | before(function () { 8 | if (!isManifestV3) { 9 | return this.skip() 10 | } 11 | browserMock.runtime.id = 'testid' 12 | }) 13 | 14 | describe('isBrave', () => { 15 | it('should create filter for brave', () => { 16 | const filter = new SubdomainRedirectRegexFilter({ 17 | originUrl: 'https://en.wikipedia-on-ipfs.org.ipns.dweb.link/wiki/InterPlanetary_File_System', 18 | redirectUrl: 'http://localhost:8080/ipns/en.wikipedia-on-ipfs.org/wiki/InterPlanetary_File_System' 19 | }) 20 | filter.computeFilter(true) 21 | filter.normalizeRegexFilter() 22 | expect(filter.regexFilter).to.equal('^https?\\:\\/\\/(.*?)\\.(ipfs|ipns)\\.dweb\\.link\\/((?:[^\\.]|$).*)$') 23 | expect(filter.regexSubstitution).to.equal('\\2://\\1\\3') 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/functional/lib/runtime-checks.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env browser, webextensions */ 3 | 4 | import { describe, it, before, beforeEach, after } from 'mocha' 5 | import { expect } from 'chai' 6 | import browser from 'sinon-chrome' 7 | import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js' 8 | const promiseStub = (result) => () => Promise.resolve(result) 9 | 10 | describe('runtime-checks.js', function () { 11 | before(() => { 12 | global.browser = browser 13 | browser.runtime.id = 'testid' 14 | global.chrome = {} 15 | }) 16 | 17 | beforeEach(function () { 18 | browser.flush() 19 | }) 20 | 21 | describe('isFirefox', function () { 22 | beforeEach(function () { 23 | browser.flush() 24 | }) 25 | 26 | it('should return true when in Firefox runtime', async function () { 27 | browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox' }) 28 | const runtime = await createRuntimeChecks(browser) 29 | expect(runtime.isFirefox).to.equal(true) 30 | }) 31 | 32 | it('should return false when not in Firefox runtime', async function () { 33 | browser.runtime.getBrowserInfo = promiseStub({ name: 'SomethingElse' }) 34 | const runtime = await createRuntimeChecks(browser) 35 | expect(runtime.isFirefox).to.equal(false) 36 | }) 37 | }) 38 | 39 | describe('requiresXHRCORSfix', function () { 40 | beforeEach(function () { 41 | browser.flush() 42 | }) 43 | 44 | it('should return true when in Firefox runtime < 69', async function () { 45 | browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox', version: '68.0.0' }) 46 | const runtime = await createRuntimeChecks(browser) 47 | expect(runtime.requiresXHRCORSfix).to.equal(true) 48 | }) 49 | 50 | it('should return false when in Firefox runtime >= 69', async function () { 51 | browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox', version: '69.0.0' }) 52 | const runtime = await createRuntimeChecks(browser) 53 | expect(runtime.requiresXHRCORSfix).to.equal(false) 54 | }) 55 | 56 | it('should return false when if getBrowserInfo is not present', async function () { 57 | browser.runtime.getBrowserInfo = undefined 58 | const runtime = await createRuntimeChecks(browser) 59 | expect(runtime.requiresXHRCORSfix).to.equal(false) 60 | }) 61 | }) 62 | 63 | describe('isAndroid', function () { 64 | beforeEach(function () { 65 | browser.flush() 66 | }) 67 | 68 | it('should return true when in Android runtime', async function () { 69 | browser.runtime.getPlatformInfo.returns({ os: 'android' }) 70 | const runtime = await createRuntimeChecks(browser) 71 | expect(runtime.isAndroid).to.equal(true) 72 | }) 73 | 74 | it('should return false when not in Android runtime', async function () { 75 | browser.runtime.getPlatformInfo.returns({ name: 'SomethingElse' }) 76 | const runtime = await createRuntimeChecks(browser) 77 | expect(runtime.isAndroid).to.equal(false) 78 | }) 79 | }) 80 | 81 | describe('hasNativeProtocolHandler', function () { 82 | beforeEach(function () { 83 | browser.flush() 84 | }) 85 | 86 | it('should return true when browser.protocol namespace is present', async function () { 87 | // pretend API is in place 88 | browser.protocol = {} 89 | browser.protocol.registerStringProtocol = () => Promise.resolve({}) 90 | const runtime = await createRuntimeChecks(browser) 91 | expect(runtime.hasNativeProtocolHandler).to.equal(true) 92 | }) 93 | 94 | it('should return false browser.protocol APIs are missing', async function () { 95 | // API is missing 96 | browser.protocol = undefined 97 | const runtime = await createRuntimeChecks(browser) 98 | expect(runtime.hasNativeProtocolHandler).to.equal(false) 99 | }) 100 | }) 101 | 102 | after(function () { 103 | delete global.browser 104 | browser.flush() 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /test/functional/lib/state.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { describe, it, beforeEach } from 'mocha' 3 | import { expect } from 'chai' 4 | import { URL } from 'url' 5 | import { initState } from '../../../add-on/src/lib/state.js' 6 | import { optionDefaults } from '../../../add-on/src/lib/options.js' 7 | 8 | describe('state.js', function () { 9 | let state 10 | 11 | beforeEach(function () { 12 | global.URL = URL 13 | state = Object.assign(initState(optionDefaults), { peerCount: 1 }) 14 | }) 15 | 16 | describe('activeIntegrations(url)', function () { 17 | it('should return false if input is undefined', async function () { 18 | expect(state.activeIntegrations(undefined)).to.equal(false) 19 | }) 20 | it('should return true if host is not on the opt-out list', async function () { 21 | state.disabledOn = ['pl.wikipedia.org'] 22 | const url = 'https://en.wikipedia.org/wiki/Main_Page' 23 | expect(state.activeIntegrations(url)).to.equal(true) 24 | }) 25 | it('should return true if parent host is on the opt-out list, but a direct match exist on opt-in one', async function () { 26 | state.disabledOn = ['wikipedia.org'] 27 | state.enabledOn = ['en.wikipedia.org'] 28 | const url = 'https://en.wikipedia.org/wiki/Main_Page' 29 | expect(state.activeIntegrations(url)).to.equal(true) 30 | }) 31 | it('should return false if parent host is on the opt-in list, but a direct match exist on opt-out one', async function () { 32 | state.disabledOn = ['en.wikipedia.org'] 33 | state.enabledOn = ['wikipedia.org'] 34 | const url = 'https://en.wikipedia.org/wiki/Main_Page' 35 | expect(state.activeIntegrations(url)).to.equal(false) 36 | }) 37 | it('should return true if direct match host is on both opt-in and opt-out lists', async function () { 38 | state.disabledOn = ['example.com'] 39 | state.enabledOn = ['example.com'] 40 | const url = 'https://example.com/path' 41 | expect(state.activeIntegrations(url)).to.equal(true) 42 | }) 43 | it('should return false if parent host is on both opt-in and opt-out lists', async function () { 44 | state.disabledOn = ['example.com'] 45 | state.enabledOn = ['example.com'] 46 | const url = 'https://subdomain.example.com/path' 47 | expect(state.activeIntegrations(url)).to.equal(false) 48 | }) 49 | it('should return false if host is not on the opt-out list but global toggle is off', async function () { 50 | state.disabledOn = ['pl.wikipedia.org'] 51 | state.active = false 52 | const url = 'https://en.wikipedia.org/wiki/Main_Page' 53 | expect(state.activeIntegrations(url)).to.equal(false) 54 | }) 55 | it('should return false if host is on the opt-out list', async function () { 56 | state.disabledOn = ['example.com', 'pl.wikipedia.org'] 57 | const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna' 58 | expect(state.activeIntegrations(url)).to.equal(false) 59 | }) 60 | it('should return false if parent host of a subdomain is on the opt-out list', async function () { 61 | state.disabledOn = ['wikipedia.org'] 62 | const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna' 63 | expect(state.activeIntegrations(url)).to.equal(false) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /test/helpers/is-mv3-testing-enabled.js: -------------------------------------------------------------------------------- 1 | const isManifestV3 = process.env.TEST_MV3 === 'true' 2 | export const manifestVersion = isManifestV3 ? 'MV3' : 'MV2' 3 | export default isManifestV3 4 | -------------------------------------------------------------------------------- /test/helpers/mock-i18n.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | getMessage (key, substititions) { 3 | if (!substititions) return key 4 | substititions = Array.isArray(substititions) ? substititions : [substititions] 5 | if (!substititions.length) return key 6 | return `${key}[${substititions}]` 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /test/helpers/mv3-test-helper.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import Sinon from 'sinon' 3 | import browser from 'sinon-chrome' 4 | import { generateAddRule } from '../../add-on/src/lib/redirect-handler/blockOrObserve' 5 | import isManifestV3 from './is-mv3-testing-enabled' 6 | import { RULE_REGEX_ENDING } from '../../add-on/src/lib/redirect-handler/blockOrObserve' 7 | 8 | /** 9 | * Ensure that the request is redirected 10 | * 11 | * @param modifiedRequestCallResp - Response from onBeforeRequest or onHeadersReceived 12 | * @param MV2Expectation - Expected redirect URL for Manifest V2 13 | * @param MV3Expectation - Expected redirect URL for Manifest V3 14 | * @param MV3Expectation.origin - Expected origin URL for Manifest V3 15 | * @param MV3Expectation.destination - Expected destination URL for Manifest V3 16 | */ 17 | export function ensureCallRedirected ({ 18 | modifiedRequestCallResp, 19 | MV2Expectation, 20 | MV3Expectation 21 | }: { 22 | modifiedRequestCallResp: { redirectUrl: string }, 23 | MV2Expectation: string, 24 | MV3Expectation: { 25 | origin: string, 26 | destination: string 27 | } 28 | }): void { 29 | if (isManifestV3) { 30 | const [args] = browser.declarativeNetRequest.updateDynamicRules.firstCall.args 31 | expect(args.addRules[0]).to.deep.equal(generateAddRule( 32 | args.addRules[0].id, 33 | MV3Expectation.origin + RULE_REGEX_ENDING, 34 | MV3Expectation.destination 35 | )) 36 | } else { 37 | expect(modifiedRequestCallResp.redirectUrl).to.equal(MV2Expectation) 38 | } 39 | } 40 | 41 | /** 42 | * Ensure that the request is not touched 43 | * 44 | * @param resp - Response from onBeforeRequest or onHeadersReceived 45 | */ 46 | export function ensureRequestUntouched (resp): void { 47 | if (isManifestV3) { 48 | Sinon.assert.notCalled(browser.declarativeNetRequest.updateDynamicRules) 49 | } else { 50 | expect(resp).to.equal(undefined) 51 | } 52 | } 53 | 54 | /** 55 | * Execute webRequest stages in order and ensure that the request 56 | * is not redirected by any of them 57 | * 58 | * @param modifyRequest - Request Modifier 59 | * @param request - Request to be modified 60 | */ 61 | export async function ensureNoRedirect (modifyRequest, request): Promise { 62 | // check webRequest stages sequentially in the same order a browser would 63 | // (each stage may modify state and must be inspected idependently, in order) 64 | await ensureRequestUntouched(await modifyRequest.onBeforeRequest(request)) 65 | await ensureRequestUntouched(await modifyRequest.onHeadersReceived(request)) 66 | } 67 | -------------------------------------------------------------------------------- /test/setup/mocha-setup.js: -------------------------------------------------------------------------------- 1 | import AbortController from 'abort-controller' 2 | import { afterEach, beforeEach } from 'mocha' 3 | import sinon, { useFakeTimers } from 'sinon' 4 | import browser from 'sinon-chrome' 5 | import DeclarativeNetRequestMock from '../functional/lib/redirect-handler/declarativeNetRequest.mock.js' 6 | import isManifestV3 from '../helpers/is-mv3-testing-enabled.js' 7 | 8 | browser.runtime.id = 'testid' 9 | global.browser = browser 10 | global.AbortController = AbortController 11 | global.chrome = browser 12 | global.navigator = { 13 | clipboard: { 14 | writeText: () => {} 15 | } 16 | } 17 | 18 | global.URL = URL 19 | browser.tabs = { ...browser.tabs, getCurrent: sinon.stub().resolves({ id: 20 }) } 20 | 21 | // need to force Date to return a particular date 22 | global.clock = useFakeTimers({ 23 | now: new Date(2017, 10, 5, 12, 1, 1) 24 | }) 25 | 26 | if (isManifestV3) { 27 | const sinonSandbox = sinon.createSandbox() 28 | beforeEach(function () { 29 | browser.runtime.getURL.returns('chrome-extension://testid/') 30 | browser.tabs = { 31 | ...browser.tabs, 32 | getCurrent: sinonSandbox.stub().resolves({ id: 20 }), 33 | query: sinonSandbox.stub().resolves([{ id: 40 }]), 34 | update: sinonSandbox.stub().resolves() 35 | } 36 | browser.declarativeNetRequest = sinonSandbox.spy(new DeclarativeNetRequestMock()) 37 | }) 38 | 39 | afterEach(function () { 40 | sinonSandbox.resetHistory() 41 | }) 42 | } else { 43 | beforeEach(function () { 44 | browser.runtime.getURL.returns('chrome-extension://testid/') 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transpileOnly": true, 3 | "compilerOptions": { 4 | "typeRoots" : ["./src/types"], 5 | "lib": ["DOM", "ESNext"], 6 | "target": "ESNext", 7 | "allowJs": true, 8 | "checkJs": true, 9 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 10 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 11 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 12 | 13 | /* Type Checking */ 14 | "strict": true, 15 | "skipLibCheck": false, 16 | "emitDeclarationOnly": true, 17 | "declaration": true, 18 | "moduleResolution": "nodenext", 19 | "allowSyntheticDefaultImports": true, 20 | "module": "NodeNext", 21 | "importHelpers": true, 22 | "noImplicitAny": true, 23 | }, 24 | "ts-node": { 25 | "transpileOnly": true, 26 | "files": true, 27 | "noImplicitAny": false 28 | }, 29 | "include": ["add-on/src/**/*.js", "add-on/src/**/*.ts"] 30 | } 31 | --------------------------------------------------------------------------------