├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── ask-questions.md
│ ├── bug--------.md
│ └── bug_report.md
└── workflows
│ ├── dev.yml
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.js
├── .stylelintignore
├── .stylelintrc.js
├── LICENSE
├── README.md
├── build.config.js
├── docs
├── .dumi
│ └── theme
│ │ └── locales
│ │ └── zh-TW.json
├── .dumirc.ts
├── .gitignore
├── README.md
├── docs
│ ├── guide
│ │ ├── FAQ.en-US.md
│ │ ├── FAQ.md
│ │ ├── FAQ.zh-TW.md
│ │ ├── cloud-backup.en-US.md
│ │ ├── cloud-backup.md
│ │ ├── cloud-backup.zh-TW.md
│ │ ├── custom-function.en-US.md
│ │ ├── custom-function.md
│ │ ├── custom-function.zh-TW.md
│ │ ├── index.en-US.md
│ │ ├── index.md
│ │ ├── index.zh-TW.md
│ │ ├── modify-body.en-US.md
│ │ ├── modify-body.md
│ │ ├── modify-body.zh-TW.md
│ │ ├── rule.en-US.md
│ │ ├── rule.md
│ │ ├── rule.zh-TW.md
│ │ ├── third-party-rules.en-US.md
│ │ ├── third-party-rules.md
│ │ └── third-party-rules.zh-TW.md
│ ├── index.en-US.md
│ ├── index.md
│ └── index.zh-TW.md
└── package.json
├── extension.json
├── locale
├── create-pr.js
├── locales.js
├── original
│ └── messages.json
├── output
│ └── .gitkeep
├── sort-origin.js
└── transifex.yml
├── package.json
├── pnpm-lock.yaml
├── public
├── _locales
│ ├── .gitkeep
│ ├── en
│ │ └── messages.json
│ ├── es
│ │ └── messages.json
│ ├── pl
│ │ └── messages.json
│ ├── pt_BR
│ │ └── messages.json
│ ├── zh_CN
│ │ └── messages.json
│ └── zh_TW
│ │ └── messages.json
├── assets
│ └── images
│ │ ├── 128.png
│ │ └── 128w.png
└── index.html
├── scripts
├── config.mjs
├── get-snapshot-version.mjs
├── pack-utils
│ ├── amo.mjs
│ ├── crx.mjs
│ ├── cws.mjs
│ ├── edge.mjs
│ ├── index.mjs
│ └── xpi.mjs
├── pack.mjs
├── release.mjs
├── webpack
│ ├── dev.js
│ ├── externals.js
│ ├── remove-html.js
│ └── webpack.plugin.js
└── www
│ └── CNAME
├── src
├── global.d.ts
├── manifest.json
├── pages
│ ├── background
│ │ ├── api-handler.ts
│ │ ├── core
│ │ │ ├── db.ts
│ │ │ └── rules.ts
│ │ ├── index.ts
│ │ ├── request-handler.ts
│ │ ├── upgrade.ts
│ │ └── utils.ts
│ ├── options
│ │ ├── components
│ │ │ └── bool-radio.tsx
│ │ ├── index.tsx
│ │ ├── sections
│ │ │ ├── group-select
│ │ │ │ └── index.tsx
│ │ │ ├── import-and-export
│ │ │ │ ├── cloud
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── import-drawer
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── options
│ │ │ │ └── index.tsx
│ │ │ └── rules
│ │ │ │ ├── edit
│ │ │ │ ├── code-editor.tsx
│ │ │ │ ├── encoding.ts
│ │ │ │ ├── headers.ts
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ │ ├── float.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── rule-group-card.tsx
│ │ │ │ └── utils.ts
│ │ └── utils.ts
│ └── popup
│ │ ├── index.tsx
│ │ └── rule
│ │ ├── group.tsx
│ │ └── rules.tsx
└── share
│ ├── components
│ ├── rule-detail.tsx
│ └── semi-locale.tsx
│ ├── core
│ ├── constant.ts
│ ├── emitter.ts
│ ├── logger.ts
│ ├── notify.ts
│ ├── prefs.ts
│ ├── rule-utils.ts
│ ├── storage.ts
│ ├── types.ts
│ └── utils.ts
│ ├── hooks
│ └── use-mark-common.ts
│ └── pages
│ ├── api.ts
│ ├── browser-sync.ts
│ ├── file.ts
│ └── is-dark-mode.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = false
9 | insert_final_newline = false
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # 忽略目录
2 | build/
3 | tests/
4 | demo/
5 | .ice/
6 | scripts/
7 | locale/
8 |
9 | # node 覆盖率文件
10 | coverage/
11 |
12 | # 忽略文件
13 | **/*-min.js
14 | **/*.min.js
15 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const { getESLintConfig } = require('@iceworks/spec');
2 |
3 | // https://www.npmjs.com/package/@iceworks/spec
4 | module.exports = getESLintConfig('react-ts', {
5 | plugins: [
6 | 'eslint-plugin-unused-imports',
7 | 'eslint-plugin-import',
8 | ],
9 | rules: {
10 | 'react/jsx-filename-extension': 0,
11 | 'react/no-access-state-in-setstate': 0,
12 | 'react-hooks/exhaustive-deps': 0,
13 | '@typescript-eslint/member-ordering': 0,
14 | '@typescript-eslint/no-require-imports': 0,
15 | '@typescript-eslint/explicit-function-return-type': 0,
16 | '@typescript-eslint/ban-ts-comment': 0,
17 | '@iceworks/best-practices/recommend-polyfill': 0,
18 | '@iceworks/best-practices/no-js-in-ts-project': 0,
19 | '@iceworks/best-practices/recommend-functional-component': 0,
20 | 'no-await-in-loop': 0,
21 | 'no-console': 0,
22 | 'no-prototype-builtins': 0,
23 | 'no-return-assign': 0,
24 | 'no-param-reassign': 0,
25 | 'unused-imports/no-unused-imports': 'warn',
26 | 'import/order': [
27 | 'warn',
28 | {
29 | 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type', 'unknown'],
30 | 'pathGroups': [
31 | {
32 | 'pattern': '@/**',
33 | 'group': 'parent',
34 | 'position': 'before'
35 | }
36 | ],
37 | 'pathGroupsExcludedImportTypes': ['builtin'],
38 | 'newlines-between': 'never'
39 | }
40 | ]
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ask-questions.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Ask Questions
3 | about: Ask any questions
4 | title: "[Question]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug--------.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug报告(中文用户)
3 | about: Bug报告
4 | title: "[BUG]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **BUG**
11 | 简单描述此BUG,包括你做了什么,你期望发生什么以及实际上发生了什么。
12 |
13 | **规则和测试地址**
14 | 请提供规则内容和测试地址。
15 |
16 | **附加信息**
17 | 其他可能有帮助的信息,如你所做的尝试、相关资料等
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is, and describe what you expected to happen.
12 |
13 | **Rule and Test address**
14 | Please provide rule contents and a test address.
15 |
16 | **Extra informations**
17 | Other information that may be helpful, such as your attempts, related materials, etc.
18 |
--------------------------------------------------------------------------------
/.github/workflows/dev.yml:
--------------------------------------------------------------------------------
1 | name: dev
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [16.x]
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: pnpm/action-setup@v2
21 | with:
22 | version: 7
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'pnpm'
28 | - name: Install dependencies
29 | run: pnpm i --frozen-lockfile
30 | - name: Get snapshot version
31 | env:
32 | TOKEN: ${{ secrets.SNAPSHOT_TOKEN }}
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | run: node ./scripts/get-snapshot-version.mjs
35 | - name: Build
36 | run: npm run build
37 | - name: Upload bundle
38 | uses: actions/upload-artifact@v3
39 | with:
40 | name: dist
41 | path: dist
42 | - name: Upload bundle analyze
43 | uses: actions/upload-artifact@v3
44 | with:
45 | name: bundle-analyze
46 | path: temp/bundle-analyze.html
47 | - name: Publish snapshot
48 | env:
49 | AMO_KEY: ${{ secrets.AMO_KEY }}
50 | AMO_SECRET: ${{ secrets.AMO_SECRET }}
51 | CRX_PRIV_KEY: ${{ secrets.CRX_PRIV_KEY }}
52 | run: npm run pack -- --platform=xpi,crx
53 | - name: Upload snapshot release
54 | uses: actions/upload-artifact@v3
55 | with:
56 | name: release
57 | path: |
58 | temp/release
59 | !temp/release/*-id.txt
60 |
61 | sync-locale:
62 | runs-on: ubuntu-latest
63 | strategy:
64 | matrix:
65 | node-version: [16.x]
66 | steps:
67 | - uses: actions/checkout@v3
68 | - name: Use Node.js ${{ matrix.node-version }}
69 | uses: actions/setup-node@v3
70 | with:
71 | node-version: ${{ matrix.node-version }}
72 | - name: Sort
73 | run: node ./locale/sort-origin.js
74 | - name: Deploy
75 | uses: JamesIves/github-pages-deploy-action@v4
76 | with:
77 | branch: sync-locale
78 | folder: locale
79 | clean: false
80 | commit-message: '[skip ci] sync locale'
81 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [16.x]
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: pnpm/action-setup@v2
21 | with:
22 | version: 7
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'pnpm'
28 | - name: Install dependencies
29 | run: pnpm i --frozen-lockfile
30 | - name: Build
31 | run: npm run build
32 | - name: Upload bundles
33 | uses: actions/upload-artifact@v3
34 | with:
35 | name: dist
36 | path: dist
37 | build-docs:
38 | runs-on: ubuntu-latest
39 | strategy:
40 | matrix:
41 | node-version: [16.x]
42 | steps:
43 | - uses: actions/checkout@v3
44 | - uses: pnpm/action-setup@v2
45 | with:
46 | version: 7
47 | - name: Use Node.js ${{ matrix.node-version }}
48 | uses: actions/setup-node@v3
49 | with:
50 | node-version: ${{ matrix.node-version }}
51 | - name: Install dependencies
52 | run: |
53 | cd $GITHUB_WORKSPACE/docs
54 | pnpm i
55 | - name: Build
56 | run: |
57 | cd $GITHUB_WORKSPACE/docs
58 | npm run build
59 | cp $GITHUB_WORKSPACE/scripts/www/* $GITHUB_WORKSPACE/docs/dist/
60 | - name: Deploy
61 | uses: JamesIves/github-pages-deploy-action@v4
62 | with:
63 | branch: gh-pages
64 | folder: docs/dist
65 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '[0-9].[0-9]+.[0-9]+'
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [16.x]
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: pnpm/action-setup@v2
21 | with:
22 | version: 7
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'pnpm'
28 | - name: Install dependencies
29 | run: pnpm i --frozen-lockfile
30 | - name: Build
31 | run: npm run build
32 | - name: Upload bundles
33 | uses: actions/upload-artifact@v3
34 | with:
35 | name: dist
36 | path: dist
37 | - name: Pack
38 | env:
39 | AMO_KEY: ${{ secrets.AMO_KEY }}
40 | AMO_SECRET: ${{ secrets.AMO_SECRET }}
41 | CRX_PRIV_KEY: ${{ secrets.CRX_PRIV_KEY }}
42 | CWS_CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }}
43 | CWS_CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }}
44 | CWS_TOKEN: ${{ secrets.CWS_TOKEN }}
45 | MS_ACCESS_TOKEN_URL: ${{ secrets.MS_ACCESS_TOKEN_URL }}
46 | MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }}
47 | MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }}
48 | MS_PRODUCT_ID: ${{ secrets.MS_PRODUCT_ID }}
49 | run: npm run pack
50 | - name: Release
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | SERVER_TOKEN: ${{ secrets.SNAPSHOT_TOKEN }}
54 | run: npm run release
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | dist/
4 | .idea/
5 | dist-merge/
6 | .theia/
7 | .recore/
8 | ~*
9 | package-lock.json
10 |
11 | # Packages #
12 | ############
13 | # it's better to unpack these files and commit the raw source
14 | # git has its own built in compression methods
15 | *.7z
16 | *.dmg
17 | *.gz
18 | *.iso
19 | *.jar
20 | *.rar
21 | *.tar
22 | *.zip
23 |
24 | # Logs and databases #
25 | ######################
26 | *.log
27 | *.sql
28 | *.sqlite
29 |
30 | # OS generated files #
31 | ######################
32 | .DS_Store
33 | .Trash*
34 | *.swp
35 | ._*
36 | .Spotlight-V100
37 | .Trashes
38 | ehthumbs.db
39 | Thumbs.db
40 |
41 | encrypt
42 |
43 | .ice
44 | .eslintcache
45 | .vscode
46 | temp
47 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | tests/
3 | demo/
4 | .ice/
5 | coverage/
6 | **/*-min.js
7 | **/*.min.js
8 | package-lock.json
9 | yarn.lock
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | const { getPrettierConfig } = require('@iceworks/spec');
2 |
3 | module.exports = getPrettierConfig('react');
4 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | # 忽略目录
2 | build/
3 | tests/
4 | demo/
5 |
6 | # node 覆盖率文件
7 | coverage/
8 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | const { getStylelintConfig } = require('@iceworks/spec');;
2 |
3 | module.exports = getStylelintConfig('react');
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Header Editor
3 |
4 |
5 | [](https://github.com/FirefoxBar/HeaderEditor/releases)
6 | [](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh)
7 | [](https://addons.mozilla.org/en-US/firefox/addon/header-editor/)
8 | [](https://github.com/FirefoxBar/HeaderEditor/blob/master/LICENSE)
9 | [](https://github.com/FirefoxBar/HeaderEditor/discussions)
10 | [](https://github.com/FirefoxBar/HeaderEditor/actions/workflows/dev.yml)
11 |
12 | An extension which can modify the request, include request headers, response headers, redirect requests, and cancel requests.
13 |
14 | It's 100% FREE, no ADs, no data collection.
15 |
16 | For more documentations, Please visit [he.firefoxcn.net](https://he.firefoxcn.net)
17 |
18 | ## Get this extension
19 |
20 |  [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/).
21 |
22 |  [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh).
23 |
24 |  [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko).
25 |
26 |  Install our [self-distributed version](https://github.com/FirefoxBar/HeaderEditor/releases).
27 |
28 | ## About Permissions
29 |
30 | Header Editor require those permissions:
31 |
32 | * `tabs`: Open links (such as the options page) or switch to a tab
33 |
34 | * `webRequest`, `webRequestBlocking`, `_all_urls_`: Modify requests
35 |
36 | * `storage`, `unlimitedStorage`: Storage rules and settings
37 |
38 | * `unsafe-eval`: Execute custom function, code at [src/share/core/rule-utils.ts#L8](https://github.com/FirefoxBar/HeaderEditor/blob/dev/src/share/core/rule-utils.ts#L8) (may change in the future, you can search for the newest location by `new Function`)
39 |
40 | ## Contribution
41 |
42 | Contribute codes: [Submitting a pull request](https://github.com/FirefoxBar/HeaderEditor/compare)
43 |
44 | Thanks to them for their contribution: [YFdyh000](https://github.com/yfdyh000) [iNaru](https://github.com/Inaru)
45 |
46 | ### Translation
47 |
48 | English: Please submit a issue or pull request for file `locale/original/messages.json`
49 |
50 | Other language: Please translate them on [Transifex](https://www.transifex.com/sytec/header-editor/)
51 |
52 | Please note that some languages (such as zh-Hans) will not be translated on transifex because the browser does not support them, click [here](https://developer.chrome.com/docs/webstore/i18n/#choosing-locales-to-support) to view full list
53 |
54 | ## How to build
55 |
56 | ### Build
57 |
58 | * Install node (14+) and pnpm.
59 |
60 | * Clone this project, or download the source code and extract it.
61 |
62 | * Run `pnpm i`.
63 |
64 | * Run `npm run build`
65 |
66 | * Find build result at `/dist`
67 |
68 | #### Development
69 |
70 | * Run `npm run start`
71 |
72 | * Open browser, load extension from `/dist` directory or `/dist/manifest.json`
73 |
74 | ## Licenses
75 |
76 | Copyright © 2017-2023 [FirefoxBar Team](https://team.firefoxcn.net)
77 |
78 | Open source licensed under [GPLv2](LICENSE).
79 |
--------------------------------------------------------------------------------
/build.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | externals: {
3 | react: 'React',
4 | 'react-dom': 'ReactDOM',
5 | },
6 | outputDir: 'dist',
7 | outputAssetsPath: {
8 | js: 'assets/js',
9 | css: 'assets/css',
10 | },
11 | mpa: true,
12 | vendor: false,
13 | browserslist: {
14 | chrome: 85,
15 | firefox: 77,
16 | edge: 85,
17 | },
18 | plugins: [
19 | [
20 | 'build-plugin-css-assets-local',
21 | {
22 | outputPath: 'assets/css-assets',
23 | relativeCssPath: '/',
24 | },
25 | ],
26 | './scripts/webpack/webpack.plugin.js',
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------
/docs/.dumi/theme/locales/zh-TW.json:
--------------------------------------------------------------------------------
1 | {
2 | "header.search.placeholder": "输入关键字搜索...",
3 | "header.color.mode.light": "亮色模式",
4 | "header.color.mode.dark": "暗色模式",
5 | "header.color.mode.auto": "跟随系统",
6 | "header.social.github": "GitHub",
7 | "header.social.weibo": "微博",
8 | "header.social.twitter": "Twitter",
9 | "header.social.gitlab": "GitLab",
10 | "header.social.facebook": "Facebook",
11 | "header.social.zhihu": "知乎",
12 | "header.social.yuque": "语雀",
13 | "header.social.linkedin": "Linkedin",
14 | "previewer.actions.code.expand": "展开代码",
15 | "previewer.actions.code.shrink": "收起代码",
16 | "previewer.actions.codesandbox": "在 CodeSandbox 中打开",
17 | "previewer.actions.codepen": "在 CodePen 中打开(未实现)",
18 | "previewer.actions.stackblitz": "在 StackBlitz 中打开",
19 | "previewer.actions.separate": "在独立页面中打开",
20 | "404.title": "页面未找到",
21 | "404.back": "返回首页",
22 | "api.component.name": "属性名",
23 | "api.component.description": "描述",
24 | "api.component.type": "类型",
25 | "api.component.default": "默认值",
26 | "api.component.required": "(必选)",
27 | "api.component.unavailable": "必须启用 apiParser 才能使用自动 API 特性",
28 | "api.component.loading": "属性定义正在解析中,稍等片刻...",
29 | "api.component.not.found": "未找到 {id} 组件的属性定义",
30 | "content.tabs.default": "文档",
31 | "search.not.found": "未找到相关内容",
32 | "layout.sidebar.btn": "侧边菜单"
33 | }
--------------------------------------------------------------------------------
/docs/.dumirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | themeConfig: {
5 | name: 'Header Editor',
6 | logo: false,
7 | footer: false,
8 | socialLinks: {
9 | github: 'https://github.com/FirefoxBar/HeaderEditor',
10 | },
11 | },
12 | locales: [
13 | { id: 'zh-CN', name: '简体中文' },
14 | { id: 'en-US', name: 'English' },
15 | { id: 'zh-TW', name: '繁體中文' },
16 | ],
17 | analytics: {
18 | baidu: 'eddab75c23e1853a476011bb95a585c9',
19 | }
20 | });
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 | .dumi/tmp
3 | .dumi/tmp-production
4 | dist
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Header Editor Docs
2 |
3 | A static site base on [dumi](https://d.umijs.org).
4 |
5 | ## Development
6 |
7 | ```bash
8 | # install dependencies
9 | $ pnpm install
10 |
11 | # start dev server
12 | $ pnpm start
13 |
14 | # build docs
15 | $ pnpm run build
16 | ```
17 |
18 | ## LICENSE
19 |
20 | MIT
21 |
--------------------------------------------------------------------------------
/docs/docs/guide/FAQ.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: Introduction
4 | title: FAQ
5 | order: 2
6 | ---
7 |
8 | ## Why is "header name" reduced to lowercase?
9 |
10 | [RFC 2616](https://tools.ietf.org/html/rfc2616.html#section-4.2) says:
11 |
12 | > Each header field consists of a name followed by a colon `(":")` and the field value. Field names are case-insensitive.
13 |
14 | So, since 4.0.0, Header Editor will reduce "header name" to lowercase. Except for custom functions: the custom function will still get the original header (except that it has been modified by other rules)
15 |
16 | ## Can I delete a header in a simple way?
17 |
18 | Yes, just modify it to `_header_editor_remove_`
19 |
20 | ## Rules disappear
21 |
22 | As we know, the rules will disappear or not work
23 |
24 | **Note: Before doing all of the following, please back up your Chrome/Firefox profile folder!**
25 |
26 | ### Not work in Private Mode
27 |
28 | Popup panel and management page is not work in Private Mode of Firefox. But the main features are available
29 |
30 | #### Chrome
31 |
32 | * Open `chrome://extensions/?id=eningockdidmgiojffjmkdblpjocbhgh`, enable "Enabled in incognito mode"
33 |
34 | #### Firefox
35 |
36 | * Open about:debugging, find the Internal UUID of Header Editor (e.g. d52e1cf2-22e5-448d-a882-e68f3211fa76).
37 | * Open Firefox options.
38 | * Go to Privacy & Security.
39 | * Set History mode to "Use custom settings".
40 | * Click "Exceptions".
41 | * Paste our URL: `moz-extension://{Internal UUID}/` (the `{Internal UUID}` is the Internal UUID of Header Editor you found in step one), for example, `moz-extension://d52e1cf2-22e5-448d-a882-e68f3211fa76/`, Then click 'Allow'.
42 | * Click "Save Changes".
43 |
44 | ### Rules automatically deleted in Firefox
45 |
46 | Thanks to [Thorin-Oakenpants](https://github.com/Thorin-Oakenpants) and [henshin](https://github.com/henshin)
47 |
48 | * Open `about:config`, make sure that `dom.indexedDB.enabled` is `true`
49 | * Try to change `extensions.webextensions.keepUuidOnUninstall` into true, is your problem solved?
50 | * Open your Firefox profile folder, if there are many files (about a thousand or more) named prefs-xxxx.js files with 0 bytes, closed firefox and deleted them.
51 |
52 | ## Other questions?
53 |
54 | Just [submit a issue](https://github.com/FirefoxBar/HeaderEditor/issues/new/choose)
55 |
--------------------------------------------------------------------------------
/docs/docs/guide/FAQ.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: 指南
3 | group: 介绍
4 | title: FAQ
5 | order: 2
6 | ---
7 |
8 | ## 为什么“头名称”变成小写了?
9 |
10 | [RFC 2616](https://tools.ietf.org/html/rfc2616.html#section-4.2)中写到:
11 |
12 | > Each header field consists of a name followed by a colon `(":")` and the field value. Field names are case-insensitive.
13 |
14 | 因此,从4.0.0开始,Header Editor会将“头名称”变为小写。但自定义函数除外:除了已被其他规则修改的头外,自定义函数获取到的仍然是原始头
15 |
16 | ## 我能以简单的方式删除头吗?
17 |
18 | 可以,只需将其修改为`_header_editor_remove_`
19 |
20 | ## 规则消失
21 |
22 | 我们已知,在某些情况下,规则会消失或不起作用
23 |
24 | **注意:在执行以下所有操作之前,请备份您的Chrome/Firefox配置文件文件夹!**
25 |
26 | ### 在隐私模式下无作用
27 |
28 | 小面板和管理页面在Firefox的隐私模式下不能使用。但是主要功能可用。
29 |
30 | #### Chrome
31 |
32 | * 打开`chrome://extensions/?id=eningockdidmgiojffjmkdblpjocbhgh`,启用“以隐身模式启用”
33 |
34 | #### Firefox
35 |
36 | * 打开about:debugging,找到Header Editor的内部UUID(例如d52e1cf2-22e5-448d-a882-e68f3211fa76)。
37 | * 打开Firefox选项。
38 | * 转到隐私和安全。
39 | * 将历史记录模式设置为“使用自定义设置”。
40 | * 单击“例外”。
41 | * 粘贴我们的URL:`moz-extension://{Internal UUID}/`(`{Internal UUID}`是您在第一步中找到的Header Editor的内部UUID),例如,`moz-extension://d52e1cf2-22e5-448d-a882-e68f3211fa76/`,然后点击“允许”。
42 | * 单击“保存更改”。
43 |
44 | ### 规则在Firefox中自动删除
45 |
46 | 感谢[Thorin-Oakenpants](https://github.com/Thorin-Oakenpants)和[henshin](https://github.com/henshin)
47 |
48 | * 打开`about:config`,确保`dom.indexedDB.enabled`为`true`
49 | * 尝试将`extensions.webextensions.keepUuidOnUninstall`更改为true,您的问题是否解决?
50 | * 打开Firefox配置文件文件夹,如果存在许多(一千+或更多)名为prefs-xxxx.js且文件大小为0的文件,请关闭Firefox并将它们删除。
51 |
52 | ## 还有问题?
53 |
54 | 请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new/choose)
55 |
--------------------------------------------------------------------------------
/docs/docs/guide/FAQ.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: 介紹
4 | title: FAQ
5 | order: 2
6 | ---
7 |
8 | ## 为什么“头名称”变成小写了?
9 |
10 | [RFC 2616](https://tools.ietf.org/html/rfc2616.html#section-4.2)中写到:
11 |
12 | > Each header field consists of a name followed by a colon `(":")` and the field value. Field names are case-insensitive.
13 |
14 | 因此,从4.0.0开始,Header Editor会将“头名称”变为小写。但自定义函数除外:除了已被其他规则修改的头外,自定义函数获取到的仍然是原始头
15 |
16 | ## 我能以简单的方式删除头吗?
17 |
18 | 可以,只需将其修改为`_header_editor_remove_`
19 |
20 | ## 规则消失
21 |
22 | 我们已知,在某些情况下,规则会消失或不起作用
23 |
24 | **注意:在执行以下所有操作之前,请备份您的Chrome/Firefox配置文件文件夹!**
25 |
26 | ### 在隐私模式下无作用
27 |
28 | 小面板和管理页面在Firefox的隐私模式下不能使用。但是主要功能可用。
29 |
30 | #### Chrome
31 |
32 | * 打开`chrome://extensions/?id=eningockdidmgiojffjmkdblpjocbhgh`,启用“以隐身模式启用”
33 |
34 | #### Firefox
35 |
36 | * 打开about:debugging,找到Header Editor的内部UUID(例如d52e1cf2-22e5-448d-a882-e68f3211fa76)。
37 | * 打开Firefox选项。
38 | * 转到隐私和安全。
39 | * 将历史记录模式设置为“使用自定义设置”。
40 | * 单击“例外”。
41 | * 粘贴我们的URL:`moz-extension://{Internal UUID}/`(`{Internal UUID}`是您在第一步中找到的Header Editor的内部UUID),例如,`moz-extension://d52e1cf2-22e5-448d-a882-e68f3211fa76/`,然后点击“允许”。
42 | * 单击“保存更改”。
43 |
44 | ### 规则在Firefox中自动删除
45 |
46 | 感谢[Thorin-Oakenpants](https://github.com/Thorin-Oakenpants)和[henshin](https://github.com/henshin)
47 |
48 | * 打开`about:config`,确保`dom.indexedDB.enabled`为`true`
49 | * 尝试将`extensions.webextensions.keepUuidOnUninstall`更改为true,您的问题是否解决?
50 | * 打开Firefox配置文件文件夹,如果存在许多(一千+或更多)名为prefs-xxxx.js且文件大小为0的文件,请关闭Firefox并将它们删除。
51 |
52 | ## 还有问题?
53 |
54 | 请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new/choose)
55 |
--------------------------------------------------------------------------------
/docs/docs/guide/cloud-backup.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: Basic features
4 | title: Cloud backup
5 | order: 2
6 | ---
7 |
8 | ## Summary
9 |
10 | Support for cloud backup started with Header Editor 4.0.5.
11 |
12 | **Important: To use cloud backup, you should login your browser's account (like Firefox account, Google account, etc), and enable synchronize in browser's setting.**
13 |
14 | Cloud backup is supported through your browser's sync feature, as in Firefox Sync, or Chrome Sync, i.e. It means that HE has no server to storage your backup, your backup is storage at your browser's provider's server (like Mozilla, Google, etc). If your browser does not support sync, this feature will take no effect.
15 |
16 | ## What contents can be backup?
17 |
18 | Your setting will be synchronize automatically, the backup feature only backup your rules, include groups.
19 |
20 | ## Limit
21 |
22 | Both Chrome and Firefox have its space limit, about 100KB. If you have too many rules, upload will be failed, but you can use the export normally.
23 |
24 | As I know, Chrome has limits on the number of operations per unit of time. It means that you **can not** upload frequently.
25 |
26 | ## Other caveats
27 |
28 | ### Chrome/Chromium
29 |
30 | * See [chrome.storage API](https://developer.chrome.com/extensions/storage#property-sync) for more technical details.
31 |
32 | ### Firefox
33 |
34 | * It seems that Firefox Sync is executed regularly, however if you want to force the cloud export you've to launch Firefox Sync manually.
35 | 
36 | * A new installation may cause cloud storage data to be blanked.
37 | * See [browser.storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage) for more technical details.
38 |
--------------------------------------------------------------------------------
/docs/docs/guide/cloud-backup.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: 指南
3 | group: 常用功能
4 | title: 云同步
5 | order: 2
6 | ---
7 |
8 | ## 综述
9 |
10 | 自Header Editor 4.0.5起,支持云同步。
11 |
12 | **注意:你需要登录你的浏览器账号(如Firefox账号、Google账号等),并启用浏览器的同步功能**
13 |
14 | 云同步基于浏览器的同步功能,如Firefox Sync、Chrome Sync等。这意味着,HE并不会在自己的服务器上存储您的备份。您的备份存储在您的浏览器提供商的服务器上(如Mozilla、Google的服务器上)。如果您的浏览器不支持云同步,此功能不会有任何效果。
15 |
16 | ## 哪些内容会被备份?
17 |
18 | 您的设置会被自动备份。备份功能仅会备份您的规则,包括分组信息。
19 |
20 | ## 限制
21 |
22 | Firefox和Chrome都有各自的空间限制,大约100KB。如果您的规则过多,上传过程会失败,但您依然可以通过传统方式导入和导出。
23 |
24 | 就我所知,Chrome还会限制上传频率,也就是说,您**不能**过快的进行上传。
25 |
26 | ## 其他技术细节
27 |
28 | ### Chrome/Chromium
29 |
30 | * 请查看[chrome.storage API](https://developer.chrome.com/extensions/storage#property-sync)获取更多技术细节。
31 |
32 | ### Firefox
33 |
34 | * 据推测,Firefox会定期进行同步。但您可以通过手动运行,强行进行一次同步。
35 | 
36 | * 重新安装扩展可能导致同步内容丢失。
37 | * 请查看[browser.storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)获取更多技术细节。
38 |
--------------------------------------------------------------------------------
/docs/docs/guide/cloud-backup.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: 常用功能
4 | title: 雲同步
5 | order: 2
6 | ---
7 |
8 | ## 综述
9 |
10 | 自Header Editor 4.0.5起,支持云同步。
11 |
12 | **注意:你需要登录你的浏览器账号(如Firefox账号、Google账号等),并启用浏览器的同步功能**
13 |
14 | 云同步基于浏览器的同步功能,如Firefox Sync、Chrome Sync等。这意味着,HE并不会在自己的服务器上存储您的备份。您的备份存储在您的浏览器提供商的服务器上(如Mozilla、Google的服务器上)。如果您的浏览器不支持云同步,此功能不会有任何效果。
15 |
16 | ## 哪些内容会被备份?
17 |
18 | 您的设置会被自动备份。备份功能仅会备份您的规则,包括分组信息。
19 |
20 | ## 限制
21 |
22 | Firefox和Chrome都有各自的空间限制,大约100KB。如果您的规则过多,上传过程会失败,但您依然可以通过传统方式导入和导出。
23 |
24 | 就我所知,Chrome还会限制上传频率,也就是说,您**不能**过快的进行上传。
25 |
26 | ## 其他技术细节
27 |
28 | ### Chrome/Chromium
29 |
30 | * 请查看[chrome.storage API](https://developer.chrome.com/extensions/storage#property-sync)获取更多技术细节。
31 |
32 | ### Firefox
33 |
34 | * 据推测,Firefox会定期进行同步。但您可以通过手动运行,强行进行一次同步。
35 | 
36 | * 重新安装扩展可能导致同步内容丢失。
37 | * 请查看[browser.storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)获取更多技术细节。
38 |
--------------------------------------------------------------------------------
/docs/docs/guide/custom-function.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: Advanced
4 | order: 3
5 | title: Custom function
6 | order: 1
7 | ---
8 |
9 | ## Summary
10 |
11 | Use custom functions to achieve more flexible functionality. So far, custom functions can be used in the following events: redirect request, modify the request headers, modify the response headers.
12 |
13 | Custom functions are also limited by matching rules and exclusion rules. Only requests that meet the matching rules and do not satisfy the exclusion rule are processed by the custom function.
14 |
15 | The priority of the custom function is not determined. It may be possible to customize the function earlier than the normal rule to the request, or it may be later. The order of execution of multiple custom functions is also variable.
16 |
17 | When you can use normal rules to complete the case, please try to use the general rules, rather than custom function
18 |
19 | Custom function writing does **NOT** include the function head and tail, including only the function body. which is:
20 |
21 | ```javascript
22 | function(val, detail) { // does not include this line
23 | // The codes you need to write
24 | } // does not include this line
25 | ```
26 |
27 | For example:
28 |
29 | 
30 |
31 | The custom function passes the arguments `val` and `detail`, where `detail` is the new parameter in version 2.3.0, see the description below. The return type varies depending on the rule type.
32 |
33 | ## Redirect request
34 |
35 | Pass the string with the full URL, if the function is not processed to return NULL or the original argument. For example, the following code will add a `_test` to every request:
36 |
37 | ```javascript
38 | if (val.includes('_test.')) {
39 | return val;
40 | }
41 | const a = val.lastIndexOf('.');
42 | if (a < 0) {
43 | return val;
44 | } else {
45 | return val.substr(0, a) + '_test' + val.substr(a);
46 | }
47 | ```
48 |
49 | Since 4.0.3, return `_header_editor_cancel_` will cancel this request, for example:
50 |
51 | ```javascript
52 | if (val.includes('utm_source')) {
53 | return '_header_editor_cancel_';
54 | }
55 | ```
56 |
57 | ## Modify the request headers and response headers
58 |
59 | The incoming parameter is an array containing all header information in the following format: `[{"name: "header name", "value": "header content"} ... ]`.
60 |
61 | Because JS pass the Object by reference, the custom function does not need any return value, only need to modify the incoming parameters to take effect. For example, this code will add ` HE/2.0.0` to the end of `User-Agent`:
62 |
63 | ```javascript
64 | for (const a in val) {
65 | if (val[a].name.toLowerCase() === 'user-agent') {
66 | val[a].value += ' HE/2.0.0';
67 | break;
68 | }
69 | }
70 | ```
71 |
72 | Note: the browser requires that value must be String, i.e.
73 |
74 | ```javascript
75 | let value = 123;
76 | val.push({"name": "test", "value": value}); //Invalid, because value is number
77 | val.push({"name": "test", "value": value.toString()}); //Valid
78 | ```
79 |
80 | ## detail object
81 |
82 | Since 2.3.0, the custom function adds the parameter `detail` for the more precise control
83 |
84 | This parameter is Object and is a read-only parameter. The structure is as follows:
85 |
86 | ```javascript
87 | {
88 | // Request id. Since 4.0.3
89 | id: 1234,
90 | // Request url. If this request has been redirected, this url is redirected url
91 | url: "http://example.com/example_redirected.png",
92 | // Tab ID. Note that this ID may be duplicated if user open multiple browser windows. Since 4.1.0
93 | tab: 2,
94 | // Request method, such as "GET", "POST", etc.
95 | method: "GET",
96 | // Request frame ID. Since 4.1.0
97 | frame: 123,
98 | // Request's parent frame ID. Since 4.1.0
99 | parentFrame: -1,
100 | // Request's proxy info. Since 4.1.0
101 | proxy: {
102 | host: "localhost",
103 | port: 8080
104 | },
105 | // Resource type
106 | type: "image",
107 | // Request time
108 | time: 1505613357577.7522,
109 | // URL of the resource which triggered the request. For example, if "https://example.com" contains a link, and the user clicks the link, then the originUrl for the resulting request is "https://example.com".
110 | // Since 4.1.0
111 | originUrl: '',
112 | // URL of the document in which the resource will be loaded. Only avaliable in Firefox. Since 4.1.0
113 | documentUrl: '',
114 | // Contains request header if enable "Include request headers in custom function" and this time is response
115 | // May be null. Since 4.1.0
116 | requestHeaders: null
117 | }
118 | ```
119 |
120 | Available resource type see [here](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType)
121 |
122 | You can use this to implement some advanced features, for example, the following code will only redirect images and videos from example.com to example.org:
123 |
124 | ```javascript
125 | if (detail.type === "media") {
126 | return val.replace("example.com", "example.org");
127 | }
128 | ```
129 |
130 | ## How to debug a custom function
131 |
132 | All custom functions are run in the background page, so to debug custom functions, open the console of the background page
133 |
134 | Chrome: Enable developer mode in `chrome://extensions/`, then click the "Inspect views" - "background page" at the bottom of Header Editor
135 |
136 | Firefox: Open `about:debugging`, enable add-on debugging, click the "Debug" at the bottom of Header Editor
137 |
--------------------------------------------------------------------------------
/docs/docs/guide/custom-function.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: 指南
3 | group:
4 | title: 高级
5 | order: 3
6 | title: 自定义函数
7 | order: 1
8 | ---
9 |
10 | ## 综述
11 |
12 | 使用自定义函数可以实现更灵活的功能。目前为止,自定义函数可以在以下事件中使用:重定向请求、修改请求头、修改响应头。
13 |
14 | 自定义函数也受匹配规则和排除规则的限制。只有满足匹配规则且不满足排除规则的请求会被自定义函数处理。
15 |
16 | 自定义函数的优先级不是确定的。可能自定义函数比普通规则更早的作用到请求上,也可能更迟。多个自定义函数的执行顺序也不定。
17 |
18 | 在可以使用普通规则完成的情况下,请尽量使用普通规则,而不是自定义函数
19 |
20 | 自定义函数编写**不包括**函数头尾,只包括函数主体。即:
21 |
22 | ```javascript
23 | function(val, detail) { //不包括这一行
24 | // 你需要编写的部分
25 | } //不包括这一行
26 | ```
27 |
28 | 例如:
29 |
30 | 
31 |
32 | 自定义函数会传入参数`val`和`detail`,其中`detail`是2.3.0版本新增的参数,请参见页面下方说明。返回类型根据规则类型不同而不同。
33 |
34 | ## 重定向请求
35 |
36 | 传入参数为完整URL的字符串,若函数不处理可返回NULL或原参数。例如,下面代码会将请求都加上一个`_test`:
37 |
38 | ```javascript
39 | if (val.includes('_test.')) {
40 | return val;
41 | }
42 | let a = val.lastIndexOf('.');
43 | if (a < 0) {
44 | return val;
45 | } else {
46 | return val.substr(0, a) + '_test' + val.substr(a);
47 | }
48 | ```
49 |
50 | 自4.0.3起,返回`_header_editor_cancel_`可取消此请求,如:
51 |
52 | ```javascript
53 | if (val.includes('utm_source')) {
54 | return '_header_editor_cancel_';
55 | }
56 | ```
57 |
58 | ## 修改请求头和响应头
59 |
60 | 传入参数为一个数组,包含所有头信息,格式为:`[{"name: "头名称", "value": "头内容"} …… ]`。
61 |
62 | 因JS传递Object时是引用传递,因此自定义函数不需要任何返回值,只需要修改传入的参数即可生效。例如,此代码会将`User-Agent`加上` HE/2.0.0`:
63 |
64 | ```javascript
65 | for (const a in val) {
66 | if (val[a].name.toLowerCase() === 'user-agent') {
67 | val[a].value += ' HE/2.0.0';
68 | break;
69 | }
70 | }
71 | ```
72 |
73 | 注意:浏览器要求value必须是String,即:
74 |
75 | ```javascript
76 | let value = 123;
77 | val.push({"name": "test", "value": value}); //不合法,因为value是number
78 | val.push({"name": "test", "value": value.toString()}); //合法
79 | ```
80 |
81 | ## detail对象
82 |
83 | 自2.3.0开始,自定义函数增加参数`detail`,用于实现更精确的控制
84 |
85 | 此参数为Object,且为只读参数。结构如下:
86 |
87 | ```javascript
88 | {
89 | // 请求ID,自4.0.3可用
90 | id: 1234,
91 | // 请求地址,如果有跳转,此地址是跳转后的地址
92 | url: "http://example.com/example_redirected.png",
93 | // 标签页ID,注意如果用户打开了多个浏览器窗口,这个ID可能会重复,自4.1.0可用
94 | tab: 2,
95 | // 请求方式,如GET、POST
96 | method: "GET",
97 | // 请求所属的frame ID,自4.1.0可用
98 | frame: 123,
99 | // 请求所属的frame的父级ID,自4.1.0可用
100 | parentFrame: -1,
101 | // 请求当前的代理信息,可能为null,自4.1.0可用
102 | proxy: {
103 | host: "localhost",
104 | port: 8080
105 | },
106 | // 资源类型
107 | type: "image",
108 | // 请求发起的时间戳
109 | time: 1505613357577.7522,
110 | // 触发此请求的URL,例如在页面A上点击了链接B,则B中可以通过此参数获取到A的地址。可能为空
111 | originUrl: '',
112 | // 资源将会被加载到的地址,仅Firefox可用,可能为空
113 | documentUrl: '',
114 | // 如果开启了“在自定义函数中包含请求头”且此次触发是在响应时,则此处是请求时的头信息,可能为null,自4.1.0可用
115 | requestHeaders: null
116 | }
117 | ```
118 |
119 | 可用资源类型见[此处](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType)
120 |
121 | 您可以借此实现一些高级功能,例如,下面的代码只会将example.com域名下的图片和视频重定向到example.org:
122 |
123 | ```javascript
124 | if (detail.type === "media") {
125 | return val.replace("example.com", "example.org");
126 | }
127 | ```
128 |
129 | ## 如何调试自定义函数
130 |
131 | 所有自定义函数的运行均位于后台页面,因此,要调试自定义函数,请打开后台页面的控制台
132 |
133 | Chrome:在`chrome://extensions/`中,启用“开发者模式”,点击Header Editor下方的“检查视图”-“背景页”
134 |
135 | Firefox:打开`about:debugging`,启用附加组件调试,点击Header Editor下方的“调试”
136 |
--------------------------------------------------------------------------------
/docs/docs/guide/custom-function.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: 高級
4 | order: 3
5 | title: 自訂函數
6 | order: 1
7 | ---
8 |
9 | ## 綜述
10 |
11 | 使用自訂函數可以實現更靈活的功能。目前為止,自訂函數可以在以下事件中使用:重新導向要求、變更要求標頭、變更回應標頭。
12 |
13 | 自訂函數也受比較規則和排除規則的限制。只有滿足比較規則且不滿足排除規則的要求會被自訂函數處理。
14 |
15 | 自訂函數的優先等級不是確定的。可能自訂函數比普通規則更早的作用到要求上,也可能更遲。多個自訂函數的執行順序也不定。
16 |
17 | 在可以使用普通規則完成的情況下,請盡量使用普通規則,而不是自訂函數
18 |
19 | 自訂函數編寫**不包括**函數頭尾,只包括函數主體。即:
20 |
21 | ```javascript
22 | function(val, detail) { //不包括這一行
23 | // 你需要編寫的部分
24 | } //不包括這一行
25 | ```
26 |
27 | 例如:
28 |
29 | 
30 |
31 | 自訂函數會傳入參數`val`和`detail`,其中`detail`是2.3.0版本新增的參數,請參見頁面下方說明。返回類型根據規則類型不同而不同。
32 |
33 | ## 重新導向要求
34 |
35 | 傳入參數為完整URL的字串,若函數不處理可返回NULL或原參數。例如,下面語法會將要求都加上一個`_test`:
36 |
37 | ```javascript
38 | if (val.includes('_test.')) {
39 | return val;
40 | }
41 | let a = val.lastIndexOf('.');
42 | if (a < 0) {
43 | return val;
44 | } else {
45 | return val.substr(0, a) + '_test' + val.substr(a);
46 | }
47 | ```
48 |
49 | 自4.0.3起,返回`_header_editor_cancel_`可取消此请求,如:
50 |
51 | ```javascript
52 | if (val.includes('utm_source')) {
53 | return '_header_editor_cancel_';
54 | }
55 | ```
56 |
57 | ## 變更要求標頭和回應標頭
58 |
59 | 傳入參數為一個陣列,包含所有標頭資訊,格式為:`[{"name: "標頭名稱", "value": "標內容"} …… ]`。
60 |
61 | 因JS傳遞Object時是參照傳遞,因此自訂函數不需要任何傳回值,只需要變更傳入的參數即可生效。例如,此語法會將`User-Agent`加上` HE/2.0.0`:
62 |
63 | ```javascript
64 | for (let a in val) {
65 | if (val[a].name.toLowerCase() === 'user-agent') {
66 | val[a].value += ' HE/2.0.0';
67 | break;
68 | }
69 | }
70 | ```
71 |
72 | 注意:浏览器要求value必须是String,即:
73 |
74 | ```javascript
75 | let value = 123;
76 | val.push({"name": "test", "value": value}); //不合法,因为value是number
77 | val.push({"name": "test", "value": value.toString()}); //合法
78 | ```
79 |
80 | ## detail对象
81 |
82 | 自2.3.0開始,自訂函數增加參數`detail`,用於實現更精確的控制
83 |
84 | 此參數為Object,且為唯讀參數。結構下列:
85 |
86 | ```javascript
87 | {
88 | // 要求ID,自4.0.3可用
89 | "id": 123456,
90 | // 要求位址,如果有跳轉,此位址是跳轉後的位址
91 | "url": "http://example.com/example_redirected.js",
92 | // 要求方式,如GET、POST
93 | "method": "GET",
94 | // 是否為iframe的要求
95 | "isFrame": 0,
96 | // 資源類型
97 | "type": "script",
98 | // 要求發起的時間戳
99 | "time": 1505613357577.7522,
100 | // 要求發起時的URL,可能為空
101 | "originUrl": "http://example.com/"
102 | }
103 | ```
104 |
105 | 可用類型參見[此处](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType)
106 |
107 | 您可以借此实现一些高级功能,例如,下面的代码只会将example.com域名下的图片和视频重定向到example.org:
108 |
109 | ```javascript
110 | if (detail.type === "media") {
111 | return val.replace("example.com", "example.org");
112 | }
113 | ```
114 |
115 | ## 如何调试自定义函数
116 |
117 | 所有自定义函数的运行均位于后台页面,因此,要调试自定义函数,请打开后台页面的控制台
118 |
119 | Chrome:在`chrome://extensions/`中,启用“开发者模式”,点击Header Editor下方的“检查视图”-“背景页”
120 |
121 | Firefox:打开`about:debugging`,启用附加组件调试,点击Header Editor下方的“调试”
122 |
--------------------------------------------------------------------------------
/docs/docs/guide/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: Guide
4 | order: 1
5 | group:
6 | title: Introduction
7 | order: 1
8 | title: Setup
9 | order: 1
10 | ---
11 |
12 | ## Install
13 |
14 | Please choose a different installation method depending on your browser:
15 |
16 | | Browser | Installation |
17 | | --- | --- |
18 | |  Firefox | [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) or our [self-distributed version](https://github.com/FirefoxBar/HeaderEditor/releases) |
19 | |  Chrome | [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) |
20 | |  Edge(Chromium) | [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko) |
21 |
22 | ## Basic usage
23 |
24 | * Click the HE icon in the upper right corner of your browser to open the HE Management Panel
25 | * Create a new rule: Click the Add button in the lower right corner, fill in the rules, and save.
26 | * Alternatively, you can download the rules of others in "Import and Export".
27 |
28 | ## Migrate from other similar extensions
29 |
30 | We provide a small tool that can help you migrate from other similar extensions to Header Editor: [migrate-to-he.firefoxcn.net](https://migrate-to-he.firefoxcn.net/index_en.html)
31 |
--------------------------------------------------------------------------------
/docs/docs/guide/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 指南
4 | order: 1
5 | group:
6 | title: 介绍
7 | order: 1
8 | title: 安装
9 | order: 1
10 | ---
11 |
12 | ## 安装
13 |
14 | 请根据您的平台不同,选择不同的安装方式:
15 |
16 | | 浏览器 | 安装 |
17 | | --- | --- |
18 | |  Firefox | [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) 或 我们的[自分发版本](https://github.com/FirefoxBar/HeaderEditor/releases) |
19 | |  Chrome | [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) |
20 | |  Edge(Chromium) | [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko) |
21 |
22 | ## 基本使用
23 |
24 | * 点击右上角的HE图标,打开HE管理面板
25 | * 新建规则:点击右下角的添加按钮,填写规则内容后,保存即可。
26 | * 或者,您可以在“导入和导出”中下载他人的规则。
27 |
28 | ## 从其他类似扩展迁移
29 |
30 | 我们提供了一个小工具,可以协助你从一些类似的扩展,快速迁移到 Header Editor: [migrate-to-he.firefoxcn.net](https://migrate-to-he.firefoxcn.net/)
31 |
--------------------------------------------------------------------------------
/docs/docs/guide/index.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 指南
4 | order: 1
5 | group:
6 | title: 介紹
7 | order: 1
8 | title: 安裝
9 | order: 1
10 | ---
11 |
12 | ## 安裝
13 |
14 | 请根据您的平台不同,选择不同的安装方式:
15 |
16 | | 浏览器 | 安装 |
17 | | --- | --- |
18 | |  Firefox | [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) 或 我们的[自分发版本](https://github.com/FirefoxBar/HeaderEditor/releases) |
19 | |  Chrome | [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) |
20 | |  Edge(Chromium) | [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko) |
21 |
22 | ## 基本使用
23 |
24 | * 点击右上角的HE图标,打开HE管理面板
25 | * 在规则界面新建规则:点击右下角的添加按钮,填写规则内容后,保存即可。
26 | * 或者,您可以在“导入和导出”中下载他人的规则。
27 |
28 | ## 從其他類似擴展遷移
29 |
30 | 我們提供了一個小工具,可以協助你從一些類似的擴展,快速遷移到 Header Editor: [migrate-to-he.firefoxcn.net](https://migrate-to-he.firefoxcn.net/index_zh_tw.html)
31 |
--------------------------------------------------------------------------------
/docs/docs/guide/modify-body.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: Advanced
4 | title: Modify response body
5 | order: 2
6 | ---
7 |
8 | ## Before use
9 |
10 | This feature can modify the response body, but it has the following requirements:
11 | * Use Firefox 57+
12 | * Check "Modify the response body (only supports Firefox)" in the options of HE
13 |
14 | If you enable this feature, you may have the following problems:
15 | * To some extent affect access speed and resource occupation
16 | * Regardless of whether you have written relevant rules, HE will intercept the request data.
17 | * Affects some content downloads
18 |
19 | ## How to use
20 |
21 | > As of now, this feature only supports custom functions.
22 |
23 | ### Encoding
24 | You need to specify the webpage encoding in order for HE to decode the data.
25 |
26 | If you don't know what encoding the webpage uses, please open the console (press F12), switch to the Network tab, refresh the current page, and observe the Content-Type in the Response Headers.
27 |
28 | ### Function writing
29 | The function has two parameters: the first parameter is the decoded text, and the second parameter is the detail object of the custom function. Returns the modified text.
30 |
31 | Detail object please see [custom function](custom-function.md) document
32 |
33 | For example, the following function will replace all "baidu" in the page with "Google"
34 | ```js
35 | return val.replace(/baidu/g, 'Google');
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/guide/modify-body.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: 指南
3 | group: 高级
4 | title: 修改响应体
5 | order: 2
6 | ---
7 |
8 | ## 使用前必读
9 |
10 | 该功能可以修改请求的响应体,但有以下要求:
11 | * 使用Firefox 57+
12 | * 在HE的选项中勾选“修改响应体(仅支持Firefox)”
13 |
14 | 如果使用了此功能,可能会有以下问题:
15 | * 一定程度上影响访问速度和资源占用
16 | * 不论您是否有编写相关规则,HE均会拦截请求数据。
17 | * 影响部分内容下载
18 |
19 | ## 如何使用
20 |
21 | > 截止目前,此功能只支持自定义函数。
22 |
23 | ### 编码
24 | 您需要指定网页相关编码,才能让HE成功解码数据。
25 |
26 | 如果您不知道网页使用何种编码,请打开控制台(按F12),切换到 Network/网络 标签,刷新当前页面,观察 Response Headers/响应头 中的 Content-Type。
27 |
28 | ### 函数编写
29 | 函数共有两个参数:首个参数为解码后的文本,第二个参数为自定义函数的detail对象。返回修改后的文本。
30 |
31 | detail对象请查看[自定义函数](custom-function.md)文档
32 |
33 | 例如,下面函数,会将网页中的所有“baidu”替换为“Google”
34 | ```js
35 | return val.replace(/baidu/g, 'Google');
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/guide/modify-body.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: 高級
4 | title: 修改響應體
5 | order: 2
6 | ---
7 |
8 | ## 使用前必读
9 |
10 | 该功能可以修改请求的响应体,但有以下要求:
11 | * 使用Firefox 57+
12 | * 在HE的选项中勾选“修改响应体(仅支持Firefox)”
13 |
14 | 如果使用了此功能,可能会有以下问题:
15 | * 一定程度上影响访问速度和资源占用
16 | * 不论您是否有编写相关规则,HE均会拦截请求数据。
17 | * 影响部分内容下载
18 |
19 | ## 如何使用
20 |
21 | > 截止目前,此功能只支持自定义函数。
22 |
23 | ### 编码
24 | 您需要指定网页相关编码,才能让HE成功解码数据。
25 |
26 | 如果您不知道网页使用何种编码,请打开控制台(按F12),切换到 Network/网络 标签,刷新当前页面,观察 Response Headers/响应头 中的 Content-Type。
27 |
28 | ### 函数编写
29 | 函数共有两个参数:首个参数为解码后的文本,第二个参数为自定义函数的detail对象。返回修改后的文本。
30 |
31 | detail对象请查看[自定义函数](custom-function.md)文档
32 |
33 | 例如,下面函数,会将网页中的所有“baidu”替换为“Google”
34 | ```js
35 | return val.replace(/baidu/g, 'Google');
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/guide/rule.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: Basic features
4 | order: 2
5 | title: Rule
6 | order: 1
7 | ---
8 |
9 | ## Rule
10 |
11 | ### Match type
12 |
13 | Rules will apply to the URL that meets the matching criteria
14 |
15 | * All: Correspond to all urls, including the Header Editor itself
16 | * Regular expression
17 | * Supports standard JS regular expressions. For example, the regular expression you entered is `str`, then, in fact, the program will use the internal `new RegExp(str)` to initialize the regular expression.
18 | * If the match rule is a regular expression, the modification result (currently including redirect to) supports the use placeholder like `$1`
19 | * Learn more about regular expression on [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
20 | * URL prefix: Including `http://` URL prefix
21 | * Domain name: The full domain name that contains the subdomain
22 | * URL: Including "?" And the full address of all subsequent content
23 |
24 | ### Exclude
25 |
26 | The rule will not take effect on the URL which is match the "exclude".
27 |
28 | ### Custom function
29 |
30 | Through a custom function to realize a more flexible function, the specific use please see [here](./custom-function.md)
31 |
32 | ## Other special features
33 |
34 | * When using "Modify request header" or "Modify response header", set the header content to `_header_editor_remove_` will remove this header (valid since 3.0.5)
35 |
36 | * When using "Redirect request" with custom function, return `_header_editor_cancel_` will cancel this request (valid since 4.0.3)
37 |
38 | ## Other considerations
39 |
40 | * If you want to set a header content to empty, different browsers have different behaviors. Chrome will keep this header but its content will be empty. Firefox will remove this header
41 |
42 | ## A common feature example
43 |
44 | The following example is not guaranteed to be valid, only as an example to help users familiarize themselves with the rules of the Header Editor
45 |
46 | ### Redirect requests
47 |
48 | For example, the Google public library is redirected to the mirror image of University of Science and Technology of China:
49 |
50 | Regular expressions is `^http(s?)://(ajax|fonts)\.googleapis\.com/(.*)`, redirect to `https://$2.proxy.ustclug.org/$3`
51 |
52 | Redirect all HTTP requests of `sale.jd.com`, `item.jd.com` and `www.jd.com` to the HTTPS:
53 |
54 | Regular expressions is `http://(sale|item|www).jd.com`, redirect to `https://$1.jd.com`
55 |
56 | Redirect all wikipedia's HTTP requests to HTTPS:
57 |
58 | Regular expressions is `^http://([^\/]+\.wikipedia\.org/.+)`, redirect to `https://$1`
59 |
60 | ### Camouflage UA
61 |
62 | Just modify the request header named User-Agent, but the function can only affect the ability of the server to determine UA, which can not be pseudo in local through JS
63 |
--------------------------------------------------------------------------------
/docs/docs/guide/rule.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: 指南
3 | group:
4 | title: 常用功能
5 | order: 2
6 | title: 规则
7 | order: 1
8 | ---
9 |
10 | ## 规则
11 |
12 | HE本身并不具备任何功能,它只是提供了管理和编写规则的能力。您需要通过编写规则,来实现相应的功能。
13 |
14 | ### 匹配类型
15 |
16 | 规则会应用到满足相应匹配条件的URL上。
17 |
18 | * 全部:对应所有URL,包括Header Editor自身。
19 | * 正则表达式:
20 | * 支持标准的JS正则表达式。例如你输入的正则表达式是`str`,那么,实际上,程序内部就会使用`new RegExp(str)`初始化正则表达式。
21 | * 如果匹配规则是正则表达式,则修改结果(目前包括重定向至)支持使用形似`$1`的占位符
22 | * 在[Mozilla Developer Network](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp)上了解更多关于正则表达式的内容
23 | * 网址前缀:包括`http://`在内的网址前缀
24 | * 域名:包含子域名在内的完整的域名
25 | * 网址:包括“?”及之后的所有内容的完整地址
26 |
27 | ### 排除规则
28 |
29 | 不论是否满足匹配规则,只要满足了排除规则,那么此条均不会对当前URL生效
30 |
31 | ### 自定义函数
32 |
33 | 通过自定义函数实现更灵活的功能,具体使用请参见[此处](./custom-function.md)
34 |
35 | ## 其他特殊功能
36 |
37 | * 使用功能“修改请求头”或“修改响应头”时,将头内容设置为`_header_editor_remove_`将会移除此头(自3.0.5起有效)
38 |
39 | * 使用功能“重定向请求”且使用自定义函数时,返回`_header_editor_cancel_`将阻止此请求(自4.0.3开始有效)
40 |
41 | ## 其他注意事项
42 |
43 | * 将头内容设置为空,不同浏览器对此处理方式不同。Chrome将会保留此头信息,但其内容为空。Firefox则会移除此头信息
44 |
45 | ## 常见功能示例
46 |
47 | 下面的例子不保证均有效,只作为示例,用于帮助用户熟悉Header Editor的规则编写
48 |
49 | #### 反-防盗链
50 |
51 | 使用说明:将URL匹配至图片域名,功能为“修改请求头”,将头内容Referer修改为任意可显示图片的网址。下列有一些常用的规则:
52 |
53 | 前缀为`http://imgsrc.baidu.com/`,修改Referer为`http://tieba.baidu.com`
54 |
55 | 正则表达式为`http://(\w?\.?)hiphotos\.baidu\.com/`,修改Referer为`http://tieba.baidu.com`
56 |
57 | #### 重定向请求
58 |
59 | 例如,将Google公共库重定向至中科大的镜像上:
60 |
61 | 正则表达式为`^http(s?)://(ajax|fonts)\.googleapis\.com/(.*)`,重定向至`https://$2.proxy.ustclug.org/$3`
62 |
63 | 将所有对`sale.jd.com`、`item.jd.com`、`www.jd.com`的HTTP请求重定向到HTTPS:
64 |
65 | 正则表达式为`http://(sale|item|www).jd.com`,重定向至`https://$1.jd.com`
66 |
67 | 将所有维基百科的HTTP请求重定向至HTTPS:
68 |
69 | 正则表达式为`^http://([^\/]+\.wikipedia\.org/.+)`,重定向至`https://$1`
70 |
71 | #### 伪装UA
72 |
73 | 修改请求头的User-Agent即可,但功能只能影响服务器判断UA的能力,对于在本地通过JS判断的,无法伪装
74 |
--------------------------------------------------------------------------------
/docs/docs/guide/rule.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: 常用功能
4 | order: 2
5 | title: 规则
6 | order: 1
7 | ---
8 |
9 | ## 规则
10 |
11 | HE本身并不具备任何功能,它只是提供了管理和编写规则的能力。您需要通过编写规则,来实现相应的功能。
12 |
13 | ### 匹配类型
14 |
15 | 规则会应用到满足相应匹配条件的URL上。
16 |
17 | * 全部:对应所有URL,包括Header Editor自身。
18 | * 正規表示式:
19 | * 支援標準的JS正規表示式。例如你輸出的正規表示式是`str`,那麼,實際上,程式內部就會使用`new RegExp(str)`初始化正規表示式。
20 | * 如果對應規則是正規表示式,則變更結果(目前包括重新導向至)支援使用形似`$1`的預留位置。
21 | * 在[Mozilla Developer Network](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/RegExp)上了解更多关于正则表达式的内容
22 | * 網址首碼:包括`http://`在內的網址首碼。
23 | * 域名:包含子域名在內的完整的域名。
24 | * 網址:包括“?”及之後的所有內容的完整位址。
25 |
26 | ### 排除规则
27 |
28 | 不论是否满足匹配规则,只要满足了排除规则,那么此条均不会对当前URL生效
29 |
30 | ### 自定义函数
31 |
32 | 通过自定义函数实现更灵活的功能,具体使用请参见[此处](./custom-function.md)
33 |
34 | ## 其他特殊功能
35 |
36 | * 使用功能“修改请求头”或“修改响应头”时,将头内容设置为`_header_editor_remove_`将会移除此头(自3.0.5起有效)
37 |
38 | * 使用功能“重定向请求”且使用自定义函数时,返回`_header_editor_cancel_`将阻止此请求(自4.0.3开始有效)
39 |
40 | ## 其他注意事项
41 |
42 | * 将头内容设置为空,不同浏览器对此处理方式不同。Chrome将会保留此头信息,但其内容为空。Firefox则会移除此头信息
43 |
44 | ## 常见功能示例
45 |
46 | 下面的例子不保证均有效,只作为示例,用于帮助用户熟悉Header Editor的规则编写
47 |
48 | #### 反-防盗链
49 |
50 | 使用说明:将URL匹配至图片域名,功能为“修改请求头”,将头内容Referer修改为任意可显示图片的网址。下列有一些常用的规则:
51 |
52 | 前缀为`http://imgsrc.baidu.com/`,修改Referer为`http://tieba.baidu.com`
53 |
54 | 正则表达式为`http://(\w?\.?)hiphotos\.baidu\.com/`,修改Referer为`http://tieba.baidu.com`
55 |
56 | #### 重定向请求
57 |
58 | 例如,将Google公共库重定向至中科大的镜像上:
59 |
60 | 正则表达式为`^http(s?)://(ajax|fonts)\.googleapis\.com/(.*)`,重定向至`https://$2.proxy.ustclug.org/$3`
61 |
62 | 将所有对`sale.jd.com`、`item.jd.com`、`www.jd.com`的HTTP请求重定向到HTTPS:
63 |
64 | 正则表达式为`http://(sale|item|www).jd.com`,重定向至`https://$1.jd.com`
65 |
66 | 将所有维基百科的HTTP请求重定向至HTTPS:
67 |
68 | 正则表达式为`^http://([^\/]+\.wikipedia\.org/.+)`,重定向至`https://$1`
69 |
70 | #### 伪装UA
71 |
72 | 修改请求头的User-Agent即可,但功能只能影响服务器判断UA的能力,对于在本地通过JS判断的,无法伪装
73 |
--------------------------------------------------------------------------------
/docs/docs/guide/third-party-rules.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: More
4 | order: 4
5 | title: Third-party rules
6 | order: 1
7 | ---
8 |
9 | ## Notice
10 |
11 | The following rules are maintained by a third party. Header Editor does not guarantee the timeliness and security of the rules. If there is any problem, please contact the rule maintainer.
12 |
13 | ## Lists
14 |
15 | * [Amazon > Amazon Smile](https://github.com/FirefoxBar/HeaderEditor/files/2384019/Header.Editor.-.Amazon.Smile.zip) By [vertigo220](https://github.com/vertigo220)
16 |
17 | ## Submitting rules here
18 |
19 | If you want your maintenance rules to appear here, please [submit an issue](https://github.com/FirefoxBar/HeaderEditor/issues/new)
20 |
--------------------------------------------------------------------------------
/docs/docs/guide/third-party-rules.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: 指南
3 | group:
4 | title: 更多
5 | order: 4
6 | title: 第三方规则
7 | order: 1
8 | ---
9 |
10 |
11 | ## 注意
12 |
13 | 下面的规则由第三方维护,Header Editor不保证规则的时效性、安全性,若出现问题请联系规则维护者
14 |
15 | ## 列表
16 |
17 | * [dupontjoy](https://github.com/dupontjoy/customization/tree/master/Rules/HeaderEditor) 主要为中文站点
18 |
19 | ## 提交规则
20 |
21 | 如果您希望您维护的规则出现在此处,请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new)
22 |
--------------------------------------------------------------------------------
/docs/docs/guide/third-party-rules.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | group:
3 | title: 更多
4 | order: 4
5 | title: 第三方規則
6 | order: 1
7 | ---
8 |
9 |
10 | ## 注意
11 |
12 | 下面的规则由第三方维护,Header Editor不保证规则的时效性、安全性,若出现问题请联系规则维护者
13 |
14 | ## 列表
15 |
16 | * [dupontjoy](https://github.com/dupontjoy/customization/tree/master/Rules/HeaderEditor) 主要为中文站点
17 |
18 | ## 提交规则
19 |
20 | 如果您希望您维护的规则出现在此处,请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new)
21 |
--------------------------------------------------------------------------------
/docs/docs/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Header Editor
3 | hero:
4 | title: Header Editor
5 | description: Manage browser's requests, include modify the request headers and response headers, redirect requests, cancel requests
6 | actions:
7 | - text: Setup
8 | link: /guide
9 | - text: GitHub
10 | link: https://github.com/FirefoxBar/HeaderEditor
11 | features:
12 | - title: Modify requests
13 | emoji: 🚥
14 | description: Based on rules, modify request headers, response headers, and redirect
15 | - title: Custom function
16 | emoji: ⚙️
17 | description: Use custom functions to achieve more flexible functionality
18 | - title: Export and Sync
19 | emoji: ☁️
20 | description: Rules can import and export, and you can use cloud sync
21 | ---
22 |
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Header Editor
3 | hero:
4 | title: Header Editor
5 | description: 管理浏览器请求,包括修改请求头和响应头、重定向请求、取消请求
6 | actions:
7 | - text: 开始使用
8 | link: /guide
9 | - text: GitHub
10 | link: https://github.com/FirefoxBar/HeaderEditor
11 | features:
12 | - title: 修改请求
13 | emoji: 🚥
14 | description: 基于规则,修改请求头、响应头,进行重定向
15 | - title: 自定义函数
16 | emoji: ⚙️
17 | description: 通过自定义函数,更精确的控制请求
18 | - title: 导出和同步
19 | emoji: ☁️
20 | description: 规则可以自由导入和导出,并可使用云同步
21 | ---
22 |
--------------------------------------------------------------------------------
/docs/docs/index.zh-TW.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Header Editor
3 | hero:
4 | title: Header Editor
5 | description: 透過此附加元件,您可以變更要求標頭和回應標頭,取消要求、重新導向的要求
6 | actions:
7 | - text: 開始使用
8 | link: /guide
9 | - text: GitHub
10 | link: https://github.com/FirefoxBar/HeaderEditor
11 | features:
12 | - title: 修改请求
13 | emoji: 🚥
14 | description: 基于规则,修改请求头、响应头,进行重定向
15 | - title: 自定义函数
16 | emoji: ⚙️
17 | description: 通过自定义函数,更精确的控制请求
18 | - title: 导出和同步
19 | emoji: ☁️
20 | description: 规则可以自由导入和导出,并可使用云同步
21 | ---
22 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "header-editor-docs",
3 | "version": "0.0.1",
4 | "description": "Header Editor official manual",
5 | "scripts": {
6 | "start": "npm run dev",
7 | "dev": "dumi dev",
8 | "build": "dumi build",
9 | "prepare": "dumi setup"
10 | },
11 | "authors": [
12 | "ShuangYa"
13 | ],
14 | "devDependencies": {
15 | "dumi": "^2.0.2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/extension.json:
--------------------------------------------------------------------------------
1 | {
2 | "dist": "HeaderEditor-{VER}",
3 | "autobuild": {
4 | "xpi": true,
5 | "amo": true,
6 | "cws": true,
7 | "crx": true
8 | },
9 | "firefox": {
10 | "xpi": "headereditor@addon.firefoxcn.net",
11 | "amo": "headereditor-amo@addon.firefoxcn.net"
12 | },
13 | "chrome": {
14 | "id": "eningockdidmgiojffjmkdblpjocbhgh",
15 | "crx": "jhigoaelcgmfbidkocglkcnhmfacajle"
16 | },
17 | "github": {
18 | "enable": true
19 | }
20 | }
--------------------------------------------------------------------------------
/locale/create-pr.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | const token = process.env.GITHUB_TOKEN;
4 |
5 | const baseURL = process.env.GITHUB_API_URL + '/repos/' + process.env.GITHUB_REPOSITORY;
6 | const request = axios.create({
7 | baseURL: baseURL,
8 | validateStatus: () => true,
9 | });
10 |
11 | request.defaults.headers.common['Accept'] = 'application/vnd.github+json';
12 | request.defaults.headers.common['Authorization'] = 'Bearer ' + token;
13 | request.defaults.headers.common['X-GitHub-Api-Version'] = '2022-11-28';
14 |
15 | async function main() {
16 | if (!token) {
17 | console.log('No token');
18 | return;
19 | }
20 |
21 | console.log('baseURL: ' + baseURL);
22 |
23 | const pulls = await request.get('/pulls', {
24 | params: {
25 | state: 'open',
26 | head: 'dev-locale',
27 | base: 'dev',
28 | }
29 | });
30 |
31 | if (pulls.data.length > 0) {
32 | // already has PR
33 | const item = pulls.data[0];
34 | console.log("PR already exists: " + item.html_url);
35 | return;
36 | }
37 |
38 | // Create new PR
39 | const create = await request.post('/pulls', JSON.stringify({
40 | title: '[locale] update locales',
41 | body: '',
42 | head: 'dev-locale',
43 | base: 'dev',
44 | }));
45 |
46 | if (create.status === 201) {
47 | console.log("PR created: " + create.data.html_url);
48 | } else {
49 | console.log("PR created failed: " + create.status);
50 | }
51 | }
52 |
53 | main();
--------------------------------------------------------------------------------
/locale/locales.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const ORIGINAL_NAME = 'en';
5 | const originalDir = path.join(__dirname, 'original');
6 | const outputDir = path.join(__dirname, 'output');
7 |
8 | function ksort(obj) {
9 | let objKeys = Object.keys(obj);
10 | objKeys.sort((k1, k2) => {
11 | let i = 0;
12 | while (i < (k1.length - 1) && i < (k2.length - 1) && k1[i] === k2[i]) {
13 | i++;
14 | }
15 | if (k1[i] === k2[i]) {
16 | return i < (k1.length - 1) ? 1 : -1;
17 | } else {
18 | return k1[i].charCodeAt() > k2[i].charCodeAt() ? 1 : -1;
19 | }
20 | });
21 | let result = {};
22 | objKeys.forEach(k => result[k] = obj[k]);
23 | return result;
24 | }
25 |
26 | function readJSON(filePath) {
27 | return JSON.parse(fs.readFileSync(filePath, {
28 | encoding: "utf8"
29 | }));
30 | }
31 |
32 | let _basicLanguage = {};
33 | function getBasicLanguage(fileName) {
34 | if (typeof _basicLanguage[fileName] === 'undefined') {
35 | _basicLanguage[fileName] = readJSON(path.join(originalDir, fileName));
36 | }
37 | return _basicLanguage[fileName];
38 | }
39 |
40 | // Get default language
41 | function main() {
42 | const dir = fs.readdirSync(outputDir);
43 |
44 | for (const lang of dir) {
45 | const langDir = path.join(outputDir, lang);
46 | // skip not a dir
47 | const stat = fs.statSync(langDir);
48 | if (!stat.isDirectory()) {
49 | console.log("[" + lang + "] skip");
50 | continue;
51 | }
52 |
53 | // get detail messages
54 | const files = fs.readdirSync(langDir);
55 | for (const file of files) {
56 | if (!file.endsWith('.json')) {
57 | console.log("[" + lang + "/" + file + "] skip file");
58 | continue;
59 | }
60 |
61 | console.log("[" + lang + "/" + file + "] read file");
62 | const basicLanguage = getBasicLanguage(file);
63 | const orignalCurrentLanguage = readJSON(path.join(langDir, file));
64 | // sort
65 | const currentLanguage = ksort(orignalCurrentLanguage);
66 |
67 | Object.keys(basicLanguage).forEach(k => {
68 | // add not exists
69 | if (typeof currentLanguage[k] === 'undefined') {
70 | console.log("[" + lang + "/" + file + "] add default locale: " + k);
71 | currentLanguage[k] = basicLanguage[k];
72 | }
73 | // add placeholder
74 | if (basicLanguage[k].placeholders) {
75 | console.log("[" + lang + "/" + file + "] add placeholder: " + k);
76 | currentLanguage[k].placeholders = basicLanguage[k].placeholders;
77 | }
78 | });
79 |
80 | Object.keys(currentLanguage).forEach(k => {
81 | // remove description
82 | delete currentLanguage[k].description;
83 | });
84 |
85 | fs.writeFileSync(path.join(langDir, file), JSON.stringify(currentLanguage), {
86 | encoding: "utf8"
87 | });
88 | console.log("[" + lang + "/" + file + "] write ok");
89 | }
90 | }
91 |
92 | // Copy original language
93 | const files = fs.readdirSync(originalDir);
94 | const originalOutput = path.join(outputDir, ORIGINAL_NAME);
95 | if (!fs.existsSync(originalOutput)) {
96 | fs.mkdirSync(originalOutput, {
97 | recursive: true,
98 | });
99 | }
100 | for (const file of files) {
101 | const basicLanguage = getBasicLanguage(file);
102 | // sort
103 | const currentLanguage = ksort(basicLanguage);
104 | Object.keys(currentLanguage).forEach(k => {
105 | // remove description
106 | delete currentLanguage[k].description;
107 | });
108 | fs.writeFileSync(path.join(originalOutput, file), JSON.stringify(currentLanguage), {
109 | encoding: "utf8"
110 | });
111 | console.log("[" + ORIGINAL_NAME + "/" + file + "] write ok");
112 | }
113 | }
114 |
115 | main();
--------------------------------------------------------------------------------
/locale/output/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/locale/output/.gitkeep
--------------------------------------------------------------------------------
/locale/sort-origin.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | function ksort(obj) {
5 | let objKeys = Object.keys(obj);
6 | objKeys.sort((k1, k2) => {
7 | let i = 0;
8 | while (i < (k1.length - 1) && i < (k2.length - 1) && k1[i] === k2[i]) {
9 | i++;
10 | }
11 | if (k1[i] === k2[i]) {
12 | return i < (k1.length - 1) ? 1 : -1;
13 | } else {
14 | return k1[i].charCodeAt() > k2[i].charCodeAt() ? 1 : -1;
15 | }
16 | });
17 | let result = {};
18 | objKeys.forEach(k => result[k] = obj[k]);
19 | return result;
20 | }
21 |
22 | let lang = require(path.join(__dirname, 'original/messages.json'));
23 | lang = ksort(lang);
24 | fs.writeFileSync(path.join(__dirname, 'original/messages.json'), JSON.stringify(lang, null, "\t"), {
25 | encoding: "utf8"
26 | });
27 | console.log("Sort success");
--------------------------------------------------------------------------------
/locale/transifex.yml:
--------------------------------------------------------------------------------
1 | git:
2 | filters:
3 | - filter_type: file
4 | file_format: CHROME
5 | source_language: en
6 | source_file: 'original/messages.json'
7 | translation_files_expression: 'output//messages.json'
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "header-editor",
3 | "version": "5.0.0",
4 | "description": "Header Editor",
5 | "author": "ShuangYa",
6 | "license": "GPL-2.0",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/FirefoxBar/HeaderEditor.git"
10 | },
11 | "scripts": {
12 | "start": "icejs start --disable-open --config build.config.js",
13 | "build": "icejs build --config build.config.js",
14 | "lint": "npm run eslint && npm run stylelint",
15 | "eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
16 | "eslint:fix": "npm run eslint -- --fix",
17 | "stylelint": "stylelint \"**/*.{css,scss,less}\"",
18 | "release": "node ./scripts/release.mjs",
19 | "pack": "node ./scripts/pack.mjs",
20 | "precommit": "lint-staged"
21 | },
22 | "dependencies": {
23 | "@codemirror/lang-javascript": "^6.1.4",
24 | "@douyinfe/semi-icons": "^2.30.1",
25 | "@douyinfe/semi-ui": "^2.30.1",
26 | "@emotion/css": "^11.10.6",
27 | "@ice/runtime": "^0.1.2",
28 | "@uiw/codemirror-theme-github": "^4.19.9",
29 | "@uiw/react-codemirror": "^4.19.9",
30 | "ahooks": "^3.7.5",
31 | "create-app-shared": "^1.2.6",
32 | "dayjs": "^1.9.5",
33 | "eventemitter3": "^4.0.0",
34 | "fast-deep-equal": "^2.0.1",
35 | "lodash-es": "^4.17.21",
36 | "query-string": "^8.1.0",
37 | "react": "^17.0.0",
38 | "react-app-renderer": "^3.1.0",
39 | "react-dom": "^17.0.0",
40 | "regenerator-runtime": "^0.13.11",
41 | "text-encoding": "^0.7.0",
42 | "tslib": "^2.5.0",
43 | "webextension-polyfill": "^0.10.0"
44 | },
45 | "devDependencies": {
46 | "@iceworks/spec": "^1.0.0",
47 | "@plasmo-corp/ewu": "^0.6.0",
48 | "@types/chrome": "^0.0.72",
49 | "@types/lodash-es": "^4.17.6",
50 | "@types/node": "^18.15.5",
51 | "@types/react": "^16.9.16",
52 | "@types/react-dom": "^16.9.4",
53 | "@types/text-encoding": "^0.0.35",
54 | "@types/webextension-polyfill": "^0.10.0",
55 | "build-plugin-css-assets-local": "^0.1.0",
56 | "copy-webpack-plugin": "^11.0.0",
57 | "crx": "^5.0.1",
58 | "eslint": "^7.30.0",
59 | "eslint-plugin-import": "^2.27.5",
60 | "eslint-plugin-unused-imports": "^2.0.0",
61 | "fs-extra": "^11.1.1",
62 | "husky": "^3.1.0",
63 | "ice.js": "^2.0.0",
64 | "lint-staged": "^9.5.0",
65 | "node-fetch": "^3.3.1",
66 | "prettier": "^1.19.1",
67 | "publish-release": "^1.6.0",
68 | "sign-addon": "^6.0.0",
69 | "stylelint": "^13.7.2",
70 | "typescript": "^3.7.3",
71 | "webpack-bundle-analyzer": "^4.8.0"
72 | },
73 | "husky": {
74 | "hooks": {
75 | "pre-commit": "npm run precommit"
76 | }
77 | },
78 | "lint-staged": {
79 | "./src/**/*.{ts,tsx}": [
80 | "eslint --cache --fix",
81 | "git add"
82 | ]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/public/_locales/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/public/_locales/.gitkeep
--------------------------------------------------------------------------------
/public/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {"action":{"message":"Action"},"add":{"message":"Add"},"auto":{"message":"Auto"},"batch_delete":{"message":"batch deletion"},"batch_mode":{"message":"Batch operation"},"cancel":{"message":"Cancel"},"choose":{"message":"Choose"},"clone":{"message":"Clone"},"cloud_backup":{"message":"Cloud backup"},"cloud_backup_at":{"message":"Last backup at $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"No backup"},"cloud_over_limit":{"message":"Exceeded backup size limit"},"code":{"message":"Code"},"code_empty":{"message":"Code can not be empty"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"},"delete":{"message":"Delete"},"delete_confirm":{"message":"Do you want to delete these rules?"},"description":{"message":"Manage browser's requests, include modify the request headers and response headers, redirect requests, cancel requests"},"disable":{"message":"Disable"},"display_common_header":{"message":"Display common header"},"download":{"message":"Download"},"download_rule":{"message":"Download rule"},"edit":{"message":"Edit"},"enable":{"message":"Enable"},"enable_he":{"message":"Enable Header Editor"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Please enter a group name"},"excludeRule":{"message":"Exclude rule"},"exec_function":{"message":"Custom function"},"exec_normal":{"message":"normal"},"exec_type":{"message":"Execute type"},"export":{"message":"Export"},"export_and_import":{"message":"Export and Import"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Group"},"headerName":{"message":"Header name"},"headerValue":{"message":"Header value"},"header_empty":{"message":"Header name can not be empty"},"help":{"message":"Help"},"import":{"message":"Import"},"import_drop":{"message":"Do not import"},"import_new":{"message":"Add"},"import_override":{"message":"Overrides existing"},"import_success":{"message":"Successfully imported"},"include_header_in_custom_function":{"message":"Include request headers in custom function"},"manage":{"message":"Manage"},"manage_collapse_group":{"message":"Collapse groups by default"},"matchRule":{"message":"Match rules"},"matchType":{"message":"Match type"},"match_all":{"message":"All"},"match_domain":{"message":"Domain"},"match_prefix":{"message":"URL prefix"},"match_regexp":{"message":"Regular expression"},"match_rule_empty":{"message":"Match rule can not be empty"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Name"},"name_empty":{"message":"Name can not be empty"},"ok":{"message":"OK"},"options":{"message":"Options"},"pack_up_or_unfurl":{"message":"Pack up or unfurl group"},"redirectTo":{"message":"Redirect to"},"redirect_empty":{"message":"Redirect target can not be empty"},"rename":{"message":"Rename"},"ruleType":{"message":"Rule type"},"rule_cancel":{"message":"Cancel request"},"rule_list":{"message":"Rules list"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modify response header"},"rule_modifySendHeader":{"message":"Modify request header"},"rule_redirect":{"message":"Redirect request"},"rules_no_effect_for_he":{"message":"Rules take no effect on Header Editor"},"save":{"message":"Save"},"save_to":{"message":"Save to"},"saved":{"message":"Saved"},"select_all":{"message":"Select/Unselect all"},"select_or_download":{"message":"Please import from a local file or download rules"},"share":{"message":"Share"},"suggested_group":{"message":"Suggested group"},"test_custom_code":{"message":"Does not support testing custom code now"},"test_exclude":{"message":"Matched but excluded"},"test_invalid_regexp":{"message":"Regular expression is invalid"},"test_mismatch":{"message":"Mismatch"},"test_url":{"message":"Test (Won't be saved)"},"third_party_rules":{"message":"Third party rules"},"ungrouped":{"message":"Ungrouped"},"upload":{"message":"Upload"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en-US/guide/cloud-backup/"},"url_help":{"message":"https://he.firefoxcn.net/en-US/"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en-US/guide/third-party-rules/"},"view":{"message":"View"}}
--------------------------------------------------------------------------------
/public/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {"action":{"message":"Acción"},"add":{"message":"Añadir"},"auto":{"message":"Auto"},"batch_delete":{"message":"batch deletion"},"batch_mode":{"message":"Batch operation"},"cancel":{"message":"Cancelar"},"choose":{"message":"Escoger"},"clone":{"message":"Duplicar"},"cloud_backup":{"message":"Copia de seguridad en línea"},"cloud_backup_at":{"message":"Última copia de seguridad del $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"No existe ninguna copia de seguridad"},"cloud_over_limit":{"message":"Número de copias de seguridad excedido"},"code":{"message":"Código"},"code_empty":{"message":"El código no puede estar vacío"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"delete":{"message":"Eliminar"},"delete_confirm":{"message":"¿Quiere eliminar estas reglas?"},"description":{"message":"Manage browser's requests, include modify the request headers and response headers, redirect requests, cancel requests"},"disable":{"message":"Disable"},"display_common_header":{"message":"Display common header"},"download":{"message":"Descarga"},"download_rule":{"message":"Regla de descarga"},"edit":{"message":"Editar"},"enable":{"message":"Habilitar"},"enable_he":{"message":"Enable Header Editor"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Favor de ingresar un nombre de grupo"},"excludeRule":{"message":"Regla de exclusión"},"exec_function":{"message":"Función personalizado"},"exec_normal":{"message":"normal"},"exec_type":{"message":"Execute type"},"export":{"message":"Exportar"},"export_and_import":{"message":"Exportar e importar"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Grupo"},"headerName":{"message":"Header name"},"headerValue":{"message":"Header value"},"header_empty":{"message":"Header name can not be empty"},"help":{"message":"Ayuda"},"import":{"message":"Importar"},"import_drop":{"message":"No importar"},"import_new":{"message":"Añadir"},"import_override":{"message":"Overrides existing"},"import_success":{"message":"Importación exitosa"},"include_header_in_custom_function":{"message":"Include request headers in custom function"},"manage":{"message":"Manerar"},"manage_collapse_group":{"message":"Colapsar grupos por predeterminado"},"matchRule":{"message":"Match rules"},"matchType":{"message":"Match type"},"match_all":{"message":"Todo"},"match_domain":{"message":"Dominio"},"match_prefix":{"message":"URL de prefijo"},"match_regexp":{"message":"Expresión regular"},"match_rule_empty":{"message":"Match rule can not be empty"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Nombre"},"name_empty":{"message":"No puede haber nombres vacíos"},"ok":{"message":"OK"},"options":{"message":"Opciones"},"pack_up_or_unfurl":{"message":"Pack up or unfurl group"},"redirectTo":{"message":"Redireccionar a"},"redirect_empty":{"message":"Redirect target can not be empty"},"rename":{"message":"Renombrar"},"ruleType":{"message":"Tipo de regla"},"rule_cancel":{"message":"Cancelar solicitud"},"rule_list":{"message":"Rules list"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modify response header"},"rule_modifySendHeader":{"message":"Modify request header"},"rule_redirect":{"message":"Redirect request"},"rules_no_effect_for_he":{"message":"Rules take no effect on Header Editor"},"save":{"message":"Guardar"},"save_to":{"message":"Guardar en..."},"saved":{"message":"Guardado"},"select_all":{"message":"Seleccionar/Deseleccionar todo"},"select_or_download":{"message":"De favor, importe desde un archivo local o por reglas de descarga"},"share":{"message":"Compartir"},"suggested_group":{"message":"Grupo sugerido"},"test_custom_code":{"message":"Does not support testing custom code now"},"test_exclude":{"message":"Matched but excluded"},"test_invalid_regexp":{"message":"La expresión regular es inválida"},"test_mismatch":{"message":"Mismatch"},"test_url":{"message":"Prueba (no se guardará)"},"third_party_rules":{"message":"Third party rules"},"ungrouped":{"message":"Desagrupado "},"upload":{"message":"Subir"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/en/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en/third-party-rules.html"},"view":{"message":"Ver"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"}}
--------------------------------------------------------------------------------
/public/_locales/pl/messages.json:
--------------------------------------------------------------------------------
1 | {"action":{"message":"Akcja"},"add":{"message":"Dodaj"},"add_anti_hot_link":{"message":"Dodaj regułę anti-anti-hotlinking"},"add_anti_hot_link_to_menu":{"message":"Dodaj \"Add anti-anti-hotlinking\" do menu kontekstowego"},"auto":{"message":"Auto"},"batch_delete":{"message":"Usuń kilka"},"batch_mode":{"message":"Wybierz"},"cancel":{"message":"Anuluj"},"choose":{"message":"Wybierz"},"clone":{"message":"Sklonuj"},"cloud_backup":{"message":"Kopia zapasowa w chmurze"},"cloud_backup_at":{"message":"Ostatnia kopia w $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"Brak kopii zapasowej"},"cloud_over_limit":{"message":"Przekroczono limit rozmiaru kopii zapasowej"},"code":{"message":"Kod"},"code_empty":{"message":"Kod nie może być pusty"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"delete":{"message":"Usuń"},"delete_confirm":{"message":"Czy chcesz usunąć te reguły?"},"description":{"message":"Zarządzaj żądaniami przeglądarki, modyfikuj nagłówki żądania i odpowiedzi, przekierowuj lub anuluj żądania"},"disable":{"message":"Disable"},"display_common_header":{"message":"Display common header"},"download":{"message":"Pobierz"},"download_rule":{"message":"Pobierz regułę"},"edit":{"message":"Edytuj"},"enable":{"message":"Włącz"},"enable_he":{"message":"Włącz Header Editor"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Wprowadź nazwę grupy"},"excludeRule":{"message":"Wyklucz regułę"},"exec_function":{"message":"Funkcja niestandardowa"},"exec_normal":{"message":"Normalne"},"exec_type":{"message":"Typ wykonania"},"export":{"message":"Eksportuj"},"export_and_import":{"message":"Eksport i Import"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Grupa"},"headerName":{"message":"Nazwa nagłówka"},"headerValue":{"message":"Wartość nagłówka"},"header_empty":{"message":"Nazwa nagłówka nie może być pusta"},"help":{"message":"Pomoc"},"import":{"message":"Importuj"},"import_drop":{"message":"Nie importuj"},"import_new":{"message":"Dodaj"},"import_override":{"message":"Zastąp istniejącą"},"import_success":{"message":"Importowano pomyślnie"},"include_header_in_custom_function":{"message":"Include request headers in custom function"},"manage":{"message":"Zarządzaj"},"manage_collapse_group":{"message":"Domyślnie zwiń grupy"},"matchRule":{"message":"Dopasuj reguły"},"matchType":{"message":"Typ dopasowania"},"match_all":{"message":"Wszystkie"},"match_domain":{"message":"Domena"},"match_prefix":{"message":"Prefiks URL"},"match_regexp":{"message":"Wyrażenie regularne"},"match_rule_empty":{"message":"Reguła dopasowania nie może być pusta"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Nazwa"},"name_empty":{"message":"Nazwa nie może być pusta"},"ok":{"message":"OK"},"options":{"message":"Opcje"},"pack_up_or_unfurl":{"message":"Zwiń lub rozwiń grupę"},"redirectTo":{"message":"Przekieruj do"},"redirect_empty":{"message":"Cel przekierowania nie może być pusty"},"rename":{"message":"Zmień nazwę"},"ruleType":{"message":"Typ reguły"},"rule_cancel":{"message":"Anuluj żądanie"},"rule_list":{"message":"Lista reguł"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modyfikuj nagłówek odpowiedzi"},"rule_modifySendHeader":{"message":"Modyfikuj nagłówek żądania"},"rule_redirect":{"message":"Przekieruj żądanie"},"rules_no_effect_for_he":{"message":"Reguły nie mają wpływu na Header Editor"},"save":{"message":"Zapisz"},"save_to":{"message":"Zapisz do"},"saved":{"message":"Zapisano"},"select_all":{"message":"Zaznacz/Odznacz wszystko"},"select_or_download":{"message":"Importuj z lokalnego pliku lub pobierz reguły"},"share":{"message":"Udostępnij"},"suggested_group":{"message":"Sugerowana grupa"},"test_custom_code":{"message":"Nie obsługuje jeszcze testowania niestandardowego kodu"},"test_exclude":{"message":"Dopasowane, ale wykluczone"},"test_invalid_regexp":{"message":"Wyrażenie regularne jest nieprawidłowe"},"test_mismatch":{"message":"Brak dopasowania"},"test_url":{"message":"Test (Nie będzie zapisany)"},"third_party_rules":{"message":"Reguły zewnętrzne"},"ungrouped":{"message":"Bez grupy"},"upload":{"message":"Prześlij"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/en/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en/third-party-rules.html"},"view":{"message":"Zobacz"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"}}
--------------------------------------------------------------------------------
/public/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {"action":{"message":"Ação"},"add":{"message":"Adicionar"},"auto":{"message":"Auto"},"batch_delete":{"message":"eliminação do lote"},"batch_mode":{"message":"Operação por lotes"},"cancel":{"message":"Cancelar"},"choose":{"message":"Escolher"},"clone":{"message":"Clonar"},"cloud_backup":{"message":"Backup em nuvem"},"cloud_backup_at":{"message":"Último backup em $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"Sem backup "},"cloud_over_limit":{"message":"Limite do tamanho de backup excedido"},"code":{"message":"Código"},"code_empty":{"message":"O código não pode estar vazio"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"delete":{"message":"Apagar"},"delete_confirm":{"message":"Deseja excluir estas regras?"},"description":{"message":"Gerencie pedidos do navegador, incluindo modificar cabeçalhos das solicitações e resposta, solicitações de redirecionamento, cancelar solicitações"},"disable":{"message":"Disable"},"display_common_header":{"message":"Exibir cabeçalho comum"},"download":{"message":"Download"},"download_rule":{"message":"Regra de download"},"edit":{"message":"Editar"},"enable":{"message":"Habilitar"},"enable_he":{"message":"Ativar editor de cabeçalho"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Digite um nome de grupo"},"excludeRule":{"message":"Excluir regra"},"exec_function":{"message":"Função personalizada"},"exec_normal":{"message":"normal"},"exec_type":{"message":"Execute o tipo"},"export":{"message":"Exportar"},"export_and_import":{"message":"Exportar e Importar"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Grupo"},"headerName":{"message":"Nome do cabeçalho"},"headerValue":{"message":"Valor do cabeçalho"},"header_empty":{"message":"Nome do cabeçalho não pode estar vazio"},"help":{"message":"Ajuda"},"import":{"message":"Importar"},"import_drop":{"message":"Não importar"},"import_new":{"message":"Adicionar"},"import_override":{"message":"Substituições existentes"},"import_success":{"message":"Importado com sucesso"},"include_header_in_custom_function":{"message":"Incluir cabeçalhos na função personalizada"},"manage":{"message":"Gerir"},"manage_collapse_group":{"message":"Reduzir grupos por padrão"},"matchRule":{"message":"Regras correspondentes"},"matchType":{"message":"Tipo de combinação"},"match_all":{"message":"Todos"},"match_domain":{"message":"Domínio"},"match_prefix":{"message":"Prefixo da URL"},"match_regexp":{"message":"Expressão regular"},"match_rule_empty":{"message":"A regra não pode estar vazia"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Nome"},"name_empty":{"message":"Nome não pode estar vazio"},"ok":{"message":"OK"},"options":{"message":"Opções"},"pack_up_or_unfurl":{"message":"Pacote acima ou desenrolar grupo"},"redirectTo":{"message":"Redirecionar para"},"redirect_empty":{"message":"O alvo de redirecionamento não pode estar vazio"},"rename":{"message":"Renomear"},"ruleType":{"message":"Tipo de regra"},"rule_cancel":{"message":"Cancelar pedido"},"rule_list":{"message":"Lista de regras"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modificar o cabeçalho da resposta"},"rule_modifySendHeader":{"message":"Modificar o cabeçalho solicitado"},"rule_redirect":{"message":"Pedido de redirecionamento"},"rules_no_effect_for_he":{"message":"Regras não têm efeito no Header Editor"},"save":{"message":"Salvar"},"save_to":{"message":"Salvar para"},"saved":{"message":"Salvo"},"select_all":{"message":"Selecionar/Desmarcar tudo"},"select_or_download":{"message":"Importe de um arquivo local ou regras de download"},"share":{"message":"Compartilhar"},"suggested_group":{"message":"Grupo sugerido"},"test_custom_code":{"message":"Não suporta testar código personalizado agora"},"test_exclude":{"message":"Combinado, mas excluído"},"test_invalid_regexp":{"message":"A expressão regular é inválida"},"test_mismatch":{"message":"Incompatibilidade"},"test_url":{"message":"Teste (Não será salvo)"},"third_party_rules":{"message":"Regras de terceiros"},"ungrouped":{"message":"Desagrupado"},"upload":{"message":"Upload"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/en/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en/third-party-rules.html"},"view":{"message":"Visualizar"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"}}
--------------------------------------------------------------------------------
/public/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {"action":{"message":"操作"},"add":{"message":"添加"},"auto":{"message":"自动"},"batch_delete":{"message":"批量删除"},"batch_mode":{"message":"批量操作"},"cancel":{"message":"取消"},"choose":{"message":"选择"},"clone":{"message":"克隆"},"cloud_backup":{"message":"云备份"},"cloud_backup_at":{"message":"最后备份于 $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"无备份"},"cloud_over_limit":{"message":"超出备份大小限制"},"code":{"message":"代码"},"code_empty":{"message":"代码不能为空"},"common_mark":{"message":"设为常用"},"common_mark_tip":{"message":"将任意规则或分组设为常用后,会在此处显示"},"common_unmark":{"message":"取消常用"},"dark_mode":{"message":"暗黑模式"},"dark_mode_help":{"message":"暗黑模式为试验性特性,刷新页面查看效果"},"debug_mode_enable":{"message":"开启调试模式"},"debug_mode_help":{"message":"当调试模式开启时,一些日志将会打印在背景页"},"delete":{"message":"删除"},"delete_confirm":{"message":"您确认要删除这些规则吗?"},"description":{"message":"管理浏览器请求,包括修改请求头和响应头、重定向请求、取消请求"},"disable":{"message":"禁用"},"display_common_header":{"message":"编辑时显示常用头"},"download":{"message":"下载"},"download_rule":{"message":"下载规则"},"edit":{"message":"编辑"},"enable":{"message":"启用"},"enable_he":{"message":"启用Header Editor"},"encoding":{"message":"编码"},"enter_group_name":{"message":"请输入组名称"},"excludeRule":{"message":"排除规则"},"exec_function":{"message":"自定义函数"},"exec_normal":{"message":"常规"},"exec_type":{"message":"执行类型"},"export":{"message":"导出"},"export_and_import":{"message":"导出和导入"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"分组"},"headerName":{"message":"头名称"},"headerValue":{"message":"头内容"},"header_empty":{"message":"头名称不能为空"},"help":{"message":"帮助"},"import":{"message":"导入"},"import_drop":{"message":"不导入"},"import_new":{"message":"添加"},"import_override":{"message":"覆盖已有"},"import_success":{"message":"导入成功"},"include_header_in_custom_function":{"message":"在自定义函数中包含请求头"},"manage":{"message":"管理"},"manage_collapse_group":{"message":"默认折叠分组"},"matchRule":{"message":"匹配规则"},"matchType":{"message":"匹配类型"},"match_all":{"message":"全部"},"match_domain":{"message":"域名"},"match_prefix":{"message":"网址前缀"},"match_regexp":{"message":"正则表达式"},"match_rule_empty":{"message":"匹配规则不能为空"},"match_url":{"message":"网址"},"modify_body":{"message":"修改响应体(仅支持Firefox)"},"name":{"message":"名称"},"name_empty":{"message":"名称不能为空"},"ok":{"message":"确定"},"options":{"message":"选项"},"pack_up_or_unfurl":{"message":"收起/展开分组"},"redirectTo":{"message":"重定向至"},"redirect_empty":{"message":"重定向目标不能为空"},"rename":{"message":"重命名"},"ruleType":{"message":"规则类型"},"rule_cancel":{"message":"阻止请求"},"rule_list":{"message":"规则列表"},"rule_modifyReceiveBody":{"message":"修改响应体"},"rule_modifyReceiveHeader":{"message":"修改响应头"},"rule_modifySendHeader":{"message":"修改请求头"},"rule_redirect":{"message":"重定向请求"},"rules_no_effect_for_he":{"message":"规则对Header Editor无效"},"save":{"message":"保存"},"save_to":{"message":"保存至"},"saved":{"message":"已保存"},"select_all":{"message":"全选/全不选"},"select_or_download":{"message":"请从文件导入或下载规则"},"share":{"message":"分享"},"suggested_group":{"message":"建议分组"},"test_custom_code":{"message":"暂不支持自定义函数"},"test_exclude":{"message":"已匹配但被排除"},"test_invalid_regexp":{"message":"正则表达式无效"},"test_mismatch":{"message":"不匹配"},"test_url":{"message":"测试(不会保存)"},"third_party_rules":{"message":"第三方规则"},"ungrouped":{"message":"未分组"},"upload":{"message":"上传"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/guide/cloud-backup/"},"url_help":{"message":"https://he.firefoxcn.net/"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/guide/third-party-rules/"},"view":{"message":"查看"}}
--------------------------------------------------------------------------------
/public/_locales/zh_TW/messages.json:
--------------------------------------------------------------------------------
1 | {"action":{"message":"動作"},"add":{"message":"新增"},"auto":{"message":"自動"},"batch_delete":{"message":"批次刪除"},"batch_mode":{"message":"批次作業"},"cancel":{"message":"取消"},"choose":{"message":"選擇"},"clone":{"message":"複製"},"cloud_backup":{"message":"雲端備份"},"cloud_backup_at":{"message":"最後備份於 $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"無備份"},"cloud_over_limit":{"message":"超出備份大小限制"},"code":{"message":"代碼"},"code_empty":{"message":"代碼不能為空"},"common_mark":{"message":"設為常用"},"common_mark_tip":{"message":"將任意規則或分組設為常用後,會在此處顯示"},"common_unmark":{"message":"取消常用"},"dark_mode":{"message":"深色模式"},"dark_mode_help":{"message":"深色模式為實驗性功能,重新整理頁面以確認效果"},"debug_mode_enable":{"message":"啟用偵錯模式"},"debug_mode_help":{"message":"當偵錯模式啟用時,將於背景頁面輸出執行記錄"},"delete":{"message":"刪除"},"delete_confirm":{"message":"您確定要刪除這些規則嗎?"},"description":{"message":"管理瀏覽器要求,包括修改要求標頭和回應標頭、重新導向要求、取消要求"},"disable":{"message":"停用"},"display_common_header":{"message":"編輯時顯示常用標頭"},"download":{"message":"下載"},"download_rule":{"message":"下載規則"},"edit":{"message":"編輯"},"enable":{"message":"啟用"},"enable_he":{"message":"啟用 Header Editor"},"encoding":{"message":"編碼"},"enter_group_name":{"message":"請輸入分組名稱"},"excludeRule":{"message":"排除規則"},"exec_function":{"message":"自訂函數"},"exec_normal":{"message":"一般"},"exec_type":{"message":"執行類型"},"export":{"message":"匯出"},"export_and_import":{"message":"匯出與匯入"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"分組"},"headerName":{"message":"標頭名稱"},"headerValue":{"message":"標頭內容"},"header_empty":{"message":"標頭名稱不能為空"},"help":{"message":"說明"},"import":{"message":"匯入"},"import_drop":{"message":"不要匯入"},"import_new":{"message":"新增"},"import_override":{"message":"覆寫現有"},"import_success":{"message":"匯入成功"},"include_header_in_custom_function":{"message":"在自訂函數中包含要求標頭"},"manage":{"message":"管理"},"manage_collapse_group":{"message":"預設摺疊分組"},"matchRule":{"message":"比對規則"},"matchType":{"message":"比對類型"},"match_all":{"message":"全部"},"match_domain":{"message":"域名"},"match_prefix":{"message":"網址首碼"},"match_regexp":{"message":"規則運算式"},"match_rule_empty":{"message":"比對規則不能為空"},"match_url":{"message":"網址"},"modify_body":{"message":"修改回應本文(僅支援 Firefox)"},"name":{"message":"名稱"},"name_empty":{"message":"名稱不能為空"},"ok":{"message":"確定"},"options":{"message":"選項"},"pack_up_or_unfurl":{"message":"收起/展開分組"},"redirectTo":{"message":"重新導向至"},"redirect_empty":{"message":"重新導向目標不能為空"},"rename":{"message":"重新命名"},"ruleType":{"message":"規則類型"},"rule_cancel":{"message":"取消要求"},"rule_list":{"message":"規則清單"},"rule_modifyReceiveBody":{"message":"修改回應本文"},"rule_modifyReceiveHeader":{"message":"修改回應標頭"},"rule_modifySendHeader":{"message":"修改要求標頭"},"rule_redirect":{"message":"重新導向要求"},"rules_no_effect_for_he":{"message":"規則對 Header Editor 無效"},"save":{"message":"儲存"},"save_to":{"message":"儲存至"},"saved":{"message":"已儲存"},"select_all":{"message":"全選/取消全選"},"select_or_download":{"message":"請從本機檔案匯入或下載規則"},"share":{"message":"分享"},"suggested_group":{"message":"建議分組"},"test_custom_code":{"message":"暫不支援自訂函數"},"test_exclude":{"message":"已符合但被排除"},"test_invalid_regexp":{"message":"正規表示式無效"},"test_mismatch":{"message":"不相符"},"test_url":{"message":"測試(不會被儲存)"},"third_party_rules":{"message":"第三方規則"},"ungrouped":{"message":"未分組"},"upload":{"message":"上傳"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/zh-TW/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/zh-TW/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/zh-TW/third-party-rules.html"},"view":{"message":"檢視"}}
--------------------------------------------------------------------------------
/public/assets/images/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/public/assets/images/128.png
--------------------------------------------------------------------------------
/public/assets/images/128w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/public/assets/images/128w.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Header Editor
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/scripts/config.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs';
2 | import { join, dirname } from 'path';
3 | import { fileURLToPath } from 'url';
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | function readJSONSync(fullPath) {
9 | return JSON.parse(readFileSync(fullPath, {
10 | encoding: 'utf8'
11 | }));
12 | }
13 |
14 | const root = join(__dirname, '..');
15 | const dist = join(root, 'dist');
16 |
17 | const extension = readJSONSync(join(root, 'extension.json'));
18 | const manifest = readJSONSync(join(dist, 'manifest.json'));
19 |
20 | const pack = join(root, 'temp/dist-pack');
21 | const release = join(root, 'temp/release');
22 |
23 | export const version = manifest.version;
24 | export const resolve = join;
25 | export const path = { root, dist, pack, release };
26 | export { extension };
27 |
--------------------------------------------------------------------------------
/scripts/get-snapshot-version.mjs:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch';
2 | import { readFile, mkdir, writeFile } from 'fs/promises';
3 | import { join, dirname } from 'path';
4 | import { fileURLToPath } from 'url';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | async function main() {
10 | const token = process.env.TOKEN;
11 |
12 | if (!token) {
13 | return;
14 | }
15 |
16 | // Get latest release version
17 | const gitHubToken = process.env.GITHUB_TOKEN;
18 | const gitHubBaseURL = process.env.GITHUB_API_URL + '/repos/' + process.env.GITHUB_REPOSITORY;
19 | const latestReleaseResp = await fetch(gitHubBaseURL + '/releases/latest', {
20 | headers: {
21 | 'Accept': 'application/vnd.github+json',
22 | 'Authorization': 'Bearer ' + gitHubToken,
23 | 'X-GitHub-Api-Version': '2022-11-28',
24 | }
25 | });
26 | const latestRelease = await latestReleaseResp.json();
27 | const versionPrefix = latestRelease.tag_name.replace(/^v/, '');
28 |
29 | // Get remote version
30 | const params = new URLSearchParams();
31 | params.append('name', 'header-editor');
32 | params.append('ver', versionPrefix);
33 | params.append('token', token);
34 |
35 | const resp = await fetch('https://ext.firefoxcn.net/api/snapshot.php?' + params.toString());
36 | const text = await resp.text();
37 |
38 | const filePath = join(__dirname, '../temp/version.txt');
39 | if (/^(\d+)$/.test(text)) {
40 | await mkdir(join(__dirname, '../temp/'), {
41 | recursive: true,
42 | });
43 | await writeFile(filePath, versionPrefix + '.' + text, {
44 | encoding: 'utf8',
45 | });
46 | }
47 |
48 | console.log('Got version: ' + text + ', wrote to: ' + filePath);
49 | }
50 |
51 | main();
--------------------------------------------------------------------------------
/scripts/pack-utils/amo.mjs:
--------------------------------------------------------------------------------
1 | import { version as _version, extension } from '../config.mjs';
2 | import { signAddon } from 'sign-addon';
3 |
4 | export default function (zipPath) {
5 | if (!process.env.AMO_KEY) {
6 | return Promise.reject(new Error('AMO_KEY not found'));
7 | }
8 | if (!process.env.AMO_SECRET) {
9 | return Promise.reject(new Error('AMO_SECRET not found'));
10 | }
11 |
12 | return signAddon({
13 | xpiPath: zipPath,
14 | version: _version,
15 | apiKey: process.env.AMO_KEY,
16 | apiSecret: process.env.AMO_SECRET,
17 | id: extension.firefox.amo,
18 | disableProgressBar: true,
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/scripts/pack-utils/crx.mjs:
--------------------------------------------------------------------------------
1 | import ChromeExtension from 'crx';
2 | import { readFile, writeFile } from 'fs/promises';
3 | import { resolve, extension, version } from '../config.mjs';
4 |
5 | async function createCrx(fileContent) {
6 | const keyContent = process.env.CRX_PRIV_KEY;
7 | if (!keyContent) {
8 | throw new Error('CRX_PRIV_KEY not found');
9 | }
10 | const crx = new ChromeExtension({
11 | codebase: 'http://localhost:8000/myExtension.crx',
12 | privateKey: keyContent,
13 | });
14 |
15 | crx.loaded = true;
16 |
17 | const crxBuffer = await crx.pack(fileContent);
18 |
19 | return crxBuffer;
20 | }
21 |
22 | async function packCrx(zipPath, outputDir) {
23 | const fileContent = await readFile(zipPath);
24 | const content = await createCrx(fileContent);
25 | const out = resolve(outputDir, `${extension.dist.replace('{VER}', version)}.crx`);
26 | await writeFile(out, content);
27 | const idFile = resolve(outputDir, `${extension.dist.replace('{VER}', version)}.crx-id.txt`);
28 | await writeFile(idFile, extension.chrome.crx);
29 | return out;
30 | }
31 |
32 | export default packCrx;
33 |
--------------------------------------------------------------------------------
/scripts/pack-utils/cws.mjs:
--------------------------------------------------------------------------------
1 | import { createReadStream } from 'fs';
2 | import fetch from 'node-fetch';
3 | import { extension } from '../config.mjs';
4 |
5 | const webStoreId = process.env.CWS_CLIENT_ID;
6 | const webStoreToken = process.env.CWS_TOKEN;
7 | const webStoreSecret = process.env.CWS_CLIENT_SECRET;
8 |
9 | let _webStoreToken = null;
10 | async function getToken() {
11 | if (_webStoreToken) {
12 | return _webStoreToken;
13 | }
14 | const resp = await fetch('https://www.googleapis.com/oauth2/v4/token', {
15 | method: 'POST',
16 | headers: {
17 | 'Content-Type': 'application/x-www-form-urlencoded',
18 | },
19 | body: new URLSearchParams({
20 | client_id: webStoreId,
21 | client_secret: webStoreSecret,
22 | refresh_token: webStoreToken,
23 | grant_type: 'refresh_token',
24 | }).toString(),
25 | });
26 | const res = await resp.json();
27 | if (res.access_token) {
28 | _webStoreToken = res.access_token;
29 | return _webStoreToken;
30 | } else {
31 | throw new Error(res.error);
32 | }
33 | }
34 |
35 | async function upload(readStream, token) {
36 | const res = await fetch(`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${extension.chrome.id}`, {
37 | method: 'PUT',
38 | headers: {
39 | Authorization: `Bearer ${token}`,
40 | 'x-goog-api-version': '2',
41 | },
42 | body: readStream,
43 | });
44 |
45 | return res.json();
46 | }
47 |
48 | async function publish(target = 'default', token) {
49 | const url = `https://www.googleapis.com/chromewebstore/v1.1/items/${extension.chrome.id}/publish?publishTarget=${target}`;
50 | const res = await fetch(url, {
51 | method: 'POST',
52 | headers: {
53 | Authorization: `Bearer ${token}`,
54 | 'x-goog-api-version': '2',
55 | },
56 | });
57 |
58 | return res.json();
59 | }
60 |
61 | async function packCws(zipPath) {
62 | if (!process.env.CWS_CLIENT_ID) {
63 | return Promise.reject(new Error('CWS_CLIENT_ID not found'));
64 | }
65 | if (!process.env.CWS_CLIENT_SECRET) {
66 | return Promise.reject(new Error('CWS_CLIENT_SECRET not found'));
67 | }
68 | if (!process.env.CWS_TOKEN) {
69 | return Promise.reject(new Error('CWS_TOKEN not found'));
70 | }
71 |
72 | const distStream = createReadStream(zipPath);
73 | const token = await getToken();
74 | await upload(distStream, token);
75 | return publish('default', token);
76 | }
77 |
78 | export default packCws;
79 |
--------------------------------------------------------------------------------
/scripts/pack-utils/edge.mjs:
--------------------------------------------------------------------------------
1 | import { EdgeWebstoreClient } from '@plasmo-corp/ewu';
2 |
3 | export default function (zipPath) {
4 | if (!process.env.MS_PRODUCT_ID) {
5 | return Promise.reject(new Error('MS_PRODUCT_ID not found'));
6 | }
7 | if (!process.env.MS_CLIENT_ID) {
8 | return Promise.reject(new Error('MS_CLIENT_ID not found'));
9 | }
10 | if (!process.env.MS_CLIENT_SECRET) {
11 | return Promise.reject(new Error('MS_CLIENT_SECRET not found'));
12 | }
13 | if (!process.env.MS_ACCESS_TOKEN_URL) {
14 | return Promise.reject(new Error('MS_ACCESS_TOKEN_URL not found'));
15 | }
16 |
17 | const client = new EdgeWebstoreClient({
18 | productId: process.env.MS_PRODUCT_ID,
19 | clientId: process.env.MS_CLIENT_ID,
20 | clientSecret: process.env.MS_CLIENT_SECRET,
21 | accessTokenUrl: process.env.MS_ACCESS_TOKEN_URL,
22 | });
23 |
24 | return client.submit({
25 | filePath: zipPath,
26 | notes: "release"
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/scripts/pack-utils/index.mjs:
--------------------------------------------------------------------------------
1 | import amo from './amo.mjs';
2 | import cws from './cws.mjs';
3 | import xpi from './xpi.mjs';
4 | import edge from './edge.mjs';
5 | import crx from './crx.mjs';
6 |
7 | // const packUtils = { amo, cws, xpi, edge, crx };
8 | const packUtils = { xpi, crx };
9 |
10 | export default packUtils;
11 |
--------------------------------------------------------------------------------
/scripts/pack-utils/xpi.mjs:
--------------------------------------------------------------------------------
1 | import { rename, writeFile } from 'fs/promises';
2 | import { version as _version, extension, resolve } from '../config.mjs';
3 | import { signAddon } from 'sign-addon';
4 |
5 | async function packXpi(zipPath, outputDir) {
6 | if (!process.env.AMO_KEY) {
7 | return Promise.reject(new Error('AMO_KEY not found'));
8 | }
9 | if (!process.env.AMO_SECRET) {
10 | return Promise.reject(new Error('AMO_SECRET not found'));
11 | }
12 |
13 | const result = await signAddon({
14 | xpiPath: zipPath,
15 | version: _version,
16 | apiKey: process.env.AMO_KEY,
17 | apiSecret: process.env.AMO_SECRET,
18 | id: extension.firefox.xpi,
19 | downloadDir: outputDir,
20 | disableProgressBar: true,
21 | });
22 | if (!result.success) {
23 | throw new Error('Sign failed');
24 | }
25 | const res = result.downloadedFiles;
26 | if (res.length === 0) {
27 | throw new Error('No signed addon found');
28 | }
29 | console.log(`Downloaded signed addon: ${res.join(', ')}`);
30 | const out = resolve(outputDir, `${extension.dist.replace('{VER}', _version)}.xpi`);
31 | const idFile = resolve(outputDir, `${extension.dist.replace('{VER}', _version)}.xpi-id.txt`);
32 | // Move download file to output dir
33 | await rename(res[0], out);
34 | await writeFile(idFile, extension.firefox.xpi);
35 | return out;
36 | }
37 |
38 | export default packXpi;
39 |
--------------------------------------------------------------------------------
/scripts/pack.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * 进行多渠道打包
3 | *
4 | * dist:原本的输出文件夹
5 | * dist-pack:用于打包的文件夹
6 | * dist-pack/{platform}:各个平台的文件夹
7 | * dist-pack/{platform}.zip:各个平台的打包文件
8 | * dist-pack/release:其他平台打包输出结果
9 | * 在这里,打包文件夹统一命名为pack
10 | */
11 | import { unlink, mkdir } from 'fs/promises';
12 | import { readJSON, outputJSON } from 'fs-extra/esm';
13 | import { join } from 'path';
14 | import { extension, resolve as _resolve, path as _path } from './config.mjs';
15 | import { exec as processExec } from 'child_process';
16 | import packUtils from './pack-utils/index.mjs';
17 |
18 | let platform = null;
19 | for (const it of process.argv) {
20 | if (it.startsWith('--platform=')) {
21 | platform = it.substr(11);
22 | if (platform.indexOf(',') > 0) {
23 | platform = platform.trim().split(',');
24 | }
25 | break;
26 | }
27 | }
28 |
29 | function exec(commands) {
30 | return new Promise((resolve, reject) => {
31 | processExec(commands, (error, stdout, stderr) => {
32 | if (error) {
33 | reject(error);
34 | } else {
35 | resolve(stdout);
36 | }
37 | });
38 | });
39 | }
40 |
41 | async function removeManifestKeys(manifest, name) {
42 | console.log('start convert manifest(' + manifest + ') for ' + name);
43 | const wantKey = `__${name}__`;
44 |
45 | const removeObjKeys = obj => {
46 | Object.keys(obj).forEach((it) => {
47 | if (it.startsWith('__')) {
48 | if (it.startsWith(wantKey)) {
49 | const finalKey = it.substr(wantKey.length);
50 | console.log('copy key ' + finalKey + ' from ' + it);
51 | obj[finalKey] = obj[it];
52 | }
53 | console.log('remove key ' + it);
54 | delete obj[it];
55 | } else if (typeof obj[it] === 'object' && !Array.isArray(obj[it])) {
56 | removeObjKeys(obj[it]);
57 | }
58 | });
59 | }
60 |
61 | try {
62 | const content = await readJSON(manifest);
63 | removeObjKeys(content);
64 | await outputJSON(manifest, content);
65 | } catch (e) {
66 | console.log(e);
67 | }
68 | }
69 |
70 | async function packOnePlatform(name) {
71 | if (typeof packUtils[name] === 'undefined') {
72 | console.error(`pack-utils for ${name} not found`);
73 | return;
74 | }
75 | const thisPack = _resolve(_path.pack, name);
76 | const zipPath = _resolve(_path.pack, `${name}.zip`);
77 | try {
78 | // 复制一份到dist下面
79 | await exec(`cp -r ${_path.dist} ${thisPack}`);
80 | // 移除掉manifest中的非本平台key
81 | await removeManifestKeys(join(thisPack, 'manifest.json'), name);
82 | // 打包成zip
83 | await exec(`cd ${thisPack} && zip -r ${zipPath} ./*`);
84 | // 执行上传等操作
85 | const res = await packUtils[name](zipPath, _path.release);
86 | console.log(`${name}: ${res}`);
87 | await unlink(zipPath);
88 | } catch (e) {
89 | console.error(e);
90 | }
91 | }
92 |
93 | async function main() {
94 | // 检查打包目录是否存在
95 | await exec(`cd ${_path.root} && rm -rf ./temp/dist-pack`);
96 | await exec(`cd ${_path.root} && rm -rf ./temp/release`);
97 | await mkdir(_path.pack, {
98 | recursive: true,
99 | });
100 | await mkdir(_path.release, {
101 | recursive: true,
102 | });
103 |
104 | if (platform) {
105 | if (Array.isArray(platform)) {
106 | platform.forEach((it) => {
107 | if (typeof packUtils[it] !== 'undefined') {
108 | packOnePlatform(it);
109 | } else {
110 | console.log(`${it} not found`);
111 | }
112 | });
113 | return;
114 | }
115 |
116 | if (typeof packUtils[platform] !== 'undefined') {
117 | packOnePlatform(platform);
118 | return;
119 | }
120 | console.log(`${platform} not found`);
121 |
122 | return;
123 | }
124 |
125 | const queue = [];
126 | Object.keys(extension.autobuild).forEach((it) => {
127 | if (extension.autobuild[it]) {
128 | queue.push(packOnePlatform(it));
129 | } else {
130 | console.log(`Skip ${it.toUpperCase()}`);
131 | }
132 | });
133 | }
134 |
135 | main();
136 |
--------------------------------------------------------------------------------
/scripts/release.mjs:
--------------------------------------------------------------------------------
1 | import { readFile, readdir, stat } from 'fs/promises';
2 | import { join } from 'path';
3 | import { createHash } from 'crypto';
4 | import publishRelease from 'publish-release';
5 | import { extension, path as _path, version } from './config.mjs';
6 |
7 | async function hashFile(filePath) {
8 | const buffer = await readFile(filePath);
9 | const fsHash = createHash('sha256');
10 | fsHash.update(buffer);
11 | return fsHash.digest('hex');
12 | }
13 |
14 | function publishReleasePromise(options) {
15 | return new Promise((resolve, reject) => {
16 | publishRelease(options, (err, release) => {
17 | if (err) {
18 | reject(err);
19 | } else {
20 | resolve(release.html_url);
21 | }
22 | });
23 | });
24 | }
25 |
26 | async function publishUpdate(params) {
27 | const token = process.env.SERVER_TOKEN;
28 | if (!token) {
29 | return;
30 | }
31 | const query = new URLSearchParams(params);
32 | query.append('name', 'header-editor');
33 | query.append('token', token);
34 |
35 | const resp = await fetch('https://ext.firefoxcn.net/api/update.php?' + query.toString());
36 | return await resp.text();
37 | }
38 |
39 | async function main() {
40 | if (!extension.github.enable) {
41 | return;
42 | }
43 | const repo = process.env.GITHUB_REPOSITORY;
44 | if (!repo) {
45 | console.log('GITHUB_REPOSITORY not found');
46 | return;
47 | }
48 | if (!process.env.GITHUB_TOKEN) {
49 | console.log('GITHUB_TOKEN not found');
50 | return;
51 | }
52 |
53 | const assets = [];
54 |
55 | const dirContent = await readdir(_path.release);
56 | for (const file of dirContent) {
57 | if (!file.endsWith('.xpi') && !file.endsWith('.crx')) {
58 | continue;
59 | }
60 | const fullPath = join(_path.release, file);
61 | const idFilePath = join(_path.release, file + '-id.txt');
62 | const statResult = await stat(fullPath);
63 | if (statResult.isFile()) {
64 | const fileHash = await hashFile(fullPath);
65 | const id = await readFile(idFilePath, {
66 | encoding: 'utf8',
67 | });
68 | assets.push({
69 | id,
70 | name: file,
71 | path: fullPath,
72 | hash: fileHash,
73 | });
74 | }
75 | }
76 |
77 | // Get git names
78 | const gitName = repo.split('/');
79 | const tagName = process.env.GITHUB_REF_NAME;
80 | await publishReleasePromise({
81 | token: process.env.GITHUB_TOKEN,
82 | owner: gitName[0],
83 | repo: gitName[1],
84 | tag: tagName,
85 | name: version,
86 | notes: assets.map(item => `> ${item.name} SHA256: ${item.hash} \n`).join('\n'),
87 | draft: false,
88 | prerelease: false,
89 | reuseRelease: false,
90 | reuseDraftOnly: false,
91 | skipAssetsCheck: false,
92 | skipDuplicatedAssets: false,
93 | skipIfPublished: true,
94 | editRelease: false,
95 | deleteEmptyTag: false,
96 | assets: assets.map(item => item.path),
97 | });
98 |
99 | // update "update info" file
100 | for (const it of assets) {
101 | const url = `https://github.com/${repo}/releases/download/${tagName}/${it.name}`;
102 | const browser = it.name.endsWith('.xpi') ? 'gecko' : 'chrome';
103 | const result = await publishUpdate({
104 | id: it.id,
105 | ver: version,
106 | url,
107 | browser,
108 | hash: it.hash,
109 | });
110 | console.log('Publish update info ', it.name, result);
111 | }
112 | }
113 |
114 | main();
--------------------------------------------------------------------------------
/scripts/webpack/dev.js:
--------------------------------------------------------------------------------
1 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
2 |
3 | module.exports = function (config, context) {
4 | // 调试模式下,开启自动重载和自动编译
5 | if (config.get('mode') === 'development') {
6 | // config.plugin('reload').use(ChromeExtensionReloader);
7 | config.devServer.hot(false);
8 | config.devServer.open(false);
9 | const devMiddleware = config.devServer.store.get('devMiddleware');
10 | config.devServer.store.set('devMiddleware', {
11 | ...devMiddleware,
12 | writeToDisk: true,
13 | });
14 | }
15 |
16 | config.plugin('bundle-analyzer').use(new BundleAnalyzerPlugin({
17 | analyzerMode: 'static',
18 | reportFilename: '../temp/bundle-analyze.html',
19 | }))
20 |
21 | return config;
22 | };
23 |
--------------------------------------------------------------------------------
/scripts/webpack/externals.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 |
5 | const copy = [
6 | {
7 | from: './node_modules/react/umd/react.production.min.js',
8 | to: 'external/react.min.js',
9 | },
10 | {
11 | from: './node_modules/react-dom/umd/react-dom.production.min.js',
12 | to: 'external/react-dom.min.js',
13 | },
14 | ];
15 |
16 | const root = path.join(__dirname, '../..');
17 |
18 | module.exports = function (config) {
19 | const { version } = require(path.join(root, 'package.json'));
20 |
21 | // 添加 snapshot 版本号
22 | let versionText = version;
23 | const forceVersionFile = path.join(__dirname, '../../temp/version.txt');
24 | if (fs.existsSync(forceVersionFile)) {
25 | versionText = fs.readFileSync(forceVersionFile, { encoding: 'utf8' }).trim();
26 | console.log('Got force version: ' + versionText);
27 | } else {
28 | console.log('No force version ' + forceVersionFile);
29 | }
30 | // 如果是tag触发的CI,强制用tag的版本号
31 | if (process.env.GITHUB_REF_TYPE && process.env.GITHUB_REF_TYPE === 'tag') {
32 | const tagName = process.env.GITHUB_REF_NAME;
33 | if (/^[0-9]\.[0-9]+\.[0-9]+$/.test(tagName)) {
34 | versionText = tagName;
35 | }
36 | }
37 |
38 | // dev 环境复制 development 的 react 资源
39 | if (config.get('mode') === 'development') {
40 | copy.forEach((x) => {
41 | if (x.from.includes('.production.min.js')) {
42 | x.from = x.from.replace('.production.min.js', '.development.js');
43 | }
44 | });
45 | }
46 |
47 | // 复制 manifest
48 | copy.push({
49 | from: './src/manifest.json',
50 | to: 'manifest.json',
51 | transform: (content) => {
52 | const jsonContent = JSON.parse(content);
53 | jsonContent.version = versionText;
54 |
55 | if (config.mode === 'development') {
56 | jsonContent['content_security_policy'] = "script-src 'self' 'unsafe-eval'; object-src 'self'";
57 | }
58 |
59 | return JSON.stringify(jsonContent);
60 | },
61 | });
62 |
63 | // 复制其他静态文件
64 | config.plugin('copy').use(new CopyWebpackPlugin({
65 | patterns: copy,
66 | }));
67 |
68 | // Add manaco into a standalone chunk
69 | config.optimization.splitChunks({
70 | chunks: 'all',
71 | minChunks: 100,
72 | cacheGroups: {
73 | default: false,
74 | codemirror: {
75 | name: 'codemirror',
76 | test: /codemirror/,
77 | enforce: true,
78 | },
79 | semi: {
80 | name: 'semi',
81 | test: /@douyinfe[/+]semi-/,
82 | enforce: true,
83 | },
84 | },
85 | });
86 | };
87 |
--------------------------------------------------------------------------------
/scripts/webpack/remove-html.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config, context) {
2 | const plugins = config.plugins.values();
3 |
4 | for (const item of plugins) {
5 | if (item.name.indexOf('HtmlWebpackPlugin_') !== 0) {
6 | continue;
7 | }
8 | const pageName = item.name.substr(18);
9 | if (pageName === 'background' || pageName.indexOf('inject-') === 0 || pageName.indexOf('worker-') === 0) {
10 | config.plugins.delete(item.name);
11 | console.log('Remove html entry: ' + item.name);
12 | }
13 | }
14 |
15 | return config;
16 | };
17 |
--------------------------------------------------------------------------------
/scripts/webpack/webpack.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ context, registerCliOption, onGetWebpackConfig }) => {
2 | onGetWebpackConfig(config => {
3 | require('./externals')(config, context);
4 | require('./dev')(config, context);
5 | require('./remove-html')(config, context);
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/scripts/www/CNAME:
--------------------------------------------------------------------------------
1 | he.firefoxcn.net
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | IS_BACKGROUND?: boolean;
3 | }
4 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_extName__",
3 | "short_name": "__MSG_extName__",
4 | "version": null,
5 | "description": "__MSG_description__",
6 | "homepage_url": "https://he.firefoxcn.net",
7 | "manifest_version": 2,
8 | "icons": {
9 | "128": "assets/images/128.png"
10 | },
11 | "permissions": [
12 | "tabs",
13 | "webRequest",
14 | "webRequestBlocking",
15 | "storage",
16 | "*://*/*",
17 | "unlimitedStorage"
18 | ],
19 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';",
20 | "background": {
21 | "scripts": [
22 | "assets/js/background.js"
23 | ]
24 | },
25 | "browser_action": {
26 | "default_icon": {
27 | "128": "assets/images/128.png"
28 | },
29 | "default_title": "__MSG_extButtonTitle__",
30 | "default_popup": "popup.html"
31 | },
32 | "default_locale": "en",
33 | "options_ui": {
34 | "page": "options.html",
35 | "open_in_tab": true
36 | },
37 | "__amo__browser_specific_settings": {
38 | "gecko": {
39 | "id": "headereditor-amo@addon.firefoxcn.net",
40 | "strict_min_version": "77.0"
41 | }
42 | },
43 | "__xpi__browser_specific_settings": {
44 | "gecko": {
45 | "id": "headereditor@addon.firefoxcn.net",
46 | "strict_min_version": "77.0",
47 | "update_url": "https://ext.firefoxcn.net/header-editor/install/update.json"
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/pages/background/api-handler.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import logger from '@/share/core/logger';
3 | import { APIs, TABLE_NAMES_ARR } from '@/share/core/constant';
4 | import { prefs } from '@/share/core/prefs';
5 | import rules from './core/rules';
6 | import { openURL } from './utils';
7 | import { getDatabase } from './core/db';
8 |
9 | function execute(request: any) {
10 | if (request.method === 'notifyBackground') {
11 | request.method = request.reason;
12 | delete request.reason;
13 | }
14 | switch (request.method) {
15 | case APIs.HEALTH_CHECK:
16 | return new Promise((resolve) => {
17 | getDatabase()
18 | .then(() => resolve(true))
19 | .catch(() => resolve(false));
20 | });
21 | case APIs.OPEN_URL:
22 | return openURL(request);
23 | case APIs.GET_RULES:
24 | return Promise.resolve(rules.get(request.type, request.options));
25 | case APIs.SAVE_RULE:
26 | return rules.save(request.rule);
27 | case APIs.DELETE_RULE:
28 | return rules.remove(request.type, request.id);
29 | case APIs.SET_PREFS:
30 | return prefs.set(request.key, request.value);
31 | case APIs.UPDATE_CACHE:
32 | if (request.type === 'all') {
33 | return Promise.all(TABLE_NAMES_ARR.map((tableName) => rules.updateCache(tableName)));
34 | } else {
35 | return rules.updateCache(request.type);
36 | }
37 | default:
38 | break;
39 | }
40 | // return false;
41 | }
42 |
43 | export default function createApiHandler() {
44 | browser.runtime.onMessage.addListener((request) => {
45 | logger.debug('Background Receive Message', request);
46 | if (request.method === 'batchExecute') {
47 | const queue = request.batch.map((item) => {
48 | const res = execute(item);
49 | if (res) {
50 | return res;
51 | }
52 | return Promise.resolve();
53 | });
54 | return Promise.allSettled(queue);
55 | }
56 | return execute(request);
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/src/pages/background/core/db.ts:
--------------------------------------------------------------------------------
1 | import { TABLE_NAMES_ARR } from '@/share/core/constant';
2 | import { upgradeRuleFormat } from '@/share/core/rule-utils';
3 | import { getGlobal } from '@/share/core/utils';
4 |
5 | export function getDatabase(): Promise {
6 | return new Promise((resolve, reject) => {
7 | const dbOpenRequest = getGlobal().indexedDB.open('headereditor', 4);
8 | dbOpenRequest.onsuccess = (e) => {
9 | // @ts-ignore
10 | resolve(e.target.result);
11 | };
12 | dbOpenRequest.onerror = (e) => {
13 | console.error(e);
14 | reject(e);
15 | };
16 | dbOpenRequest.onupgradeneeded = (event) => {
17 | if (event.oldVersion === 0) {
18 | // Installed
19 | TABLE_NAMES_ARR.forEach((t) => {
20 | // @ts-ignore
21 | event.target.result.createObjectStore(t, { keyPath: 'id', autoIncrement: true });
22 | });
23 | } else {
24 | TABLE_NAMES_ARR.forEach((k) => {
25 | // @ts-ignore
26 | const tx = event.target.transaction;
27 | if (!tx.objectStoreNames.contains(k)) {
28 | // @ts-ignore
29 | event.target.result.createObjectStore(k, { keyPath: 'id', autoIncrement: true });
30 | return;
31 | }
32 | const os = tx.objectStore(k);
33 | os.openCursor().onsuccess = (e: any) => {
34 | const cursor = e.target.result;
35 | if (cursor) {
36 | const s = cursor.value;
37 | s.id = cursor.key;
38 | // upgrade rule format
39 | os.put(upgradeRuleFormat(s));
40 | cursor.continue();
41 | }
42 | };
43 | });
44 | }
45 | };
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/pages/background/core/rules.ts:
--------------------------------------------------------------------------------
1 | import { cloneDeep } from 'lodash-es';
2 | import { convertToRule, convertToBasicRule, isMatchUrl, upgradeRuleFormat, initRule } from '@/share/core/rule-utils';
3 | import { getLocal } from '@/share/core/storage';
4 | import { getTableName } from '@/share/core/utils';
5 | import { APIs, EVENTs, IS_MATCH, TABLE_NAMES, TABLE_NAMES_ARR } from '@/share/core/constant';
6 | import type { InitdRule, Rule, RuleFilterOptions } from '@/share/core/types';
7 | import notify from '@/share/core/notify';
8 | import { getDatabase } from './db';
9 |
10 | const cache: { [key: string]: null | InitdRule[] } = {};
11 | TABLE_NAMES_ARR.forEach((t) => {
12 | cache[t] = null;
13 | });
14 |
15 | const updateCacheQueue: { [x: string]: Array<{ resolve: () => void; reject: (error: any) => void }> } = {};
16 |
17 | async function updateCache(type: TABLE_NAMES): Promise {
18 | return new Promise((resolve, reject) => {
19 | // 如果正在Update,则放到回调组里面
20 | if (typeof updateCacheQueue[type] !== 'undefined') {
21 | updateCacheQueue[type].push({ resolve, reject });
22 | return;
23 | } else {
24 | updateCacheQueue[type] = [{ resolve, reject }];
25 | }
26 | getDatabase()
27 | .then((db) => {
28 | const tx = db.transaction([type], 'readonly');
29 | const os = tx.objectStore(type);
30 | const all: InitdRule[] = [];
31 | os.openCursor().onsuccess = (event) => {
32 | // @ts-ignore
33 | const cursor = event.target.result;
34 | if (cursor) {
35 | const s: InitdRule = cursor.value;
36 | s.id = cursor.key;
37 | // Init function here
38 | try {
39 | all.push(initRule(s));
40 | } catch (e) {
41 | console.error('Cannot init rule', s, e);
42 | }
43 | cursor.continue();
44 | } else {
45 | cache[type] = all;
46 | updateCacheQueue[type].forEach((it) => {
47 | it.resolve();
48 | });
49 | delete updateCacheQueue[type];
50 | }
51 | };
52 | })
53 | .catch((e) => {
54 | updateCacheQueue[type].forEach((it) => {
55 | it.reject(e);
56 | });
57 | delete updateCacheQueue[type];
58 | });
59 | });
60 | }
61 |
62 | function filter(fromRules: InitdRule[], options: RuleFilterOptions) {
63 | let rules = Array.from(fromRules);
64 | if (options === null || typeof options !== 'object') {
65 | return rules;
66 | }
67 | const url = typeof options.url !== 'undefined' ? options.url : null;
68 |
69 | if (typeof options.id !== 'undefined') {
70 | rules = rules.filter((rule) => {
71 | if (Array.isArray(options.id)) {
72 | return options.id.includes(rule.id);
73 | }
74 | return rule.id === Number(options.id);
75 | });
76 | }
77 |
78 | if (options.name) {
79 | rules = rules.filter((rule) => {
80 | return rule.name === options.name;
81 | });
82 | }
83 |
84 | if (typeof options.enable !== 'undefined') {
85 | rules = rules.filter((rule) => {
86 | return rule.enable === options.enable;
87 | });
88 | }
89 |
90 | if (url != null) {
91 | rules = rules.filter((rule) => isMatchUrl(rule, url) === IS_MATCH.MATCH);
92 | }
93 | return rules;
94 | }
95 |
96 | async function save(o: Rule) {
97 | const tableName = getTableName(o.ruleType);
98 | if (!tableName) {
99 | throw new Error(`Unknown type ${o.ruleType}`);
100 | }
101 | const rule = convertToRule(o);
102 | return new Promise((resolve) => {
103 | getDatabase().then((db) => {
104 | const tx = db.transaction([tableName], 'readwrite');
105 | const os = tx.objectStore(tableName);
106 | // Check base informations
107 | upgradeRuleFormat(rule);
108 | // Update
109 | if (rule.id && rule.id !== -1) {
110 | const request = os.get(Number(rule.id));
111 | request.onsuccess = () => {
112 | const existsRule = request.result || {};
113 | const originalRule = cloneDeep(existsRule);
114 | for (const prop in rule) {
115 | if (prop === 'id') {
116 | continue;
117 | }
118 | existsRule[prop] = rule[prop];
119 | }
120 | const req = os.put(existsRule);
121 | req.onsuccess = () => {
122 | updateCache(tableName);
123 | notify.other({ method: APIs.ON_EVENT, event: EVENTs.RULE_UPDATE, from: originalRule, target: existsRule });
124 | resolve(rule);
125 | };
126 | };
127 | } else {
128 | // Create
129 | // Make sure it's not null - that makes indexeddb sad
130 | // @ts-ignore
131 | delete rule.id;
132 | const request = os.add(rule);
133 | request.onsuccess = (event) => {
134 | updateCache(tableName);
135 | // Give it the ID that was generated
136 | // @ts-ignore
137 | rule.id = event.target.result;
138 | notify.other({ method: APIs.ON_EVENT, event: EVENTs.RULE_UPDATE, from: null, target: rule });
139 | resolve(rule);
140 | };
141 | }
142 | });
143 | });
144 | }
145 |
146 | function remove(tableName: TABLE_NAMES, id: number): Promise {
147 | return new Promise((resolve) => {
148 | getDatabase().then((db) => {
149 | const tx = db.transaction([tableName], 'readwrite');
150 | const os = tx.objectStore(tableName);
151 | const request = os.delete(Number(id));
152 | request.onsuccess = () => {
153 | updateCache(tableName);
154 | notify.other({ method: APIs.ON_EVENT, event: EVENTs.RULE_DELETE, table: tableName, id: Number(id) });
155 | // check common mark
156 | getLocal().get('common_rule').then((result) => {
157 | const key = `${tableName}-${id}`;
158 | if (Array.isArray(result.common_rule) && result.common_rule.includes(key)) {
159 | const newKeys = [...result.common_rule];
160 | newKeys.splice(newKeys.indexOf(key), 1);
161 | getLocal().set({
162 | common_rule: newKeys,
163 | });
164 | }
165 | });
166 | resolve();
167 | };
168 | });
169 | });
170 | }
171 |
172 | function get(type: TABLE_NAMES, options?: RuleFilterOptions) {
173 | // When browser is starting up, pass all requests
174 | const all = cache[type];
175 | if (!all) {
176 | return null;
177 | }
178 | return options ? filter(all, options) : all;
179 | }
180 |
181 | function init() {
182 | setTimeout(() => {
183 | const queue: Array> = TABLE_NAMES_ARR.map((tableName) => updateCache(tableName));
184 | Promise.all(queue).then(() => {
185 | if (TABLE_NAMES_ARR.some((tableName) => cache[tableName] === null)) {
186 | init();
187 | }
188 | });
189 | });
190 | }
191 |
192 | init();
193 |
194 | export default {
195 | get,
196 | filter,
197 | save,
198 | remove,
199 | updateCache,
200 | convertToBasicRule,
201 | };
202 |
--------------------------------------------------------------------------------
/src/pages/background/index.ts:
--------------------------------------------------------------------------------
1 | import createApiHandler from './api-handler';
2 | import createRequestHandler from './request-handler';
3 | import './upgrade';
4 |
5 | if (typeof window !== 'undefined') {
6 | window.IS_BACKGROUND = true;
7 | }
8 |
9 | // 开始初始化
10 | createApiHandler();
11 | createRequestHandler();
12 |
--------------------------------------------------------------------------------
/src/pages/background/upgrade.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import * as storage from '@/share/core/storage';
3 | import { TABLE_NAMES_ARR } from '@/share/core/constant';
4 | import notify from '@/share/core/notify';
5 | import { getDatabase } from './core/db';
6 |
7 | // Upgrade
8 | const downloadHistory = localStorage.getItem('dl_history');
9 | if (downloadHistory) {
10 | storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) });
11 | localStorage.removeItem('dl_history');
12 | }
13 |
14 | // Put a version mark
15 | storage
16 | .getLocal()
17 | .get('version_mark')
18 | .then((v) => {
19 | const version = v.version_mark ? parseInt(v.version_mark, 10) : 0;
20 | if (!(version >= 1)) {
21 | storage.getLocal().set({
22 | version_mark: 1,
23 | });
24 | // Upgrade group
25 | const rebindRuleWithGroup = (group) => {
26 | return new Promise((resolve) => {
27 | const cacheQueue: Array> = [];
28 | function findGroup(type, id) {
29 | let result = browser.i18n.getMessage('ungrouped');
30 | for (const k in group) {
31 | if (group[k].includes(`${type}-${id}`)) {
32 | result = k;
33 | break;
34 | }
35 | }
36 | return result;
37 | }
38 | TABLE_NAMES_ARR.forEach((k) => {
39 | getDatabase().then((db) => {
40 | const tx = db.transaction([k], 'readwrite');
41 | const os = tx.objectStore(k);
42 | os.openCursor().onsuccess = (e) => {
43 | if (!e.target) {
44 | return;
45 | }
46 | const cursor = (e.target as any).result;
47 | if (cursor) {
48 | const s = cursor.value;
49 | s.id = cursor.key;
50 | if (typeof s.group === 'undefined') {
51 | s.group = findGroup(k, s.id);
52 | os.put(s);
53 | }
54 | cursor.continue();
55 | } else {
56 | cacheQueue.push(notify.other({ method: 'updateCache', type: k }));
57 | }
58 | };
59 | });
60 | });
61 | Promise.all(cacheQueue).then(resolve);
62 | });
63 | };
64 |
65 | const groups = localStorage.getItem('groups');
66 | if (groups) {
67 | const g = JSON.parse(groups);
68 | localStorage.removeItem('groups');
69 | rebindRuleWithGroup(g);
70 | } else {
71 | storage
72 | .getLocal()
73 | .get('groups')
74 | .then((r) => {
75 | if (r.groups !== undefined) {
76 | rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups'));
77 | } else {
78 | const g = {};
79 | g[browser.i18n.getMessage('ungrouped')] = [];
80 | rebindRuleWithGroup(g);
81 | }
82 | });
83 | }
84 | }
85 | });
86 |
--------------------------------------------------------------------------------
/src/pages/background/utils.ts:
--------------------------------------------------------------------------------
1 | import { getActiveTab } from '@/share/core/utils';
2 | import browser from 'webextension-polyfill';
3 |
4 | interface OpenURLOptions {
5 | method?: string;
6 | url: string;
7 | active?: boolean;
8 | }
9 |
10 | export function openURL(options: OpenURLOptions) {
11 | delete options.method;
12 | return new Promise((resolve) => {
13 | const doCreate = () => browser.tabs.create(options).then(resolve);
14 | browser.tabs
15 | .query({ currentWindow: true, url: options.url })
16 | .then((tabs) => {
17 | if (tabs.length) {
18 | browser.tabs
19 | .update(tabs[0].id, {
20 | active: true,
21 | })
22 | .then(resolve)
23 | .catch(doCreate);
24 | } else {
25 | getActiveTab().then((tab) => {
26 | const url = tab.url || '';
27 | // re-use an active new tab page
28 | // Firefox may have more than 1 newtab url, so check all
29 | const isNewTab =
30 | url.indexOf('about:newtab') === 0 ||
31 | url.indexOf('about:home') === 0 ||
32 | url.indexOf('chrome://newtab/') === 0;
33 | if (isNewTab) {
34 | browser.tabs
35 | .update(tab.id, options)
36 | .then(resolve)
37 | .catch(doCreate);
38 | } else {
39 | doCreate();
40 | }
41 | });
42 | }
43 | })
44 | .catch(doCreate);
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/src/pages/options/components/bool-radio.tsx:
--------------------------------------------------------------------------------
1 | import { RadioGroup, withField } from '@douyinfe/semi-ui';
2 | import type { OptionItem, RadioChangeEvent, RadioGroupProps } from '@douyinfe/semi-ui/lib/es/radio';
3 | import React, { useMemo } from 'react';
4 |
5 | interface BoolOptionItem extends Omit {
6 | value: boolean;
7 | }
8 |
9 | interface BoolRadioGroupProps extends Omit {
10 | value?: boolean;
11 | defaultValue?: boolean;
12 | onChange?: (v: boolean) => void;
13 | options?: BoolOptionItem[];
14 | }
15 |
16 | const BoolRadioGroup = (props: BoolRadioGroupProps) => {
17 | const { value: valueProp, defaultValue: defaultValueProp, onChange: onChangeProp, options: optionsProp } = props;
18 |
19 | const options = useMemo(() => optionsProp?.map((x) => ({ ...x, value: x.value ? 'y' : 'n' })), [optionsProp]);
20 |
21 | const defaultValue = defaultValueProp ? 'y' : 'n';
22 | // eslint-disable-next-line no-nested-ternary
23 | const value = typeof valueProp === 'boolean' ? valueProp ? 'y' : 'n' : undefined;
24 |
25 | const onChange = (e: RadioChangeEvent) => onChangeProp?.(e.target.value === 'y');
26 |
27 | return ;
28 | };
29 |
30 | export const BoolRadioGroupField = withField(BoolRadioGroup);
31 | export default BoolRadioGroup;
32 |
--------------------------------------------------------------------------------
/src/pages/options/index.tsx:
--------------------------------------------------------------------------------
1 | import { Nav } from '@douyinfe/semi-ui';
2 | import React, { useCallback, useEffect, useRef, useState } from 'react';
3 | import { css } from '@emotion/css';
4 | import { IconFolderOpen, IconHelpCircle, IconMenu, IconSetting } from '@douyinfe/semi-icons';
5 | import { useGetState, useResponsive } from 'ahooks';
6 | import { convertToRule } from '@/share/core/rule-utils';
7 | import { t } from '@/share/core/utils';
8 | import { prefs } from '@/share/core/prefs';
9 | import type { Rule } from '@/share/core/types';
10 | import SemiLocale from '@/share/components/semi-locale';
11 | import isDarkMode from '@/share/pages/is-dark-mode';
12 | import GroupSelect from './sections/group-select';
13 | import ImportAndExportSection from './sections/import-and-export';
14 | import OptionsSection from './sections/options';
15 | import RulesSection from './sections/rules';
16 | import Edit from './sections/rules/edit';
17 | import type { OnSelectedData } from '@douyinfe/semi-ui/lib/es/navigation';
18 |
19 | const Options = () => {
20 | const [editShow, setEditShow] = useState(false);
21 | const [editRule, setEditRule] = useState();
22 | const [navCollapse, setNavCollapse, getNavCollapse] = useGetState(false);
23 | const [active, setActive, getActive] = useGetState('rules');
24 | // 保存切换到帮助前是否为展开状态
25 | const isCollapsedRef = useRef(true);
26 |
27 | const responsive = useResponsive();
28 |
29 | useEffect(() => {
30 | prefs.ready(() => {
31 | if (isDarkMode()) {
32 | document.body.setAttribute('theme-mode', 'dark');
33 | }
34 | });
35 | }, []);
36 |
37 | const handleSwitch = useCallback((data: OnSelectedData) => {
38 | const newActive = data.itemKey as string;
39 | if (newActive && newActive !== getActive()) {
40 | if (newActive === 'help') {
41 | isCollapsedRef.current = getNavCollapse();
42 | }
43 | if (getActive() === 'help') {
44 | setNavCollapse(isCollapsedRef.current);
45 | } else {
46 | setNavCollapse(getNavCollapse() || newActive === 'help');
47 | }
48 | setActive(newActive);
49 | window.scrollTo(0, 0);
50 | }
51 | }, []);
52 |
53 | const handleEditClose = useCallback(() => {
54 | setEditShow(false);
55 | setEditRule(undefined);
56 | }, []);
57 |
58 | const handleEdit = useCallback((rule?: Rule) => {
59 | setEditShow(true);
60 | setEditRule(rule ? convertToRule(rule) : undefined);
61 | }, []);
62 |
63 | useEffect(() => {
64 | // 小屏幕主动收起侧边栏
65 | if (!responsive.lg && getNavCollapse()) {
66 | setNavCollapse(false);
67 | }
68 | }, [responsive.lg]);
69 |
70 | return (
71 |
72 | .navbar {
78 | /* width: 240px; */
79 | flex-grow: 0;
80 | flex-shrink: 0;
81 | height: 100vh;
82 | }
83 |
84 | > .main-content {
85 | flex-grow: 1;
86 | flex-shrink: 1;
87 | height: 100vh;
88 | overflow: auto;
89 | box-sizing: border-box;
90 | padding: 16px;
91 | background-color: var(--semi-color-fill-0);
92 |
93 | > .in-visible {
94 | display: none;
95 | }
96 |
97 | > section {
98 | > .semi-card {
99 | margin-bottom: 16px;
100 | }
101 | }
102 | }
103 | `}
104 | >
105 |
},
111 | { itemKey: 'options', text: t('options'), icon:
},
112 | { itemKey: 'export_and_import', text: t('export_and_import'), icon:
},
113 | { itemKey: 'help', text: t('help'), icon:
},
114 | ]}
115 | isCollapsed={navCollapse}
116 | onCollapseChange={setNavCollapse}
117 | footer={{
118 | collapseButton: true,
119 | }}
120 | />
121 |
122 |
123 |
124 |
125 | {active === 'help' && (
126 | iframe {
131 | border: 0;
132 | width: 100%;
133 | height: 100%;
134 | }
135 | `}
136 | >
137 |
138 |
139 | )}
140 |
141 |
142 |
143 |
144 |
145 | );
146 | };
147 |
148 | export default Options;
149 |
--------------------------------------------------------------------------------
/src/pages/options/sections/group-select/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Input, Modal, Select } from '@douyinfe/semi-ui';
3 | import emitter from '@/share/core/emitter';
4 | import { t } from '@/share/core/utils';
5 |
6 | interface GroupSelectState {
7 | group: string[];
8 | show: boolean;
9 | selected: string;
10 | newName: string;
11 | }
12 |
13 | export default class GroupSelect extends React.Component {
14 | private newValue = `_new_${Math.random().toString()}`;
15 |
16 | constructor(props: any) {
17 | super(props);
18 |
19 | this.handleEventShow = this.handleEventShow.bind(this);
20 | this.handleEventUpdate = this.handleEventUpdate.bind(this);
21 | this.handleNew = this.handleNew.bind(this);
22 | this.handleChange = this.handleChange.bind(this);
23 | this.handleSubmit = this.handleSubmit.bind(this);
24 | this.handleCancel = this.handleCancel.bind(this);
25 |
26 | this.state = {
27 | selected: '',
28 | group: [],
29 | show: false,
30 | newName: '',
31 | };
32 | }
33 |
34 | componentDidMount() {
35 | emitter.on(emitter.EVENT_GROUP_UPDATE, this.handleEventUpdate);
36 | emitter.on(emitter.ACTION_SELECT_GROUP, this.handleEventShow);
37 | }
38 |
39 | // 分组更新事件
40 | handleEventUpdate(group: string[]) {
41 | this.setState({
42 | group,
43 | });
44 | }
45 | // 显示选择器
46 | handleEventShow(selected?: string) {
47 | this.setState({
48 | show: true,
49 | selected: selected || '',
50 | });
51 | }
52 |
53 | componentWillUnmount() {
54 | emitter.off(emitter.EVENT_GROUP_UPDATE, this.handleEventUpdate);
55 | emitter.off(emitter.ACTION_SELECT_GROUP, this.handleEventShow);
56 | }
57 |
58 | handleNew(value: string) {
59 | this.setState({
60 | newName: value,
61 | });
62 | }
63 |
64 | handleChange(value: string) {
65 | this.setState({
66 | selected: value,
67 | });
68 | }
69 |
70 | handleSubmit() {
71 | if (this.state.selected === this.newValue) {
72 | // 新建不能是空的,如果是的话,就视为取消了
73 | if (this.state.newName === '') {
74 | this.handleCancel();
75 | return;
76 | }
77 | const groups = Array.from(this.state.group);
78 | if (!groups.includes(this.state.newName)) {
79 | groups.push(this.state.newName);
80 | emitter.emit(emitter.EVENT_GROUP_UPDATE, groups);
81 | }
82 | emitter.emit(emitter.INNER_GROUP_SELECTED, this.state.newName);
83 | } else {
84 | emitter.emit(emitter.INNER_GROUP_SELECTED, this.state.selected);
85 | }
86 | this.handleCancel();
87 | }
88 |
89 | handleCancel() {
90 | this.setState({
91 | show: false,
92 | selected: '',
93 | newName: '',
94 | });
95 | // 触发一个失败事件,让emitter去掉监听
96 | emitter.emit(emitter.INNER_GROUP_CANCEL);
97 | }
98 |
99 | render() {
100 | return (
101 |
108 |
109 |
118 | {this.state.selected === this.newValue && (
119 |
120 |
121 |
122 | )}
123 |
124 | );
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/pages/options/sections/import-and-export/cloud/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconDownload, IconUpload, IconExternalOpen } from '@douyinfe/semi-icons';
2 | import { Button, Modal, Tag, Toast } from '@douyinfe/semi-ui';
3 | import { css } from '@emotion/css';
4 | import dayjs from 'dayjs';
5 | import * as React from 'react';
6 | import Api from '@/share/pages/api';
7 | import browserSync from '@/share/pages/browser-sync';
8 | import { createExport } from '@/share/core/rule-utils';
9 | import { t } from '@/share/core/utils';
10 | import type { BasicRule } from '@/share/core/types';
11 |
12 | interface CloudProps {
13 | visible: boolean;
14 | onClose: () => void;
15 | onImport: (rules: { [key: string]: BasicRule[] }) => void;
16 | }
17 |
18 | interface CloudState {
19 | has: boolean;
20 | time: number;
21 | }
22 |
23 | export default class Cloud extends React.Component {
24 | constructor(props: any) {
25 | super(props);
26 |
27 | this.handleDelete = this.handleDelete.bind(this);
28 | this.handleUpload = this.handleUpload.bind(this);
29 | this.handleDownload = this.handleDownload.bind(this);
30 |
31 | this.state = {
32 | has: false,
33 | time: 0,
34 | };
35 | }
36 |
37 | private refresh() {
38 | browserSync.getMeta().then((r) => {
39 | if (r && r.time) {
40 | this.setState({
41 | has: true,
42 | time: r.time,
43 | });
44 | }
45 | });
46 | }
47 |
48 | componentDidMount() {
49 | this.refresh();
50 | }
51 |
52 | handleUpload() {
53 | Api.getAllRules()
54 | .then((result) => browserSync.save(createExport(result)))
55 | .then(() => browserSync.getMeta())
56 | .then(() => this.refresh())
57 | .catch(() => Toast.error('cloud_over_limit'));
58 | }
59 |
60 | handleDownload() {
61 | this.props.onClose();
62 | browserSync.getContent().then((r) => {
63 | this.props.onImport(r);
64 | });
65 | }
66 |
67 | handleDelete(from: string) {
68 | browserSync.clear().then(() =>
69 | this.setState({
70 | has: false,
71 | time: 0,
72 | }));
73 | return true;
74 | }
75 |
76 | handleHelp() {
77 | Api.openURL(t('url_cloud_backup'));
78 | }
79 |
80 | render() {
81 | return (
82 |
98 | }>
99 | {t('help')}
100 |
101 | }>
102 | {t('download')}
103 |
104 | }>
105 | {t('upload')}
106 |
107 |
108 | }
109 | visible={this.props.visible}
110 | onCancel={this.props.onClose}
111 | >
112 | {this.state.has && (
113 |
114 | {t('cloud_backup_at', dayjs(this.state.time).format('lll'))}
115 |
116 | )}
117 | {!this.state.has && t('cloud_no_backup')}
118 |
119 | );
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/pages/options/sections/import-and-export/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconCloud, IconDownload, IconFolderOpen, IconSave, IconSearch } from '@douyinfe/semi-icons';
2 | import { Button, Card, Input, Space, Table, Toast } from '@douyinfe/semi-ui';
3 | import * as React from 'react';
4 | import { openURL } from '@/pages/background/utils';
5 | import { getExportName } from '@/pages/options/utils';
6 | import file from '@/share/pages/file';
7 | import { createExport } from '@/share/core/rule-utils';
8 | import { getLocal } from '@/share/core/storage';
9 | import { fetchUrl, t } from '@/share/core/utils';
10 | import type { BasicRule } from '@/share/core/types';
11 | import Api from '@/share/pages/api';
12 | import Cloud from './cloud';
13 | import ImportDrawer from './import-drawer';
14 |
15 | interface IEProps {
16 | visible: boolean;
17 | }
18 |
19 | interface IEState {
20 | downloadUrl: string;
21 | downloading: boolean;
22 | showCloud: boolean;
23 | downloadHistory: string[];
24 | }
25 |
26 | export default class ImportAndExport extends React.Component {
27 | private importRef: React.RefObject = React.createRef();
28 | constructor(props: any) {
29 | super(props);
30 |
31 | this.handleImport = this.handleImport.bind(this);
32 | this.handleDownload = this.handleDownload.bind(this);
33 | this.handleCloudImport = this.handleCloudImport.bind(this);
34 |
35 | this.state = {
36 | downloadUrl: '',
37 | downloading: false,
38 | showCloud: false,
39 | downloadHistory: [],
40 | };
41 | }
42 |
43 | componentDidMount() {
44 | // Load download history
45 | getLocal().get('dl_history').then((r) => {
46 | if (Array.isArray(r.dl_history)) {
47 | this.setState({
48 | downloadHistory: r.dl_history,
49 | });
50 | }
51 | });
52 | }
53 |
54 | handleImport() {
55 | file.load('.json').then((content) => {
56 | try {
57 | this.importRef.current!.show(JSON.parse(content));
58 | } catch (e) {
59 | Toast.error(e.message);
60 | }
61 | });
62 | }
63 |
64 | async handleDownload() {
65 | this.setState({ downloading: true });
66 | try {
67 | const res = await fetchUrl({
68 | url: this.state.downloadUrl,
69 | });
70 | this.importRef.current!.show(JSON.parse(res));
71 |
72 | if (!this.state.downloadHistory.includes(this.state.downloadUrl)) {
73 | this.setState((prevState) => {
74 | const newHistory = [this.state.downloadUrl, ...prevState.downloadHistory];
75 | getLocal().set({
76 | dl_history: newHistory,
77 | });
78 | return {
79 | downloadHistory: newHistory,
80 | };
81 | });
82 | }
83 | } catch (e) {
84 | Toast.error(e.message);
85 | }
86 | this.setState({ downloading: false });
87 | }
88 |
89 | handleCloudImport(res: { [key: string]: BasicRule[] }) {
90 | try {
91 | this.importRef.current!.show(res);
92 | } catch (e) {
93 | Toast.error(e.message);
94 | }
95 | }
96 |
97 | async handleExport() {
98 | const result = await Api.getAllRules();
99 | file.save(JSON.stringify(createExport(result), null, '\t'), getExportName());
100 | }
101 |
102 | handleOpenThird() {
103 | openURL({
104 | url: t('url_third_party_rules'),
105 | });
106 | }
107 |
108 | render() {
109 | return (
110 |
111 |
112 |
113 | }>
114 | {t('export')}
115 |
116 | }>
117 | {t('import')}
118 |
119 |
122 |
123 |
124 |
125 |
126 | this.setState({ downloadUrl })}
131 | />
132 | } loading={this.state.downloading}>
133 | {t('download')}
134 |
135 | } onClick={this.handleOpenThird}>
136 | {t('third_party_rules')}
137 |
138 |
139 | ({ url: x }))}
143 | size="small"
144 | columns={[
145 | {
146 | dataIndex: 'url',
147 | },
148 | {
149 | dataIndex: '',
150 | render: (_, record) => (
151 |
152 |
162 |
172 |
193 |
194 | ),
195 | },
196 | ]}
197 | pagination={false}
198 | />
199 |
200 |
201 | this.setState({ showCloud: false })}
204 | onImport={this.handleCloudImport}
205 | />
206 |
207 | );
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/pages/options/sections/options/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Checkbox, Col, Form, Row, Select, Typography } from '@douyinfe/semi-ui';
2 | import * as React from 'react';
3 | import Api from '@/share/pages/api';
4 | import emitter from '@/share/core/emitter';
5 | import { prefs } from '@/share/core/prefs';
6 | import { t } from '@/share/core/utils';
7 | import type { PrefValue } from '@/share/core/types';
8 | import { defaultPrefValue } from '@/share/core/constant';
9 |
10 | interface OptionsProps {
11 | visible: boolean;
12 | }
13 |
14 | interface OptionsState {
15 | prefs: PrefValue;
16 | }
17 |
18 | const checkPrefs: { [key: string]: string } = {
19 | 'manage-collapse-group': t('manage_collapse_group'),
20 | 'exclude-he': t('rules_no_effect_for_he'),
21 | 'show-common-header': t('display_common_header'),
22 | 'include-headers': t('include_header_in_custom_function'),
23 | 'modify-body': t('modify_body'),
24 | 'is-debug': t('debug_mode_enable'),
25 | };
26 |
27 | interface SelectItem {
28 | title: string;
29 | options: Array<{
30 | label: string;
31 | value: string;
32 | }>;
33 | }
34 | const selectPrefs: { [key: string]: SelectItem } = {
35 | 'dark-mode': {
36 | title: t('dark_mode'),
37 | options: [
38 | {
39 | label: t('auto'),
40 | value: 'auto',
41 | },
42 | {
43 | label: t('enable'),
44 | value: 'on',
45 | },
46 | {
47 | label: t('disable'),
48 | value: 'off',
49 | },
50 | ],
51 | },
52 | };
53 |
54 | export default class Options extends React.Component {
55 | constructor(props: any) {
56 | super(props);
57 |
58 | this.handleChange = this.handleChange.bind(this);
59 | this.handleUpdate = this.handleUpdate.bind(this);
60 |
61 | this.state = {
62 | prefs: { ...defaultPrefValue },
63 | };
64 | }
65 |
66 | componentDidMount() {
67 | prefs.ready(() => {
68 | const newPrefs = { ...this.state.prefs };
69 | Object.keys(newPrefs).forEach((it) => {
70 | newPrefs[it] = prefs.get(it);
71 | });
72 | this.setState({
73 | prefs: newPrefs,
74 | });
75 | });
76 | emitter.on(emitter.EVENT_PREFS_UPDATE, this.handleUpdate);
77 | }
78 |
79 | componentWillUnmount() {
80 | emitter.off(emitter.EVENT_PREFS_UPDATE, this.handleUpdate);
81 | }
82 |
83 | handleUpdate(key: string, val: any) {
84 | if (this.state.prefs[key] === val) {
85 | return;
86 | }
87 | this.setState((prevState) => ({
88 | prefs: {
89 | ...prevState.prefs,
90 | [key]: val,
91 | },
92 | }));
93 | }
94 |
95 | handleChange(name: string, value: any) {
96 | this.setState((prevState) => {
97 | const newPrefs = { ...prevState.prefs, [name]: value };
98 | Api.setPrefs(name, value);
99 | prefs.set(name, value);
100 | return { prefs: newPrefs };
101 | });
102 | }
103 |
104 | render() {
105 | return (
106 |
107 |
112 | * {t('dark_mode_help')}
113 | * {t('debug_mode_help')}
114 |
115 | }
116 | >
117 |
118 | {Object.entries(checkPrefs).map((it) => {
119 | return (
120 |
121 | this.handleChange(it[0], Boolean(e.target.checked))}
123 | checked={this.state.prefs[it[0]]}
124 | >
125 | {it[1]}
126 |
127 |
128 | );
129 | })}
130 | {Object.entries(selectPrefs).map((it) => {
131 | return (
132 |
133 |
134 |
140 |
141 | );
142 | })}
143 |
144 |
145 |
146 | );
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/pages/options/sections/rules/edit/code-editor.tsx:
--------------------------------------------------------------------------------
1 | import { withField } from '@douyinfe/semi-ui';
2 | import React from 'react';
3 | import CodeMirror, { ReactCodeMirrorProps } from '@uiw/react-codemirror';
4 | import { githubLight, githubDark } from '@uiw/codemirror-theme-github';
5 | import { javascript } from '@codemirror/lang-javascript';
6 | import isDarkMode from '@/share/pages/is-dark-mode';
7 |
8 | type CodeEditorProps = ReactCodeMirrorProps;
9 |
10 | const CodeEditor = (props: CodeEditorProps) => (
11 |
16 | );
17 |
18 | export default CodeEditor;
19 | export const CodeEditorField = withField(CodeEditor);
20 |
--------------------------------------------------------------------------------
/src/pages/options/sections/rules/edit/encoding.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | 'UTF-8',
3 | 'GBK',
4 | 'gb18030',
5 | 'Big5',
6 | 'EUC-JP',
7 | 'ISO-2022-JP',
8 | 'Shift_JIS',
9 | 'EUC-KR',
10 | 'UTF-16BE',
11 | 'UTF-16LE',
12 | 'IBM866',
13 | 'KOI8-R',
14 | 'KOI8-U',
15 | 'macintosh',
16 | 'replacement',
17 | 'x-user-defined',
18 | 'x-mac-cyrillic',
19 | 'ISO-8859-2',
20 | 'ISO-8859-3',
21 | 'ISO-8859-4',
22 | 'ISO-8859-5',
23 | 'ISO-8859-6',
24 | 'ISO-8859-7',
25 | 'ISO-8859-8',
26 | 'ISO-8859-8-I',
27 | 'ISO-8859-10',
28 | 'ISO-8859-13',
29 | 'ISO-8859-14',
30 | 'ISO-8859-15',
31 | 'ISO-8859-16',
32 | 'windows-874',
33 | 'windows-1250',
34 | 'windows-1251',
35 | 'windows-1252',
36 | 'windows-1253',
37 | 'windows-1254',
38 | 'windows-1255',
39 | 'windows-1256',
40 | 'windows-1257',
41 | 'windows-1258',
42 | ];
43 |
--------------------------------------------------------------------------------
/src/pages/options/sections/rules/edit/headers.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | request: [
3 | 'a-im',
4 | 'accept',
5 | 'accept-charset',
6 | 'accept-datetime',
7 | 'accept-encoding',
8 | 'accept-language',
9 | 'access-control-request-headers',
10 | 'access-control-request-method',
11 | 'authorization',
12 | 'cache-control',
13 | 'connection',
14 | 'content-length',
15 | 'content-md5',
16 | 'content-type',
17 | 'cookie',
18 | 'date',
19 | 'dnt',
20 | 'expect',
21 | 'forwarded',
22 | 'from',
23 | 'front-end-https',
24 | 'host',
25 | 'http2-settings',
26 | 'if-match',
27 | 'if-modified-since',
28 | 'if-none-match',
29 | 'if-range',
30 | 'if-unmodified-since',
31 | 'max-forwards',
32 | 'origin',
33 | 'pragma',
34 | 'proxy-authorization',
35 | 'proxy-connection',
36 | 'range',
37 | 'referer',
38 | 'save-data',
39 | 'te',
40 | 'upgrade',
41 | 'upgrade-insecure-requests',
42 | 'user-agent',
43 | 'via',
44 | 'warning',
45 | 'x-att-deviceid',
46 | 'x-correlation-id',
47 | 'x-csrf-token',
48 | 'x-forwarded-for',
49 | 'x-forwarded-host',
50 | 'x-forwarded-proto',
51 | 'x-http-method-override',
52 | 'x-request-id',
53 | 'x-requested-with',
54 | 'x-uidh',
55 | 'x-wap-profile',
56 | ],
57 | response: [
58 | 'accept-patch',
59 | 'accept-ranges',
60 | 'access-control-allow-credentials',
61 | 'access-control-allow-headers',
62 | 'access-control-allow-methods',
63 | 'access-control-allow-origin',
64 | 'access-control-expose-headers',
65 | 'access-control-max-age',
66 | 'age',
67 | 'allow',
68 | 'alt-svc',
69 | 'cache-control',
70 | 'connection',
71 | 'content-disposition',
72 | 'content-encoding',
73 | 'content-language',
74 | 'content-length',
75 | 'content-location',
76 | 'content-md5',
77 | 'content-range',
78 | 'content-security-policy',
79 | 'content-type',
80 | 'date',
81 | 'delta-base',
82 | 'etag',
83 | 'expires',
84 | 'im',
85 | 'last-modified',
86 | 'link',
87 | 'location',
88 | 'p3p',
89 | 'pragma',
90 | 'proxy-authenticate',
91 | 'public-key-pins',
92 | 'refresh',
93 | 'retry-after',
94 | 'server',
95 | 'set-cookie',
96 | 'status',
97 | 'strict-transport-security',
98 | 'timing-allow-origin',
99 | 'tk',
100 | 'trailer',
101 | 'transfer-encoding',
102 | 'upgrade',
103 | 'vary',
104 | 'via',
105 | 'warning',
106 | 'www-authenticate',
107 | 'x-content-duration',
108 | 'x-content-security-policy',
109 | 'x-content-type-options',
110 | 'x-correlation-id',
111 | 'x-frame-options',
112 | 'x-powered-by',
113 | 'x-request-id',
114 | 'x-ua-compatible',
115 | 'x-webkit-csp',
116 | 'x-xss-protection',
117 | ],
118 | };
119 |
--------------------------------------------------------------------------------
/src/pages/options/sections/rules/edit/index.less:
--------------------------------------------------------------------------------
1 | @import './prism-vs.css';
2 |
3 | .edit-drawer {
4 | width: 100%;
5 | max-width: 800px;
6 |
7 | .next-form-item-control {
8 | > .next-select {
9 | width: 100%;
10 | }
11 | }
12 |
13 | .code-editor {
14 | display: block;
15 | width: 100%;
16 | font-size: 12px;
17 | line-height: 18px;
18 | border-radius: 3px;
19 | min-height: 100px;
20 |
21 | pre, textarea {
22 | font-family: "Fira Code", "Source Code Pro", "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/pages/options/sections/rules/float.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Typography } from '@douyinfe/semi-ui';
3 | import { IconClose } from '@douyinfe/semi-icons';
4 | import { css, cx } from '@emotion/css';
5 | import RuleDetail from '@/share/components/rule-detail';
6 | import type { Rule } from '@/share/core/types';
7 |
8 | function isTouchEvent(obj: Event): obj is TouchEvent {
9 | return typeof TouchEvent !== 'undefined' && obj instanceof TouchEvent;
10 | }
11 |
12 | interface FloatProps {
13 | rule: Rule;
14 | onClose: () => void;
15 | }
16 |
17 | const Float = (props: FloatProps) => {
18 | const { rule, onClose } = props;
19 |
20 | const handleStart = (re: any) => {
21 | const e: TouchEvent | MouseEvent = re.nativeEvent;
22 | const box: HTMLElement = ((el) => {
23 | let p: any = el;
24 | while (p) {
25 | if (p.classList.contains('float-card')) {
26 | return p;
27 | }
28 | p = p.parentElement;
29 | }
30 | })(e.target);
31 | const offset = ((el) => {
32 | const rect = el.getBoundingClientRect();
33 | return {
34 | top: rect.top,
35 | left: rect.left,
36 | };
37 | })(box);
38 | const last = isTouchEvent(e)
39 | ? {
40 | x: e.touches[0].pageX,
41 | y: e.touches[0].pageY,
42 | }
43 | : {
44 | x: e.pageX,
45 | y: e.pageY,
46 | };
47 | let end = false;
48 | if (isTouchEvent(e)) {
49 | const onTouchMove = (ev: TouchEvent) => {
50 | offset.top += ev.touches[0].pageY - last.y;
51 | last.y = ev.touches[0].pageY;
52 | offset.left += ev.touches[0].pageX - last.x;
53 | last.x = ev.touches[0].pageX;
54 | ev.stopPropagation();
55 | ev.preventDefault();
56 | };
57 | document.body.addEventListener('touchmove', onTouchMove, { passive: false });
58 | document.body.addEventListener('touchend', () => {
59 | end = true;
60 | document.body.removeEventListener('touchmove', onTouchMove);
61 | });
62 | document.body.addEventListener('touchcancel', () => {
63 | end = true;
64 | document.body.removeEventListener('touchmove', onTouchMove);
65 | });
66 | } else {
67 | const onMouseMove = (ev: MouseEvent) => {
68 | offset.top += ev.pageY - last.y;
69 | last.y = ev.pageY;
70 | offset.left += ev.pageX - last.x;
71 | last.x = ev.pageX;
72 | };
73 | document.body.addEventListener('mousemove', onMouseMove);
74 | document.body.addEventListener('mouseup', () => {
75 | end = true;
76 | document.body.removeEventListener('mousemove', onMouseMove);
77 | });
78 | }
79 | function setNewOffset() {
80 | box.style.top = `${offset.top}px`;
81 | box.style.left = `${offset.left}px`;
82 | if (!end) {
83 | requestAnimationFrame(setNewOffset);
84 | }
85 | }
86 | setNewOffset();
87 | };
88 |
89 | return (
90 |
107 |
108 | {rule.name}
109 | }
112 | size="small"
113 | theme="borderless"
114 | type="tertiary"
115 | onClick={onClose}
116 | />
117 |
118 |
119 |
120 | );
121 | };
122 |
123 | export default Float;
124 |
--------------------------------------------------------------------------------
/src/pages/options/sections/rules/utils.ts:
--------------------------------------------------------------------------------
1 | import Api from '@/share/pages/api';
2 | import { TABLE_NAMES_ARR } from '@/share/core/constant';
3 | import file from '@/share/pages/file';
4 | import { createExport } from '@/share/core/rule-utils';
5 | import { getTableName } from '@/share/core/utils';
6 | import type { Rule } from '@/share/core/types';
7 | import { getExportName } from '../../utils';
8 |
9 | export function toggleRule(rule: Rule, enable: boolean) {
10 | rule.enable = enable;
11 | return Api.saveRule(rule);
12 | }
13 |
14 | export function remove(rule: Rule) {
15 | const table = getTableName(rule.ruleType);
16 | return table ? Api.removeRule(table, rule.id) : Promise.resolve();
17 | }
18 |
19 | export function save(rule: Rule) {
20 | return Api.saveRule(rule);
21 | }
22 |
23 | export function batchShare(rules: Rule[]) {
24 | const result: any = {};
25 | TABLE_NAMES_ARR.forEach((tb) => {
26 | result[tb] = [];
27 | });
28 | rules.forEach((e) => result[getTableName(e.ruleType)].push(e));
29 | file.save(JSON.stringify(createExport(result), null, '\t'), getExportName());
30 | }
31 |
--------------------------------------------------------------------------------
/src/pages/options/utils.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import emitter from '@/share/core/emitter';
3 |
4 | export function getExportName(additional?: string) {
5 | const date = dayjs().format('YYYYMMDD_HHmmss');
6 | return `HE_${date}${additional ? '_' + additional : ''}.json`;
7 | }
8 |
9 | emitter.on(emitter.INNER_GROUP_CANCEL, () => emitter.removeAllListeners(emitter.INNER_GROUP_SELECTED));
10 | export function selectGroup(selected?: string): Promise {
11 | return new Promise(resolve => {
12 | emitter.emit(emitter.ACTION_SELECT_GROUP, selected);
13 | emitter.once(emitter.INNER_GROUP_SELECTED, resolve);
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/pages/popup/index.tsx:
--------------------------------------------------------------------------------
1 | import { Nav, Switch, Tooltip, Typography } from '@douyinfe/semi-ui';
2 | import { IconMenu, IconSetting } from '@douyinfe/semi-icons';
3 | import { css, cx } from '@emotion/css';
4 | import React, { useCallback, useEffect, useState } from 'react';
5 | import browser from 'webextension-polyfill';
6 | import { IS_ANDROID, t } from '@/share/core/utils';
7 | import { prefs } from '@/share/core/prefs';
8 | import Api from '@/share/pages/api';
9 | import SemiLocale from '@/share/components/semi-locale';
10 | import isDarkMode from '@/share/pages/is-dark-mode';
11 | import Rules from './rule/rules';
12 | import Group from './rule/group';
13 | import type { OnSelectedData } from '@douyinfe/semi-ui/lib/es/navigation';
14 |
15 | const basicStyle = css`
16 | min-width: 340px;
17 | min-height: 440px;
18 | height: 100vh;
19 | width: 100vw;
20 | justify-content: stretch;
21 | display: flex;
22 | flex-direction: row;
23 |
24 | > .navbar {
25 | flex-grow: 0;
26 | flex-shrink: 0;
27 | }
28 |
29 | > .main-content {
30 | flex-grow: 1;
31 | flex-shrink: 1;
32 | overflow: auto;
33 | background-color: var(--semi-color-fill-0);
34 | display: flex;
35 | flex-direction: column;
36 |
37 | .cell-enable {
38 | padding-right: 0;
39 | .switch-container {
40 | display: flex;
41 | align-items: center;
42 | }
43 | }
44 |
45 | .cell-action {
46 | padding-top: 2px !important;
47 | padding-bottom: 2px !important;
48 | }
49 | }
50 | `;
51 |
52 | const mobileStyle = css`
53 | min-height: auto;
54 | min-width: auto;
55 | min-width: auto;
56 | max-width: auto;
57 | `;
58 |
59 | const Popup = () => {
60 | const [enable, setEnable] = useState(true);
61 |
62 | useEffect(() => {
63 | prefs.ready(() => {
64 | setEnable(!prefs.get('disable-all'));
65 | // Get dark mode setting
66 | if (isDarkMode()) {
67 | document.body.setAttribute('theme-mode', 'dark');
68 | }
69 | });
70 | }, []);
71 |
72 | const handleEnableChange = useCallback((checked: boolean) => {
73 | setEnable(checked);
74 | Api.setPrefs('disable-all', !checked);
75 | }, []);
76 |
77 | const handleNavSelect = useCallback((data: OnSelectedData) => {
78 | const newActive = data.itemKey as string;
79 | if (newActive === 'setting') {
80 | Api.openURL(browser.runtime.getURL('options.html'));
81 | window.close();
82 | }
83 | }, []);
84 |
85 | return (
86 |
87 |
92 |
,
98 | text: 'Header Editor',
99 | }}
100 | items={[
101 | { itemKey: 'rules', text: t('rule_list'), icon:
},
102 | { itemKey: 'setting', text: t('manage'), icon:
},
103 | ]}
104 | isCollapsed
105 | footer={
106 |
107 |
108 |
109 |
110 |
111 | }
112 | />
113 |
114 |
115 |
116 |
117 | {t('common_mark_tip')}
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default Popup;
125 |
--------------------------------------------------------------------------------
/src/pages/popup/rule/group.tsx:
--------------------------------------------------------------------------------
1 | import { IconLock, IconUnlock } from '@douyinfe/semi-icons';
2 | import { Button, ButtonGroup, Table, Tooltip } from '@douyinfe/semi-ui';
3 | import { flatten } from 'lodash-es';
4 | import React, { useMemo } from 'react';
5 | import Api from '@/share/pages/api';
6 | import useMarkCommon from '@/share/hooks/use-mark-common';
7 | import { t } from '@/share/core/utils';
8 |
9 | const toggleGroup = async (name: string, target: boolean) => {
10 | const rules = flatten(Object.values(await Api.getAllRules()));
11 | const toUpdate = rules.filter((x) => x.group === name);
12 | return Promise.all(
13 | toUpdate.map((x) => {
14 | x.enable = target;
15 | return Api.saveRule(x);
16 | }),
17 | );
18 | };
19 |
20 | const Group = () => {
21 | const { keys } = useMarkCommon('group');
22 |
23 | const tableData = useMemo(() => keys.map((x) => ({
24 | name: x,
25 | })), [keys]);
26 |
27 | if (!keys || keys.length === 0) {
28 | return null;
29 | }
30 |
31 | return (
32 | (
48 |
49 |
50 |
52 |
53 |
55 |
56 | ),
57 | },
58 | ]}
59 | pagination={false}
60 | />
61 | );
62 | };
63 |
64 | export default Group;
65 |
--------------------------------------------------------------------------------
/src/pages/popup/rule/rules.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useLatest, useRequest } from 'ahooks';
3 | import { Popover, Switch, Table } from '@douyinfe/semi-ui';
4 | import Api from '@/share/pages/api';
5 | import { getVirtualKey, parseVirtualKey } from '@/share/core/utils';
6 | import type { Rule } from '@/share/core/types';
7 | import { VIRTUAL_KEY, EVENTs } from '@/share/core/constant';
8 | import useMarkCommon from '@/share/hooks/use-mark-common';
9 | import RuleDetail from '@/share/components/rule-detail';
10 | import notify from '@/share/core/notify';
11 |
12 | const Rules = () => {
13 | const { keys } = useMarkCommon('rule');
14 | const keysRef = useLatest(keys);
15 | const { data = [], loading, mutate } = useRequest(() =>
16 | Promise.all(
17 | keys.map(async (key) => {
18 | const item = parseVirtualKey(key);
19 | const result = await Api.getRules(item.table, {
20 | id: item.id,
21 | });
22 | return {
23 | ...result[0],
24 | [VIRTUAL_KEY]: key,
25 | };
26 | }),
27 | ),
28 | {
29 | manual: false,
30 | refreshDeps: [keys],
31 | });
32 |
33 | useEffect(() => {
34 | const handleRuleUpdate = (request: any) => {
35 | const rule: Rule = request.target;
36 | const key = getVirtualKey(rule);
37 | if (keysRef.current.includes(key)) {
38 | mutate((currentData) => {
39 | if (!currentData) {
40 | return;
41 | }
42 | const index = currentData.findIndex((x) => x[VIRTUAL_KEY] === key);
43 | if (index === -1) {
44 | return currentData;
45 | }
46 | const result = [...currentData];
47 | result.splice(index, 1, {
48 | ...rule,
49 | [VIRTUAL_KEY]: key,
50 | });
51 | return result;
52 | });
53 | }
54 | };
55 |
56 | notify.event.on(EVENTs.RULE_UPDATE, handleRuleUpdate);
57 |
58 | return () => {
59 | notify.event.off(EVENTs.RULE_UPDATE, handleRuleUpdate);
60 | };
61 | }, []);
62 |
63 | if (data.length === 0 && !loading) {
64 | return null;
65 | }
66 |
67 | return (
68 | (
82 |
83 | {
87 | item.enable = checked;
88 | return Api.saveRule(item);
89 | }}
90 | />
91 |
92 | ),
93 | },
94 | {
95 | title: 'name',
96 | dataIndex: 'name',
97 | render: (value: string, item: Rule) => (
98 | } style={{ maxWidth: '300px' }}>
99 | {value}
100 |
101 | ),
102 | },
103 | ]}
104 | pagination={false}
105 | />
106 | );
107 | };
108 |
109 | export default Rules;
110 |
--------------------------------------------------------------------------------
/src/share/components/rule-detail.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { css } from '@emotion/css';
3 | import { t } from '@/share/core/utils';
4 | import type { Rule } from '@/share/core/types';
5 |
6 | interface RuleDetailProps {
7 | rule: Rule;
8 | }
9 |
10 | const RuleDetail = (props: RuleDetailProps) => {
11 | const { rule } = props;
12 |
13 | const isModifyHeader =
14 | rule.ruleType === 'modifySendHeader' || (rule.ruleType === 'modifyReceiveHeader' && !rule.isFunction);
15 |
16 | return (
17 |
31 |
32 | {t('matchType')}: {t(`match_${rule.matchType}`)}
33 |
34 | {rule.matchType !== 'all' && (
35 |
36 | {t('matchRule')}: {rule.pattern}
37 |
38 | )}
39 |
40 | {t('exec_type')}: {t(`exec_${rule.isFunction ? 'function' : 'normal'}`)}
41 |
42 | {rule.ruleType === 'redirect' && (
43 |
44 | {t('redirectTo')}: {rule.to}
45 |
46 | )}
47 | {rule.ruleType === 'modifyReceiveBody' && (
48 |
49 | {t('encoding')}: {rule.encoding}
50 |
51 | )}
52 | {isModifyHeader && (
53 |
54 |
55 | {t('headerName')}: {typeof rule.action === 'object' && rule.action.name}
56 |
57 |
58 | {t('headerValue')}: {typeof rule.action === 'object' && rule.action.value}
59 |
60 |
61 | )}
62 |
63 | );
64 | };
65 |
66 | export default RuleDetail;
67 |
--------------------------------------------------------------------------------
/src/share/components/semi-locale.tsx:
--------------------------------------------------------------------------------
1 | import { i18n } from 'webextension-polyfill';
2 | import { LocaleProvider } from '@douyinfe/semi-ui';
3 | import React from 'react';
4 |
5 | const allLocales = {};
6 |
7 | // @ts-ignore
8 | const context = require.context('@douyinfe/semi-ui/lib/es/locale/source', false, /\.js$/);
9 | context.keys().forEach((key: string) => {
10 | const locale = context(key);
11 | if (locale.default) {
12 | const name = locale.default.code;
13 | if (typeof allLocales[name] === 'undefined') {
14 | allLocales[name] = locale.default;
15 | }
16 | }
17 | });
18 |
19 | // 默认使用 en-US
20 | const lang = i18n.getUILanguage();
21 | const currentLocale = typeof allLocales[lang] === 'object' ? allLocales[lang] : allLocales['en-US'];
22 |
23 | const SemiLocale = (props: any) => (
24 |
25 | {props.children}
26 |
27 | );
28 |
29 | export default SemiLocale;
30 |
--------------------------------------------------------------------------------
/src/share/core/constant.ts:
--------------------------------------------------------------------------------
1 | import { PrefValue } from './types';
2 |
3 | export enum TABLE_NAMES {
4 | request = 'request',
5 | sendHeader = 'sendHeader',
6 | receiveHeader = 'receiveHeader',
7 | receiveBody = 'receiveBody',
8 | }
9 |
10 | export const TABLE_NAMES_ARR = Object.values(TABLE_NAMES);
11 |
12 | export const VIRTUAL_KEY = '_v_key';
13 |
14 | export enum RULE_TYPE {
15 | CANCEL = 'cancel',
16 | REDIRECT = 'redirect',
17 | MODIFY_SEND_HEADER = 'modifySendHeader',
18 | MODIFY_RECV_HEADER = 'modifyReceiveHeader',
19 | MODIFY_RECV_BODY = 'modifyReceiveBody',
20 | }
21 |
22 | export enum RULE_MATCH_TYPE {
23 | ALL = 'all',
24 | REGEXP = 'regexp',
25 | PREFIX = 'prefix',
26 | DOMAIN = 'domain',
27 | URL = 'url',
28 | }
29 |
30 | export const defaultPrefValue: PrefValue = {
31 | 'disable-all': false,
32 | 'manage-collapse-group': true,
33 | 'exclude-he': true,
34 | 'show-common-header': true,
35 | 'include-headers': false,
36 | 'modify-body': false,
37 | 'is-debug': false,
38 | 'dark-mode': 'auto',
39 | };
40 |
41 | export enum APIs {
42 | HEALTH_CHECK = 'check',
43 | OPEN_URL = 'open_url',
44 | GET_RULES = 'get_rules',
45 | SAVE_RULE = 'save_rule',
46 | DELETE_RULE = 'del_rule',
47 | UPDATE_CACHE = 'update_cache',
48 | SET_PREFS = 'set_pref',
49 | ON_EVENT = 'event',
50 | }
51 |
52 | export enum IS_MATCH {
53 | MATCH,
54 | MATCH_BUT_EXCLUDE,
55 | NOT_MATCH,
56 | }
57 |
58 | export enum EVENTs {
59 | RULE_UPDATE = 'rule_update',
60 | RULE_DELETE = 'rule_delete',
61 | }
62 |
--------------------------------------------------------------------------------
/src/share/core/emitter.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'eventemitter3';
2 |
3 | class Emitter extends EventEmitter {
4 | EVENT_GROUP_UPDATE = 'a1';
5 | ACTION_SELECT_GROUP = 'a2';
6 | INNER_GROUP_SELECTED = 'a3';
7 | INNER_GROUP_CANCEL = 'a4';
8 |
9 | EVENT_PREFS_UPDATE = 'b3';
10 | EVENT_PREFS_READY = 'b4';
11 | }
12 | const emitter = new Emitter();
13 |
14 | export default emitter;
15 |
--------------------------------------------------------------------------------
/src/share/core/logger.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import { prefs } from './prefs';
3 |
4 | export interface LogItem {
5 | time: Date;
6 | message: string;
7 | data?: any[];
8 | }
9 |
10 | class Logger {
11 | debug(message: string, ...data: any[]) {
12 | if (!prefs.get('is-debug')) {
13 | return;
14 | }
15 | console.log(
16 | ['%cHeader Editor%c [', dayjs().format('YYYY-MM-DD HH:mm:ss.SSS'), ']%c ', message].join(''),
17 | 'color:#5584ff;',
18 | 'color:#ff9300;',
19 | '',
20 | );
21 | if (data && data.length > 0) {
22 | console.log(data);
23 | }
24 | }
25 | }
26 |
27 | const logger = new Logger();
28 | export default logger;
29 |
--------------------------------------------------------------------------------
/src/share/core/notify.ts:
--------------------------------------------------------------------------------
1 | import browser, { Tabs } from 'webextension-polyfill';
2 | import EventEmitter from 'eventemitter3';
3 | import logger from './logger';
4 | import { canAccess, getGlobal, IS_ANDROID } from './utils';
5 | import { APIs } from './constant';
6 |
7 | class Notify {
8 | event = new EventEmitter();
9 | private messageQueue: Array<{
10 | request: any;
11 | resolve: (v: any) => void;
12 | reject: (e: any) => void;
13 | }> = [];
14 | private messageTimer: number | null = null;
15 |
16 | constructor() {
17 | const handleMessage = (request: any, sender?: any) => {
18 | if (request.method === 'notifyBackground') {
19 | request.method = request.reason;
20 | delete request.reason;
21 | }
22 | if (request.method !== APIs.ON_EVENT) {
23 | return;
24 | }
25 | logger.debug(`[nofity:event] ${request.event}`, request);
26 | this.event.emit(request.event, request);
27 | };
28 |
29 | browser.runtime.onMessage.addListener((request, sender) => {
30 | // 批量消息
31 | if (request.method === 'batchExecute') {
32 | request.batch.forEach((item) => handleMessage(item));
33 | return;
34 | }
35 | handleMessage(request);
36 | });
37 | }
38 |
39 | private startSendMessage() {
40 | if (this.messageTimer !== null) {
41 | return;
42 | }
43 | this.messageTimer = getGlobal().setTimeout(async () => {
44 | const currentQueue = this.messageQueue;
45 | this.messageQueue = [];
46 | // 只要开始发了,就把timer设置成null
47 | this.messageTimer = null;
48 | if (currentQueue.length === 0) {
49 | return;
50 | }
51 | if (currentQueue.length === 1) {
52 | const first = currentQueue[0];
53 | browser.runtime.sendMessage(first.request).then(first.resolve).catch(first.reject);
54 | return;
55 | }
56 | // 有多条并行执行
57 | const messages = currentQueue.map((x) => x.request);
58 | const result = await browser.runtime.sendMessage({
59 | method: 'batchExecute',
60 | batch: messages,
61 | });
62 | if (Array.isArray(result)) {
63 | result.forEach((item, index) => {
64 | if (item.status === 'rejected') {
65 | currentQueue[index].reject(item.reason);
66 | } else {
67 | currentQueue[index].resolve(item.value);
68 | }
69 | });
70 | }
71 | });
72 | }
73 | sendMessage(request: any): Promise {
74 | return new Promise((resolve, reject) => {
75 | this.messageQueue.push({ request, resolve, reject });
76 | this.startSendMessage();
77 | });
78 | }
79 |
80 | other(request: any) {
81 | return this.sendMessage(request);
82 | }
83 |
84 | background(request: any) {
85 | return this.sendMessage({ ...request, method: 'notifyBackground', reason: request.method });
86 | }
87 |
88 | async tabs(request: any, filterTab?: (tab: Tabs.Tab) => boolean) {
89 | if (IS_ANDROID) {
90 | const tabs = await browser.tabs.query({});
91 |
92 | return Promise.all(
93 | tabs.map((tab: Tabs.Tab) => {
94 | if (!canAccess(tab.url)) {
95 | return Promise.resolve();
96 | }
97 | if (filterTab && !filterTab(tab)) {
98 | return Promise.resolve();
99 | }
100 | return browser.tabs.sendMessage(tab.id!, request);
101 | }),
102 | );
103 | }
104 |
105 | // notify other tabs
106 | const windows = await browser.windows.getAll({ populate: true });
107 | return Promise.all(
108 | windows.map((win) => {
109 | if (!win.tabs) {
110 | return Promise.resolve();
111 | }
112 | return Promise.all(
113 | win.tabs.map((tab) => {
114 | if (!canAccess(tab.url)) {
115 | return Promise.resolve();
116 | }
117 | if (filterTab && !filterTab(tab)) {
118 | return Promise.resolve();
119 | }
120 | return browser.tabs.sendMessage(tab.id!, request);
121 | }),
122 | );
123 | }),
124 | );
125 | }
126 | }
127 |
128 | const notify = new Notify();
129 |
130 | export default notify;
131 |
--------------------------------------------------------------------------------
/src/share/core/prefs.ts:
--------------------------------------------------------------------------------
1 | import equal from 'fast-deep-equal';
2 | import browser from 'webextension-polyfill';
3 | import emitter from './emitter';
4 | import { defaultPrefValue } from './constant';
5 | import { getSync } from './storage';
6 | import type { PrefValue } from './types';
7 |
8 | class Prefs {
9 | private boundMethods: { [key: string]: (value: any) => any } = {};
10 | private boundWrappers: { [key: string]: any } = {};
11 | // when browser is strarting up, the setting is default
12 | private isDefault = true;
13 | private values: PrefValue;
14 |
15 | constructor() {
16 | this.values = { ...defaultPrefValue };
17 |
18 | Object.entries(defaultPrefValue).forEach((it) => {
19 | this.set(it[0], it[1], true);
20 | });
21 |
22 | getSync()
23 | .get('settings')
24 | .then((result) => {
25 | const synced = result.settings;
26 | for (const key in defaultPrefValue) {
27 | if (synced && key in synced) {
28 | this.set(key, synced[key], true);
29 | } else {
30 | const value = tryMigrating(key);
31 | if (value !== undefined) {
32 | this.set(key, value);
33 | }
34 | }
35 | }
36 | this.isDefault = false;
37 | emitter.emit(emitter.EVENT_PREFS_READY);
38 | });
39 |
40 | browser.storage.onChanged.addListener((changes, area) => {
41 | if (area === 'sync' && 'settings' in changes) {
42 | const synced = changes.settings.newValue;
43 | if (synced) {
44 | for (const key in defaultPrefValue) {
45 | if (key in synced) {
46 | this.set(key, synced[key], true);
47 | }
48 | }
49 | } else {
50 | // user manually deleted our settings, we'll recreate them
51 | getSync().set({ settings: this.values });
52 | }
53 | }
54 | });
55 |
56 | function tryMigrating(key: string) {
57 | if (!(key in localStorage)) {
58 | return undefined;
59 | }
60 | const value = localStorage[key];
61 | delete localStorage[key];
62 | localStorage[`DEPRECATED: ${key}`] = value;
63 | switch (typeof defaultPrefValue[key]) {
64 | case 'boolean':
65 | return value.toLowerCase() === 'true';
66 | case 'number':
67 | return Number(value);
68 | case 'object':
69 | try {
70 | return JSON.parse(value);
71 | } catch (e) {
72 | console.error("Cannot migrate from localStorage %s = '%s': %o", key, value, e);
73 | return undefined;
74 | }
75 | }
76 | return value;
77 | }
78 | }
79 | get(key: string, defaultValue?: any) {
80 | if (key in this.boundMethods) {
81 | if (key in this.boundWrappers) {
82 | return this.boundWrappers[key];
83 | } else if (key in this.values) {
84 | this.boundWrappers[key] = this.boundMethods[key](this.values[key]);
85 | return this.boundWrappers[key];
86 | }
87 | }
88 | if (key in this.values) {
89 | return this.values[key];
90 | }
91 | if (defaultValue !== undefined) {
92 | return defaultValue;
93 | }
94 | if (key in defaultPrefValue) {
95 | return defaultPrefValue[key];
96 | }
97 | console.warn(`No default preference for ${key}`);
98 | }
99 | getAll() {
100 | return { ...this.values };
101 | }
102 | set(key: string, value: any, noSync = false) {
103 | const oldValue = this.values[key];
104 | if (!equal(value, oldValue)) {
105 | this.values[key] = value;
106 | emitter.emit(emitter.EVENT_PREFS_UPDATE, key, value);
107 | if (!noSync) {
108 | getSync().set({
109 | settings: this.values,
110 | });
111 | }
112 | }
113 | }
114 | bindAPI(apiName: string, apiMethod: (value: any) => any) {
115 | this.boundMethods[apiName] = apiMethod;
116 | }
117 | remove(key: string) {
118 | this.set(key, undefined);
119 | }
120 | ready(cb: () => void) {
121 | if (!this.isDefault) {
122 | cb();
123 | } else {
124 | emitter.once(emitter.EVENT_PREFS_READY, cb);
125 | }
126 | }
127 | }
128 |
129 | interface BackgroundWindow extends Window {
130 | prefs?: Prefs;
131 | }
132 | const backgroundWindow = browser.extension.getBackgroundPage() as BackgroundWindow;
133 | export const prefs = backgroundWindow && backgroundWindow.prefs ? backgroundWindow.prefs : new Prefs();
134 |
--------------------------------------------------------------------------------
/src/share/core/rule-utils.ts:
--------------------------------------------------------------------------------
1 | import { getDomain } from './utils';
2 | import { isBasicRule } from './types';
3 | import { IS_MATCH, TABLE_NAMES_ARR } from './constant';
4 | import type { InitdRule, Rule, BasicRule } from './types';
5 |
6 | export function initRule(rule: Rule): InitdRule {
7 | const initd: any = { ...rule };
8 | if (initd.isFunction) {
9 | // eslint-disable-next-line no-new-func
10 | initd._func = new Function('val', 'detail', initd.code);
11 | }
12 | // Init regexp
13 | if (initd.matchType === 'regexp') {
14 | initd._reg = new RegExp(initd.pattern, 'g');
15 | }
16 | if (typeof initd.exclude === 'string' && initd.exclude.length > 0) {
17 | initd._exclude = new RegExp(initd.exclude);
18 | }
19 | return initd;
20 | }
21 |
22 | export function createExport(arr: { [key: string]: Array }) {
23 | const result: { [key: string]: BasicRule[] } = {};
24 | Object.keys(arr).forEach((k) => {
25 | result[k] = arr[k].map((e) => convertToBasicRule(e));
26 | });
27 | return result;
28 | }
29 |
30 | export function convertToRule(rule: InitdRule | Rule): Rule {
31 | const item = { ...rule };
32 | delete item._reg;
33 | delete item._func;
34 | delete item._v_key;
35 | return item;
36 | }
37 |
38 | export function convertToBasicRule(rule: InitdRule | Rule | BasicRule): BasicRule {
39 | if (isBasicRule(rule)) {
40 | return rule;
41 | }
42 | const item = convertToRule(rule) as BasicRule;
43 | delete item.id;
44 | return item;
45 | }
46 |
47 | export function fromJson(str: string) {
48 | const list: { [key: string]: Rule[] } = JSON.parse(str);
49 | TABLE_NAMES_ARR.forEach((e) => {
50 | if (list[e]) {
51 | list[e].map((ee: BasicRule) => {
52 | delete ee.id;
53 | return upgradeRuleFormat(ee);
54 | });
55 | }
56 | });
57 | return list;
58 | }
59 |
60 | export function upgradeRuleFormat(s: any) {
61 | if (typeof s.matchType === 'undefined') {
62 | s.matchType = s.type;
63 | delete s.type;
64 | }
65 | if (typeof s.isFunction === 'undefined') {
66 | s.isFunction = false;
67 | } else {
68 | s.isFunction = !!s.isFunction;
69 | }
70 | if (typeof s.enable === 'undefined') {
71 | s.enable = true;
72 | } else {
73 | s.enable = !!s.enable;
74 | }
75 | if ((s.ruleType === 'modifySendHeader' || s.ruleType === 'modifyReceiveHeader') && !s.isFunction) {
76 | s.action.name = s.action.name.toLowerCase();
77 | }
78 | return s;
79 | }
80 |
81 | export function isMatchUrl(rule: InitdRule, url: string): IS_MATCH {
82 | let result = false;
83 | switch (rule.matchType) {
84 | case 'all':
85 | result = true;
86 | break;
87 | case 'regexp':
88 | rule._reg.lastIndex = 0;
89 | result = rule._reg.test(url);
90 | break;
91 | case 'prefix':
92 | result = url.indexOf(rule.pattern) === 0;
93 | break;
94 | case 'domain':
95 | result = getDomain(url) === rule.pattern;
96 | break;
97 | case 'url':
98 | result = url === rule.pattern;
99 | break;
100 | default:
101 | break;
102 | }
103 | if (!result) {
104 | return IS_MATCH.NOT_MATCH;
105 | }
106 |
107 | if (rule._exclude) {
108 | return rule._exclude.test(url) ? IS_MATCH.MATCH_BUT_EXCLUDE : IS_MATCH.MATCH;
109 | }
110 |
111 | return IS_MATCH.MATCH;
112 | }
113 |
--------------------------------------------------------------------------------
/src/share/core/storage.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 |
3 | export function getSync() {
4 | // For development mode
5 | if (typeof localStorage !== 'undefined' && localStorage.getItem('storage') === 'local') {
6 | return browser.storage.local;
7 | }
8 | try {
9 | if ('sync' in browser.storage) {
10 | return browser.storage.sync;
11 | }
12 | } catch (e) {
13 | // Do nothing
14 | }
15 | return browser.storage.local;
16 | }
17 |
18 | export function getLocal() {
19 | return browser.storage.local;
20 | }
21 |
--------------------------------------------------------------------------------
/src/share/core/types.ts:
--------------------------------------------------------------------------------
1 | import type { RULE_MATCH_TYPE, RULE_TYPE } from './constant';
2 |
3 | export interface RuleFilterOptions {
4 | enable?: boolean;
5 | url?: string;
6 | id?: number | number[];
7 | name?: string;
8 | }
9 |
10 | export type RULE_ACTION =
11 | | 'cancel'
12 | | {
13 | name: string;
14 | value: string;
15 | };
16 |
17 | export interface BasicRule {
18 | [key: string]: any;
19 | enable: boolean;
20 | name: string;
21 | ruleType: RULE_TYPE;
22 | matchType: RULE_MATCH_TYPE;
23 | pattern: string;
24 | isFunction: boolean;
25 | code: string;
26 | exclude: string;
27 | group: string;
28 | encoding?: string;
29 | to?: string;
30 | action: RULE_ACTION;
31 | }
32 |
33 | export function isBasicRule(obj: any): obj is BasicRule {
34 | return !obj.id && !!obj.ruleType;
35 | }
36 |
37 | export interface Rule extends BasicRule {
38 | id: number;
39 | }
40 |
41 | export interface ImportRule extends Rule {
42 | importAction: number;
43 | importOldId: number;
44 | }
45 |
46 | export interface InitdRule extends Rule {
47 | _reg: RegExp;
48 | _exclude?: RegExp;
49 | _func: (val: any, detail: any) => any;
50 | }
51 |
52 | export interface PrefValue {
53 | [key: string]: any;
54 | 'disable-all': boolean;
55 | 'manage-collapse-group': boolean; // Collapse groups
56 | 'exclude-he': boolean; // rules take no effect on HE or not
57 | 'show-common-header': boolean;
58 | 'include-headers': boolean; // Include headers in custom function
59 | 'modify-body': boolean; // Enable modify received body feature
60 | 'is-debug': boolean;
61 | 'dark-mode': 'auto' | 'on' | 'off';
62 | }
63 |
--------------------------------------------------------------------------------
/src/share/core/utils.ts:
--------------------------------------------------------------------------------
1 | import browser, { Tabs } from 'webextension-polyfill';
2 | import { RULE_TYPE, TABLE_NAMES } from './constant';
3 | import { Rule } from './types';
4 |
5 | export const IS_ANDROID = navigator.userAgent.includes('Android');
6 | export const IS_CHROME = /Chrome\/(\d+)\.(\d+)/.test(navigator.userAgent);
7 | export const CHROME_VERSION = IS_CHROME
8 | ? (() => {
9 | const a = navigator.userAgent.match(/Chrome\/(\d+)\.(\d+)/);
10 | return a ? parseFloat(`${a[1]}.${a[2]}`) : 0;
11 | })()
12 | : 0;
13 | export const IS_FIREFOX = !IS_CHROME;
14 | export const FIREFOX_VERSION = IS_FIREFOX
15 | ? (() => {
16 | const a = navigator.userAgent.match(/Firefox\/(\d+)\.(\d+)/);
17 | return a ? parseFloat(`${a[1]}.${a[2]}`) : 0;
18 | })()
19 | : 0;
20 |
21 | export const IS_SUPPORT_STREAM_FILTER = typeof browser.webRequest.filterResponseData === 'function';
22 |
23 | // Get Active Tab
24 | export function getActiveTab(): Promise {
25 | return new Promise((resolve) => {
26 | browser.tabs
27 | .query({ currentWindow: true, active: true })
28 | .then((tabs) => tabs[0])
29 | .then(resolve);
30 | });
31 | }
32 | export function trimNewLines(s: string) {
33 | return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
34 | }
35 |
36 | interface FetchUrlParam {
37 | post?: any;
38 | query?: any;
39 | url: string;
40 | header?: { [key: string]: string };
41 | }
42 | export function fetchUrl(param: FetchUrlParam): Promise {
43 | return new Promise((resolve, reject) => {
44 | const fetchParam: RequestInit = {
45 | method: param.post ? 'POST' : 'GET',
46 | };
47 | const headers: Record = {};
48 | let { url } = param;
49 | if (param.query) {
50 | url += `?${new URLSearchParams(param.query).toString()}`;
51 | }
52 | if (fetchParam.method === 'POST') {
53 | // 遍历一下,查找是否有File
54 | let hasFile = false;
55 | for (const name in param.post) {
56 | if (param.post[name] instanceof File) {
57 | hasFile = true;
58 | break;
59 | }
60 | }
61 | if (hasFile) {
62 | const formBody = new FormData();
63 | for (const name in param.post) {
64 | if (param.post[name] instanceof File) {
65 | formBody.append(name, param.post[name], param.post[name].name);
66 | } else {
67 | formBody.append(name, param.post[name]);
68 | }
69 | }
70 | fetchParam.body = formBody;
71 | } else {
72 | headers['Content-Type'] = 'application/x-www-form-urlencoded';
73 | fetchParam.body = new URLSearchParams(param.post).toString();
74 | }
75 | }
76 | if (param.header) {
77 | Object.keys(param.header).forEach((name) => {
78 | headers[name] = param.header![name];
79 | });
80 | }
81 | fetchParam.headers = headers;
82 | fetch(url, fetchParam)
83 | .then((r) => r.text())
84 | .then(resolve)
85 | .catch(reject);
86 | });
87 | }
88 |
89 | export function getTableName(ruleType: RULE_TYPE): TABLE_NAMES {
90 | switch (ruleType) {
91 | case 'cancel':
92 | case 'redirect':
93 | return TABLE_NAMES.request;
94 | case 'modifySendHeader':
95 | return TABLE_NAMES.sendHeader;
96 | case 'modifyReceiveHeader':
97 | return TABLE_NAMES.receiveHeader;
98 | case 'modifyReceiveBody':
99 | return TABLE_NAMES.receiveBody;
100 | default:
101 | return TABLE_NAMES.request;
102 | }
103 | }
104 |
105 | export function canAccess(url?: string) {
106 | if (!url) {
107 | return true;
108 | }
109 | // only http, https, file, extension allowed
110 | if (
111 | url.indexOf('http') !== 0 &&
112 | url.indexOf('file') !== 0 &&
113 | url.indexOf('moz-extension') !== 0 &&
114 | url.indexOf('chrome-extension') !== 0 &&
115 | url.indexOf('ftp') !== 0
116 | ) {
117 | return false;
118 | }
119 | // other extensions can't be styled
120 | if (
121 | (url.indexOf('moz-extension') === 0 || url.indexOf('chrome-extension') === 0) &&
122 | url.indexOf(browser.runtime.getURL('')) !== 0
123 | ) {
124 | return false;
125 | }
126 | if (IS_CHROME && url.indexOf('https://chrome.google.com/webstore') === 0) {
127 | return false;
128 | }
129 | return true;
130 | }
131 |
132 | export function t(key: string, params?: any) {
133 | const s = browser.i18n.getMessage(key, params);
134 | return s || key;
135 | }
136 |
137 | export function getDomain(url: string) {
138 | if (url.indexOf('file:') === 0) {
139 | return '';
140 | }
141 | const d = /.*?:\/*([^/:]+)/.exec(url);
142 | return d ? d[1] : null;
143 | }
144 |
145 | export function getGlobal() {
146 | if (typeof window !== 'undefined') {
147 | return window;
148 | }
149 | return globalThis;
150 | }
151 |
152 | export function isBackground() {
153 | if (typeof window === 'undefined') {
154 | return true;
155 | }
156 | return typeof window.IS_BACKGROUND !== 'undefined';
157 | }
158 |
159 | export function getVirtualKey(rule: Rule) {
160 | return `${getTableName(rule.ruleType)}-${rule.id}`;
161 | }
162 |
163 | export function parseVirtualKey(key: string) {
164 | const [table, id] = key.split('-');
165 | return {
166 | table: table as TABLE_NAMES,
167 | id: Number(id),
168 | };
169 | }
170 |
--------------------------------------------------------------------------------
/src/share/hooks/use-mark-common.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef, useState, useEffect } from 'react';
2 | import { getLocal } from '@/share/core/storage';
3 |
4 | type CommonMarkType = 'rule' | 'group';
5 | const useMarkCommon = (type: CommonMarkType) => {
6 | const ready = useRef(false);
7 | const [keys, setKeys] = useState([]);
8 | const storageKey = `common_${type}`;
9 |
10 | useEffect(() => {
11 | const local = getLocal();
12 |
13 | local.get(storageKey)
14 | .then((result) => {
15 | if (Array.isArray(result[storageKey])) {
16 | setKeys(result[storageKey]);
17 | }
18 | ready.current = true;
19 | });
20 |
21 | const handleChange = (changes: any) => {
22 | if (typeof changes[storageKey] === 'undefined') {
23 | return;
24 | }
25 | setKeys(changes[storageKey].newValue);
26 | };
27 |
28 | if (local.onChanged) {
29 | local.onChanged.addListener(handleChange);
30 | }
31 |
32 | return () => {
33 | ready.current = false;
34 | if (local.onChanged) {
35 | local.onChanged.removeListener(handleChange);
36 | }
37 | };
38 | }, []);
39 |
40 | const add = useCallback((key: string) => {
41 | if (!ready.current) {
42 | return;
43 | }
44 | setKeys((currentKeys) => {
45 | if (currentKeys.includes(key)) {
46 | return currentKeys;
47 | }
48 | const result = [...currentKeys, key];
49 | getLocal().set({ [storageKey]: result });
50 | return result;
51 | });
52 | }, []);
53 |
54 | const remove = useCallback((key: string) => {
55 | if (!ready.current) {
56 | return;
57 | }
58 | setKeys((currentKeys) => {
59 | if (!currentKeys.includes(key)) {
60 | return currentKeys;
61 | }
62 | const result = [...currentKeys];
63 | result.splice(result.indexOf(key), 1);
64 | getLocal().set({ [storageKey]: result });
65 | return result;
66 | });
67 | }, []);
68 |
69 | return {
70 | keys,
71 | add,
72 | remove,
73 | };
74 | };
75 |
76 | export default useMarkCommon;
77 |
--------------------------------------------------------------------------------
/src/share/pages/api.ts:
--------------------------------------------------------------------------------
1 | import notify from '@/share/core/notify';
2 | import type { Rule, BasicRule } from '@/share/core/types';
3 | import { isBasicRule } from '@/share/core/types';
4 | import { APIs } from '@/share/core/constant';
5 | import { convertToRule } from '../core/rule-utils';
6 | import { TABLE_NAMES, TABLE_NAMES_ARR } from '../core/constant';
7 | import type { RuleFilterOptions } from '../core/types';
8 |
9 | /**
10 | * Background API封装
11 | */
12 | const Api = {
13 | openURL(url: string) {
14 | return notify.background({
15 | method: APIs.OPEN_URL,
16 | url,
17 | });
18 | },
19 | updateCache(type: TABLE_NAMES | 'all') {
20 | return notify.background({
21 | method: APIs.UPDATE_CACHE,
22 | type,
23 | });
24 | },
25 | getRules(type: TABLE_NAMES, options?: RuleFilterOptions): Promise {
26 | return notify.background({
27 | method: APIs.GET_RULES,
28 | type,
29 | options,
30 | });
31 | },
32 | getAllRules(): Promise<{ [x: string]: Rule[] }> {
33 | return Promise.all(TABLE_NAMES_ARR.map((k) => this.getRules(k))).then((res) => {
34 | const result: any = {};
35 | res.forEach((it, index) => {
36 | result[TABLE_NAMES_ARR[index]] = it;
37 | });
38 | return result;
39 | });
40 | },
41 | saveRule(rule: Rule | BasicRule) {
42 | return notify.background({
43 | method: APIs.SAVE_RULE,
44 | rule: isBasicRule(rule) ? rule : convertToRule(rule),
45 | });
46 | },
47 | removeRule(table: TABLE_NAMES, id: number) {
48 | return notify.background({
49 | method: APIs.DELETE_RULE,
50 | type: table,
51 | id,
52 | });
53 | },
54 | setPrefs(key: string, value: any) {
55 | return notify.background({
56 | method: APIs.SET_PREFS,
57 | key,
58 | value,
59 | });
60 | },
61 | };
62 |
63 | export default Api;
64 |
--------------------------------------------------------------------------------
/src/share/pages/browser-sync.ts:
--------------------------------------------------------------------------------
1 | import { TABLE_NAMES_ARR } from '../core/constant';
2 | import { getSync } from '../core/storage';
3 | import { IS_CHROME } from '../core/utils';
4 | import type { BasicRule } from '../core/types';
5 |
6 | function getTotalCount(rules: { [key: string]: BasicRule[] }) {
7 | let count = 0;
8 | TABLE_NAMES_ARR.forEach((e) => {
9 | count += rules[e].length;
10 | });
11 | return count;
12 | }
13 |
14 | interface SyncMeta {
15 | time: number;
16 | index: number;
17 | }
18 |
19 | class BrowserSync {
20 | save(rules: { [key: string]: BasicRule[] }) {
21 | if (IS_CHROME) {
22 | const toSave: { [key: string]: any } = {};
23 | // split
24 | // @ts-ignore
25 | const limit = chrome.storage.sync.QUOTA_BYTES_PER_ITEM - 500;
26 | let index = 0;
27 | while (getTotalCount(rules) > 0) {
28 | const one: { [key: string]: BasicRule[] } = {};
29 | TABLE_NAMES_ARR.forEach((e) => {
30 | one[e] = [];
31 | });
32 | let t = 0;
33 | let toPut: BasicRule | null = null;
34 | while (JSON.stringify(one).length < limit) {
35 | // find available
36 | while (TABLE_NAMES_ARR[t] && rules[TABLE_NAMES_ARR[t]].length === 0) {
37 | t++;
38 | }
39 | if (!TABLE_NAMES_ARR[t]) {
40 | break;
41 | }
42 | toPut = rules[TABLE_NAMES_ARR[t]].splice(0, 1)[0];
43 | one[TABLE_NAMES_ARR[t]].push(toPut);
44 | }
45 | if (TABLE_NAMES_ARR[t] && toPut) {
46 | rules[TABLE_NAMES_ARR[t]].push(toPut);
47 | one[TABLE_NAMES_ARR[t]].splice(one[TABLE_NAMES_ARR[t]].indexOf(toPut), 1);
48 | }
49 | toSave[`backup_${index++}`] = one;
50 | }
51 | toSave.backup = {
52 | time: new Date().getTime(),
53 | index: index - 1,
54 | };
55 | return getSync().set(toSave);
56 | }
57 |
58 | return getSync().set({
59 | backup: {
60 | time: new Date().getTime(),
61 | index: 0,
62 | },
63 | backup_0: rules,
64 | });
65 | }
66 | async getMeta(): Promise {
67 | const e = await (getSync().get('backup'));
68 | return e.backup;
69 | }
70 | async getContent(): Promise<{ [key: string]: BasicRule[] }> {
71 | const e = await (getSync().get('backup'));
72 | const { index } = e.backup;
73 | const result: { [key: string]: BasicRule[] } = {};
74 | TABLE_NAMES_ARR.forEach((it) => {
75 | result[it] = [];
76 | });
77 | const toGet: string[] = [];
78 | for (let i = 0; i <= index; i++) {
79 | toGet.push(`backup_${i}`);
80 | }
81 | const res = await (getSync().get(toGet));
82 | toGet.forEach((name) => {
83 | TABLE_NAMES_ARR.forEach((it) => {
84 | result[it] = result[it].concat(res[name][it]);
85 | });
86 | });
87 | return result;
88 | }
89 | async clear() {
90 | const toRemove = ['backup'];
91 | const e = await (getSync().get('backup'));
92 | if (e.backup) {
93 | const { index } = e.backup;
94 | const result: { [key: string]: BasicRule[] } = {};
95 | TABLE_NAMES_ARR.forEach((it) => {
96 | result[it] = [];
97 | });
98 | for (let i = 0; i <= index; i++) {
99 | toRemove.push(`backup_${i}`);
100 | }
101 | }
102 | await getSync().remove(toRemove);
103 | }
104 | }
105 |
106 | export default new BrowserSync();
107 |
--------------------------------------------------------------------------------
/src/share/pages/file.ts:
--------------------------------------------------------------------------------
1 | class File {
2 | save(text: string, fileName: string) {
3 | const blob = new Blob([text]);
4 | const fileUrl = URL.createObjectURL(blob);
5 | const link = document.createElement('a');
6 | link.href = fileUrl;
7 | link.download = fileName;
8 | link.style.display = 'none';
9 | link.target = '_blank';
10 | document.body.appendChild(link);
11 | link.click();
12 | setTimeout(() => link.remove(), 500);
13 | }
14 | load(formatToFilter: string): Promise {
15 | return new Promise((resolve) => {
16 | const fileInput = document.createElement('input');
17 | fileInput.style.display = 'none';
18 | fileInput.type = 'file';
19 | fileInput.accept = formatToFilter || '.json';
20 | // @ts-ignore
21 | fileInput.acceptCharset = 'utf8';
22 |
23 | document.body.appendChild(fileInput);
24 |
25 | function changeHandler() {
26 | if (fileInput.files && fileInput.files.length > 0) {
27 | const fReader = new FileReader();
28 | fReader.readAsText(fileInput.files[0]);
29 | fReader.onloadend = (event) => {
30 | fileInput.removeEventListener('change', changeHandler);
31 | fileInput.remove();
32 | const result = event.target!.result as string;
33 | resolve(result || '');
34 | };
35 | }
36 | }
37 |
38 | fileInput.addEventListener('change', changeHandler);
39 | fileInput.click();
40 | });
41 | }
42 | }
43 |
44 | const file = new File();
45 |
46 | export default file;
47 |
--------------------------------------------------------------------------------
/src/share/pages/is-dark-mode.ts:
--------------------------------------------------------------------------------
1 | import { prefs } from '@/share/core/prefs';
2 |
3 | const isDarkMode = () => {
4 | const darkMode = prefs.get('dark-mode');
5 | switch (darkMode) {
6 | case 'auto':
7 | try {
8 | const mql = window.matchMedia('(prefers-color-scheme: dark)');
9 | return mql.matches;
10 | } catch (e) {
11 | // ignore
12 | }
13 | break;
14 | case 'on':
15 | return true;
16 | default:
17 | break;
18 | }
19 | return false;
20 | };
21 |
22 | export default isDarkMode;
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "buildOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "outDir": "build",
7 | "module": "esnext",
8 | "target": "es2020",
9 | "jsx": "react",
10 | "moduleResolution": "node",
11 | "allowSyntheticDefaultImports": true,
12 | "lib": ["es2020", "dom"],
13 | "sourceMap": true,
14 | "allowJs": true,
15 | "rootDir": "./",
16 | "forceConsistentCasingInFileNames": true,
17 | "noImplicitReturns": true,
18 | "noImplicitThis": true,
19 | "noImplicitAny": false,
20 | "importHelpers": true,
21 | "strictNullChecks": true,
22 | "suppressImplicitAnyIndexErrors": true,
23 | "noUnusedLocals": true,
24 | "skipLibCheck": true,
25 | "types": ["node"],
26 | "paths": {
27 | "@/*": ["./src/*"],
28 | "ice": [".ice/index.ts"],
29 | "ice/*": [".ice/pages/*"]
30 | }
31 | },
32 | "include": ["src", ".ice"],
33 | "exclude": ["node_modules", "build", "public"]
34 | }
--------------------------------------------------------------------------------