├── .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 |
43 | ${i18n.getMessage('request_permissions_page_button')}
44 |
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}
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: [''] }))) {
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 /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:////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} [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}
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 {
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): 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 |
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 |
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 |
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 |
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 |
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 |
26 |
27 |
28 | ${browser.i18n.getMessage('option_redirect_rules_row_origin')}: ${origin}
29 |
30 |
31 | ${browser.i18n.getMessage('option_redirect_rules_row_target')}: ${target}
32 |
33 |
34 |
35 | emit('redirectRuleDeleteRequest', id)}>X
36 |
37 |
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 |
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 |
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 |
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 |
46 | ${globalToggleForm({
47 | active: state.options.active,
48 | onOptionChange
49 | })}
50 |
51 | `
52 | }
53 | return html`
54 |
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 |
117 | ${resetForm({
118 | onOptionsReset
119 | })}
120 | ${!supportsDeclarativeNetRequest()
121 | ? ''
122 | : redirectRuleForm({
123 | redirectRules: state.redirectRules,
124 | emit
125 | })}
126 |
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 |
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 |
14 | ${label}
15 | ${value}
16 |
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 |
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 |
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 |
16 |
17 |
18 |
22 | ${logo({
23 | size: 54,
24 | path: '../../../icons',
25 | ipfsNodeType,
26 | isIpfsOnline: (active && isIpfsOnline)
27 | })}
28 |
29 |
30 |
31 |
32 | IPFS
33 |
34 |
35 |
${ipfsVersion(props)}
36 |
37 |
38 |
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 |
58 |
59 |
60 | ${gatewayStatus(props)}
61 |
62 |
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 |
17 | `
18 | }
19 |
--------------------------------------------------------------------------------
/add-on/src/popup/browser-action/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
14 | ${value.substring(0, 20)}
15 |
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 |
10 | ${browser.i18n.getMessage(label)}
11 |
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 |
33 | ${text}
${switchToggle({ checked: switchValue, disabled, style: 'fr ml2' })}
34 | ${helperText}
35 |
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 |
17 |
18 |
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 |
32 |
33 | ${header(headerProps)}
34 | ${tools(opsProps)}
35 |
36 | ${activeTabActions(activeTabActionsProps)}
37 |
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 |
17 |
18 |
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 |
17 |
18 |
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 |
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 |
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 |
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 |
19 |
20 |
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 |
29 |
30 |
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 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
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/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 |
62 | ${i18n.getMessage('recovery_page_button')}
63 |
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://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 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------