迁移部分内容
Some checks failed
test / Run tests (push) Failing after 55s
build / Build (push) Failing after 1m18s
Some checks failed
test / Run tests (push) Failing after 55s
build / Build (push) Failing after 1m18s
This commit is contained in:
parent
c2e5c7600e
commit
af15d67cb3
45
.github/workflows/build.yaml
vendored
Normal file
45
.github/workflows/build.yaml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/*
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Build
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Package with Node
|
||||||
|
env:
|
||||||
|
CHROME_PEM: ${{ secrets.CHROME_PEM }}
|
||||||
|
run: |
|
||||||
|
mkdir dist
|
||||||
|
echo "$CHROME_PEM" > ./dist/scriptcat.pem
|
||||||
|
chmod 600 ./dist/scriptcat.pem
|
||||||
|
npm ci
|
||||||
|
npm run pack
|
||||||
|
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: all-artifacts
|
||||||
|
path: |
|
||||||
|
dist/*.zip
|
||||||
|
dist/*.crx
|
||||||
|
|
||||||
|
- name: Archive extension
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: scriptcat
|
||||||
|
path: |
|
||||||
|
dist/ext/*
|
76
.github/workflows/packageRelease.yml
vendored
Normal file
76
.github/workflows/packageRelease.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
name: Auto_Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Package with Node
|
||||||
|
env:
|
||||||
|
CHROME_PEM: ${{ secrets.CHROME_PEM }}
|
||||||
|
run: |
|
||||||
|
mkdir dist
|
||||||
|
echo "$CHROME_PEM" > ./dist/scriptcat.pem
|
||||||
|
chmod 600 ./dist/scriptcat.pem
|
||||||
|
npm ci
|
||||||
|
npm test
|
||||||
|
npm run pack
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@latest
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
body: |
|
||||||
|
'no description'
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Asset zip
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/scriptcat-${{ github.ref_name }}-chrome.zip
|
||||||
|
asset_name: scriptcat-${{ github.ref_name }}-chrome.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload FireFox Release Asset zip
|
||||||
|
id: upload-firefox-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/scriptcat-${{ github.ref_name }}-firefox.zip
|
||||||
|
asset_name: scriptcat-${{ github.ref_name }}-firefox.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload Crx Release Asset zip
|
||||||
|
id: upload-crx-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/scriptcat-${{ github.ref_name }}-chrome.crx
|
||||||
|
asset_name: scriptcat-${{ github.ref_name }}-chrome.crx
|
||||||
|
asset_content_type: application/zip
|
30
.github/workflows/test.yaml
vendored
Normal file
30
.github/workflows/test.yaml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/*
|
||||||
|
- dev
|
||||||
|
- develop/*
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Run tests
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Unit Test
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm test
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov with GitHub Action
|
||||||
|
uses: codecov/codecov-action@v3
|
260
eslint/compat-grant.js
Normal file
260
eslint/compat-grant.js
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
// Fork from eslint-plugin-userscripts
|
||||||
|
// Documentation:
|
||||||
|
// - Tampermonkey: https://www.tampermonkey.net/documentation.php#_grant
|
||||||
|
// - Violentmonkey: https://violentmonkey.github.io/api/gm
|
||||||
|
// - Greasemonkey: https://wiki.greasespot.net/Greasemonkey_Manual:API
|
||||||
|
// - ScriptCat: https://docs.scriptcat.org/docs/dev/cat-api/
|
||||||
|
const compatMap = {
|
||||||
|
CAT_userConfig: [{ type: "scriptcat", versionConstraint: ">=0.11.0-beta" }],
|
||||||
|
CAT_fileStorage: [{ type: "scriptcat", versionConstraint: ">=0.11.0" }],
|
||||||
|
"GM.addElement": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.11.6113" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.13.0-beta.3" },
|
||||||
|
],
|
||||||
|
GM_addElement: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.11.6113" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.13.0-beta.3" },
|
||||||
|
],
|
||||||
|
"GM.addStyle": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
],
|
||||||
|
GM_addStyle: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.6.1.4 <4" },
|
||||||
|
],
|
||||||
|
"GM.addValueChangeListener": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
],
|
||||||
|
GM_addValueChangeListener: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.3.2607" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
],
|
||||||
|
"GM.cookie": [{ type: "tampermonkey", versionConstraint: ">=4.8" }],
|
||||||
|
GM_cookie: [{ type: "tampermonkey", versionConstraint: ">=4.8" }],
|
||||||
|
"GM.deleteValue": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4.0" },
|
||||||
|
],
|
||||||
|
GM_deleteValue: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.8.20090123.1 <4" },
|
||||||
|
],
|
||||||
|
"GM.download": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||||
|
GM_download: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.8" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.9.5" },
|
||||||
|
],
|
||||||
|
"GM.getResourceText": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||||
|
GM_getResourceText: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.8.20080609.0 <4" },
|
||||||
|
],
|
||||||
|
"GM.getResourceURL": [
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0 <2.13.0.10" },
|
||||||
|
],
|
||||||
|
GM_getResourceURL: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.8.20080609.0 <4" },
|
||||||
|
],
|
||||||
|
"GM.getResourceUrl": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.13.0.10" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4.0" },
|
||||||
|
],
|
||||||
|
"GM.getTab": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||||
|
GM_getTab: [{ type: "tampermonkey", versionConstraint: ">=4.0.10" }],
|
||||||
|
"GM.getTabs": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||||
|
GM_getTabs: [{ type: "tampermonkey", versionConstraint: ">=4.0.10" }],
|
||||||
|
"GM.getValue": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4.0" },
|
||||||
|
],
|
||||||
|
GM_getValue: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.3-beta <4" },
|
||||||
|
],
|
||||||
|
"GM.info": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_info: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.4.2718" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.16 <4" },
|
||||||
|
],
|
||||||
|
"GM.listValues": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_listValues: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.8.20090123.1 <4" },
|
||||||
|
],
|
||||||
|
"GM.log": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_log: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.3-beta <4" },
|
||||||
|
],
|
||||||
|
"GM.notification": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_notification: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2344" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.5.0" },
|
||||||
|
],
|
||||||
|
"GM.openInTab": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_openInTab: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.5-beta <4" },
|
||||||
|
],
|
||||||
|
"GM.registerMenuCommand": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4.11" },
|
||||||
|
],
|
||||||
|
GM_registerMenuCommand: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.2.5 <4" },
|
||||||
|
],
|
||||||
|
"GM.removeValueChangeListener": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
],
|
||||||
|
GM_removeValueChangeListener: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.3.2607" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
],
|
||||||
|
"GM.saveTab": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||||
|
GM_saveTab: [{ type: "tampermonkey", versionConstraint: ">=4.0.10" }],
|
||||||
|
"GM.setClipboard": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_setClipboard: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.6.2767" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.5.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=1.10 <4" },
|
||||||
|
],
|
||||||
|
"GM.setValue": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4" },
|
||||||
|
],
|
||||||
|
GM_setValue: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.3-beta <4" },
|
||||||
|
],
|
||||||
|
"GM.unregisterMenuCommand": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
],
|
||||||
|
GM_unregisterMenuCommand: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.6.3737" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.9.4" },
|
||||||
|
],
|
||||||
|
"GM.webRequest": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||||
|
GM_webRequest: [{ type: "tampermonkey", versionConstraint: ">=4.4" }],
|
||||||
|
GM_xmlhttpRequest: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.2.5 <4" },
|
||||||
|
],
|
||||||
|
"GM.xmlHttpRequest": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=4.0" },
|
||||||
|
],
|
||||||
|
none: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
unsafeWindow: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.5-beta" },
|
||||||
|
],
|
||||||
|
"window.close": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.12.58" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.6.2" },
|
||||||
|
],
|
||||||
|
"window.focus": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.12.58" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.10" },
|
||||||
|
],
|
||||||
|
"window.onurlchange": [{ type: "tampermonkey", versionConstraint: ">=4.11" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const gmPolyfillOverride = {
|
||||||
|
GM_addStyle: "ignore",
|
||||||
|
GM_registerMenuCommand: "ignore",
|
||||||
|
GM_getResourceText: {
|
||||||
|
deps: ["GM.getResourceUrl", "GM.log"],
|
||||||
|
},
|
||||||
|
"GM.log": "ignore",
|
||||||
|
"GM.info": {
|
||||||
|
deps: ["GM_info"],
|
||||||
|
},
|
||||||
|
"GM.addStyle": {
|
||||||
|
deps: ["GM_addStyle"],
|
||||||
|
},
|
||||||
|
"GM.deleteValue": {
|
||||||
|
deps: ["GM_deleteValue"],
|
||||||
|
},
|
||||||
|
"GM.getResourceUrl": {
|
||||||
|
deps: ["GM_getResourceURL"],
|
||||||
|
},
|
||||||
|
"GM.getValue": {
|
||||||
|
deps: ["GM_getValue"],
|
||||||
|
},
|
||||||
|
"GM.listValues": {
|
||||||
|
deps: ["GM_listValues"],
|
||||||
|
},
|
||||||
|
"GM.notification": {
|
||||||
|
deps: ["GM_notification"],
|
||||||
|
},
|
||||||
|
"GM.openInTab": {
|
||||||
|
deps: ["GM_openInTab"],
|
||||||
|
},
|
||||||
|
"GM.registerMenuCommand": {
|
||||||
|
deps: ["GM_registerMenuCommand"],
|
||||||
|
},
|
||||||
|
"GM.setClipboard": {
|
||||||
|
deps: ["GM_setClipboard"],
|
||||||
|
},
|
||||||
|
"GM.setValue": {
|
||||||
|
deps: ["GM_setValue"],
|
||||||
|
},
|
||||||
|
"GM.xmlHttpRequest": {
|
||||||
|
deps: ["GM_xmlhttpRequest"],
|
||||||
|
},
|
||||||
|
"GM.getResourceText": {
|
||||||
|
deps: ["GM_getResourceText"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.compatMap = compatMap;
|
||||||
|
module.exports.gmPolyfillOverride = gmPolyfillOverride;
|
201
eslint/compat-headers.js
Normal file
201
eslint/compat-headers.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
// Fork from eslint-plugin-userscripts
|
||||||
|
// Documentation:
|
||||||
|
// - Tampermonkey: https://www.tampermonkey.net/documentation.php
|
||||||
|
// - Violentmonkey: https://violentmonkey.github.io/api/metadata-block/
|
||||||
|
// - Greasemonkey: https://wiki.greasespot.net/Metadata_Block
|
||||||
|
// - ScriptCat: https://docs.scriptcat.org/docs/dev/
|
||||||
|
const compatMap = {
|
||||||
|
localized: {
|
||||||
|
name: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.9" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.1.6.8" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=2.2 <4 || >=4.11" },
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.9" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.1.6.8" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=2.2 <4 || >=4.11" },
|
||||||
|
],
|
||||||
|
antifeature: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.12" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.10" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
unlocalized: {
|
||||||
|
include: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
"exclude-match": [{ type: "violentmonkey", versionConstraint: ">=2.6.2" }],
|
||||||
|
version: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.0" },
|
||||||
|
],
|
||||||
|
"run-at": [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=1.1.2190" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.8" },
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.8.20080609.0" },
|
||||||
|
],
|
||||||
|
require: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.8.20080609.0" },
|
||||||
|
],
|
||||||
|
match: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=1.1.2190" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.8" },
|
||||||
|
],
|
||||||
|
"user-agent": [{ type: "tampermonkey", versionConstraint: ">=2.8.2894" }],
|
||||||
|
unwrap: [
|
||||||
|
{ type: "greasemonkey", versionConstraint: "0.8.1 - 0.9.22" },
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.14" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.13.0.16" },
|
||||||
|
],
|
||||||
|
grant: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.0.3389" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.1.6.1" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=1" },
|
||||||
|
],
|
||||||
|
noframes: [
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.8.17" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=2.3" },
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2355" },
|
||||||
|
],
|
||||||
|
connect: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.0" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.10" },
|
||||||
|
],
|
||||||
|
webRequest: [{ type: "tampermonkey", versionConstraint: ">=4.4" }],
|
||||||
|
"inject-into": [{ type: "violentmonkey", versionConstraint: ">=2.10.0" }],
|
||||||
|
domain: [], // Scriptish
|
||||||
|
nocompat: [{ type: "tampermonkey", versionConstraint: ">=2.4.2683" }],
|
||||||
|
namespace: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.2.5" },
|
||||||
|
],
|
||||||
|
sandbox: [{ type: "tampermonkey", versionConstraint: ">=4.18" }],
|
||||||
|
},
|
||||||
|
nonFunctional: {
|
||||||
|
name: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
author: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
antifeature: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=4.12" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.12.10" },
|
||||||
|
],
|
||||||
|
copyright: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: "*" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
license: [{ type: "tampermonkey", versionConstraint: "*" }],
|
||||||
|
icon: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2359" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.0" },
|
||||||
|
],
|
||||||
|
defaulticon: [{ type: "tampermonkey", versionConstraint: ">=2.0.2359" }],
|
||||||
|
icon64: [{ type: "tampermonkey", versionConstraint: ">=2.0.2359" }],
|
||||||
|
iconURL: [{ type: "tampermonkey", versionConstraint: ">=2.0.2359" }],
|
||||||
|
icon64URL: [{ type: "tampermonkey", versionConstraint: ">=2.0.2359" }],
|
||||||
|
homepage: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2395" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
],
|
||||||
|
homepageURL: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2395" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.1.5" },
|
||||||
|
],
|
||||||
|
website: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2395" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.13.1.2" },
|
||||||
|
],
|
||||||
|
source: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.0.2395" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.13.1.2" },
|
||||||
|
],
|
||||||
|
downloadURL: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.5.64" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.14" },
|
||||||
|
],
|
||||||
|
updateURL: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=2.5.64" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: "*" },
|
||||||
|
{ type: "greasemonkey", versionConstraint: ">=0.9.12" },
|
||||||
|
],
|
||||||
|
installURL: [{ type: "greasemonkey", versionConstraint: ">=0.9.2" }],
|
||||||
|
supportURL: [
|
||||||
|
{ type: "tampermonkey", versionConstraint: ">=3.8" },
|
||||||
|
{ type: "violentmonkey", versionConstraint: ">=2.1.6.2" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// OpenUserJS
|
||||||
|
collaborator: [],
|
||||||
|
unstableMinify: [],
|
||||||
|
"oujs:author": [],
|
||||||
|
"oujs:collaborator": [],
|
||||||
|
|
||||||
|
// UserScripts.org
|
||||||
|
"uso:script": [],
|
||||||
|
"uso:version": [],
|
||||||
|
"uso:timestamp": [],
|
||||||
|
"uso:hash": [],
|
||||||
|
"uso:rating": [],
|
||||||
|
"uso:installs": [],
|
||||||
|
"uso:reviews": [],
|
||||||
|
"uso:discussions": [],
|
||||||
|
"uso:fans": [],
|
||||||
|
"uso:unlisted": [],
|
||||||
|
contributor: [],
|
||||||
|
contributors: [],
|
||||||
|
major: [],
|
||||||
|
minor: [],
|
||||||
|
build: [],
|
||||||
|
|
||||||
|
// GreasyFork
|
||||||
|
contributionURL: [],
|
||||||
|
contributionAmount: [],
|
||||||
|
incompatible: [],
|
||||||
|
compatible: [{ type: "violentmonkey", versionConstraint: ">=2.12.10" }],
|
||||||
|
|
||||||
|
// Popular but not documented
|
||||||
|
history: [],
|
||||||
|
developer: [],
|
||||||
|
|
||||||
|
// SctiptCat
|
||||||
|
background: [],
|
||||||
|
crontab: [],
|
||||||
|
cloudCat: [],
|
||||||
|
cloudServer: [],
|
||||||
|
exportValue: [],
|
||||||
|
exportCookie: [],
|
||||||
|
scriptUrl: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = compatMap;
|
131
eslint/linter-config.ts
Normal file
131
eslint/linter-config.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 由于原库(eslint-plugin-userscripts)使用了 fs 模块,无法在 webpack5 中直接使用,故改写成如下形式
|
||||||
|
const userscriptsConfig = {
|
||||||
|
rules: {
|
||||||
|
"userscripts/filename-user": ["error", "always"],
|
||||||
|
"userscripts/no-invalid-metadata": ["error", { top: "required" }],
|
||||||
|
"userscripts/require-name": ["error", "required"],
|
||||||
|
"userscripts/require-description": ["error", "required"],
|
||||||
|
"userscripts/require-version": ["error", "required"],
|
||||||
|
"userscripts/require-attribute-space-prefix": "error",
|
||||||
|
"userscripts/use-homepage-and-url": "error",
|
||||||
|
"userscripts/use-download-and-update-url": "error",
|
||||||
|
"userscripts/align-attributes": ["error", 2],
|
||||||
|
"userscripts/metadata-spacing": ["error", "always"],
|
||||||
|
"userscripts/no-invalid-headers": "error",
|
||||||
|
"userscripts/no-invalid-grant": "error",
|
||||||
|
"userscripts/compat-grant": "off",
|
||||||
|
"userscripts/compat-headers": "off",
|
||||||
|
"userscripts/better-use-match": "warn",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const userscriptsRules = Object.fromEntries(
|
||||||
|
Object.keys(userscriptsConfig.rules).map((name) => {
|
||||||
|
const ruleName = name.split("/")[1];
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require, global-require
|
||||||
|
const ruleMeta = require(`eslint-plugin-userscripts/lib/rules/${ruleName}.js`);
|
||||||
|
return [
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
...ruleMeta,
|
||||||
|
meta: {
|
||||||
|
...ruleMeta.meta,
|
||||||
|
docs: {
|
||||||
|
...ruleMeta.meta.docs,
|
||||||
|
url: `https://yash-singh1.github.io/eslint-plugin-userscripts/#/rules/${ruleName}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 默认规则
|
||||||
|
const config = {
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "script",
|
||||||
|
ecmaFeatures: {
|
||||||
|
globalReturn: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
CATRetryError: "readonly",
|
||||||
|
CAT_fileStorage: "readonly",
|
||||||
|
CAT_userConfig: "readonly",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"constructor-super": ["error"],
|
||||||
|
"for-direction": ["error"],
|
||||||
|
"getter-return": ["error"],
|
||||||
|
"no-async-promise-executor": ["error"],
|
||||||
|
"no-case-declarations": ["error"],
|
||||||
|
"no-class-assign": ["error"],
|
||||||
|
"no-compare-neg-zero": ["error"],
|
||||||
|
"no-cond-assign": ["error"],
|
||||||
|
"no-const-assign": ["error"],
|
||||||
|
"no-constant-condition": ["error"],
|
||||||
|
"no-control-regex": ["error"],
|
||||||
|
"no-debugger": ["error"],
|
||||||
|
"no-delete-var": ["error"],
|
||||||
|
"no-dupe-args": ["error"],
|
||||||
|
"no-dupe-class-members": ["error"],
|
||||||
|
"no-dupe-else-if": ["error"],
|
||||||
|
"no-dupe-keys": ["error"],
|
||||||
|
"no-duplicate-case": ["error"],
|
||||||
|
"no-empty": ["error"],
|
||||||
|
"no-empty-character-class": ["error"],
|
||||||
|
"no-empty-pattern": ["error"],
|
||||||
|
"no-ex-assign": ["error"],
|
||||||
|
"no-extra-boolean-cast": ["error"],
|
||||||
|
"no-extra-semi": ["error"],
|
||||||
|
"no-fallthrough": ["error"],
|
||||||
|
"no-func-assign": ["error"],
|
||||||
|
"no-global-assign": ["error"],
|
||||||
|
"no-import-assign": ["error"],
|
||||||
|
"no-inner-declarations": ["error"],
|
||||||
|
"no-invalid-regexp": ["error"],
|
||||||
|
"no-irregular-whitespace": ["error"],
|
||||||
|
"no-loss-of-precision": ["error"],
|
||||||
|
"no-misleading-character-class": ["error"],
|
||||||
|
"no-mixed-spaces-and-tabs": ["error"],
|
||||||
|
"no-new-symbol": ["error"],
|
||||||
|
"no-nonoctal-decimal-escape": ["error"],
|
||||||
|
"no-obj-calls": ["error"],
|
||||||
|
"no-octal": ["error"],
|
||||||
|
"no-prototype-builtins": ["error"],
|
||||||
|
"no-redeclare": ["error"],
|
||||||
|
"no-regex-spaces": ["error"],
|
||||||
|
"no-self-assign": ["error"],
|
||||||
|
"no-setter-return": ["error"],
|
||||||
|
"no-shadow-restricted-names": ["error"],
|
||||||
|
"no-sparse-arrays": ["error"],
|
||||||
|
"no-this-before-super": ["error"],
|
||||||
|
"no-undef": ["warn"],
|
||||||
|
"no-unexpected-multiline": ["error"],
|
||||||
|
"no-unreachable": ["error"],
|
||||||
|
"no-unsafe-finally": ["error"],
|
||||||
|
"no-unsafe-negation": ["error"],
|
||||||
|
"no-unsafe-optional-chaining": ["error"],
|
||||||
|
"no-unused-labels": ["error"],
|
||||||
|
"no-unused-vars": ["error"],
|
||||||
|
"no-useless-backreference": ["error"],
|
||||||
|
"no-useless-catch": ["error"],
|
||||||
|
"no-useless-escape": ["error"],
|
||||||
|
"no-with": ["error"],
|
||||||
|
"require-yield": ["error"],
|
||||||
|
"use-isnan": ["error"],
|
||||||
|
"valid-typeof": ["error"],
|
||||||
|
...userscriptsConfig.rules,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
browser: true,
|
||||||
|
greasemonkey: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 以文本形式导出默认规则
|
||||||
|
const defaultConfig = JSON.stringify(config);
|
||||||
|
|
||||||
|
export { defaultConfig, userscriptsConfig, userscriptsRules };
|
50
example/cat_file_storage.js
Normal file
50
example/cat_file_storage.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name cat file storage
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 脚本同步储存空间操作
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant CAT_fileStorage
|
||||||
|
// @run-at document-start
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
CAT_fileStorage("upload", {
|
||||||
|
path: "test.txt",
|
||||||
|
baseDir: "test-dir",
|
||||||
|
data: new Blob(["Hello World"]),
|
||||||
|
onload() {
|
||||||
|
CAT_fileStorage("list", {
|
||||||
|
baseDir: "test-dir",
|
||||||
|
onload(list) {
|
||||||
|
console.log(list);
|
||||||
|
list.forEach(value => {
|
||||||
|
if (value.name === "test.txt") {
|
||||||
|
CAT_fileStorage("download", {
|
||||||
|
file: value,
|
||||||
|
baseDir: "test-dir",
|
||||||
|
async onload(data) {
|
||||||
|
console.log(await data.text());
|
||||||
|
CAT_fileStorage("delete", {
|
||||||
|
path: value.name,
|
||||||
|
baseDir: "test-dir",
|
||||||
|
onload() {
|
||||||
|
console.log('ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, onerror(err) {
|
||||||
|
console.log(err);
|
||||||
|
switch (err.code) {
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
CAT_fileStorage("config");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
16
example/cloudcat.js
Normal file
16
example/cloudcat.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name cloudscript
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 可以导出成nodejs可执行的包,在云端执行
|
||||||
|
// @author You
|
||||||
|
// @crontab * * once * *
|
||||||
|
// @cloudCat
|
||||||
|
// @exportCookie domain=.scriptscat.org
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Your code here...
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
18
example/error_retry.js
Normal file
18
example/error_retry.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 重试示例
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author You
|
||||||
|
// @crontab * * once * *
|
||||||
|
// @grant GM_notification
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Your code here...
|
||||||
|
GM_notification({
|
||||||
|
title: "retry",
|
||||||
|
text: "10秒后重试"
|
||||||
|
});
|
||||||
|
reject(new CATRetryError("xxx错误", 10));
|
||||||
|
});
|
15
example/gm_add_element.js
Normal file
15
example/gm_add_element.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm add element
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 在页面中插入元素,可以绕过CSP限制
|
||||||
|
// @author You
|
||||||
|
// @match https://github.com/scriptscat/scriptcat
|
||||||
|
// @grant GM_addElement
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const el = GM_addElement(document.querySelector('.BorderGrid-cell'), "img", {
|
||||||
|
src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1"
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(el);
|
18
example/gm_bg_menu.js
Normal file
18
example/gm_bg_menu.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name bg gm menu
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 在后台脚本中使用菜单
|
||||||
|
// @author You
|
||||||
|
// @background
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// @grant GM_unregisterMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const id = GM_registerMenuCommand("测试菜单", () => {
|
||||||
|
console.log(id);
|
||||||
|
GM_unregisterMenuCommand(id);
|
||||||
|
resolve();
|
||||||
|
}, "z");
|
||||||
|
});
|
11
example/gm_clipboard.js
Normal file
11
example/gm_clipboard.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm clipboard
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_setClipboard
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
GM_setClipboard("我爱ScriptCat");
|
44
example/gm_cookie.js
Normal file
44
example/gm_cookie.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name New Userscript
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 可以控制浏览器的cookie, 必须指定@connect, 并且每次一个新的域调用都需要用户确定
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_cookie
|
||||||
|
// @connect example.com
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
// GM_cookie("store") 方法请看gm_value.js的例子, 可用于隐身窗口的操作
|
||||||
|
|
||||||
|
GM_cookie("set", {
|
||||||
|
url: "http://example.com/cookie",
|
||||||
|
name: "cookie1", value: "value"
|
||||||
|
}, () => {
|
||||||
|
GM_cookie("set", {
|
||||||
|
url: "http://www.example.com/",
|
||||||
|
domain: ".example.com", path: "/path",
|
||||||
|
name: "cookie2", value: "path"
|
||||||
|
}, () => {
|
||||||
|
GM_cookie("list", {
|
||||||
|
domain: "example.com"
|
||||||
|
}, (cookies) => {
|
||||||
|
console.log("domain", cookies);
|
||||||
|
});
|
||||||
|
GM_cookie("list", {
|
||||||
|
url: "http://example.com/cookie",
|
||||||
|
}, (cookies) => {
|
||||||
|
console.log("domain", cookies);
|
||||||
|
});
|
||||||
|
GM_cookie("delete", {
|
||||||
|
url: "http://www.example.com/path",
|
||||||
|
name: "cookie2"
|
||||||
|
}, () => {
|
||||||
|
GM_cookie("list", {
|
||||||
|
domain: "example.com"
|
||||||
|
}, (cookies) => {
|
||||||
|
console.log("delete", cookies);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
20
example/gm_download.js
Normal file
20
example/gm_download.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm download
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_download
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
GM_download({
|
||||||
|
url: "https://scriptcat.org/api/v1/gm_crx/download/ScriptCat",
|
||||||
|
name: "scriptcat.crx",
|
||||||
|
headers: {
|
||||||
|
"referer": "http://www.example.com/",
|
||||||
|
"origin": "www.example.com"
|
||||||
|
}, onprogress(data) {
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
});
|
17
example/gm_get_resource.js
Normal file
17
example/gm_get_resource.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm get resource
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 通过@resource引用资源,这个资源会被管理器进行缓存,不可修改
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @resource bbs https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_getResourceURL
|
||||||
|
// @grant GM_getResourceText
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
|
||||||
|
console.log(GM_getResourceURL("bbs"));
|
||||||
|
console.log(GM_getResourceURL("bbs", false));
|
||||||
|
console.log(GM_getResourceURL("bbs", true));
|
||||||
|
console.log(GM_getResourceText("bbs"));
|
11
example/gm_log.js
Normal file
11
example/gm_log.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm log
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 日志功能,为你的脚本加上丰富的日志吧,支持日志分级与日志标签
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_log
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
GM_log("log message", "info", { component: "example" });
|
16
example/gm_menu.js
Normal file
16
example/gm_menu.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm menu
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 创建菜单, 可以显示在右上角的插件弹出页和浏览器右键菜单中
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// @grant GM_unregisterMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
|
||||||
|
const id = GM_registerMenuCommand("测试菜单", () => {
|
||||||
|
console.log(id);
|
||||||
|
GM_unregisterMenuCommand(id);
|
||||||
|
}, "h");
|
43
example/gm_notification.js
Normal file
43
example/gm_notification.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm notification
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 用来发送一个浏览器通知, 支持图标/文字/进度条(进度条只在 Chrome 有效)
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_notification
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
let i;
|
||||||
|
GM_notification({
|
||||||
|
title: '倒计时',
|
||||||
|
text: '准备进入倒计时,创建和获取通知id',
|
||||||
|
ondone: (byUser) => {
|
||||||
|
console.log('done user:', byUser);
|
||||||
|
clearInterval(i);
|
||||||
|
},
|
||||||
|
onclick: () => {
|
||||||
|
console.log('click');
|
||||||
|
},
|
||||||
|
oncreate: (id) => {
|
||||||
|
let t = 1;
|
||||||
|
i = setInterval(() => {
|
||||||
|
GM_updateNotification(id, {
|
||||||
|
title: '倒计时',
|
||||||
|
text: (60 - t) + 's倒计时',
|
||||||
|
progress: 100 / 60 * t
|
||||||
|
});
|
||||||
|
if (t == 60) {
|
||||||
|
clearInterval(i);
|
||||||
|
GM_updateNotification(id, {
|
||||||
|
title: '倒计时',
|
||||||
|
text: '倒计时结束',
|
||||||
|
progress: 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
t++;
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
// 开启进度条模式
|
||||||
|
progress: 0,
|
||||||
|
});
|
21
example/gm_save_tab.js
Normal file
21
example/gm_save_tab.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm get/save tab
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 用于保存当前标签页的数据, 关闭后会自动删除, 可以获取其它标签页的数据
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_saveTab
|
||||||
|
// @grant GM_getTab
|
||||||
|
// @grant GM_getTabs
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
GM_saveTab({ test: "save" });
|
||||||
|
|
||||||
|
GM_getTab(data => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
GM_getTabs(data => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
20
example/gm_tab.js
Normal file
20
example/gm_tab.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm open tab
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 打开一个标签页
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @grant GM_openInTab
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const tab = GM_openInTab("https://scriptcat.org/search");
|
||||||
|
|
||||||
|
tab.onclose = () => {
|
||||||
|
console.log("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
tab.close();
|
||||||
|
}, 3000)
|
||||||
|
|
32
example/gm_value.js
Normal file
32
example/gm_value.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm value
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 可以持久化存储数据, 并且可以监听数据变化
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @run-at document-start
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_addValueChangeListener
|
||||||
|
// @grant GM_listValues
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_cookie
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
|
||||||
|
GM_cookie("store", tabid,(storeId) => {
|
||||||
|
console.log("store",storeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
console.log(GM_getValue("test_set"));
|
||||||
|
console.log(GM_listValues());
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
GM_deleteValue("test_set");
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
GM_setValue("test_set", new Date().getTime());
|
36
example/gm_xhr.js
Normal file
36
example/gm_xhr.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name gm xhr
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 无视浏览器的cors的跨域请求,可以设置各种unsafeHeader与cookie,需要使用@connect获取权限,或者由用户确认
|
||||||
|
// @author You
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// @connect tampermonkey.net.cn
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
data.append("username", "admin");
|
||||||
|
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
url: "https://bbs.tampermonkey.net.cn/",
|
||||||
|
method: "POST",
|
||||||
|
responseType: "blob",
|
||||||
|
data: data,
|
||||||
|
headers: {
|
||||||
|
"referer": "http://www.example.com/",
|
||||||
|
"origin": "www.example.com",
|
||||||
|
// 为空将不会发送此header
|
||||||
|
"sec-ch-ua-mobile": "",
|
||||||
|
},
|
||||||
|
onload(resp) {
|
||||||
|
console.log("onload", resp);
|
||||||
|
},
|
||||||
|
onreadystatechange(resp) {
|
||||||
|
console.log("onreadystatechange", resp);
|
||||||
|
},
|
||||||
|
onloadend(resp) {
|
||||||
|
console.log("onloadend", resp);
|
||||||
|
},
|
||||||
|
});
|
92
example/userconfig.js
Normal file
92
example/userconfig.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name userconfig
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description 会在页面上显示用户配置,可以可视化的进行配置
|
||||||
|
// @author You
|
||||||
|
// @background
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant CAT_userConfig
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ==UserConfig==
|
||||||
|
group1:
|
||||||
|
configA: # 键值为group.config,例如本键为:group1.configA
|
||||||
|
title: 配置A # 配置的标题
|
||||||
|
description: 这是一个文本类型的配置 # 配置的描述内容
|
||||||
|
type: text # 选项类型,如果不填写会根据数据自动识别
|
||||||
|
default: 默认值 # 配置的默认值
|
||||||
|
min: 2 # 文本最短2个字符
|
||||||
|
max: 18 # 文本最长18个字符
|
||||||
|
password: true # 设置为密码
|
||||||
|
configB:
|
||||||
|
title: 配置B
|
||||||
|
description: 这是一个选择框的配置
|
||||||
|
type: checkbox
|
||||||
|
default: true
|
||||||
|
configC:
|
||||||
|
title: 配置C
|
||||||
|
description: 这是一个列表选择的配置
|
||||||
|
type: select
|
||||||
|
default: 1
|
||||||
|
values: [1,2,3,4,5]
|
||||||
|
configD:
|
||||||
|
title: 配置D
|
||||||
|
description: 这是一个动态列表选择的配置
|
||||||
|
type: select
|
||||||
|
bind: $cookies # 动态显示绑定的values,值是以$开头的key,value需要是一个数组
|
||||||
|
configE:
|
||||||
|
title: 配置E
|
||||||
|
description: 这是一个多选列表的配置
|
||||||
|
type: mult-select
|
||||||
|
default: [1]
|
||||||
|
values: [1,2,3,4,5]
|
||||||
|
configF:
|
||||||
|
title: 配置F
|
||||||
|
description: 这是一个动态多选列表的配置
|
||||||
|
type: mult-select
|
||||||
|
bind: $cookies
|
||||||
|
configG:
|
||||||
|
title: 配置G
|
||||||
|
description: 这是一个数字的配置
|
||||||
|
type: number
|
||||||
|
default: 11
|
||||||
|
min: 10 # 最小值
|
||||||
|
max: 16 # 最大值
|
||||||
|
unit: 分 # 表示单位
|
||||||
|
configH:
|
||||||
|
title: 配置H
|
||||||
|
description: 这是一个长文本类型的配置
|
||||||
|
type: textarea
|
||||||
|
default: 默认值
|
||||||
|
rows: 6
|
||||||
|
---
|
||||||
|
group2:
|
||||||
|
configX:
|
||||||
|
title: 配置A
|
||||||
|
description: 这是一个文本类型的配置
|
||||||
|
default: 默认值
|
||||||
|
==/UserConfig== */
|
||||||
|
|
||||||
|
// 通过GM_info新方法获取UserConfig对象
|
||||||
|
const rawUserConfig = GM_info.userConfig;
|
||||||
|
// 定义一个对象暂存读取到的UserConfig值
|
||||||
|
const userConfig = {};
|
||||||
|
// 解构遍历读取UserConfig并赋缺省值
|
||||||
|
Object.entries(rawUserConfig).forEach(([mainKey, configs]) => {
|
||||||
|
Object.entries(configs).forEach(([subKey, { default: defaultValue }]) => {
|
||||||
|
userConfig[`${mainKey}.${subKey}`] = GM_getValue(`${mainKey}.${subKey}`, defaultValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// 传统方法读取UserConfig,每个缺省值需要单独静态声明,修改UserConfig缺省值后代码也需要手动修改
|
||||||
|
console.log(GM_getValue("group1.configA", "默认值"));
|
||||||
|
console.log(GM_getValue("group1.configG", 11));
|
||||||
|
// GM_info新方法读取UserConfig,可直接关联读取缺省值,无需额外修改
|
||||||
|
console.log(userConfig["group1.configA"]);
|
||||||
|
console.log(userConfig["group1.configG"]);
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
// 打开用户配置
|
||||||
|
CAT_userConfig();
|
8
example/usersubscribe.user.sub.js
Normal file
8
example/usersubscribe.user.sub.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// ==UserSubscribe==
|
||||||
|
// @name 订阅脚本
|
||||||
|
// @description 可以通过指定脚本url订阅一系列的脚本
|
||||||
|
// @version 1.0.0
|
||||||
|
// @author You
|
||||||
|
// @connect www.baidu.com
|
||||||
|
// @scriptUrl https://scriptcat.org/scripts/code/22/test.user.js
|
||||||
|
// ==/UserSubscribe==
|
13
example/vscode.user.js
Normal file
13
example/vscode.user.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name vscode 同步测试
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description vscode scriptcat 插件同步测试
|
||||||
|
// @author You
|
||||||
|
// @match https://bbs.tampermonkey.net.cn/
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
// Your code here...
|
||||||
|
})();
|
@ -2,6 +2,9 @@ import { Server } from "@Packages/message/server";
|
|||||||
import { WindowMessage } from "@Packages/message/window_message";
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
import { preparationSandbox } from "../offscreen/client";
|
import { preparationSandbox } from "../offscreen/client";
|
||||||
import { Script, SCRIPT_TYPE_BACKGROUND } from "@App/app/repo/scripts";
|
import { Script, SCRIPT_TYPE_BACKGROUND } from "@App/app/repo/scripts";
|
||||||
|
import { CronJob } from "cron";
|
||||||
|
import ExecScript from "@App/runtime/content/exec_script";
|
||||||
|
import { Runtime } from "./runtime";
|
||||||
|
|
||||||
// sandbox环境的管理器
|
// sandbox环境的管理器
|
||||||
export class SandboxManager {
|
export class SandboxManager {
|
||||||
@ -9,20 +12,9 @@ export class SandboxManager {
|
|||||||
|
|
||||||
constructor(private windowMessage: WindowMessage) {}
|
constructor(private windowMessage: WindowMessage) {}
|
||||||
|
|
||||||
enableScript(data: Script) {
|
|
||||||
// 开启脚本, 判断脚本是后台脚本还是定时脚本
|
|
||||||
if(data.type === SCRIPT_TYPE_BACKGROUND) {
|
|
||||||
// 后台脚本直接运行起来
|
|
||||||
}else{
|
|
||||||
// 定时脚本加入定时任务
|
|
||||||
}
|
|
||||||
eval("console.log('hello')");
|
|
||||||
console.log("enableScript", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
initManager() {
|
initManager() {
|
||||||
this.api.on("enableScript", this.enableScript.bind(this));
|
const runtime = new Runtime(this.windowMessage, this.api);
|
||||||
|
runtime.init();
|
||||||
// 通知初始化好环境了
|
// 通知初始化好环境了
|
||||||
preparationSandbox(this.windowMessage);
|
preparationSandbox(this.windowMessage);
|
||||||
}
|
}
|
||||||
|
92
src/app/service/sandbox/runtime.ts
Normal file
92
src/app/service/sandbox/runtime.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import { Script, SCRIPT_TYPE_BACKGROUND, ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import ExecScript from "@App/runtime/content/exec_script";
|
||||||
|
import { Server } from "@Packages/message/server";
|
||||||
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
|
import { CronJob } from "cron";
|
||||||
|
|
||||||
|
export class Runtime {
|
||||||
|
cronJob: Map<string, Array<CronJob>> = new Map();
|
||||||
|
|
||||||
|
execScripts: Map<string, ExecScript> = new Map();
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
retryList: {
|
||||||
|
script: ScriptRunResouce;
|
||||||
|
retryTime: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private windowMessage: WindowMessage,
|
||||||
|
private api: Server
|
||||||
|
) {
|
||||||
|
this.logger = LoggerCore.getInstance().logger({ component: "sandbox" });
|
||||||
|
// 重试队列,5s检查一次
|
||||||
|
setInterval(() => {
|
||||||
|
if (!this.retryList.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
const retryList = [];
|
||||||
|
for (let i = 0; i < this.retryList.length; i += 1) {
|
||||||
|
const item = this.retryList[i];
|
||||||
|
if (item.retryTime < now) {
|
||||||
|
this.retryList.splice(i, 1);
|
||||||
|
i -= 1;
|
||||||
|
retryList.push(item.script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retryList.forEach((script) => {
|
||||||
|
script.nextruntime = 0;
|
||||||
|
this.execScript(script);
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
joinRetryList(script: ScriptRunResouce) {
|
||||||
|
if (script.nextruntime) {
|
||||||
|
this.retryList.push({
|
||||||
|
script,
|
||||||
|
retryTime: script.nextruntime,
|
||||||
|
});
|
||||||
|
this.retryList.sort((a, b) => a.retryTime - b.retryTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRetryList(scriptId: number) {
|
||||||
|
for (let i = 0; i < this.retryList.length; i += 1) {
|
||||||
|
if (this.retryList[i].script.id === scriptId) {
|
||||||
|
this.retryList.splice(i, 1);
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableScript(data: Script) {
|
||||||
|
// 开启脚本, 判断脚本是后台脚本还是定时脚本
|
||||||
|
if (data.type === SCRIPT_TYPE_BACKGROUND) {
|
||||||
|
// 后台脚本直接运行起来
|
||||||
|
} else {
|
||||||
|
// 定时脚本加入定时任务
|
||||||
|
}
|
||||||
|
eval("console.log('hello')");
|
||||||
|
console.log("enableScript", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
disableScript(data: Script) {
|
||||||
|
// 关闭脚本, 判断脚本是后台脚本还是定时脚本
|
||||||
|
if (data.type === SCRIPT_TYPE_BACKGROUND) {
|
||||||
|
// 后台脚本直接停止
|
||||||
|
} else {
|
||||||
|
// 定时脚本停止定时任务
|
||||||
|
}
|
||||||
|
console.log("disableScript", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.api.on("enableScript", this.enableScript.bind(this));
|
||||||
|
this.api.on("disableScript", this.disableScript.bind(this));
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import Cache from "@App/app/cache";
|
import Cache from "@App/app/cache";
|
||||||
import { LinterWorker } from "@App/pkg/utils/monaco-editor";
|
import { LinterWorker } from "@App/pkg/utils/monaco-editor";
|
||||||
import { useAppSelector } from "@App/store/hooks";
|
import { useAppSelector } from "@App/pages/store/hooks";
|
||||||
import { editor, Range } from "monaco-editor";
|
import { editor, Range } from "monaco-editor";
|
||||||
import React, { useEffect, useImperativeHandle, useState } from "react";
|
import React, { useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@ import { IconDesktop, IconDown, IconLink, IconMoonFill, IconSunFill } from "@arc
|
|||||||
import React, { ReactNode, useRef, useState } from "react";
|
import React, { ReactNode, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { useAppDispatch, useAppSelector } from "@App/store/hooks";
|
import { useAppDispatch, useAppSelector } from "@App/pages/store/hooks";
|
||||||
import { selectThemeMode, setDarkMode } from "@App/store/features/setting";
|
import { selectThemeMode, setDarkMode } from "@App/pages/store/features/setting";
|
||||||
import { RiFileCodeLine, RiImportLine, RiPlayListAddLine, RiTerminalBoxLine, RiTimerLine } from "react-icons/ri";
|
import { RiFileCodeLine, RiImportLine, RiPlayListAddLine, RiTerminalBoxLine, RiTimerLine } from "react-icons/ri";
|
||||||
|
|
||||||
const MainLayout: React.FC<{
|
const MainLayout: React.FC<{
|
||||||
|
@ -6,7 +6,7 @@ import "@arco-design/web-react/dist/css/arco.css";
|
|||||||
import "@App/locales/locales";
|
import "@App/locales/locales";
|
||||||
import "@App/index.css";
|
import "@App/index.css";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "@App/store/store.ts";
|
import { store } from "@App/pages/store/store.ts";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
|
|||||||
import MainLayout from "../components/layout/MainLayout.tsx";
|
import MainLayout from "../components/layout/MainLayout.tsx";
|
||||||
import Sider from "../components/layout/Sider.tsx";
|
import Sider from "../components/layout/Sider.tsx";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "@App/store/store.ts";
|
import { store } from "@App/pages/store/store.ts";
|
||||||
import "@arco-design/web-react/dist/css/arco.css";
|
import "@arco-design/web-react/dist/css/arco.css";
|
||||||
import "@App/locales/locales";
|
import "@App/locales/locales";
|
||||||
import "@App/index.css";
|
import "@App/index.css";
|
||||||
|
@ -69,7 +69,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { nextTime, semTime } from "@App/pkg/utils/utils";
|
import { nextTime, semTime } from "@App/pkg/utils/utils";
|
||||||
import { i18nName } from "@App/locales/locales";
|
import { i18nName } from "@App/locales/locales";
|
||||||
import { getValues, ListHomeRender, ScriptIcons } from "./utils";
|
import { getValues, ListHomeRender, ScriptIcons } from "./utils";
|
||||||
import { useAppDispatch, useAppSelector } from "@App/store/hooks";
|
import { useAppDispatch, useAppSelector } from "@App/pages/store/hooks";
|
||||||
import {
|
import {
|
||||||
deleteScript,
|
deleteScript,
|
||||||
requestEnableScript,
|
requestEnableScript,
|
||||||
@ -79,8 +79,8 @@ import {
|
|||||||
selectScripts,
|
selectScripts,
|
||||||
sortScript,
|
sortScript,
|
||||||
upsertScript,
|
upsertScript,
|
||||||
} from "@App/store/features/script";
|
} from "@App/pages/store/features/script";
|
||||||
import { selectScriptListColumnWidth } from "@App/store/features/setting";
|
import { selectScriptListColumnWidth } from "@App/pages/store/features/setting";
|
||||||
import { Broker } from "@Packages/message/message_queue";
|
import { Broker } from "@Packages/message/message_queue";
|
||||||
import { subscribeScriptDelete, subscribeScriptInstall } from "@App/app/service/service_worker/client";
|
import { subscribeScriptDelete, subscribeScriptInstall } from "@App/app/service/service_worker/client";
|
||||||
import { ExtensionMessage } from "@Packages/message/extension_message";
|
import { ExtensionMessage } from "@Packages/message/extension_message";
|
||||||
|
6
src/pkg/utils/lodash.ts
Normal file
6
src/pkg/utils/lodash.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// 因为这个包出过好几次问题, 从原仓库单独剥离出来使用
|
||||||
|
// copyright: https://github.com/lodash/lodash
|
||||||
|
|
||||||
|
export function has(object: any, key: any) {
|
||||||
|
return object != null && Object.prototype.hasOwnProperty.call(object, key);
|
||||||
|
}
|
961
src/runtime/background/gm_api.ts
Normal file
961
src/runtime/background/gm_api.ts
Normal file
@ -0,0 +1,961 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import { Channel } from "@App/app/message/channel";
|
||||||
|
import { MessageHander, MessageSender } from "@App/app/message/message";
|
||||||
|
import { Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import ValueManager from "@App/app/service/value/manager";
|
||||||
|
import CacheKey from "@App/pkg/utils/cache_key";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { base64ToBlob } from "@App/pkg/utils/script";
|
||||||
|
import { isFirefox } from "@App/pkg/utils/utils";
|
||||||
|
import Hook from "@App/app/service/hook";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import FileSystemFactory from "@Pkg/filesystem/factory";
|
||||||
|
import FileSystem from "@Pkg/filesystem/filesystem";
|
||||||
|
import { joinPath } from "@Pkg/filesystem/utils";
|
||||||
|
import i18next from "i18next";
|
||||||
|
import { i18nName } from "@App/locales/locales";
|
||||||
|
import { isWarpTokenError } from "@Pkg/filesystem/error";
|
||||||
|
import PermissionVerify, {
|
||||||
|
ConfirmParam,
|
||||||
|
IPermissionVerify,
|
||||||
|
} from "./permission_verify";
|
||||||
|
import {
|
||||||
|
dealFetch,
|
||||||
|
dealXhr,
|
||||||
|
getFetchHeader,
|
||||||
|
getIcon,
|
||||||
|
listenerWebRequest,
|
||||||
|
setXhrHeader,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
// GMApi,处理脚本的GM API调用请求
|
||||||
|
|
||||||
|
export type MessageRequest = {
|
||||||
|
scriptId: number; // 脚本id
|
||||||
|
api: string;
|
||||||
|
runFlag: string;
|
||||||
|
params: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Request = MessageRequest & {
|
||||||
|
script: Script;
|
||||||
|
sender: MessageSender;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Api = (request: Request, connect?: Channel) => Promise<any>;
|
||||||
|
|
||||||
|
export default class GMApi {
|
||||||
|
message: MessageHander;
|
||||||
|
|
||||||
|
script: ScriptDAO;
|
||||||
|
|
||||||
|
permissionVerify: IPermissionVerify;
|
||||||
|
|
||||||
|
valueManager: ValueManager;
|
||||||
|
|
||||||
|
logger: Logger = LoggerCore.getLogger({ component: "GMApi" });
|
||||||
|
|
||||||
|
static hook: Hook<"registerMenu" | "unregisterMenu"> = new Hook();
|
||||||
|
|
||||||
|
systemConfig: SystemConfig;
|
||||||
|
|
||||||
|
constructor(message: MessageHander, permissionVerify: IPermissionVerify) {
|
||||||
|
this.message = message;
|
||||||
|
this.script = new ScriptDAO();
|
||||||
|
this.permissionVerify = permissionVerify;
|
||||||
|
this.systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||||
|
// 证明是后台运行的,生成一个随机的headerFlag
|
||||||
|
if (permissionVerify instanceof PermissionVerify) {
|
||||||
|
this.systemConfig.scriptCatFlag = `x-cat-${uuidv4()}`;
|
||||||
|
}
|
||||||
|
this.valueManager = IoC.instance(ValueManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.message.setHandler(
|
||||||
|
"gmApi",
|
||||||
|
async (_action: string, data: MessageRequest, sender: MessageSender) => {
|
||||||
|
const api = PermissionVerify.apis.get(data.api);
|
||||||
|
if (!api) {
|
||||||
|
return Promise.reject(new Error("api is not found"));
|
||||||
|
}
|
||||||
|
const req = await this.parseRequest(data, sender);
|
||||||
|
try {
|
||||||
|
await this.permissionVerify.verify(req, api);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error("verify error", { api: data.api }, Logger.E(e));
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
return api.api.call(this, req);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.message.setHandlerWithChannel(
|
||||||
|
"gmApiChannel",
|
||||||
|
async (
|
||||||
|
connect: Channel,
|
||||||
|
_action: string,
|
||||||
|
data: MessageRequest,
|
||||||
|
sender: MessageSender
|
||||||
|
) => {
|
||||||
|
const api = PermissionVerify.apis.get(data.api);
|
||||||
|
if (!api) {
|
||||||
|
return connect.throw("api is not found");
|
||||||
|
}
|
||||||
|
const req = await this.parseRequest(data, sender);
|
||||||
|
try {
|
||||||
|
await this.permissionVerify.verify(req, api);
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error("verify error", { api: data.api }, Logger.E(e));
|
||||||
|
return connect.throw(e.message);
|
||||||
|
}
|
||||||
|
return api.api.call(this, req, connect);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 只有background页才监听web请求
|
||||||
|
if (this.permissionVerify instanceof PermissionVerify) {
|
||||||
|
listenerWebRequest(this.systemConfig.scriptCatFlag);
|
||||||
|
}
|
||||||
|
// 处理sandbox来的CAT_fetchBlob和CAT_createBlobUrl
|
||||||
|
this.message.setHandler("CAT_createBlobUrl", (_: string, blob: Blob) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 60 * 1000);
|
||||||
|
return Promise.resolve(url);
|
||||||
|
});
|
||||||
|
this.message.setHandler("CAT_fetchBlob", (_: string, url: string) => {
|
||||||
|
return fetch(url).then((data) => data.blob());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求
|
||||||
|
async parseRequest(
|
||||||
|
data: MessageRequest,
|
||||||
|
sender: MessageSender
|
||||||
|
): Promise<Request> {
|
||||||
|
const script = await Cache.getInstance().getOrSet(
|
||||||
|
CacheKey.script(data.scriptId),
|
||||||
|
() => {
|
||||||
|
return this.script.findById(data.scriptId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!script) {
|
||||||
|
return Promise.reject(new Error("script is not found"));
|
||||||
|
}
|
||||||
|
const req: Request = <Request>data;
|
||||||
|
req.script = script;
|
||||||
|
req.sender = sender;
|
||||||
|
return Promise.resolve(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_setValue(request: Request): Promise<any> {
|
||||||
|
if (!request.params || request.params.length !== 2) {
|
||||||
|
return Promise.reject(new Error("param is failed"));
|
||||||
|
}
|
||||||
|
const [key, value] = request.params;
|
||||||
|
const sender = <MessageSender & { runFlag: string }>request.sender;
|
||||||
|
sender.runFlag = request.runFlag;
|
||||||
|
return this.valueManager.setValue(request.script, key, value, sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理GM_xmlhttpRequest fetch的情况,先只处理ReadableStream的情况
|
||||||
|
// 且不考虑复杂的情况
|
||||||
|
CAT_fetch(request: Request, channel: Channel): Promise<any> {
|
||||||
|
const config = <GMSend.XHRDetails>request.params[0];
|
||||||
|
const { url } = config;
|
||||||
|
return fetch(url, {
|
||||||
|
method: config.method || "GET",
|
||||||
|
body: <any>config.data,
|
||||||
|
headers: getFetchHeader(this.systemConfig.scriptCatFlag, config),
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
const send = dealFetch(
|
||||||
|
this.systemConfig.scriptCatFlag,
|
||||||
|
config,
|
||||||
|
resp,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const reader = resp.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error("read is not found");
|
||||||
|
}
|
||||||
|
const { scriptCatFlag } = this.systemConfig;
|
||||||
|
reader.read().then(function read({ done, value }) {
|
||||||
|
if (done) {
|
||||||
|
const data = dealFetch(scriptCatFlag, config, resp, 4);
|
||||||
|
channel.send({ event: "onreadystatechange", data });
|
||||||
|
channel.send({ event: "onload", data });
|
||||||
|
channel.send({ event: "onloadend", data });
|
||||||
|
channel.disChannel();
|
||||||
|
} else {
|
||||||
|
channel.send({ event: "onstream", data: Array.from(value) });
|
||||||
|
reader.read().then(read);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
channel.send({ event: "onloadstart", data: send });
|
||||||
|
send.readyState = 2;
|
||||||
|
channel.send({ event: "onreadystatechange", data: send });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
channel.throw(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
confirm: (request: Request) => {
|
||||||
|
const config = <GMSend.XHRDetails>request.params[0];
|
||||||
|
const url = new URL(config.url);
|
||||||
|
if (request.script.metadata.connect) {
|
||||||
|
const { connect } = request.script.metadata;
|
||||||
|
for (let i = 0; i < connect.length; i += 1) {
|
||||||
|
if (url.hostname.endsWith(connect[i])) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const metadata: { [key: string]: string } = {};
|
||||||
|
metadata[i18next.t("script_name")] = i18nName(request.script);
|
||||||
|
metadata[i18next.t("request_domain")] = url.hostname;
|
||||||
|
metadata[i18next.t("request_url")] = config.url;
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
permission: "cors",
|
||||||
|
permissionValue: url.hostname,
|
||||||
|
title: i18next.t("script_accessing_cross_origin_resource"),
|
||||||
|
metadata,
|
||||||
|
describe: i18next.t("confirm_operation_description"),
|
||||||
|
wildcard: true,
|
||||||
|
permissionContent: i18next.t("domain"),
|
||||||
|
} as ConfirmParam);
|
||||||
|
},
|
||||||
|
alias: ["GM.xmlHttpRequest"],
|
||||||
|
})
|
||||||
|
async GM_xmlhttpRequest(request: Request, channel: Channel): Promise<any> {
|
||||||
|
const config = <GMSend.XHRDetails>request.params[0];
|
||||||
|
if (config.responseType === "stream") {
|
||||||
|
// 只有fetch支持ReadableStream
|
||||||
|
return this.CAT_fetch(request, channel);
|
||||||
|
}
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(
|
||||||
|
config.method || "GET",
|
||||||
|
config.url,
|
||||||
|
true,
|
||||||
|
config.user || "",
|
||||||
|
config.password || ""
|
||||||
|
);
|
||||||
|
if (config.overrideMimeType) {
|
||||||
|
xhr.overrideMimeType(config.overrideMimeType);
|
||||||
|
}
|
||||||
|
if (config.responseType !== "json") {
|
||||||
|
xhr.responseType = config.responseType || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const deal = async (event: string, data?: any) => {
|
||||||
|
const response: any = await dealXhr(
|
||||||
|
this.systemConfig.scriptCatFlag,
|
||||||
|
config,
|
||||||
|
xhr
|
||||||
|
);
|
||||||
|
if (data) {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
response[key] = data[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
channel.send({ event, data: response });
|
||||||
|
if (event === "onload") {
|
||||||
|
channel.disChannel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onload = () => {
|
||||||
|
deal("onload");
|
||||||
|
};
|
||||||
|
xhr.onloadstart = () => {
|
||||||
|
deal("onloadstart");
|
||||||
|
};
|
||||||
|
xhr.onloadend = () => {
|
||||||
|
deal("onloadstart");
|
||||||
|
};
|
||||||
|
xhr.onabort = () => {
|
||||||
|
deal("onabort");
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
deal("onerror");
|
||||||
|
};
|
||||||
|
xhr.onprogress = (event) => {
|
||||||
|
const respond: GMTypes.XHRProgress = {
|
||||||
|
done: xhr.DONE,
|
||||||
|
lengthComputable: event.lengthComputable,
|
||||||
|
loaded: event.loaded,
|
||||||
|
total: event.total,
|
||||||
|
totalSize: event.total,
|
||||||
|
};
|
||||||
|
deal("onprogress", respond);
|
||||||
|
};
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
deal("onreadystatechange");
|
||||||
|
};
|
||||||
|
xhr.ontimeout = () => {
|
||||||
|
channel.send({ event: "ontimeout" });
|
||||||
|
};
|
||||||
|
setXhrHeader(this.systemConfig.scriptCatFlag, config, xhr);
|
||||||
|
|
||||||
|
if (config.timeout) {
|
||||||
|
xhr.timeout = config.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrideMimeType) {
|
||||||
|
xhr.overrideMimeType(config.overrideMimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.dataType === "FormData") {
|
||||||
|
const data = new FormData();
|
||||||
|
if (config.data && config.data instanceof Array) {
|
||||||
|
config.data.forEach((val: GMSend.XHRFormData) => {
|
||||||
|
if (val.type === "file") {
|
||||||
|
data.append(val.key, base64ToBlob(val.val), val.filename);
|
||||||
|
} else {
|
||||||
|
data.append(val.key, val.val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.send(data);
|
||||||
|
}
|
||||||
|
} else if (config.dataType === "Blob") {
|
||||||
|
if (!config.data) {
|
||||||
|
return channel.throw("data is null");
|
||||||
|
}
|
||||||
|
const resp = await (await fetch(<string>config.data)).blob();
|
||||||
|
xhr.send(resp);
|
||||||
|
} else {
|
||||||
|
xhr.send(<string>config.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.setDisChannelHandler(() => {
|
||||||
|
xhr.abort();
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
listener() {
|
||||||
|
chrome.notifications.onClosed.addListener((id, user) => {
|
||||||
|
const ret = Cache.getInstance().get(`GM_notification:${id}`);
|
||||||
|
if (ret) {
|
||||||
|
const channel = <Channel>ret;
|
||||||
|
channel.send({ event: "done", id, user });
|
||||||
|
channel.disChannel();
|
||||||
|
Cache.getInstance().del(`GM_notification:${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chrome.notifications.onClicked.addListener((id) => {
|
||||||
|
const ret = Cache.getInstance().get(`GM_notification:${id}`);
|
||||||
|
if (ret) {
|
||||||
|
const channel = <Channel>ret;
|
||||||
|
channel.send({ event: "click", id, index: undefined });
|
||||||
|
channel.send({ event: "done", id, user: true });
|
||||||
|
channel.disChannel();
|
||||||
|
Cache.getInstance().del(`GM_notification:${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chrome.notifications.onButtonClicked.addListener((id, buttonIndex) => {
|
||||||
|
const ret = Cache.getInstance().get(`GM_notification:${id}`);
|
||||||
|
if (ret) {
|
||||||
|
const channel = <Channel>ret;
|
||||||
|
channel.send({ event: "click", id, index: buttonIndex });
|
||||||
|
channel.send({ event: "done", id, user: true });
|
||||||
|
channel.disChannel();
|
||||||
|
Cache.getInstance().del(`GM_notification:${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
GM_notification(request: Request, channel: Channel): any {
|
||||||
|
if (request.params.length === 0) {
|
||||||
|
return channel.throw("param is failed");
|
||||||
|
}
|
||||||
|
const details: GMTypes.NotificationDetails = request.params[0];
|
||||||
|
const options: chrome.notifications.NotificationOptions<true> = {
|
||||||
|
title: details.title || "ScriptCat",
|
||||||
|
message: details.text || "无消息内容",
|
||||||
|
iconUrl:
|
||||||
|
details.image ||
|
||||||
|
getIcon(request.script) ||
|
||||||
|
chrome.runtime.getURL("assets/logo.png"),
|
||||||
|
type:
|
||||||
|
isFirefox() || details.progress === undefined ? "basic" : "progress",
|
||||||
|
};
|
||||||
|
if (!isFirefox()) {
|
||||||
|
options.silent = details.silent;
|
||||||
|
options.buttons = details.buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.notifications.create(options, (notificationId) => {
|
||||||
|
Cache.getInstance().set(`GM_notification:${notificationId}`, channel);
|
||||||
|
channel.send({ event: "create", id: notificationId });
|
||||||
|
if (details.timeout) {
|
||||||
|
setTimeout(() => {
|
||||||
|
chrome.notifications.clear(notificationId);
|
||||||
|
channel.send({ event: "done", id: notificationId, user: false });
|
||||||
|
channel.disChannel();
|
||||||
|
Cache.getInstance().del(`GM_notification:${notificationId}`);
|
||||||
|
}, details.timeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_closeNotification(request: Request): Promise<boolean> {
|
||||||
|
chrome.notifications.clear(<string>request.params[0]);
|
||||||
|
const ret = Cache.getInstance().get(
|
||||||
|
`GM_notification:${<string>request.params[0]}`
|
||||||
|
);
|
||||||
|
if (ret) {
|
||||||
|
const channel = <Channel>ret;
|
||||||
|
channel.send({ event: "done", id: request.params[0], user: false });
|
||||||
|
Cache.getInstance().del(`GM_notification:${<string>request.params[0]}`);
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_updateNotification(request: Request): Promise<boolean> {
|
||||||
|
if (isFirefox()) {
|
||||||
|
return Promise.reject(new Error("firefox does not support this method"));
|
||||||
|
}
|
||||||
|
const id = request.params[0];
|
||||||
|
const details: GMTypes.NotificationDetails = request.params[1];
|
||||||
|
const options: chrome.notifications.NotificationOptions = {
|
||||||
|
title: details.title,
|
||||||
|
message: details.text,
|
||||||
|
iconUrl: details.image,
|
||||||
|
type: details.progress === undefined ? "basic" : "progress",
|
||||||
|
silent: details.silent,
|
||||||
|
progress: details.progress,
|
||||||
|
};
|
||||||
|
chrome.notifications.update(<string>id, options);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_log(request: Request): Promise<boolean> {
|
||||||
|
const message = request.params[0];
|
||||||
|
const level = request.params[1] || "info";
|
||||||
|
const labels = request.params[2] || {};
|
||||||
|
LoggerCore.getLogger(labels).log(level, message, {
|
||||||
|
scriptId: request.scriptId,
|
||||||
|
component: "GM_log",
|
||||||
|
});
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
listener: () => {
|
||||||
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
const channel = <Channel>(
|
||||||
|
Cache.getInstance().get(`GM_openInTab:${tabId}`)
|
||||||
|
);
|
||||||
|
if (channel) {
|
||||||
|
channel.send({ event: "onclose" });
|
||||||
|
channel.disChannel();
|
||||||
|
Cache.getInstance().del(`GM_openInTab:${tabId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
GM_openInTab(request: Request, channel: Channel) {
|
||||||
|
const url = request.params[0];
|
||||||
|
const options = request.params[1] || {};
|
||||||
|
if (options.useOpen === true) {
|
||||||
|
const newWindow = window.open(url);
|
||||||
|
if (newWindow) {
|
||||||
|
// 由于不符合同源策略无法直接监听newWindow关闭事件,因此改用CDP方法监听
|
||||||
|
// 由于window.open强制在前台打开标签,因此获取状态为{ active:true }的标签即为新标签
|
||||||
|
chrome.tabs.query({ active: true }, ([tab]) => {
|
||||||
|
Cache.getInstance().set(`GM_openInTab:${tab.id}`, channel);
|
||||||
|
channel.send({ event: "oncreate", tabId: tab.id });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭
|
||||||
|
// 似乎在Firefox中禁止在background页面使用window.open(),强制返回null
|
||||||
|
channel.send({ event: "onclose" });
|
||||||
|
channel.disChannel();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chrome.tabs.create({ url, active: options.active }, (tab) => {
|
||||||
|
Cache.getInstance().set(`GM_openInTab:${tab.id}`, channel);
|
||||||
|
channel.send({ event: "oncreate", tabId: tab.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
link: "GM_openInTab",
|
||||||
|
})
|
||||||
|
async GM_closeInTab(request: Request): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await chrome.tabs.remove(<number>request.params[0]);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error("GM_closeInTab", Logger.E(e));
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static tabData = new Map<number, Map<number | string, any>>();
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
listener: () => {
|
||||||
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
GMApi.tabData.forEach((value) => {
|
||||||
|
value.forEach((v, tabIdKey) => {
|
||||||
|
if (tabIdKey === tabId) {
|
||||||
|
value.delete(tabIdKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
GM_getTab(request: Request) {
|
||||||
|
return Promise.resolve(
|
||||||
|
GMApi.tabData
|
||||||
|
.get(request.scriptId)
|
||||||
|
?.get(request.sender.tabId || request.sender.targetTag)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_saveTab(request: Request) {
|
||||||
|
const data = request.params[0];
|
||||||
|
const tabId = request.sender.tabId || request.sender.targetTag;
|
||||||
|
if (!GMApi.tabData.has(request.scriptId)) {
|
||||||
|
GMApi.tabData.set(request.scriptId, new Map());
|
||||||
|
}
|
||||||
|
GMApi.tabData.get(request.scriptId)?.set(tabId, data);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_getTabs(request: Request) {
|
||||||
|
if (!GMApi.tabData.has(request.scriptId)) {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
const resp: { [key: string | number]: object } = {};
|
||||||
|
GMApi.tabData.get(request.scriptId)?.forEach((value, key) => {
|
||||||
|
resp[key] = value;
|
||||||
|
});
|
||||||
|
return Promise.resolve(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_download(request: Request, channel: Channel) {
|
||||||
|
const config = <GMTypes.DownloadDetails>request.params[0];
|
||||||
|
// blob本地文件直接下载
|
||||||
|
if (config.url.startsWith("blob:")) {
|
||||||
|
chrome.downloads.download(
|
||||||
|
{
|
||||||
|
url: config.url,
|
||||||
|
saveAs: config.saveAs,
|
||||||
|
filename: config.name,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
channel.send({ event: "onload" });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 使用ajax下载blob,再使用download api创建下载
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(config.method || "GET", config.url, true);
|
||||||
|
xhr.responseType = "blob";
|
||||||
|
const deal = (event: string, data?: any) => {
|
||||||
|
const removeXCat = new RegExp(`${this.systemConfig.scriptCatFlag}-`, "g");
|
||||||
|
const respond: any = {
|
||||||
|
finalUrl: xhr.responseURL || config.url,
|
||||||
|
readyState: <any>xhr.readyState,
|
||||||
|
status: xhr.status,
|
||||||
|
statusText: xhr.statusText,
|
||||||
|
responseHeaders: xhr.getAllResponseHeaders().replace(removeXCat, ""),
|
||||||
|
};
|
||||||
|
if (data) {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
respond[key] = data[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
channel.send({ event, data: respond });
|
||||||
|
};
|
||||||
|
xhr.onload = () => {
|
||||||
|
deal("onload");
|
||||||
|
const url = URL.createObjectURL(xhr.response);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 6000);
|
||||||
|
chrome.downloads.download({
|
||||||
|
url,
|
||||||
|
saveAs: config.saveAs,
|
||||||
|
filename: config.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
deal("onerror");
|
||||||
|
};
|
||||||
|
xhr.onprogress = (event) => {
|
||||||
|
const respond: GMTypes.XHRProgress = {
|
||||||
|
done: xhr.DONE,
|
||||||
|
lengthComputable: event.lengthComputable,
|
||||||
|
loaded: event.loaded,
|
||||||
|
total: event.total,
|
||||||
|
totalSize: event.total,
|
||||||
|
};
|
||||||
|
deal("onprogress", respond);
|
||||||
|
};
|
||||||
|
xhr.ontimeout = () => {
|
||||||
|
channel.send({ event: "ontimeout" });
|
||||||
|
};
|
||||||
|
setXhrHeader(this.systemConfig.scriptCatFlag, config, xhr);
|
||||||
|
|
||||||
|
if (config.timeout) {
|
||||||
|
xhr.timeout = config.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send();
|
||||||
|
channel.setDisChannelHandler(() => {
|
||||||
|
xhr.abort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static clipboardData: { type?: string; data: string } | undefined;
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
listener() {
|
||||||
|
PermissionVerify.textarea.style.display = "none";
|
||||||
|
document.documentElement.appendChild(PermissionVerify.textarea);
|
||||||
|
document.addEventListener("copy", (e: ClipboardEvent) => {
|
||||||
|
if (!GMApi.clipboardData || !e.clipboardData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
const { type, data } = GMApi.clipboardData;
|
||||||
|
e.clipboardData.setData(type || "text/plain", data);
|
||||||
|
GMApi.clipboardData = undefined;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
GM_setClipboard(request: Request) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
GMApi.clipboardData = {
|
||||||
|
type: request.params[1],
|
||||||
|
data: request.params[0],
|
||||||
|
};
|
||||||
|
PermissionVerify.textarea.focus();
|
||||||
|
document.execCommand("copy", false, <any>null);
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
confirm(request: Request) {
|
||||||
|
if (request.params[0] === "store") {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
const detail = <GMTypes.CookieDetails>request.params[1];
|
||||||
|
if (!detail.url && !detail.domain) {
|
||||||
|
return Promise.reject(new Error("there must be one of url or domain"));
|
||||||
|
}
|
||||||
|
let url: URL = <URL>{};
|
||||||
|
if (detail.url) {
|
||||||
|
url = new URL(detail.url);
|
||||||
|
} else {
|
||||||
|
url.host = detail.domain || "";
|
||||||
|
url.hostname = detail.domain || "";
|
||||||
|
}
|
||||||
|
let flag = false;
|
||||||
|
if (request.script.metadata.connect) {
|
||||||
|
const { connect } = request.script.metadata;
|
||||||
|
for (let i = 0; i < connect.length; i += 1) {
|
||||||
|
if (url.hostname.endsWith(connect[i])) {
|
||||||
|
flag = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!flag) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("hostname must be in the definition of connect")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const metadata: { [key: string]: string } = {};
|
||||||
|
metadata[i18next.t("script_name")] = i18nName(request.script);
|
||||||
|
metadata[i18next.t("request_domain")] = url.host;
|
||||||
|
return Promise.resolve({
|
||||||
|
permission: "cookie",
|
||||||
|
permissionValue: url.host,
|
||||||
|
title: i18next.t("access_cookie_content")!,
|
||||||
|
metadata,
|
||||||
|
describe: i18next.t("confirm_script_operation")!,
|
||||||
|
permissionContent: i18next.t("cookie_domain")!,
|
||||||
|
uuid: "",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
GM_cookie(request: Request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const param = request.params;
|
||||||
|
if (param.length !== 2) {
|
||||||
|
reject(new Error("there must be two parameters"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const detail = <GMTypes.CookieDetails>request.params[1];
|
||||||
|
if (param[0] === "store") {
|
||||||
|
chrome.cookies.getAllCookieStores((res) => {
|
||||||
|
const data: any[] = [];
|
||||||
|
res.forEach((val) => {
|
||||||
|
if (detail.tabId) {
|
||||||
|
for (let n = 0; n < val.tabIds.length; n += 1) {
|
||||||
|
if (val.tabIds[n] === detail.tabId) {
|
||||||
|
data.push({ storeId: val.id });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.push({ storeId: val.id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// url或者域名不能为空
|
||||||
|
if (detail.url) {
|
||||||
|
detail.url = detail.url.trim();
|
||||||
|
}
|
||||||
|
if (detail.domain) {
|
||||||
|
detail.domain = detail.domain.trim();
|
||||||
|
}
|
||||||
|
if (!detail.url && !detail.domain) {
|
||||||
|
reject(new Error("there must be one of url or domain"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (param[0]) {
|
||||||
|
case "list": {
|
||||||
|
chrome.cookies.getAll(
|
||||||
|
{
|
||||||
|
domain: detail.domain,
|
||||||
|
name: detail.name,
|
||||||
|
path: detail.path,
|
||||||
|
secure: detail.secure,
|
||||||
|
session: detail.session,
|
||||||
|
url: detail.url,
|
||||||
|
storeId: detail.storeId,
|
||||||
|
},
|
||||||
|
(cookies) => {
|
||||||
|
resolve(cookies);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete": {
|
||||||
|
if (!detail.url || !detail.name) {
|
||||||
|
reject(new Error("delete operation must have url and name"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.cookies.remove(
|
||||||
|
{
|
||||||
|
name: detail.name,
|
||||||
|
url: detail.url,
|
||||||
|
storeId: detail.storeId,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "set": {
|
||||||
|
if (!detail.url || !detail.name) {
|
||||||
|
reject(new Error("set operation must have name and value"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.cookies.set(
|
||||||
|
{
|
||||||
|
url: detail.url,
|
||||||
|
name: detail.name,
|
||||||
|
domain: detail.domain,
|
||||||
|
value: detail.value,
|
||||||
|
expirationDate: detail.expirationDate,
|
||||||
|
path: detail.path,
|
||||||
|
httpOnly: detail.httpOnly,
|
||||||
|
secure: detail.secure,
|
||||||
|
storeId: detail.storeId,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
reject(new Error("action can only be: get, set, delete, store"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_registerMenuCommand(request: Request, channel: Channel) {
|
||||||
|
GMApi.hook.trigger("registerMenu", request, channel);
|
||||||
|
channel.setDisChannelHandler(() => {
|
||||||
|
GMApi.hook.trigger("unregisterMenu", request.params[0], request);
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
GM_unregisterMenuCommand(request: Request) {
|
||||||
|
GMApi.hook.trigger("unregisterMenu", request.params[0], request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API()
|
||||||
|
CAT_userConfig(request: Request) {
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: `/src/options.html#/?userConfig=${request.scriptId}`,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermissionVerify.API({
|
||||||
|
confirm: (request: Request) => {
|
||||||
|
const [action, details] = request.params;
|
||||||
|
if (action === "config") {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
const dir = details.baseDir ? details.baseDir : request.script.uuid;
|
||||||
|
const metadata: { [key: string]: string } = {};
|
||||||
|
metadata[i18next.t("script_name")] = i18nName(request.script);
|
||||||
|
return Promise.resolve({
|
||||||
|
permission: "file_storage",
|
||||||
|
permissionValue: dir,
|
||||||
|
title: i18next.t("script_operation_title"),
|
||||||
|
metadata,
|
||||||
|
describe: i18next.t("script_operation_description", { dir }),
|
||||||
|
wildcard: false,
|
||||||
|
permissionContent: i18next.t("script_permission_content"),
|
||||||
|
} as ConfirmParam);
|
||||||
|
},
|
||||||
|
alias: ["GM.xmlHttpRequest"],
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
async CAT_fileStorage(request: Request, channel: Channel) {
|
||||||
|
const [action, details] = request.params;
|
||||||
|
if (action === "config") {
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: `/src/options.html#/setting`,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
const fsConfig = this.systemConfig.catFileStorage;
|
||||||
|
if (fsConfig.status === "unset") {
|
||||||
|
return channel.throw({ code: 1, error: "file storage is disable" });
|
||||||
|
}
|
||||||
|
if (fsConfig.status === "error") {
|
||||||
|
return channel.throw({ code: 2, error: "file storge is error" });
|
||||||
|
}
|
||||||
|
let fs: FileSystem;
|
||||||
|
const baseDir = `ScriptCat/app/${
|
||||||
|
details.baseDir ? details.baseDir : request.script.uuid
|
||||||
|
}`;
|
||||||
|
try {
|
||||||
|
fs = await FileSystemFactory.create(
|
||||||
|
fsConfig.filesystem,
|
||||||
|
fsConfig.params[fsConfig.filesystem]
|
||||||
|
);
|
||||||
|
await FileSystemFactory.mkdirAll(fs, baseDir);
|
||||||
|
fs = await fs.openDir(baseDir);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (isWarpTokenError(e)) {
|
||||||
|
fsConfig.status = "error";
|
||||||
|
this.systemConfig.catFileStorage = fsConfig;
|
||||||
|
return channel.throw({ code: 2, error: e.error.message });
|
||||||
|
}
|
||||||
|
return channel.throw({ code: 8, error: e.message });
|
||||||
|
}
|
||||||
|
switch (action) {
|
||||||
|
case "list":
|
||||||
|
fs.list()
|
||||||
|
.then((list) => {
|
||||||
|
list.forEach((file) => {
|
||||||
|
(<any>file).absPath = file.path;
|
||||||
|
file.path = joinPath(
|
||||||
|
file.path.substring(file.path.indexOf(baseDir) + baseDir.length)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
channel.send({ action: "onload", data: list });
|
||||||
|
channel.disChannel();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
channel.throw({ code: 3, error: e.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "upload":
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const w = await fs.create(details.path);
|
||||||
|
w.write(await (await fetch(<string>details.data)).blob())
|
||||||
|
.then(() => {
|
||||||
|
channel.send({ action: "onload", data: true });
|
||||||
|
channel.disChannel();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
channel.throw({ code: 4, error: e.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "download":
|
||||||
|
// eslint-disable-next-line no-case-declarations, no-undef
|
||||||
|
const info = <CATType.FileStorageFileInfo>details.file;
|
||||||
|
fs = await fs.openDir(`${info.path}`);
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const r = await fs.open({
|
||||||
|
fsid: (<any>info).fsid,
|
||||||
|
name: info.name,
|
||||||
|
path: info.absPath,
|
||||||
|
size: info.size,
|
||||||
|
digest: info.digest,
|
||||||
|
createtime: info.createtime,
|
||||||
|
updatetime: info.updatetime,
|
||||||
|
});
|
||||||
|
r.read("blob")
|
||||||
|
.then((blob) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 6000);
|
||||||
|
channel.send({ action: "onload", data: url });
|
||||||
|
channel.disChannel();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
channel.throw({ code: 5, error: e.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
fs.delete(`${details.path}`)
|
||||||
|
.then(() => {
|
||||||
|
channel.send({ action: "onload", data: true });
|
||||||
|
channel.disChannel();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
channel.throw({ code: 6, error: e.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
channel.disChannel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
410
src/runtime/background/permission_verify.ts
Normal file
410
src/runtime/background/permission_verify.ts
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
// gm api 权限验证
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import { Permission, PermissionDAO } from "@App/app/repo/permission";
|
||||||
|
import { Script } from "@App/app/repo/scripts";
|
||||||
|
import CacheKey from "@App/pkg/utils/cache_key";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import MessageQueue from "@App/pkg/utils/message_queue";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import { MessageHander } from "@App/app/message/message";
|
||||||
|
import { Api, Request } from "./gm_api";
|
||||||
|
|
||||||
|
export interface ConfirmParam {
|
||||||
|
// 权限名
|
||||||
|
permission: string;
|
||||||
|
// 权限值
|
||||||
|
permissionValue?: string;
|
||||||
|
// 确认权限标题
|
||||||
|
title?: string;
|
||||||
|
// 权限详情内容
|
||||||
|
metadata?: { [key: string]: string };
|
||||||
|
// 权限描述
|
||||||
|
describe?: string;
|
||||||
|
// 是否通配
|
||||||
|
wildcard?: boolean;
|
||||||
|
// 权限内容
|
||||||
|
permissionContent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserConfirm {
|
||||||
|
allow: boolean;
|
||||||
|
type: number; // 1: 允许一次 2: 临时允许全部 3: 临时允许此 4: 永久允许全部 5: 永久允许此
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiParam {
|
||||||
|
// 默认提供的函数
|
||||||
|
default?: boolean;
|
||||||
|
// 是否只有后台环境中才能执行
|
||||||
|
background?: boolean;
|
||||||
|
// 是否需要弹出页面让用户进行确认
|
||||||
|
confirm?: (request: Request) => Promise<boolean | ConfirmParam>;
|
||||||
|
// 监听方法
|
||||||
|
listener?: () => void;
|
||||||
|
// 别名
|
||||||
|
alias?: string[];
|
||||||
|
// 关联
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiValue {
|
||||||
|
api: Api;
|
||||||
|
param: ApiParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPermissionVerify {
|
||||||
|
verify(request: Request, api: ApiValue): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PermissionVerify {
|
||||||
|
static apis: Map<string, ApiValue> = new Map();
|
||||||
|
|
||||||
|
static textarea: HTMLTextAreaElement = document.createElement("textarea");
|
||||||
|
|
||||||
|
public static API(param: ApiParam = {}) {
|
||||||
|
return (
|
||||||
|
target: any,
|
||||||
|
propertyName: string,
|
||||||
|
descriptor: PropertyDescriptor
|
||||||
|
) => {
|
||||||
|
const key = propertyName;
|
||||||
|
if (param.listener) {
|
||||||
|
param.listener();
|
||||||
|
}
|
||||||
|
PermissionVerify.apis.set(key, {
|
||||||
|
api: descriptor.value,
|
||||||
|
param,
|
||||||
|
});
|
||||||
|
// 兼容GM.*
|
||||||
|
const dot = key.replace("_", ".");
|
||||||
|
if (dot !== key) {
|
||||||
|
PermissionVerify.apis.set(dot, {
|
||||||
|
api: descriptor.value,
|
||||||
|
param,
|
||||||
|
});
|
||||||
|
if (param.alias) {
|
||||||
|
param.alias.push(dot);
|
||||||
|
} else {
|
||||||
|
param.alias = [dot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理别名
|
||||||
|
if (param.alias) {
|
||||||
|
param.alias.forEach((alias) => {
|
||||||
|
PermissionVerify.apis.set(alias, {
|
||||||
|
api: descriptor.value,
|
||||||
|
param,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionDAO: PermissionDAO;
|
||||||
|
|
||||||
|
// 确认队列
|
||||||
|
confirmQueue: MessageQueue<{
|
||||||
|
request: Request;
|
||||||
|
confirm: ConfirmParam | boolean;
|
||||||
|
resolve: (value: boolean) => void;
|
||||||
|
reject: (reason: any) => void;
|
||||||
|
}> = new MessageQueue();
|
||||||
|
|
||||||
|
removePermissionCache(scriptId: number) {
|
||||||
|
// 先删除缓存
|
||||||
|
Cache.getInstance()
|
||||||
|
.list()
|
||||||
|
.forEach((key) => {
|
||||||
|
if (key.startsWith(`permission:${scriptId.toString()}:`)) {
|
||||||
|
Cache.getInstance().del(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.permissionDAO = new PermissionDAO();
|
||||||
|
// 监听用户确认消息
|
||||||
|
const message = <MessageHander>IoC.instance(MessageHander);
|
||||||
|
message.setHandler(
|
||||||
|
"permissionConfirm",
|
||||||
|
(_action, data: { uuid: string; userConfirm: UserConfirm }) => {
|
||||||
|
const confirm = this.confirmMap.get(data.uuid);
|
||||||
|
if (!confirm) {
|
||||||
|
if (data.userConfirm.type === 0) {
|
||||||
|
// 忽略
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("confirm not found"));
|
||||||
|
}
|
||||||
|
this.confirmMap.delete(data.uuid);
|
||||||
|
confirm.resolve(data.userConfirm);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 监听获取用户确认消息
|
||||||
|
message.setHandler("getConfirm", (_action, uuid: string) => {
|
||||||
|
const data = this.confirmMap.get(uuid);
|
||||||
|
if (!data) {
|
||||||
|
return Promise.reject(new Error("uuid not found"));
|
||||||
|
}
|
||||||
|
// 查询允许统配的有多少个相同等待确认权限
|
||||||
|
let likeNum = 0;
|
||||||
|
if (data.confirm.wildcard) {
|
||||||
|
this.confirmQueue.list.forEach((value) => {
|
||||||
|
const confirm = value.confirm as ConfirmParam;
|
||||||
|
if (
|
||||||
|
confirm.wildcard &&
|
||||||
|
value.request.scriptId === data.script.id &&
|
||||||
|
confirm.permission === data.confirm.permission
|
||||||
|
) {
|
||||||
|
likeNum += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
script: data.script,
|
||||||
|
confirm: data.confirm,
|
||||||
|
likeNum,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 监听删除权限
|
||||||
|
message.setHandler(
|
||||||
|
"deletePermission",
|
||||||
|
async (_action, data: { scriptId: number; confirm: ConfirmParam }) => {
|
||||||
|
// 先删除缓存
|
||||||
|
this.removePermissionCache(data.scriptId);
|
||||||
|
// 再删除数据库
|
||||||
|
const m = await this.permissionDAO.findOne({
|
||||||
|
scriptId: data.scriptId,
|
||||||
|
permission: data.confirm.permission,
|
||||||
|
permissionValue: data.confirm.permissionValue || "",
|
||||||
|
});
|
||||||
|
if (!m) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
await this.permissionDAO.delete(m.id);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 监听添加权限
|
||||||
|
message.setHandler(
|
||||||
|
"addPermission",
|
||||||
|
async (_action, data: { scriptId: number; permission: Permission }) => {
|
||||||
|
// 先删除缓存
|
||||||
|
this.removePermissionCache(data.scriptId);
|
||||||
|
// 从数据库中查询是否有此权限
|
||||||
|
const m = await this.permissionDAO.findOne({
|
||||||
|
scriptId: data.scriptId,
|
||||||
|
permission: data.permission.permission,
|
||||||
|
permissionValue: data.permission.permissionValue || "",
|
||||||
|
});
|
||||||
|
if (!m) {
|
||||||
|
// 没有添加
|
||||||
|
await this.permissionDAO.save(data.permission);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
// 有则更新
|
||||||
|
data.permission.id = m.id;
|
||||||
|
data.permission.createtime = m.createtime;
|
||||||
|
data.permission.updatetime = new Date().getTime();
|
||||||
|
this.permissionDAO.update(m.id, data.permission);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 监听重置权限
|
||||||
|
message.setHandler(
|
||||||
|
"resetPermission",
|
||||||
|
async (_action, data: { scriptId: number }) => {
|
||||||
|
// 先删除缓存
|
||||||
|
this.removePermissionCache(data.scriptId);
|
||||||
|
// 从数据库中查询是否有此权限
|
||||||
|
await this.permissionDAO.delete({
|
||||||
|
scriptId: data.scriptId,
|
||||||
|
});
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.dealConfirmQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否有权限
|
||||||
|
verify(request: Request, api: ApiValue): Promise<boolean> {
|
||||||
|
if (api.param.default) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
// 没有其它条件,从metadata.grant中判断
|
||||||
|
const { grant } = request.script.metadata;
|
||||||
|
if (!grant) {
|
||||||
|
return Promise.reject(new Error("grant is undefined"));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < grant.length; i += 1) {
|
||||||
|
if (
|
||||||
|
// 名称相等
|
||||||
|
grant[i] === request.api ||
|
||||||
|
// 别名相等
|
||||||
|
(api.param.alias && api.param.alias.includes(grant[i])) ||
|
||||||
|
// 有关联的
|
||||||
|
grant[i] === api.param.link
|
||||||
|
) {
|
||||||
|
// 需要用户确认
|
||||||
|
if (api.param.confirm) {
|
||||||
|
return this.pushConfirmQueue(request, api);
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("permission not requested"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async dealConfirmQueue() {
|
||||||
|
// 处理确认队列
|
||||||
|
const data = await this.confirmQueue.pop();
|
||||||
|
if (!data) {
|
||||||
|
this.dealConfirmQueue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const ret = await this.confirm(data.request, data.confirm);
|
||||||
|
data.resolve(ret);
|
||||||
|
} catch (e) {
|
||||||
|
data.reject(e);
|
||||||
|
}
|
||||||
|
this.dealConfirmQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认队列,为了防止一次性打开过多的窗口
|
||||||
|
async pushConfirmQueue(request: Request, api: ApiValue): Promise<boolean> {
|
||||||
|
const confirm = await api.param.confirm!(request);
|
||||||
|
if (confirm === true) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.confirmQueue.push({ request, confirm, resolve, reject });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm(
|
||||||
|
request: Request,
|
||||||
|
confirm: boolean | ConfirmParam
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (typeof confirm === "boolean") {
|
||||||
|
return confirm;
|
||||||
|
}
|
||||||
|
const cacheKey = CacheKey.permissionConfirm(request.script.id, confirm);
|
||||||
|
// 从数据库中查询是否有此权限
|
||||||
|
const ret = await Cache.getInstance().getOrSet(cacheKey, async () => {
|
||||||
|
let model = await this.permissionDAO.findOne({
|
||||||
|
scriptId: request.scriptId,
|
||||||
|
permission: confirm.permission,
|
||||||
|
permissionValue: confirm.permissionValue || "",
|
||||||
|
});
|
||||||
|
if (!model) {
|
||||||
|
// 允许通配
|
||||||
|
if (confirm.wildcard) {
|
||||||
|
model = await this.permissionDAO.findOne({
|
||||||
|
scriptId: request.scriptId,
|
||||||
|
permission: confirm.permission,
|
||||||
|
permissionValue: "*",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(model);
|
||||||
|
});
|
||||||
|
// 有查询到结果,进入判断,不再需要用户确认
|
||||||
|
if (ret) {
|
||||||
|
if (ret.allow) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
// 权限拒绝
|
||||||
|
return Promise.reject(new Error("permission denied"));
|
||||||
|
}
|
||||||
|
// 没有权限,则弹出页面让用户进行确认
|
||||||
|
const userConfirm = await this.confirmWindow(request.script, confirm);
|
||||||
|
// 成功存入数据库
|
||||||
|
const model = {
|
||||||
|
id: 0,
|
||||||
|
scriptId: request.scriptId,
|
||||||
|
permission: confirm.permission,
|
||||||
|
permissionValue: "",
|
||||||
|
allow: userConfirm.allow,
|
||||||
|
createtime: new Date().getTime(),
|
||||||
|
updatetime: 0,
|
||||||
|
};
|
||||||
|
switch (userConfirm.type) {
|
||||||
|
case 4:
|
||||||
|
case 2: {
|
||||||
|
// 通配
|
||||||
|
model.permissionValue = "*";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 5:
|
||||||
|
case 3: {
|
||||||
|
model.permissionValue = confirm.permissionValue || "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 临时 放入缓存
|
||||||
|
if (userConfirm.type >= 2) {
|
||||||
|
Cache.getInstance().set(cacheKey, model);
|
||||||
|
}
|
||||||
|
// 总是 放入数据库
|
||||||
|
if (userConfirm.type >= 4) {
|
||||||
|
const oldConfirm = await this.permissionDAO.findOne({
|
||||||
|
scriptId: request.scriptId,
|
||||||
|
permission: model.permission,
|
||||||
|
permissionValue: model.permissionValue,
|
||||||
|
});
|
||||||
|
if (!oldConfirm) {
|
||||||
|
await this.permissionDAO.save(model);
|
||||||
|
} else {
|
||||||
|
await this.permissionDAO.update(oldConfirm.id, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userConfirm.allow) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("permission not allowed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认map
|
||||||
|
confirmMap: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
confirm: ConfirmParam;
|
||||||
|
script: Script;
|
||||||
|
resolve: (value: UserConfirm) => void;
|
||||||
|
reject: (reason: any) => void;
|
||||||
|
}
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
|
// 弹出窗口让用户进行确认
|
||||||
|
async confirmWindow(
|
||||||
|
script: Script,
|
||||||
|
confirm: ConfirmParam
|
||||||
|
): Promise<UserConfirm> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
// 超时处理
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.confirmMap.delete(uuid);
|
||||||
|
reject(new Error("permission confirm timeout"));
|
||||||
|
}, 40 * 1000);
|
||||||
|
// 保存到map中
|
||||||
|
this.confirmMap.set(uuid, {
|
||||||
|
confirm,
|
||||||
|
script,
|
||||||
|
resolve: (value: UserConfirm) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(value);
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
// 打开窗口
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: chrome.runtime.getURL(`src/confirm.html?uuid=${uuid}`),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
735
src/runtime/background/runtime.ts
Normal file
735
src/runtime/background/runtime.ts
Normal file
@ -0,0 +1,735 @@
|
|||||||
|
// 脚本运行时,主要负责脚本的加载和匹配
|
||||||
|
// 油猴脚本将监听页面的创建,将代码注入到页面中
|
||||||
|
import MessageSandbox from "@App/app/message/sandbox";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import {
|
||||||
|
Script,
|
||||||
|
SCRIPT_RUN_STATUS,
|
||||||
|
SCRIPT_STATUS_ENABLE,
|
||||||
|
SCRIPT_TYPE_NORMAL,
|
||||||
|
ScriptDAO,
|
||||||
|
ScriptRunResouce,
|
||||||
|
SCRIPT_RUN_STATUS_RUNNING,
|
||||||
|
Metadata,
|
||||||
|
} from "@App/app/repo/scripts";
|
||||||
|
import ResourceManager from "@App/app/service/resource/manager";
|
||||||
|
import ValueManager from "@App/app/service/value/manager";
|
||||||
|
import { dealScript, randomString } from "@App/pkg/utils/utils";
|
||||||
|
import { UrlInclude, UrlMatch } from "@App/pkg/utils/match";
|
||||||
|
import {
|
||||||
|
MessageHander,
|
||||||
|
MessageSender,
|
||||||
|
TargetTag,
|
||||||
|
} from "@App/app/message/message";
|
||||||
|
import ScriptManager from "@App/app/service/script/manager";
|
||||||
|
import { Channel } from "@App/app/message/channel";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import Manager from "@App/app/service/manager";
|
||||||
|
import Hook from "@App/app/service/hook";
|
||||||
|
import { i18nName } from "@App/locales/locales";
|
||||||
|
import { compileInjectScript, compileScriptCode } from "../content/utils";
|
||||||
|
import GMApi, { Request } from "./gm_api";
|
||||||
|
import { genScriptMenu } from "./utils";
|
||||||
|
|
||||||
|
export type RuntimeEvent = "start" | "stop" | "watchRunStatus";
|
||||||
|
|
||||||
|
export type ScriptMenuItem = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
accessKey?: string;
|
||||||
|
sender: MessageSender;
|
||||||
|
channelFlag: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptMenu = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
updatetime: number;
|
||||||
|
hasUserConfig: boolean;
|
||||||
|
metadata: Metadata;
|
||||||
|
runStatus?: SCRIPT_RUN_STATUS;
|
||||||
|
runNum: number;
|
||||||
|
runNumByIframe: number;
|
||||||
|
menus?: ScriptMenuItem[];
|
||||||
|
customExclude?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 后台脚本将会将代码注入到沙盒中
|
||||||
|
@IoC.Singleton(MessageHander, ResourceManager, ValueManager)
|
||||||
|
export default class Runtime extends Manager {
|
||||||
|
messageSandbox?: MessageSandbox;
|
||||||
|
|
||||||
|
scriptDAO: ScriptDAO;
|
||||||
|
|
||||||
|
resourceManager: ResourceManager;
|
||||||
|
|
||||||
|
valueManager: ValueManager;
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
match: UrlMatch<ScriptRunResouce> = new UrlMatch();
|
||||||
|
|
||||||
|
include: UrlInclude<ScriptRunResouce> = new UrlInclude();
|
||||||
|
|
||||||
|
// 自定义排除
|
||||||
|
customizeExclude: UrlMatch<ScriptRunResouce> = new UrlMatch();
|
||||||
|
|
||||||
|
static hook = new Hook<"runStatus">();
|
||||||
|
|
||||||
|
// 运行中和开启的后台脚本
|
||||||
|
runBackScript: Map<number, Script> = new Map();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: MessageHander,
|
||||||
|
resourceManager: ResourceManager,
|
||||||
|
valueManager: ValueManager
|
||||||
|
) {
|
||||||
|
super(message, "runtime");
|
||||||
|
this.scriptDAO = new ScriptDAO();
|
||||||
|
this.resourceManager = resourceManager;
|
||||||
|
this.valueManager = valueManager;
|
||||||
|
this.logger = LoggerCore.getInstance().logger({ component: "runtime" });
|
||||||
|
ScriptManager.hook.addListener("upsert", this.scriptUpdate.bind(this));
|
||||||
|
ScriptManager.hook.addListener("delete", this.scriptDelete.bind(this));
|
||||||
|
ScriptManager.hook.addListener("enable", this.scriptUpdate.bind(this));
|
||||||
|
ScriptManager.hook.addListener("disable", this.scriptUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
// 监听前端消息
|
||||||
|
// 此处是处理执行单次脚本的消息
|
||||||
|
this.listenEvent("start", (id) => {
|
||||||
|
return this.scriptDAO
|
||||||
|
.findById(id)
|
||||||
|
.then((script) => {
|
||||||
|
if (!script) {
|
||||||
|
throw new Error("script not found");
|
||||||
|
}
|
||||||
|
// 因为如果直接引用Runtime,会导致循环依赖,暂时这样处理,后面再梳理梳理
|
||||||
|
return this.startBackgroundScript(script);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.logger.error("run error", Logger.E(e));
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listenEvent("stop", (id) => {
|
||||||
|
return this.scriptDAO
|
||||||
|
.findById(id)
|
||||||
|
.then((script) => {
|
||||||
|
if (!script) {
|
||||||
|
throw new Error("script not found");
|
||||||
|
}
|
||||||
|
// 因为如果直接引用Runtime,会导致循环依赖,暂时这样处理
|
||||||
|
return this.stopBackgroundScript(id);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.logger.error("stop error", Logger.E(e));
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 监听脚本运行状态
|
||||||
|
this.listenScriptRunStatus();
|
||||||
|
|
||||||
|
// 启动普通脚本
|
||||||
|
this.scriptDAO.table.toArray((items) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
// 容错处理
|
||||||
|
if (!item) {
|
||||||
|
this.logger.error("script is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.type !== SCRIPT_TYPE_NORMAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载所有的脚本
|
||||||
|
if (item.status === SCRIPT_STATUS_ENABLE) {
|
||||||
|
this.enable(item);
|
||||||
|
} else {
|
||||||
|
// 只处理未开启的普通页面脚本
|
||||||
|
this.disable(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 接受消息,注入脚本
|
||||||
|
// 获取注入源码
|
||||||
|
|
||||||
|
// 监听菜单创建
|
||||||
|
const scriptMenu: Map<
|
||||||
|
number | TargetTag,
|
||||||
|
Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
request: Request;
|
||||||
|
channel: Channel;
|
||||||
|
}[]
|
||||||
|
>
|
||||||
|
> = new Map();
|
||||||
|
GMApi.hook.addListener(
|
||||||
|
"registerMenu",
|
||||||
|
(request: Request, channel: Channel) => {
|
||||||
|
let senderId: number | TargetTag;
|
||||||
|
if (!request.sender.tabId) {
|
||||||
|
// 非页面脚本
|
||||||
|
senderId = request.sender.targetTag;
|
||||||
|
} else {
|
||||||
|
senderId = request.sender.tabId;
|
||||||
|
}
|
||||||
|
let tabMap = scriptMenu.get(senderId);
|
||||||
|
if (!tabMap) {
|
||||||
|
tabMap = new Map();
|
||||||
|
scriptMenu.set(senderId, tabMap);
|
||||||
|
}
|
||||||
|
let menuArr = tabMap.get(request.scriptId);
|
||||||
|
if (!menuArr) {
|
||||||
|
menuArr = [];
|
||||||
|
tabMap.set(request.scriptId, menuArr);
|
||||||
|
}
|
||||||
|
// 查询菜单是否已经存在
|
||||||
|
for (let i = 0; i < menuArr.length; i += 1) {
|
||||||
|
// id 相等 跳过,选第一个,并close链接
|
||||||
|
if (menuArr[i].request.params[0] === request.params[0]) {
|
||||||
|
channel.disChannel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menuArr.push({ request, channel });
|
||||||
|
// 偷懒行为, 直接重新生成菜单
|
||||||
|
genScriptMenu(senderId, scriptMenu);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
GMApi.hook.addListener("unregisterMenu", (id, request: Request) => {
|
||||||
|
let senderId: number | TargetTag;
|
||||||
|
if (!request.sender.tabId) {
|
||||||
|
// 非页面脚本
|
||||||
|
senderId = request.sender.targetTag;
|
||||||
|
} else {
|
||||||
|
senderId = request.sender.tabId;
|
||||||
|
}
|
||||||
|
const tabMap = scriptMenu.get(senderId);
|
||||||
|
if (tabMap) {
|
||||||
|
const menuArr = tabMap.get(request.scriptId);
|
||||||
|
if (menuArr) {
|
||||||
|
// 从菜单数组中遍历删除
|
||||||
|
for (let i = 0; i < menuArr.length; i += 1) {
|
||||||
|
if (menuArr[i].request.params[0] === id) {
|
||||||
|
menuArr.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (menuArr.length === 0) {
|
||||||
|
tabMap.delete(request.scriptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tabMap.size) {
|
||||||
|
scriptMenu.delete(senderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 偷懒行为
|
||||||
|
genScriptMenu(senderId, scriptMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听页面切换加载菜单
|
||||||
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||||
|
genScriptMenu(activeInfo.tabId, scriptMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
Runtime.hook.addListener("runStatus", async (scriptId: number) => {
|
||||||
|
const script = await this.scriptDAO.findById(scriptId);
|
||||||
|
if (!script) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
script.status !== SCRIPT_STATUS_ENABLE &&
|
||||||
|
script.runStatus !== "running"
|
||||||
|
) {
|
||||||
|
// 没开启并且不是运行中的脚本,删除
|
||||||
|
this.runBackScript.delete(scriptId);
|
||||||
|
} else {
|
||||||
|
// 否则进行一次更新
|
||||||
|
this.runBackScript.set(scriptId, script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录运行次数与iframe运行
|
||||||
|
const runScript = new Map<
|
||||||
|
number,
|
||||||
|
Map<number, { script: Script; runNum: number; runNumByIframe: number }>
|
||||||
|
>();
|
||||||
|
const addRunScript = (
|
||||||
|
tabId: number,
|
||||||
|
script: Script,
|
||||||
|
iframe: boolean,
|
||||||
|
num: number = 1
|
||||||
|
) => {
|
||||||
|
let scripts = runScript.get(tabId);
|
||||||
|
if (!scripts) {
|
||||||
|
scripts = new Map();
|
||||||
|
runScript.set(tabId, scripts);
|
||||||
|
}
|
||||||
|
let scriptNum = scripts.get(script.id);
|
||||||
|
if (!scriptNum) {
|
||||||
|
scriptNum = { script, runNum: 0, runNumByIframe: 0 };
|
||||||
|
scripts.set(script.id, scriptNum);
|
||||||
|
}
|
||||||
|
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||||
|
scriptNum.runNum += num;
|
||||||
|
if (iframe) {
|
||||||
|
scriptNum.runNumByIframe += num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
runScript.delete(tabId);
|
||||||
|
});
|
||||||
|
// 给popup页面获取运行脚本,与菜单
|
||||||
|
this.message.setHandler(
|
||||||
|
"queryPageScript",
|
||||||
|
async (action: string, { url, tabId }: any) => {
|
||||||
|
const tabMap = scriptMenu.get(tabId);
|
||||||
|
const run = runScript.get(tabId);
|
||||||
|
let matchScripts = [];
|
||||||
|
if (!run) {
|
||||||
|
matchScripts = this.matchUrl(url).map((item) => {
|
||||||
|
return { runNum: 0, runNumByIframe: 0, script: item };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
matchScripts = Array.from(run.values());
|
||||||
|
}
|
||||||
|
const allPromise: Promise<ScriptMenu>[] = matchScripts.map(
|
||||||
|
async (item) => {
|
||||||
|
const menus: ScriptMenuItem[] = [];
|
||||||
|
if (tabMap) {
|
||||||
|
tabMap.get(item.script.id)?.forEach((scriptItem) => {
|
||||||
|
menus.push({
|
||||||
|
name: scriptItem.request.params[1],
|
||||||
|
accessKey: scriptItem.request.params[2],
|
||||||
|
id: scriptItem.request.params[0],
|
||||||
|
sender: scriptItem.request.sender,
|
||||||
|
channelFlag: scriptItem.channel.flag,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const script = await this.scriptDAO.findById(item.script.id);
|
||||||
|
if (!script) {
|
||||||
|
return {
|
||||||
|
id: item.script.id,
|
||||||
|
name: i18nName(item.script),
|
||||||
|
enable: item.script.status === SCRIPT_STATUS_ENABLE,
|
||||||
|
updatetime: item.script.updatetime || item.script.createtime,
|
||||||
|
metadata: item.script.metadata,
|
||||||
|
hasUserConfig: !!item.script.config,
|
||||||
|
runNum: item.runNum,
|
||||||
|
runNumByIframe: item.runNumByIframe,
|
||||||
|
customExclude:
|
||||||
|
item.script.selfMetadata && item.script.selfMetadata.exclude,
|
||||||
|
menus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: script.id,
|
||||||
|
name: i18nName(script),
|
||||||
|
enable: script.status === SCRIPT_STATUS_ENABLE,
|
||||||
|
updatetime: script.updatetime || script.createtime,
|
||||||
|
metadata: item.script.metadata,
|
||||||
|
hasUserConfig: !!script?.config,
|
||||||
|
runNum: item.runNum,
|
||||||
|
runNumByIframe: item.runNumByIframe,
|
||||||
|
customExclude: script.selfMetadata && script.selfMetadata.exclude,
|
||||||
|
menus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const scriptList: ScriptMenu[] = await Promise.all(allPromise);
|
||||||
|
|
||||||
|
const backScriptList: ScriptMenu[] = [];
|
||||||
|
const sandboxMenuMap = scriptMenu.get("sandbox");
|
||||||
|
this.runBackScript.forEach((item) => {
|
||||||
|
const menus: ScriptMenuItem[] = [];
|
||||||
|
if (sandboxMenuMap) {
|
||||||
|
sandboxMenuMap?.get(item.id)?.forEach((scriptItem) => {
|
||||||
|
menus.push({
|
||||||
|
name: scriptItem.request.params[1],
|
||||||
|
accessKey: scriptItem.request.params[2],
|
||||||
|
id: scriptItem.request.params[0],
|
||||||
|
sender: scriptItem.request.sender,
|
||||||
|
channelFlag: scriptItem.channel.flag,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
backScriptList.push({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
enable: item.status === SCRIPT_STATUS_ENABLE,
|
||||||
|
updatetime: item.updatetime || item.createtime,
|
||||||
|
metadata: item.metadata,
|
||||||
|
runStatus: item.runStatus,
|
||||||
|
hasUserConfig: !!item.config,
|
||||||
|
runNum:
|
||||||
|
item.runStatus && item.runStatus === SCRIPT_RUN_STATUS_RUNNING
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
menus,
|
||||||
|
runNumByIframe: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.resolve({
|
||||||
|
scriptList,
|
||||||
|
backScriptList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// content页发送页面加载完成消息,注入脚本
|
||||||
|
this.message.setHandler(
|
||||||
|
"pageLoad",
|
||||||
|
(_action: string, data: any, sender: MessageSender) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!sender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(sender.url && sender.tabId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sender.frameId === undefined) {
|
||||||
|
// 清理之前的数据
|
||||||
|
runScript.delete(sender.tabId);
|
||||||
|
}
|
||||||
|
// 未开启
|
||||||
|
if (localStorage.enable_script === "false") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exclude = this.customizeExclude.match(sender.url);
|
||||||
|
// 自定义排除的, buildScriptRunResource时会将selfMetadata合并,所以后续不需要再处理metadata.exclude,这算是一个隐性的坑,后面看看要不要处理
|
||||||
|
exclude.forEach((val) => {
|
||||||
|
addRunScript(sender.tabId!, val, false, 0);
|
||||||
|
});
|
||||||
|
const filter: ScriptRunResouce[] = this.matchUrl(
|
||||||
|
sender.url,
|
||||||
|
(script) => {
|
||||||
|
// 如果是iframe,判断是否允许在iframe里运行
|
||||||
|
if (sender.frameId !== undefined) {
|
||||||
|
if (script.metadata.noframes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
addRunScript(sender.tabId!, script, true);
|
||||||
|
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||||
|
}
|
||||||
|
addRunScript(sender.tabId!, script, false);
|
||||||
|
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!filter.length) {
|
||||||
|
resolve({ scripts: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ scripts: filter });
|
||||||
|
|
||||||
|
// 注入脚本
|
||||||
|
filter.forEach((script) => {
|
||||||
|
let runAt = "document_idle";
|
||||||
|
if (script.metadata["run-at"]) {
|
||||||
|
[runAt] = script.metadata["run-at"];
|
||||||
|
}
|
||||||
|
switch (runAt) {
|
||||||
|
case "document-body":
|
||||||
|
case "document-start":
|
||||||
|
runAt = "document_start";
|
||||||
|
break;
|
||||||
|
case "document-end":
|
||||||
|
runAt = "document_end";
|
||||||
|
break;
|
||||||
|
case "document-idle":
|
||||||
|
default:
|
||||||
|
runAt = "document_idle";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chrome.tabs.executeScript(sender.tabId!, {
|
||||||
|
frameId: sender.frameId,
|
||||||
|
code: `(function(){
|
||||||
|
let temp = document.createElementNS("http://www.w3.org/1999/xhtml", "script");
|
||||||
|
temp.setAttribute('type', 'text/javascript');
|
||||||
|
temp.innerHTML = "${script.code}";
|
||||||
|
temp.className = "injected-js";
|
||||||
|
document.documentElement.appendChild(temp);
|
||||||
|
temp.remove();
|
||||||
|
}())`,
|
||||||
|
runAt,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 角标和脚本
|
||||||
|
chrome.browserAction.getBadgeText(
|
||||||
|
{
|
||||||
|
tabId: sender.tabId,
|
||||||
|
},
|
||||||
|
(res: string) => {
|
||||||
|
chrome.browserAction.setBadgeText({
|
||||||
|
text: (filter.length + (parseInt(res, 10) || 0)).toString(),
|
||||||
|
tabId: sender.tabId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
chrome.browserAction.setBadgeBackgroundColor({
|
||||||
|
color: "#4e5969",
|
||||||
|
tabId: sender.tabId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessageSandbox(messageSandbox: MessageSandbox) {
|
||||||
|
this.messageSandbox = messageSandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动沙盒相关脚本
|
||||||
|
startSandbox(messageSandbox: MessageSandbox) {
|
||||||
|
this.messageSandbox = messageSandbox;
|
||||||
|
this.scriptDAO.table.toArray((items) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
// 容错处理
|
||||||
|
if (!item) {
|
||||||
|
this.logger.error("script is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.type === SCRIPT_TYPE_NORMAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载所有的脚本
|
||||||
|
if (item.status === SCRIPT_STATUS_ENABLE) {
|
||||||
|
this.enable(item);
|
||||||
|
this.runBackScript.set(item.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listenScriptRunStatus() {
|
||||||
|
// 监听沙盒发送的脚本运行状态消息
|
||||||
|
this.message.setHandler(
|
||||||
|
"scriptRunStatus",
|
||||||
|
(action, [scriptId, runStatus, error, nextruntime]: any) => {
|
||||||
|
this.scriptDAO.update(scriptId, {
|
||||||
|
runStatus,
|
||||||
|
lastruntime: new Date().getTime(),
|
||||||
|
nextruntime,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
Runtime.hook.trigger("runStatus", scriptId, runStatus);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 处理前台发送的脚本运行状态监听请求
|
||||||
|
this.message.setHandlerWithChannel("watchRunStatus", (channel) => {
|
||||||
|
const hook = (scriptId: number, status: SCRIPT_RUN_STATUS) => {
|
||||||
|
channel.send([scriptId, status]);
|
||||||
|
};
|
||||||
|
Runtime.hook.addListener("runStatus", hook);
|
||||||
|
channel.setDisChannelHandler(() => {
|
||||||
|
Runtime.hook.removeListener("runStatus", hook);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本发生变动
|
||||||
|
async scriptUpdate(script: Script): Promise<boolean> {
|
||||||
|
// 脚本更新先更新资源
|
||||||
|
await this.resourceManager.checkScriptResource(script);
|
||||||
|
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||||
|
return this.enable(script as ScriptRunResouce);
|
||||||
|
}
|
||||||
|
return this.disable(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchUrl(url: string, filterFunc?: (script: Script) => boolean) {
|
||||||
|
const scripts = this.match.match(url);
|
||||||
|
// 再include中匹配
|
||||||
|
scripts.push(...this.include.match(url));
|
||||||
|
const filter: { [key: string]: ScriptRunResouce } = {};
|
||||||
|
// 去重
|
||||||
|
scripts.forEach((script) => {
|
||||||
|
if (filterFunc && filterFunc(script)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filter[script.id] = script;
|
||||||
|
});
|
||||||
|
// 转换成数组
|
||||||
|
return Object.keys(filter).map((key) => filter[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本删除
|
||||||
|
async scriptDelete(script: Script): Promise<boolean> {
|
||||||
|
// 清理匹配资源
|
||||||
|
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||||
|
this.match.del(<ScriptRunResouce>script);
|
||||||
|
this.include.del(<ScriptRunResouce>script);
|
||||||
|
} else {
|
||||||
|
this.unloadBackgroundScript(script);
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本开启
|
||||||
|
async enable(script: Script): Promise<boolean> {
|
||||||
|
// 编译脚本运行资源
|
||||||
|
const scriptRes = await this.buildScriptRunResource(script);
|
||||||
|
if (script.type !== SCRIPT_TYPE_NORMAL) {
|
||||||
|
return this.loadBackgroundScript(scriptRes);
|
||||||
|
}
|
||||||
|
return this.loadPageScript(scriptRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本关闭
|
||||||
|
disable(script: Script): Promise<boolean> {
|
||||||
|
if (script.type !== SCRIPT_TYPE_NORMAL) {
|
||||||
|
return this.unloadBackgroundScript(script);
|
||||||
|
}
|
||||||
|
return this.unloadPageScript(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载页面脚本
|
||||||
|
loadPageScript(script: ScriptRunResouce) {
|
||||||
|
// 重构code
|
||||||
|
const logger = this.logger.with({
|
||||||
|
scriptId: script.id,
|
||||||
|
name: script.name,
|
||||||
|
});
|
||||||
|
script.code = dealScript(compileInjectScript(script));
|
||||||
|
|
||||||
|
this.match.del(<ScriptRunResouce>script);
|
||||||
|
this.include.del(<ScriptRunResouce>script);
|
||||||
|
if (script.metadata.match) {
|
||||||
|
script.metadata.match.forEach((url) => {
|
||||||
|
try {
|
||||||
|
this.match.add(url, script);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("url load error", Logger.E(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (script.metadata.include) {
|
||||||
|
script.metadata.include.forEach((url) => {
|
||||||
|
try {
|
||||||
|
this.include.add(url, script);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("url load error", Logger.E(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (script.metadata.exclude) {
|
||||||
|
script.metadata.exclude.forEach((url) => {
|
||||||
|
try {
|
||||||
|
this.include.exclude(url, script);
|
||||||
|
this.match.exclude(url, script);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("url load error", Logger.E(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (script.selfMetadata && script.selfMetadata.exclude) {
|
||||||
|
script.selfMetadata.exclude.forEach((url) => {
|
||||||
|
try {
|
||||||
|
this.customizeExclude.add(url, script);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("url load error", Logger.E(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载页面脚本
|
||||||
|
unloadPageScript(script: Script) {
|
||||||
|
return this.loadPageScript(<ScriptRunResouce>script);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载并启动后台脚本
|
||||||
|
loadBackgroundScript(script: ScriptRunResouce): Promise<boolean> {
|
||||||
|
this.runBackScript.set(script.id, script);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 清除重试数据
|
||||||
|
script.nextruntime = 0;
|
||||||
|
this.messageSandbox
|
||||||
|
?.syncSend("enable", script)
|
||||||
|
.then(() => {
|
||||||
|
resolve(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error("backscript load error", Logger.E(err));
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载并停止后台脚本
|
||||||
|
unloadBackgroundScript(script: Script): Promise<boolean> {
|
||||||
|
this.runBackScript.delete(script.id);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.messageSandbox
|
||||||
|
?.syncSend("disable", script.id)
|
||||||
|
.then(() => {
|
||||||
|
resolve(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error("backscript stop error", Logger.E(err));
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startBackgroundScript(script: Script) {
|
||||||
|
const scriptRes = await this.buildScriptRunResource(script);
|
||||||
|
this.messageSandbox?.syncSend("start", scriptRes);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopBackgroundScript(scriptId: number) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.messageSandbox
|
||||||
|
?.syncSend("stop", scriptId)
|
||||||
|
.then((resp) => {
|
||||||
|
resolve(resp);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error("backscript stop error", Logger.E(err));
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildScriptRunResource(script: Script): Promise<ScriptRunResouce> {
|
||||||
|
const ret: ScriptRunResouce = <ScriptRunResouce>Object.assign(script);
|
||||||
|
|
||||||
|
// 自定义配置
|
||||||
|
if (ret.selfMetadata) {
|
||||||
|
ret.metadata = { ...ret.metadata };
|
||||||
|
Object.keys(ret.selfMetadata).forEach((key) => {
|
||||||
|
ret.metadata[key] = ret.selfMetadata![key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.value = await this.valueManager.getScriptValues(ret);
|
||||||
|
|
||||||
|
ret.resource = await this.resourceManager.getScriptResources(ret);
|
||||||
|
|
||||||
|
ret.flag = randomString(16);
|
||||||
|
ret.sourceCode = ret.code;
|
||||||
|
ret.code = compileScriptCode(ret);
|
||||||
|
|
||||||
|
ret.grantMap = {};
|
||||||
|
|
||||||
|
ret.metadata.grant?.forEach((val: string) => {
|
||||||
|
ret.grantMap[val] = "ok";
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(ret);
|
||||||
|
}
|
||||||
|
}
|
535
src/runtime/background/utils.ts
Normal file
535
src/runtime/background/utils.ts
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import { Channel } from "@App/app/message/channel";
|
||||||
|
import { SCRIPT_STATUS_ENABLE, Script } from "@App/app/repo/scripts";
|
||||||
|
import { isFirefox } from "@App/pkg/utils/utils";
|
||||||
|
import MessageCenter from "@App/app/message/center";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import { Request } from "./gm_api";
|
||||||
|
import Runtime from "./runtime";
|
||||||
|
|
||||||
|
export const unsafeHeaders: { [key: string]: boolean } = {
|
||||||
|
// 部分浏览器中并未允许
|
||||||
|
"user-agent": true,
|
||||||
|
// 这两个是前缀
|
||||||
|
"proxy-": true,
|
||||||
|
"sec-": true,
|
||||||
|
// cookie已经特殊处理
|
||||||
|
cookie: true,
|
||||||
|
"accept-charset": true,
|
||||||
|
"accept-encoding": true,
|
||||||
|
"access-control-request-headers": true,
|
||||||
|
"access-control-request-method": true,
|
||||||
|
connection: true,
|
||||||
|
"content-length": true,
|
||||||
|
date: true,
|
||||||
|
dnt: true,
|
||||||
|
expect: true,
|
||||||
|
"feature-policy": true,
|
||||||
|
host: true,
|
||||||
|
"keep-alive": true,
|
||||||
|
origin: true,
|
||||||
|
referer: true,
|
||||||
|
te: true,
|
||||||
|
trailer: true,
|
||||||
|
"transfer-encoding": true,
|
||||||
|
upgrade: true,
|
||||||
|
via: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const responseHeaders: { [key: string]: boolean } = {
|
||||||
|
"set-cookie": true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isUnsafeHeaders(header: string) {
|
||||||
|
return unsafeHeaders[header.toLocaleLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExtensionRequest(
|
||||||
|
details: chrome.webRequest.ResourceRequest & { originUrl?: string }
|
||||||
|
): boolean {
|
||||||
|
return !!(
|
||||||
|
(details.initiator &&
|
||||||
|
chrome.runtime.getURL("").startsWith(details.initiator)) ||
|
||||||
|
(details.originUrl &&
|
||||||
|
details.originUrl.startsWith(chrome.runtime.getURL("")))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听web请求,处理unsafeHeaders
|
||||||
|
export function listenerWebRequest(headerFlag: string) {
|
||||||
|
const reqOpt = ["blocking", "requestHeaders"];
|
||||||
|
const respOpt = ["blocking", "responseHeaders"];
|
||||||
|
if (!isFirefox()) {
|
||||||
|
reqOpt.push("extraHeaders");
|
||||||
|
respOpt.push("extraHeaders");
|
||||||
|
}
|
||||||
|
const maxRedirects = new Map<string, [number, number]>();
|
||||||
|
const isRedirects = new Map<string, boolean>();
|
||||||
|
// 处理发送请求的unsafeHeaders
|
||||||
|
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
(details) => {
|
||||||
|
if (!isExtensionRequest(details)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// 处理unsafeHeaders
|
||||||
|
let cookie = "";
|
||||||
|
let setCookie = "";
|
||||||
|
let anonymous = false;
|
||||||
|
let isGmXhr = false;
|
||||||
|
const requestHeaders: chrome.webRequest.HttpHeader[] = [];
|
||||||
|
const preRequestHeaders: { [key: string]: string | null } = {};
|
||||||
|
details.requestHeaders?.forEach((val) => {
|
||||||
|
const lowerCase = val.name.toLowerCase();
|
||||||
|
if (lowerCase.startsWith(`${headerFlag}-`)) {
|
||||||
|
const headerKey = lowerCase.substring(headerFlag.length + 1);
|
||||||
|
// 处理unsafeHeaders
|
||||||
|
switch (headerKey) {
|
||||||
|
case "cookie":
|
||||||
|
setCookie = val.value || "";
|
||||||
|
break;
|
||||||
|
case "max-redirects":
|
||||||
|
maxRedirects.set(details.requestId, [
|
||||||
|
0,
|
||||||
|
parseInt(val.value || "", 10),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case "anonymous":
|
||||||
|
anonymous = true;
|
||||||
|
break;
|
||||||
|
case "gm-xhr":
|
||||||
|
isGmXhr = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
preRequestHeaders[headerKey] = val.value || null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 原生header
|
||||||
|
switch (lowerCase) {
|
||||||
|
case "cookie":
|
||||||
|
cookie = val.value || "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 如果是unsafeHeaders,则判断是否已经有值,有值则不进行处理
|
||||||
|
if (
|
||||||
|
unsafeHeaders[lowerCase] ||
|
||||||
|
lowerCase.startsWith("sec-") ||
|
||||||
|
lowerCase.startsWith("proxy-")
|
||||||
|
) {
|
||||||
|
// null表示不发送此header
|
||||||
|
if (preRequestHeaders[lowerCase] !== null) {
|
||||||
|
preRequestHeaders[lowerCase] =
|
||||||
|
preRequestHeaders[lowerCase] || val.value || "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestHeaders.push(val);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 不是由GM XHR发起的请求,不处理
|
||||||
|
if (!isGmXhr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// 匿名移除掉cookie
|
||||||
|
if (anonymous) {
|
||||||
|
cookie = "";
|
||||||
|
}
|
||||||
|
// 有设置cookie,则进行处理
|
||||||
|
if (setCookie) {
|
||||||
|
// 判断结尾是否有分号,没有则添加,然后进行拼接
|
||||||
|
if (!cookie || cookie.endsWith(";")) {
|
||||||
|
cookie += setCookie;
|
||||||
|
} else {
|
||||||
|
cookie += `;${setCookie}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 有cookie,则进行处理
|
||||||
|
if (cookie) {
|
||||||
|
requestHeaders.push({
|
||||||
|
name: "Cookie",
|
||||||
|
value: cookie,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.keys(preRequestHeaders).forEach((key) => {
|
||||||
|
// null表示不发送此header
|
||||||
|
if (preRequestHeaders[key] !== null) {
|
||||||
|
requestHeaders.push({
|
||||||
|
name: key,
|
||||||
|
value: preRequestHeaders[key]!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
requestHeaders,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: ["<all_urls>"],
|
||||||
|
},
|
||||||
|
reqOpt
|
||||||
|
);
|
||||||
|
// 处理无法读取的responseHeaders
|
||||||
|
chrome.webRequest.onHeadersReceived.addListener(
|
||||||
|
(details) => {
|
||||||
|
if (!isExtensionRequest(details)) {
|
||||||
|
// 判断是否为页面请求
|
||||||
|
if (
|
||||||
|
!(details.type === "main_frame" || details.type === "sub_frame") ||
|
||||||
|
!isFirefox()
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// 判断页面上是否有脚本会运行,如果有判断是否有csp,有则移除csp策略
|
||||||
|
const runtime = IoC.instance(Runtime) as Runtime;
|
||||||
|
// 这块代码与runtime里的pageLoad一样,考虑后面要不要优化
|
||||||
|
const result = runtime.matchUrl(details.url, (script) => {
|
||||||
|
// 如果是iframe,判断是否允许在iframe里运行
|
||||||
|
if (details.type === "sub_frame") {
|
||||||
|
if (script.metadata.noframes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||||
|
}
|
||||||
|
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||||
|
});
|
||||||
|
if (result.length > 0 && details.responseHeaders) {
|
||||||
|
// 移除csp
|
||||||
|
for (let i = 0; i < details.responseHeaders.length; i += 1) {
|
||||||
|
if (
|
||||||
|
details.responseHeaders[i].name.toLowerCase() ===
|
||||||
|
"content-security-policy"
|
||||||
|
) {
|
||||||
|
details.responseHeaders[i].value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
responseHeaders: details.responseHeaders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const appendHeaders: chrome.webRequest.HttpHeader[] = [];
|
||||||
|
details.responseHeaders?.forEach((val) => {
|
||||||
|
const lowerCase = val.name.toLowerCase();
|
||||||
|
if (responseHeaders[lowerCase]) {
|
||||||
|
const copy = { ...val };
|
||||||
|
copy.name = `${headerFlag}-${val.name}`;
|
||||||
|
appendHeaders.push(copy);
|
||||||
|
}
|
||||||
|
// 处理最大重定向次数
|
||||||
|
if (lowerCase === "location") {
|
||||||
|
isRedirects.set(details.requestId, true);
|
||||||
|
const nums = maxRedirects.get(details.requestId);
|
||||||
|
if (nums) {
|
||||||
|
nums[0] += 1;
|
||||||
|
// 当前重定向次数大于最大重定向次数时,修改掉locatin,防止重定向
|
||||||
|
if (nums[0] > nums[1]) {
|
||||||
|
val.name = `${headerFlag}-${val.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
details.responseHeaders?.push(...appendHeaders);
|
||||||
|
// 判断是否为重定向请求,如果是,将url注入到finalUrl
|
||||||
|
if (isRedirects.has(details.requestId)) {
|
||||||
|
details.responseHeaders?.push({
|
||||||
|
name: `${headerFlag}-final-url`,
|
||||||
|
value: details.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
responseHeaders: details.responseHeaders,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: ["<all_urls>"],
|
||||||
|
},
|
||||||
|
respOpt
|
||||||
|
);
|
||||||
|
chrome.webRequest.onCompleted.addListener(
|
||||||
|
(details) => {
|
||||||
|
if (!isExtensionRequest(details)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 删除最大重定向数缓存
|
||||||
|
maxRedirects.delete(details.requestId);
|
||||||
|
isRedirects.delete(details.requestId);
|
||||||
|
},
|
||||||
|
{ urls: ["<all_urls>"] }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给xhr添加headers,包括unsafeHeaders
|
||||||
|
export function setXhrHeader(
|
||||||
|
headerFlag: string,
|
||||||
|
config: GMSend.XHRDetails,
|
||||||
|
xhr: XMLHttpRequest
|
||||||
|
) {
|
||||||
|
xhr.setRequestHeader(`${headerFlag}-gm-xhr`, "true");
|
||||||
|
if (config.headers) {
|
||||||
|
let hasOrigin = false;
|
||||||
|
Object.keys(config.headers).forEach((key) => {
|
||||||
|
const lowKey = key.toLowerCase();
|
||||||
|
if (lowKey === "origin") {
|
||||||
|
hasOrigin = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
unsafeHeaders[lowKey] ||
|
||||||
|
lowKey.startsWith("sec-") ||
|
||||||
|
lowKey.startsWith("proxy-")
|
||||||
|
) {
|
||||||
|
xhr.setRequestHeader(
|
||||||
|
`${headerFlag}-${lowKey}`,
|
||||||
|
config.headers![key]!
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 直接设置header
|
||||||
|
xhr.setRequestHeader(key, config.headers![key]!);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error(
|
||||||
|
"GM XHR setRequestHeader error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!hasOrigin) {
|
||||||
|
xhr.setRequestHeader(`${headerFlag}-origin`, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.maxRedirects !== undefined) {
|
||||||
|
xhr.setRequestHeader(
|
||||||
|
`${headerFlag}-max-redirects`,
|
||||||
|
config.maxRedirects.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.cookie) {
|
||||||
|
try {
|
||||||
|
xhr.setRequestHeader(`${headerFlag}-cookie`, config.cookie);
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error(
|
||||||
|
"GM XHR setRequestHeader cookie error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.anonymous) {
|
||||||
|
xhr.setRequestHeader(`${headerFlag}-anonymous`, "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchHeader(
|
||||||
|
headerFlag: string,
|
||||||
|
config: GMSend.XHRDetails
|
||||||
|
): any {
|
||||||
|
const headers: { [key: string]: string } = {};
|
||||||
|
headers[`${headerFlag}-gm-xhr`] = "true";
|
||||||
|
if (config.headers) {
|
||||||
|
Object.keys(config.headers).forEach((key) => {
|
||||||
|
const lowKey = key.toLowerCase();
|
||||||
|
if (
|
||||||
|
unsafeHeaders[lowKey] ||
|
||||||
|
lowKey.startsWith("sec-") ||
|
||||||
|
lowKey.startsWith("proxy-")
|
||||||
|
) {
|
||||||
|
headers[`${headerFlag}-${lowKey}`] = config.headers![key]!;
|
||||||
|
} else {
|
||||||
|
// 直接设置header
|
||||||
|
headers[key] = config.headers![key]!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.maxRedirects !== undefined) {
|
||||||
|
headers[`${headerFlag}-max-redirects`] = config.maxRedirects.toString();
|
||||||
|
}
|
||||||
|
if (config.cookie) {
|
||||||
|
headers[`${headerFlag}-cookie`] = config.cookie;
|
||||||
|
}
|
||||||
|
if (config.anonymous) {
|
||||||
|
headers[`${headerFlag}-anonymous`] = "true";
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dealXhr(
|
||||||
|
headerFlag: string,
|
||||||
|
config: GMSend.XHRDetails,
|
||||||
|
xhr: XMLHttpRequest
|
||||||
|
): Promise<GMTypes.XHRResponse> {
|
||||||
|
let finalUrl = xhr.responseURL || config.url;
|
||||||
|
// 判断是否有headerFlag-final-url,有则替换finalUrl
|
||||||
|
const finalUrlHeader = xhr.getResponseHeader(`${headerFlag}-final-url`);
|
||||||
|
if (finalUrlHeader) {
|
||||||
|
finalUrl = finalUrlHeader;
|
||||||
|
}
|
||||||
|
const removeXCat = new RegExp(`${headerFlag}-`, "g");
|
||||||
|
const respond: GMTypes.XHRResponse = {
|
||||||
|
finalUrl,
|
||||||
|
readyState: <any>xhr.readyState,
|
||||||
|
status: xhr.status,
|
||||||
|
statusText: xhr.statusText,
|
||||||
|
responseHeaders: xhr.getAllResponseHeaders().replace(removeXCat, ""),
|
||||||
|
responseType: config.responseType,
|
||||||
|
};
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (
|
||||||
|
config.responseType?.toLowerCase() === "arraybuffer" ||
|
||||||
|
config.responseType?.toLowerCase() === "blob"
|
||||||
|
) {
|
||||||
|
let blob: Blob;
|
||||||
|
if (xhr.response instanceof ArrayBuffer) {
|
||||||
|
blob = new Blob([xhr.response]);
|
||||||
|
respond.response = URL.createObjectURL(blob);
|
||||||
|
} else {
|
||||||
|
blob = <Blob>xhr.response;
|
||||||
|
respond.response = URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (xhr.getResponseHeader("Content-Type")?.indexOf("text") !== -1) {
|
||||||
|
// 如果是文本类型,则尝试转换为文本
|
||||||
|
respond.responseText = await blob.text();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error(
|
||||||
|
"GM XHR getResponseHeader error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(<string>respond.response);
|
||||||
|
}, 60e3);
|
||||||
|
} else if (config.responseType === "json") {
|
||||||
|
try {
|
||||||
|
respond.response = JSON.parse(xhr.responseText);
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error("GM XHR JSON parse error");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
respond.responseText = xhr.responseText;
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error("GM XHR getResponseText error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
respond.response = xhr.response;
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error("GM XHR response error");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
respond.responseText = xhr.responseText || undefined;
|
||||||
|
} catch (e) {
|
||||||
|
LoggerCore.getLogger(Logger.E(e)).error("GM XHR getResponseText error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(respond);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dealFetch(
|
||||||
|
headerFlag: string,
|
||||||
|
config: GMSend.XHRDetails,
|
||||||
|
response: Response,
|
||||||
|
readyState: 0 | 1 | 2 | 3 | 4
|
||||||
|
) {
|
||||||
|
const removeXCat = new RegExp(`${headerFlag}-`, "g");
|
||||||
|
let respHeader = "";
|
||||||
|
response.headers &&
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
respHeader += `${key.replace(removeXCat, "")}: ${value}\n`;
|
||||||
|
});
|
||||||
|
const respond: GMTypes.XHRResponse = {
|
||||||
|
finalUrl: response.url || config.url,
|
||||||
|
readyState,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
responseHeaders: respHeader,
|
||||||
|
responseType: config.responseType,
|
||||||
|
};
|
||||||
|
return respond;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIcon(script: Script): string {
|
||||||
|
return (
|
||||||
|
(script.metadata.icon && script.metadata.icon[0]) ||
|
||||||
|
(script.metadata.iconurl && script.metadata.iconurl[0]) ||
|
||||||
|
(script.metadata.defaulticon && script.metadata.defaulticon[0]) ||
|
||||||
|
(script.metadata.icon64 && script.metadata.icon64[0]) ||
|
||||||
|
(script.metadata.icon64url && script.metadata.icon64url[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function genScriptMenuByTabMap(
|
||||||
|
tabMap: Map<number, { request: Request; channel: Channel }[]>
|
||||||
|
) {
|
||||||
|
tabMap.forEach((menuArr, scriptId) => {
|
||||||
|
// 创建脚本菜单
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: `scriptMenu_${scriptId}`,
|
||||||
|
title: menuArr[0].request.script.name,
|
||||||
|
contexts: ["all"],
|
||||||
|
parentId: "scriptMenu",
|
||||||
|
});
|
||||||
|
menuArr.forEach((menu) => {
|
||||||
|
// 创建菜单
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: `scriptMenu_menu_${scriptId}_${menu.request.params[0]}`,
|
||||||
|
title: menu.request.params[1],
|
||||||
|
contexts: ["all"],
|
||||||
|
parentId: `scriptMenu_${scriptId}`,
|
||||||
|
onclick: () => {
|
||||||
|
(IoC.instance(MessageCenter) as MessageCenter).sendNative(
|
||||||
|
{
|
||||||
|
tag: menu.request.sender.targetTag,
|
||||||
|
id: [
|
||||||
|
menu.request.sender.frameId || menu.request.sender.tabId || 0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stream: menu.channel.flag,
|
||||||
|
channel: true,
|
||||||
|
data: "click",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成chrome菜单
|
||||||
|
export function genScriptMenu(
|
||||||
|
tabId: number | string,
|
||||||
|
scriptMenu: Map<
|
||||||
|
number | string,
|
||||||
|
Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
request: Request;
|
||||||
|
channel: Channel;
|
||||||
|
}[]
|
||||||
|
>
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
// 移除之前所有的菜单
|
||||||
|
chrome.contextMenus.removeAll();
|
||||||
|
const tabMap = scriptMenu.get(tabId);
|
||||||
|
const backTabMap = scriptMenu.get("sandbox");
|
||||||
|
if (!tabMap && !backTabMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 创建根菜单
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: "scriptMenu",
|
||||||
|
title: "ScriptCat",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
if (tabMap) {
|
||||||
|
genScriptMenuByTabMap(tabMap);
|
||||||
|
}
|
||||||
|
// 后台脚本的菜单
|
||||||
|
if (tabId !== "sandbox") {
|
||||||
|
if (backTabMap) {
|
||||||
|
genScriptMenuByTabMap(backTabMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
src/runtime/content/content.ts
Normal file
145
src/runtime/content/content.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { ExternalMessage } from "@App/app/const";
|
||||||
|
import MessageContent from "@App/app/message/content";
|
||||||
|
import MessageInternal from "@App/app/message/internal";
|
||||||
|
import { MessageHander, MessageManager } from "@App/app/message/message";
|
||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
|
||||||
|
// content页的处理
|
||||||
|
export default class ContentRuntime {
|
||||||
|
contentMessage: MessageHander & MessageManager;
|
||||||
|
|
||||||
|
internalMessage: MessageInternal;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
contentMessage: MessageHander & MessageManager,
|
||||||
|
internalMessage: MessageInternal
|
||||||
|
) {
|
||||||
|
this.contentMessage = contentMessage;
|
||||||
|
this.internalMessage = internalMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(resp: { scripts: ScriptRunResouce[] }) {
|
||||||
|
// 由content到background
|
||||||
|
// 转发gmApi消息
|
||||||
|
this.contentMessage.setHandler("gmApi", (action, data) => {
|
||||||
|
return this.internalMessage.syncSend(action, data);
|
||||||
|
});
|
||||||
|
// 转发log消息
|
||||||
|
this.contentMessage.setHandler("log", (action, data) => {
|
||||||
|
this.internalMessage.send(action, data);
|
||||||
|
});
|
||||||
|
// 转发externalMessage消息
|
||||||
|
this.contentMessage.setHandler(ExternalMessage, (action, data) => {
|
||||||
|
return this.internalMessage.syncSend(action, data);
|
||||||
|
});
|
||||||
|
// 处理GM_addElement
|
||||||
|
// @ts-ignore
|
||||||
|
this.contentMessage.setHandler("GM_addElement", (action, data) => {
|
||||||
|
const parma = data.param;
|
||||||
|
let attr: { [x: string]: any; textContent?: any };
|
||||||
|
let textContent = "";
|
||||||
|
if (!parma[1]) {
|
||||||
|
attr = {};
|
||||||
|
} else {
|
||||||
|
attr = { ...parma[1] };
|
||||||
|
if (attr.textContent) {
|
||||||
|
textContent = attr.textContent;
|
||||||
|
delete attr.textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const el = <Element>document.createElement(parma[0]);
|
||||||
|
Object.keys(attr).forEach((key) => {
|
||||||
|
el.setAttribute(key, attr[key]);
|
||||||
|
});
|
||||||
|
if (textContent) {
|
||||||
|
el.innerHTML = textContent;
|
||||||
|
}
|
||||||
|
let parentNode;
|
||||||
|
if (data.relatedTarget) {
|
||||||
|
parentNode = (<MessageContent>(
|
||||||
|
this.contentMessage
|
||||||
|
)).getAndDelRelatedTarget(data.relatedTarget);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
<Element>parentNode ||
|
||||||
|
document.head ||
|
||||||
|
document.body ||
|
||||||
|
document.querySelector("*")
|
||||||
|
).appendChild(el);
|
||||||
|
return {
|
||||||
|
relatedTarget: el,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转发长连接的gmApi消息
|
||||||
|
this.contentMessage.setHandlerWithChannel(
|
||||||
|
"gmApiChannel",
|
||||||
|
(inject, action, data) => {
|
||||||
|
const background = this.internalMessage.channel();
|
||||||
|
// 转发inject->background
|
||||||
|
inject.setHandler((req) => {
|
||||||
|
background.send(req.data);
|
||||||
|
});
|
||||||
|
inject.setCatch((err) => {
|
||||||
|
background.throw(err);
|
||||||
|
});
|
||||||
|
inject.setDisChannelHandler(() => {
|
||||||
|
background.disChannel();
|
||||||
|
});
|
||||||
|
// 转发background->inject
|
||||||
|
background.setHandler((bgResp) => {
|
||||||
|
inject.send(bgResp);
|
||||||
|
});
|
||||||
|
background.setCatch((err) => {
|
||||||
|
inject.throw(err);
|
||||||
|
});
|
||||||
|
background.setDisChannelHandler(() => {
|
||||||
|
inject.disChannel();
|
||||||
|
});
|
||||||
|
// 建立连接
|
||||||
|
background.channel(action, data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.listenCATApi();
|
||||||
|
|
||||||
|
// 由background到content
|
||||||
|
// 转发value更新事件
|
||||||
|
this.internalMessage.setHandler("valueUpdate", (action, data) => {
|
||||||
|
this.contentMessage.send(action, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.contentMessage.send("pageLoad", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
listenCATApi() {
|
||||||
|
// 处理特殊的消息,不需要转发到background
|
||||||
|
this.contentMessage.setHandler("CAT_fetchBlob", (_action, data: string) => {
|
||||||
|
return fetch(data).then((res) => res.blob());
|
||||||
|
});
|
||||||
|
this.contentMessage.setHandler(
|
||||||
|
"CAT_createBlobUrl",
|
||||||
|
(_action, data: Blob) => {
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 60 * 1000);
|
||||||
|
return Promise.resolve(url);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 处理CAT_fetchDocument
|
||||||
|
this.contentMessage.setHandler("CAT_fetchDocument", (_action, data) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = "document";
|
||||||
|
xhr.open("GET", data);
|
||||||
|
xhr.onload = () => {
|
||||||
|
resolve({
|
||||||
|
relatedTarget: xhr.response,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
120
src/runtime/content/exec_script.test.ts
Normal file
120
src/runtime/content/exec_script.test.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import initTestEnv from "@App/pkg/utils/test_utils";
|
||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import ExecScript from "./exec_script";
|
||||||
|
import { compileScript, compileScriptCode } from "./utils";
|
||||||
|
import { ExtVersion } from "@App/app/const";
|
||||||
|
|
||||||
|
initTestEnv();
|
||||||
|
|
||||||
|
const scriptRes = {
|
||||||
|
id: 0,
|
||||||
|
name: "test",
|
||||||
|
metadata: {
|
||||||
|
version: ["1.0.0"],
|
||||||
|
},
|
||||||
|
code: "console.log('test')",
|
||||||
|
sourceCode: "sourceCode",
|
||||||
|
value: {},
|
||||||
|
grantMap: {
|
||||||
|
none: true,
|
||||||
|
},
|
||||||
|
} as unknown as ScriptRunResouce;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const noneExec = new ExecScript(scriptRes);
|
||||||
|
|
||||||
|
const scriptRes2 = {
|
||||||
|
id: 0,
|
||||||
|
name: "test",
|
||||||
|
metadata: {
|
||||||
|
version: ["1.0.0"],
|
||||||
|
},
|
||||||
|
code: "console.log('test')",
|
||||||
|
sourceCode: "sourceCode",
|
||||||
|
value: {},
|
||||||
|
grantMap: {},
|
||||||
|
} as unknown as ScriptRunResouce;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const sandboxExec = new ExecScript(scriptRes2);
|
||||||
|
|
||||||
|
describe("GM_info", () => {
|
||||||
|
it("none", async () => {
|
||||||
|
scriptRes.code = "return GM_info";
|
||||||
|
noneExec.scriptFunc = compileScript(compileScriptCode(scriptRes));
|
||||||
|
const ret = await noneExec.exec();
|
||||||
|
expect(ret.version).toEqual(ExtVersion);
|
||||||
|
expect(ret.script.version).toEqual("1.0.0");
|
||||||
|
});
|
||||||
|
it("sandbox", async () => {
|
||||||
|
scriptRes2.code = "return GM_info";
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret = await sandboxExec.exec();
|
||||||
|
expect(ret.version).toEqual(ExtVersion);
|
||||||
|
expect(ret.script.version).toEqual("1.0.0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("unsafeWindow", () => {
|
||||||
|
it("sandbox", async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
global.testUnsafeWindow = "ok";
|
||||||
|
scriptRes2.code = "return unsafeWindow.testUnsafeWindow";
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual("ok");
|
||||||
|
scriptRes2.code = "return window.testUnsafeWindow";
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret2 = await sandboxExec.exec();
|
||||||
|
expect(ret2).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sandbox", () => {
|
||||||
|
it("global", async () => {
|
||||||
|
scriptRes2.code = "window.testObj = 'ok';return window.testObj";
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
let ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual("ok");
|
||||||
|
scriptRes2.code = "window.testObj = 'ok2';return testObj";
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual("ok2");
|
||||||
|
});
|
||||||
|
it("this", async () => {
|
||||||
|
scriptRes2.code = "this.testObj='ok2';return testObj;";
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual("ok2");
|
||||||
|
});
|
||||||
|
it("this2", async () => {
|
||||||
|
scriptRes2.code = `
|
||||||
|
!function(t, e) {
|
||||||
|
"object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
|
||||||
|
console.log("object" == typeof exports,"function" == typeof define)
|
||||||
|
} (this, function () {
|
||||||
|
return { test: "ok3" }
|
||||||
|
});
|
||||||
|
console.log(CryptoJS)
|
||||||
|
return CryptoJS.test;`;
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual("ok3");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 沉浸式翻译, 常量值被改变
|
||||||
|
it("NodeFilter #214", async () => {
|
||||||
|
scriptRes2.code = `return NodeFilter.FILTER_REJECT;`;
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// RegExp.$x 内容被覆盖 https://github.com/scriptscat/scriptcat/issues/293
|
||||||
|
it("RegExp", async () => {
|
||||||
|
scriptRes2.code = `let ok = /12(3)/.test('123');return RegExp.$1;`;
|
||||||
|
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
|
||||||
|
const ret = await sandboxExec.exec();
|
||||||
|
expect(ret).toEqual("3");
|
||||||
|
});
|
||||||
|
});
|
68
src/runtime/content/exec_script.ts
Normal file
68
src/runtime/content/exec_script.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import { Value } from "@App/app/repo/value";
|
||||||
|
import GMApi from "./gm_api";
|
||||||
|
import { compileScript, createContext, proxyContext, ScriptFunc } from "./utils";
|
||||||
|
|
||||||
|
export type ValueUpdateData = {
|
||||||
|
oldValue: any;
|
||||||
|
value: Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行脚本,控制脚本执行与停止
|
||||||
|
export default class ExecScript {
|
||||||
|
scriptRes: ScriptRunResouce;
|
||||||
|
|
||||||
|
scriptFunc: ScriptFunc;
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
proxyContent: any;
|
||||||
|
|
||||||
|
sandboxContent?: GMApi;
|
||||||
|
|
||||||
|
GM_info: any;
|
||||||
|
|
||||||
|
constructor(scriptRes: ScriptRunResouce, scriptFunc?: ScriptFunc, thisContext?: { [key: string]: any }) {
|
||||||
|
this.scriptRes = scriptRes;
|
||||||
|
this.logger = LoggerCore.getInstance().logger({
|
||||||
|
component: "exec",
|
||||||
|
uuid: this.scriptRes.uuid,
|
||||||
|
name: this.scriptRes.name,
|
||||||
|
});
|
||||||
|
this.GM_info = GMApi.GM_info(this.scriptRes);
|
||||||
|
this.proxyMessage = new ProxyMessageManager(message);
|
||||||
|
if (scriptFunc) {
|
||||||
|
this.scriptFunc = scriptFunc;
|
||||||
|
} else {
|
||||||
|
// 构建脚本资源
|
||||||
|
this.scriptFunc = compileScript(this.scriptRes.code);
|
||||||
|
}
|
||||||
|
if (scriptRes.grantMap.none) {
|
||||||
|
// 不注入任何GM api
|
||||||
|
this.proxyContent = global;
|
||||||
|
} else {
|
||||||
|
// 构建脚本GM上下文
|
||||||
|
this.sandboxContent = createContext(scriptRes, this.GM_info, this.proxyMessage);
|
||||||
|
this.proxyContent = proxyContext(global, this.sandboxContent, thisContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发值更新
|
||||||
|
valueUpdate(data: ValueUpdateData) {
|
||||||
|
this.sandboxContent?.valueUpdate(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
exec() {
|
||||||
|
this.logger.debug("script start");
|
||||||
|
return this.scriptFunc.apply(this.proxyContent, [this.proxyContent, this.GM_info]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 实现脚本的停止,资源释放
|
||||||
|
stop() {
|
||||||
|
this.logger.debug("script stop");
|
||||||
|
this.proxyMessage.cleanChannel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
92
src/runtime/content/exec_warp.ts
Normal file
92
src/runtime/content/exec_warp.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable func-names */
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { MessageManager } from "@App/app/message/message";
|
||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import ExecScript from "./exec_script";
|
||||||
|
|
||||||
|
export class CATRetryError {
|
||||||
|
msg: string;
|
||||||
|
|
||||||
|
time: Date;
|
||||||
|
|
||||||
|
constructor(msg: string, time: number | Date) {
|
||||||
|
this.msg = msg;
|
||||||
|
if (typeof time === "number") {
|
||||||
|
this.time = new Date(Date.now() + time * 1000);
|
||||||
|
} else {
|
||||||
|
this.time = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BgExecScriptWarp extends ExecScript {
|
||||||
|
setTimeout: Map<number, boolean>;
|
||||||
|
|
||||||
|
setInterval: Map<number, boolean>;
|
||||||
|
|
||||||
|
constructor(scriptRes: ScriptRunResouce, message: MessageManager) {
|
||||||
|
const thisContext: { [key: string]: any } = {};
|
||||||
|
const setTimeout = new Map<number, any>();
|
||||||
|
const setInterval = new Map<number, any>();
|
||||||
|
thisContext.setTimeout = function (
|
||||||
|
handler: () => void,
|
||||||
|
timeout: number | undefined,
|
||||||
|
...args: any
|
||||||
|
) {
|
||||||
|
const t = global.setTimeout(
|
||||||
|
function () {
|
||||||
|
setTimeout.delete(t);
|
||||||
|
if (typeof handler === "function") {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
setTimeout.set(t, true);
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
thisContext.clearTimeout = function (t: number) {
|
||||||
|
setTimeout.delete(t);
|
||||||
|
global.clearTimeout(t);
|
||||||
|
};
|
||||||
|
thisContext.setInterval = function (
|
||||||
|
handler: () => void,
|
||||||
|
timeout: number | undefined,
|
||||||
|
...args: any
|
||||||
|
) {
|
||||||
|
const t = global.setInterval(
|
||||||
|
function () {
|
||||||
|
if (typeof handler === "function") {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
setInterval.set(t, true);
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
thisContext.clearInterval = function (t: number) {
|
||||||
|
setInterval.delete(t);
|
||||||
|
global.clearInterval(t);
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
thisContext.CATRetryError = CATRetryError;
|
||||||
|
super(scriptRes, message, undefined, thisContext);
|
||||||
|
this.setTimeout = setTimeout;
|
||||||
|
this.setInterval = setInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.setTimeout.forEach((_, t) => {
|
||||||
|
global.clearTimeout(t);
|
||||||
|
});
|
||||||
|
this.setTimeout.clear();
|
||||||
|
this.setInterval.forEach((_, t) => {
|
||||||
|
global.clearInterval(t);
|
||||||
|
});
|
||||||
|
this.setInterval.clear();
|
||||||
|
return super.stop();
|
||||||
|
}
|
||||||
|
}
|
913
src/runtime/content/gm_api.ts
Normal file
913
src/runtime/content/gm_api.ts
Normal file
@ -0,0 +1,913 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { ExtVersion } from "@App/app/const";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import { Channel, ChannelHandler } from "@App/app/message/channel";
|
||||||
|
import MessageContent from "@App/app/message/content";
|
||||||
|
import { MessageManager } from "@App/app/message/message";
|
||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import {
|
||||||
|
base64ToBlob,
|
||||||
|
blobToBase64,
|
||||||
|
getMetadataStr,
|
||||||
|
getUserConfigStr,
|
||||||
|
parseUserConfig,
|
||||||
|
} from "@App/pkg/utils/script";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { ValueUpdateData } from "./exec_script";
|
||||||
|
|
||||||
|
interface ApiParam {
|
||||||
|
depend?: string[];
|
||||||
|
listener?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiValue {
|
||||||
|
api: any;
|
||||||
|
param: ApiParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GMContext {
|
||||||
|
static apis: Map<string, ApiValue> = new Map();
|
||||||
|
|
||||||
|
public static API(param: ApiParam = {}) {
|
||||||
|
return (
|
||||||
|
target: any,
|
||||||
|
propertyName: string,
|
||||||
|
descriptor: PropertyDescriptor
|
||||||
|
) => {
|
||||||
|
const key = propertyName;
|
||||||
|
if (param.listener) {
|
||||||
|
param.listener();
|
||||||
|
}
|
||||||
|
if (key === "GMdotXmlHttpRequest") {
|
||||||
|
GMContext.apis.set("GM.xmlHttpRequest", {
|
||||||
|
api: descriptor.value,
|
||||||
|
param,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GMContext.apis.set(key, {
|
||||||
|
api: descriptor.value,
|
||||||
|
param,
|
||||||
|
});
|
||||||
|
// 兼容GM.*
|
||||||
|
const dot = key.replace("_", ".");
|
||||||
|
if (dot !== key) {
|
||||||
|
// 特殊处理GM.xmlHttpRequest
|
||||||
|
if (dot === "GM.xmlhttpRequest") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GMContext.apis.set(dot, {
|
||||||
|
api: descriptor.value,
|
||||||
|
param,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GMApi {
|
||||||
|
scriptRes!: ScriptRunResouce;
|
||||||
|
|
||||||
|
message!: MessageManager;
|
||||||
|
|
||||||
|
runFlag!: string;
|
||||||
|
|
||||||
|
valueChangeListener = new Map<
|
||||||
|
number,
|
||||||
|
{ name: string; listener: GMTypes.ValueChangeListener }
|
||||||
|
>();
|
||||||
|
|
||||||
|
// 单次回调使用
|
||||||
|
public sendMessage(api: string, params: any[]) {
|
||||||
|
return this.message.syncSend("gmApi", {
|
||||||
|
api,
|
||||||
|
scriptId: this.scriptRes.id,
|
||||||
|
params,
|
||||||
|
runFlag: this.runFlag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 长连接使用,connect只用于接受消息,不能发送消息
|
||||||
|
public connect(api: string, params: any[], handler: ChannelHandler): Channel {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
const channel = this.message.channel(uuid);
|
||||||
|
channel.setHandler(handler);
|
||||||
|
channel.channel("gmApiChannel", {
|
||||||
|
api,
|
||||||
|
scriptId: this.scriptRes.id,
|
||||||
|
params,
|
||||||
|
runFlag: this.runFlag,
|
||||||
|
});
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public valueUpdate(data: ValueUpdateData) {
|
||||||
|
const { storagename } = this.scriptRes.metadata;
|
||||||
|
if (
|
||||||
|
data.value.scriptId === this.scriptRes.id ||
|
||||||
|
(storagename &&
|
||||||
|
data.value.storageName &&
|
||||||
|
storagename[0] === data.value.storageName)
|
||||||
|
) {
|
||||||
|
// 触发,并更新值
|
||||||
|
if (data.value.value === undefined) {
|
||||||
|
delete this.scriptRes.value[data.value.key];
|
||||||
|
} else {
|
||||||
|
this.scriptRes.value[data.value.key] = data.value;
|
||||||
|
}
|
||||||
|
this.valueChangeListener.forEach((item) => {
|
||||||
|
if (item.name === data.value.key) {
|
||||||
|
item.listener(
|
||||||
|
data.value.key,
|
||||||
|
data.oldValue,
|
||||||
|
data.value.value,
|
||||||
|
data.sender.runFlag !== this.runFlag,
|
||||||
|
data.sender.tabId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取脚本信息和管理器信息
|
||||||
|
static GM_info(script: ScriptRunResouce) {
|
||||||
|
const metadataStr = getMetadataStr(script.sourceCode);
|
||||||
|
const userConfigStr = getUserConfigStr(script.sourceCode) || "";
|
||||||
|
const options = {
|
||||||
|
description:
|
||||||
|
(script.metadata.description && script.metadata.description[0]) || null,
|
||||||
|
matches: script.metadata.match || [],
|
||||||
|
includes: script.metadata.include || [],
|
||||||
|
"run-at":
|
||||||
|
(script.metadata["run-at"] && script.metadata["run-at"][0]) ||
|
||||||
|
"document-idle",
|
||||||
|
icon: (script.metadata.icon && script.metadata.icon[0]) || null,
|
||||||
|
icon64: (script.metadata.icon64 && script.metadata.icon64[0]) || null,
|
||||||
|
header: metadataStr,
|
||||||
|
grant: script.metadata.grant || [],
|
||||||
|
connects: script.metadata.connect || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// downloadMode
|
||||||
|
// isIncognito
|
||||||
|
scriptWillUpdate: true,
|
||||||
|
scriptHandler: "ScriptCat",
|
||||||
|
scriptUpdateURL: script.downloadUrl,
|
||||||
|
scriptMetaStr: metadataStr,
|
||||||
|
userConfig: parseUserConfig(userConfigStr),
|
||||||
|
userConfigStr,
|
||||||
|
// scriptSource: script.sourceCode,
|
||||||
|
version: ExtVersion,
|
||||||
|
script: {
|
||||||
|
// TODO: 更多完整的信息(为了兼容Tampermonkey,后续待定)
|
||||||
|
name: script.name,
|
||||||
|
namespace: script.namespace,
|
||||||
|
version: script.metadata.version && script.metadata.version[0],
|
||||||
|
author: script.author,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取脚本的值,可以通过@storageName让多个脚本共享一个储存空间
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_getValue(key: string, defaultValue?: any) {
|
||||||
|
const ret = this.scriptRes.value[key];
|
||||||
|
if (ret) {
|
||||||
|
return ret.value;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_setValue(key: string, value: any) {
|
||||||
|
// 对object的value进行一次转化
|
||||||
|
if (typeof value === "object") {
|
||||||
|
value = JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
let ret = this.scriptRes.value[key];
|
||||||
|
if (ret) {
|
||||||
|
ret.value = value;
|
||||||
|
} else {
|
||||||
|
ret = {
|
||||||
|
id: 0,
|
||||||
|
scriptId: this.scriptRes.id,
|
||||||
|
storageName:
|
||||||
|
(this.scriptRes.metadata.storagename &&
|
||||||
|
this.scriptRes.metadata.storagename[0]) ||
|
||||||
|
"",
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
createtime: new Date().getTime(),
|
||||||
|
updatetime: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value === undefined) {
|
||||||
|
delete this.scriptRes.value[key];
|
||||||
|
} else {
|
||||||
|
this.scriptRes.value[key] = ret;
|
||||||
|
}
|
||||||
|
return this.sendMessage("GM_setValue", [key, value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API({ depend: ["GM_setValue"] })
|
||||||
|
public GM_deleteValue(name: string): void {
|
||||||
|
this.GM_setValue(name, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_listValues(): string[] {
|
||||||
|
return Object.keys(this.scriptRes.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_addValueChangeListener(
|
||||||
|
name: string,
|
||||||
|
listener: GMTypes.ValueChangeListener
|
||||||
|
): number {
|
||||||
|
const id = Math.random() * 10000000;
|
||||||
|
this.valueChangeListener.set(id, { name, listener });
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_removeValueChangeListener(listenerId: number): void {
|
||||||
|
this.valueChangeListener.delete(listenerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助GM_xml获取blob数据
|
||||||
|
@GMContext.API()
|
||||||
|
public CAT_fetchBlob(url: string): Promise<Blob> {
|
||||||
|
return this.message.syncSend("CAT_fetchBlob", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public CAT_fetchDocument(url: string): Promise<Document | undefined> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let el: Document | undefined;
|
||||||
|
(<MessageContent>this.message).sendCallback(
|
||||||
|
"CAT_fetchDocument",
|
||||||
|
url,
|
||||||
|
(resp) => {
|
||||||
|
el = <Document>(
|
||||||
|
(<unknown>(
|
||||||
|
(<MessageContent>this.message).getAndDelRelatedTarget(
|
||||||
|
resp.relatedTarget
|
||||||
|
)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
resolve(el);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助GM_xml发送blob数据
|
||||||
|
@GMContext.API()
|
||||||
|
public CAT_createBlobUrl(blob: Blob): Promise<string> {
|
||||||
|
return this.message.syncSend("CAT_createBlobUrl", blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于脚本跨域请求,需要@connect domain指定允许的域名
|
||||||
|
@GMContext.API({
|
||||||
|
depend: [
|
||||||
|
"CAT_fetchBlob",
|
||||||
|
"CAT_createBlobUrl",
|
||||||
|
"CAT_fetchDocument",
|
||||||
|
"GM_xmlhttpRequest",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
GMdotXmlHttpRequest(details: GMTypes.XHRDetails) {
|
||||||
|
let abort: any;
|
||||||
|
const ret = new Promise((resolve, reject) => {
|
||||||
|
const oldOnload = details.onload;
|
||||||
|
details.onload = (data) => {
|
||||||
|
resolve(data);
|
||||||
|
oldOnload && oldOnload(data);
|
||||||
|
};
|
||||||
|
const oldOnerror = details.onerror;
|
||||||
|
details.onerror = (data) => {
|
||||||
|
reject(data);
|
||||||
|
oldOnerror && oldOnerror(data);
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
abort = this.GM_xmlhttpRequest(details);
|
||||||
|
});
|
||||||
|
if (abort && abort.abort) {
|
||||||
|
// @ts-ignore
|
||||||
|
ret.abort = abort.abort;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于脚本跨域请求,需要@connect domain指定允许的域名
|
||||||
|
@GMContext.API({
|
||||||
|
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
|
||||||
|
})
|
||||||
|
public GM_xmlhttpRequest(details: GMTypes.XHRDetails) {
|
||||||
|
let connect: Channel;
|
||||||
|
|
||||||
|
const u = new URL(details.url, window.location.href);
|
||||||
|
if (details.headers) {
|
||||||
|
Object.keys(details.headers).forEach((key) => {
|
||||||
|
if (key.toLowerCase() === "cookie") {
|
||||||
|
details.cookie = details.headers![key];
|
||||||
|
delete details.headers![key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const param: GMSend.XHRDetails = {
|
||||||
|
method: details.method,
|
||||||
|
timeout: details.timeout,
|
||||||
|
url: u.href,
|
||||||
|
headers: details.headers,
|
||||||
|
cookie: details.cookie,
|
||||||
|
context: details.context,
|
||||||
|
responseType: details.responseType,
|
||||||
|
overrideMimeType: details.overrideMimeType,
|
||||||
|
anonymous: details.anonymous,
|
||||||
|
user: details.user,
|
||||||
|
password: details.password,
|
||||||
|
maxRedirects: details.maxRedirects,
|
||||||
|
};
|
||||||
|
if (!param.headers) {
|
||||||
|
param.headers = {};
|
||||||
|
}
|
||||||
|
if (details.nocache) {
|
||||||
|
param.headers["Cache-Control"] = "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = async () => {
|
||||||
|
if (details.data) {
|
||||||
|
if (details.data instanceof FormData) {
|
||||||
|
param.dataType = "FormData";
|
||||||
|
const data: Array<GMSend.XHRFormData> = [];
|
||||||
|
const keys: { [key: string]: boolean } = {};
|
||||||
|
details.data.forEach((val, key) => {
|
||||||
|
keys[key] = true;
|
||||||
|
});
|
||||||
|
const asyncArr = Object.keys(keys).map((key) => {
|
||||||
|
const values = (<FormData>details.data).getAll(key);
|
||||||
|
const asyncArr2 = values.map((val) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (val instanceof File) {
|
||||||
|
blobToBase64(val).then((base64) => {
|
||||||
|
data.push({
|
||||||
|
key,
|
||||||
|
type: "file",
|
||||||
|
val: base64 || "",
|
||||||
|
filename: val.name,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
data.push({
|
||||||
|
key,
|
||||||
|
type: "text",
|
||||||
|
val,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.all(asyncArr2);
|
||||||
|
});
|
||||||
|
await Promise.all(asyncArr);
|
||||||
|
param.data = data;
|
||||||
|
} else if (details.data instanceof Blob) {
|
||||||
|
param.dataType = "Blob";
|
||||||
|
param.data = await this.CAT_createBlobUrl(details.data);
|
||||||
|
} else {
|
||||||
|
param.data = details.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let readerStream: ReadableStream<Uint8Array> | undefined;
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||||
|
// 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob
|
||||||
|
// 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象
|
||||||
|
const responseType = details.responseType?.toLocaleLowerCase();
|
||||||
|
const warpResponse = (old: Function) => {
|
||||||
|
if (responseType === "stream") {
|
||||||
|
readerStream = new ReadableStream<Uint8Array>({
|
||||||
|
start(ctrl) {
|
||||||
|
controller = ctrl;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return async (xhr: GMTypes.XHRResponse) => {
|
||||||
|
if (xhr.response) {
|
||||||
|
if (responseType === "document") {
|
||||||
|
xhr.response = await this.CAT_fetchDocument(<string>xhr.response);
|
||||||
|
xhr.responseXML = xhr.response;
|
||||||
|
xhr.responseType = "document";
|
||||||
|
} else {
|
||||||
|
const resp = await this.CAT_fetchBlob(<string>xhr.response);
|
||||||
|
if (responseType === "arraybuffer") {
|
||||||
|
xhr.response = await resp.arrayBuffer();
|
||||||
|
} else {
|
||||||
|
xhr.response = resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (responseType === "stream") {
|
||||||
|
xhr.response = readerStream;
|
||||||
|
}
|
||||||
|
old(xhr);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
responseType === "arraybuffer" ||
|
||||||
|
responseType === "blob" ||
|
||||||
|
responseType === "document" ||
|
||||||
|
responseType === "stream"
|
||||||
|
) {
|
||||||
|
if (details.onload) {
|
||||||
|
details.onload = warpResponse(details.onload);
|
||||||
|
}
|
||||||
|
if (details.onreadystatechange) {
|
||||||
|
details.onreadystatechange = warpResponse(details.onreadystatechange);
|
||||||
|
}
|
||||||
|
if (details.onloadend) {
|
||||||
|
details.onloadend = warpResponse(details.onloadend);
|
||||||
|
}
|
||||||
|
// document类型读取blob,然后在content页转化为document对象
|
||||||
|
if (responseType === "document") {
|
||||||
|
param.responseType = "blob";
|
||||||
|
}
|
||||||
|
if (responseType === "stream") {
|
||||||
|
if (details.onloadstart) {
|
||||||
|
details.onloadstart = warpResponse(details.onloadstart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect = this.connect("GM_xmlhttpRequest", [param], (resp: any) => {
|
||||||
|
const data = <GMTypes.XHRResponse>resp.data || {};
|
||||||
|
switch (resp.event) {
|
||||||
|
case "onload":
|
||||||
|
details.onload && details.onload(data);
|
||||||
|
break;
|
||||||
|
case "onloadend":
|
||||||
|
details.onloadend && details.onloadend(data);
|
||||||
|
if (readerStream) {
|
||||||
|
controller?.close();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "onloadstart":
|
||||||
|
details.onloadstart && details.onloadstart(data);
|
||||||
|
break;
|
||||||
|
case "onprogress":
|
||||||
|
details.onprogress && details.onprogress(<GMTypes.XHRProgress>data);
|
||||||
|
break;
|
||||||
|
case "onreadystatechange":
|
||||||
|
details.onreadystatechange && details.onreadystatechange(data);
|
||||||
|
break;
|
||||||
|
case "ontimeout":
|
||||||
|
details.ontimeout && details.ontimeout();
|
||||||
|
break;
|
||||||
|
case "onerror":
|
||||||
|
details.onerror && details.onerror("");
|
||||||
|
break;
|
||||||
|
case "onabort":
|
||||||
|
details.onabort && details.onabort();
|
||||||
|
break;
|
||||||
|
case "onstream":
|
||||||
|
controller?.enqueue(new Uint8Array(resp.data));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LoggerCore.getLogger().warn("GM_xmlhttpRequest resp is error", {
|
||||||
|
resp,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect.setCatch((err) => {
|
||||||
|
details.onerror && details.onerror(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
handler();
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
if (connect) {
|
||||||
|
connect.disChannel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public async GM_notification(
|
||||||
|
detail: GMTypes.NotificationDetails | string,
|
||||||
|
ondone?: GMTypes.NotificationOnDone | string,
|
||||||
|
image?: string,
|
||||||
|
onclick?: GMTypes.NotificationOnClick
|
||||||
|
) {
|
||||||
|
let data: GMTypes.NotificationDetails = {};
|
||||||
|
if (typeof detail === "string") {
|
||||||
|
data.text = detail;
|
||||||
|
switch (arguments.length) {
|
||||||
|
case 4:
|
||||||
|
data.onclick = onclick;
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case 3:
|
||||||
|
data.image = image;
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case 2:
|
||||||
|
data.title = <string>ondone;
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = detail;
|
||||||
|
data.ondone = data.ondone || <GMTypes.NotificationOnDone>ondone;
|
||||||
|
}
|
||||||
|
let click: GMTypes.NotificationOnClick;
|
||||||
|
let done: GMTypes.NotificationOnDone;
|
||||||
|
let create: GMTypes.NotificationOnClick;
|
||||||
|
if (data.onclick) {
|
||||||
|
click = data.onclick;
|
||||||
|
delete data.onclick;
|
||||||
|
}
|
||||||
|
if (data.ondone) {
|
||||||
|
done = data.ondone;
|
||||||
|
delete data.ondone;
|
||||||
|
}
|
||||||
|
if (data.oncreate) {
|
||||||
|
create = data.oncreate;
|
||||||
|
delete data.oncreate;
|
||||||
|
}
|
||||||
|
this.connect("GM_notification", [data], (resp: any) => {
|
||||||
|
switch (resp.event) {
|
||||||
|
case "click": {
|
||||||
|
click && click.apply({ id: resp.id }, [resp.id, resp.index]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "done": {
|
||||||
|
done && done.apply({ id: resp.id }, [resp.user]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "create": {
|
||||||
|
create && create.apply({ id: resp.id }, [resp.id]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
LoggerCore.getLogger().warn("GM_notification resp is error", {
|
||||||
|
resp,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_closeNotification(id: string) {
|
||||||
|
this.sendMessage("GM_closeNotification", [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_updateNotification(
|
||||||
|
id: string,
|
||||||
|
details: GMTypes.NotificationDetails
|
||||||
|
): void {
|
||||||
|
this.sendMessage("GM_updateNotification", [id, details]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_log(
|
||||||
|
message: string,
|
||||||
|
level?: GMTypes.LoggerLevel,
|
||||||
|
labels?: GMTypes.LoggerLabel
|
||||||
|
) {
|
||||||
|
if (typeof message !== "string") {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
return this.sendMessage("GM_log", [message, level, labels]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API({ depend: ["GM_closeInTab"] })
|
||||||
|
public GM_openInTab(
|
||||||
|
url: string,
|
||||||
|
options?: GMTypes.OpenTabOptions | boolean
|
||||||
|
): GMTypes.Tab {
|
||||||
|
let option: GMTypes.OpenTabOptions = {};
|
||||||
|
if (arguments.length === 1) {
|
||||||
|
option.active = true;
|
||||||
|
} else if (typeof options === "boolean") {
|
||||||
|
option.active = !options;
|
||||||
|
} else {
|
||||||
|
option = <GMTypes.OpenTabOptions>options;
|
||||||
|
}
|
||||||
|
if (option.active === undefined) {
|
||||||
|
option.active = true;
|
||||||
|
}
|
||||||
|
let tabid: any;
|
||||||
|
|
||||||
|
const ret: GMTypes.Tab = {
|
||||||
|
close: () => {
|
||||||
|
this.GM_closeInTab(tabid);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = this.connect("GM_openInTab", [url, option], (data) => {
|
||||||
|
switch (data.event) {
|
||||||
|
case "oncreate":
|
||||||
|
tabid = data.tabId;
|
||||||
|
break;
|
||||||
|
case "onclose":
|
||||||
|
ret.onclose && ret.onclose();
|
||||||
|
ret.closed = true;
|
||||||
|
connect.disChannel();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
public GM_closeInTab(tabid: string) {
|
||||||
|
return this.sendMessage("GM_closeInTab", [tabid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_getResourceText(name: string): string | undefined {
|
||||||
|
if (!this.scriptRes.resource) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const r = this.scriptRes.resource[name];
|
||||||
|
if (r) {
|
||||||
|
return r.content;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined {
|
||||||
|
if (!this.scriptRes.resource) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const r = this.scriptRes.resource[name];
|
||||||
|
if (r) {
|
||||||
|
if (isBlobUrl) {
|
||||||
|
return URL.createObjectURL(base64ToBlob(r.base64));
|
||||||
|
}
|
||||||
|
return r.base64;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_addStyle(css: string) {
|
||||||
|
let el: Element | undefined;
|
||||||
|
// 与content页的消息通讯实际是同步,此方法不需要经过background
|
||||||
|
// 所以可以直接在then中赋值el再返回
|
||||||
|
(<MessageContent>this.message).sendCallback(
|
||||||
|
"GM_addElement",
|
||||||
|
{
|
||||||
|
param: [
|
||||||
|
"style",
|
||||||
|
{
|
||||||
|
textContent: css,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(resp) => {
|
||||||
|
el = (<MessageContent>this.message).getAndDelRelatedTarget(
|
||||||
|
resp.relatedTarget
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
async GM_getTab(callback: (data: any) => void) {
|
||||||
|
const resp = await this.sendMessage("GM_getTab", []);
|
||||||
|
callback(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_saveTab(obj: object) {
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
obj = JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
return this.sendMessage("GM_saveTab", [obj]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
async GM_getTabs(
|
||||||
|
callback: (objs: { [key: string | number]: object }) => any
|
||||||
|
) {
|
||||||
|
const resp = await this.sendMessage("GM_getTabs", []);
|
||||||
|
callback(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_download(
|
||||||
|
url: GMTypes.DownloadDetails | string,
|
||||||
|
filename?: string
|
||||||
|
): GMTypes.AbortHandle<void> {
|
||||||
|
let details: GMTypes.DownloadDetails;
|
||||||
|
if (typeof url === "string") {
|
||||||
|
details = {
|
||||||
|
name: filename || "",
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
details = url;
|
||||||
|
}
|
||||||
|
const connect = this.connect(
|
||||||
|
"GM_download",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
method: details.method,
|
||||||
|
url: details.url,
|
||||||
|
name: details.name,
|
||||||
|
headers: details.headers,
|
||||||
|
saveAs: details.saveAs,
|
||||||
|
timeout: details.timeout,
|
||||||
|
cookie: details.cookie,
|
||||||
|
anonymous: details.anonymous,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
(resp: any) => {
|
||||||
|
const data = <GMTypes.XHRResponse>resp.data || {};
|
||||||
|
switch (resp.event) {
|
||||||
|
case "onload":
|
||||||
|
details.onload && details.onload(data);
|
||||||
|
break;
|
||||||
|
case "onprogress":
|
||||||
|
details.onprogress && details.onprogress(<GMTypes.XHRProgress>data);
|
||||||
|
break;
|
||||||
|
case "ontimeout":
|
||||||
|
details.ontimeout && details.ontimeout();
|
||||||
|
break;
|
||||||
|
case "onerror":
|
||||||
|
details.onerror &&
|
||||||
|
details.onerror({
|
||||||
|
error: "unknown",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LoggerCore.getLogger().warn("GM_download resp is error", {
|
||||||
|
resp,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
connect.disChannel();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_setClipboard(
|
||||||
|
data: string,
|
||||||
|
info?: string | { type?: string; minetype?: string }
|
||||||
|
) {
|
||||||
|
return this.sendMessage("GM_setClipboard", [data, info]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_cookie(
|
||||||
|
action: string,
|
||||||
|
details: GMTypes.CookieDetails,
|
||||||
|
done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void
|
||||||
|
) {
|
||||||
|
this.sendMessage("GM_cookie", [action, details])
|
||||||
|
.then((resp: any) => {
|
||||||
|
done && done(resp, undefined);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
done && done(undefined, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menuId: number | undefined;
|
||||||
|
|
||||||
|
menuMap: Map<number, string> | undefined;
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_registerMenuCommand(
|
||||||
|
name: string,
|
||||||
|
listener: () => void,
|
||||||
|
accessKey?: string
|
||||||
|
): number {
|
||||||
|
if (!this.menuMap) {
|
||||||
|
this.menuMap = new Map();
|
||||||
|
}
|
||||||
|
let flag = 0;
|
||||||
|
this.menuMap.forEach((val, key) => {
|
||||||
|
if (val === name) {
|
||||||
|
flag = key;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (flag) {
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
if (!this.menuId) {
|
||||||
|
this.menuId = 1;
|
||||||
|
} else {
|
||||||
|
this.menuId += 1;
|
||||||
|
}
|
||||||
|
const id = this.menuId;
|
||||||
|
this.connect("GM_registerMenuCommand", [id, name, accessKey], () => {
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
this.menuMap.set(id, name);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
GM_unregisterMenuCommand(id: number): void {
|
||||||
|
if (!this.menuMap) {
|
||||||
|
this.menuMap = new Map();
|
||||||
|
}
|
||||||
|
this.menuMap.delete(id);
|
||||||
|
this.sendMessage("GM_unregisterMenuCommand", [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API()
|
||||||
|
CAT_userConfig() {
|
||||||
|
return this.sendMessage("CAT_userConfig", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此API在content页实现
|
||||||
|
@GMContext.API()
|
||||||
|
GM_addElement(parentNode: Element | string, tagName: any, attrs?: any) {
|
||||||
|
let el: Element | undefined;
|
||||||
|
// 与content页的消息通讯实际是同步,此方法不需要经过background
|
||||||
|
// 所以可以直接在then中赋值el再返回
|
||||||
|
(<MessageContent>this.message).sendCallback(
|
||||||
|
"GM_addElement",
|
||||||
|
{
|
||||||
|
param: [
|
||||||
|
typeof parentNode === "string" ? parentNode : tagName,
|
||||||
|
typeof parentNode === "string" ? tagName : attrs,
|
||||||
|
],
|
||||||
|
relatedTarget: typeof parentNode === "string" ? null : parentNode,
|
||||||
|
},
|
||||||
|
(resp) => {
|
||||||
|
el = (<MessageContent>this.message).getAndDelRelatedTarget(
|
||||||
|
resp.relatedTarget
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GMContext.API({
|
||||||
|
depend: ["CAT_fetchBlob", "CAT_createBlobUrl"],
|
||||||
|
})
|
||||||
|
async CAT_fileStorage(
|
||||||
|
action: "list" | "download" | "upload" | "delete" | "config",
|
||||||
|
details: any
|
||||||
|
) {
|
||||||
|
if (action === "config") {
|
||||||
|
this.sendMessage("CAT_fileStorage", ["config"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sendDetails: { [key: string]: string } = {
|
||||||
|
baseDir: details.baseDir || "",
|
||||||
|
path: details.path || "",
|
||||||
|
filename: details.filename,
|
||||||
|
file: details.file,
|
||||||
|
};
|
||||||
|
if (action === "upload") {
|
||||||
|
const url = await this.CAT_createBlobUrl(details.data);
|
||||||
|
sendDetails.data = url;
|
||||||
|
}
|
||||||
|
const channel = this.connect(
|
||||||
|
"CAT_fileStorage",
|
||||||
|
[action, sendDetails],
|
||||||
|
async (resp: any) => {
|
||||||
|
if (action === "download") {
|
||||||
|
// 读取blob
|
||||||
|
const blob = await this.CAT_fetchBlob(resp.data);
|
||||||
|
details.onload && details.onload(blob);
|
||||||
|
} else {
|
||||||
|
details.onload && details.onload(resp.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
channel.setCatch((err) => {
|
||||||
|
if (typeof err.code === "undefined") {
|
||||||
|
details.onerror && details.onerror({ code: -1, message: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
details.onerror && details.onerror(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
143
src/runtime/content/inject.ts
Normal file
143
src/runtime/content/inject.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { ExternalMessage, ExternalWhitelist } from "@App/app/const";
|
||||||
|
import MessageContent from "@App/app/message/content";
|
||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import ExecScript, { ValueUpdateData } from "./exec_script";
|
||||||
|
import { addStyle, ScriptFunc } from "./utils";
|
||||||
|
|
||||||
|
// 注入脚本的沙盒环境
|
||||||
|
export default class InjectRuntime {
|
||||||
|
scripts: ScriptRunResouce[];
|
||||||
|
|
||||||
|
flag: string;
|
||||||
|
|
||||||
|
message: MessageContent;
|
||||||
|
|
||||||
|
execList: ExecScript[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: MessageContent,
|
||||||
|
scripts: ScriptRunResouce[],
|
||||||
|
flag: string
|
||||||
|
) {
|
||||||
|
this.message = message;
|
||||||
|
this.scripts = scripts;
|
||||||
|
this.flag = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.scripts.forEach((script) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const scriptFunc = window[script.flag];
|
||||||
|
if (scriptFunc) {
|
||||||
|
this.execScript(script, scriptFunc);
|
||||||
|
} else {
|
||||||
|
// 监听脚本加载,和屏蔽读取
|
||||||
|
Object.defineProperty(window, script.flag, {
|
||||||
|
configurable: true,
|
||||||
|
set: (val: ScriptFunc) => {
|
||||||
|
this.execScript(script, val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 监听值变化
|
||||||
|
MessageContent.getInstance().setHandler(
|
||||||
|
"valueUpdate",
|
||||||
|
(_action, data: ValueUpdateData) => {
|
||||||
|
this.execList.forEach((exec) => {
|
||||||
|
exec.valueUpdate(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 注入允许外部调用
|
||||||
|
this.externalMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
execScript(script: ScriptRunResouce, scriptFunc: ScriptFunc) {
|
||||||
|
// @ts-ignore
|
||||||
|
delete window[script.flag];
|
||||||
|
const exec = new ExecScript(
|
||||||
|
script,
|
||||||
|
MessageContent.getInstance(),
|
||||||
|
scriptFunc
|
||||||
|
);
|
||||||
|
this.execList.push(exec);
|
||||||
|
// 注入css
|
||||||
|
if (script.metadata["require-css"]) {
|
||||||
|
script.metadata["require-css"].forEach((val) => {
|
||||||
|
const res = script.resource[val];
|
||||||
|
if (res) {
|
||||||
|
addStyle(res.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
script.metadata["run-at"] &&
|
||||||
|
script.metadata["run-at"][0] === "document-body"
|
||||||
|
) {
|
||||||
|
// 等待页面加载完成
|
||||||
|
this.waitBody(() => {
|
||||||
|
exec.exec();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
exec.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考了tm的实现
|
||||||
|
waitBody(callback: () => void) {
|
||||||
|
if (document.body) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const listen = () => {
|
||||||
|
document.removeEventListener("load", listen, false);
|
||||||
|
document.removeEventListener("DOMNodeInserted", listen, false);
|
||||||
|
document.removeEventListener("DOMContentLoaded", listen, false);
|
||||||
|
this.waitBody(callback);
|
||||||
|
};
|
||||||
|
document.addEventListener("load", listen, false);
|
||||||
|
document.addEventListener("DOMNodeInserted", listen, false);
|
||||||
|
document.addEventListener("DOMContentLoaded", listen, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
externalMessage() {
|
||||||
|
const { message } = this;
|
||||||
|
// 对外接口白名单
|
||||||
|
for (let i = 0; i < ExternalWhitelist.length; i += 1) {
|
||||||
|
if (window.location.host.endsWith(ExternalWhitelist[i])) {
|
||||||
|
// 注入
|
||||||
|
(<{ external: any }>(<unknown>window)).external = window.external || {};
|
||||||
|
(<
|
||||||
|
{
|
||||||
|
external: {
|
||||||
|
Scriptcat: {
|
||||||
|
isInstalled: (
|
||||||
|
name: string,
|
||||||
|
namespace: string,
|
||||||
|
callback: any
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(<unknown>window)).external.Scriptcat = {
|
||||||
|
async isInstalled(name: string, namespace: string, callback: any) {
|
||||||
|
const resp = await message.syncSend(ExternalMessage, {
|
||||||
|
action: "isInstalled",
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
callback(resp);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
(<{ external: { Tampermonkey: any } }>(
|
||||||
|
(<unknown>window)
|
||||||
|
)).external.Tampermonkey = (<{ external: { Scriptcat: any } }>(
|
||||||
|
(<unknown>window)
|
||||||
|
)).external.Scriptcat;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/runtime/content/runtime.ts
Normal file
51
src/runtime/content/runtime.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import MessageInternal from "@App/app/message/internal";
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import { Script } from "@App/app/repo/scripts";
|
||||||
|
import CacheKey from "@App/pkg/utils/cache_key";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import Runtime, { RuntimeEvent } from "../background/runtime";
|
||||||
|
|
||||||
|
@IoC.Singleton(MessageInternal)
|
||||||
|
export default class RuntimeController {
|
||||||
|
internal: MessageInternal;
|
||||||
|
|
||||||
|
runtime!: Runtime;
|
||||||
|
|
||||||
|
constructor(internal: MessageInternal) {
|
||||||
|
this.internal = internal;
|
||||||
|
try {
|
||||||
|
this.runtime = IoC.instance(Runtime) as Runtime;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispatchEvent(event: RuntimeEvent, data: any): Promise<any> {
|
||||||
|
return this.internal.syncSend(`runtime-${event}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试脚本,需要先启动GM环境
|
||||||
|
async debugScript(script: Script) {
|
||||||
|
// 清理脚本缓存,避免GMApi中的缓存影响
|
||||||
|
Cache.getInstance().del(CacheKey.script(script.id));
|
||||||
|
Cache.getInstance().del(
|
||||||
|
CacheKey.scriptValue(script.id, script.metadata.storagename)
|
||||||
|
);
|
||||||
|
// 构建脚本代码
|
||||||
|
return this.runtime.startBackgroundScript(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchRunStatus() {
|
||||||
|
const channel = this.internal.channel();
|
||||||
|
channel.channel("watchRunStatus");
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
startScript(id: number) {
|
||||||
|
return this.dispatchEvent("start", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopScript(id: number) {
|
||||||
|
return this.dispatchEvent("stop", id);
|
||||||
|
}
|
||||||
|
}
|
328
src/runtime/content/sandbox.ts
Normal file
328
src/runtime/content/sandbox.ts
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
import MessageSandbox from "@App/app/message/sandbox";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import {
|
||||||
|
SCRIPT_RUN_STATUS_COMPLETE,
|
||||||
|
SCRIPT_RUN_STATUS_ERROR,
|
||||||
|
SCRIPT_RUN_STATUS_RUNNING,
|
||||||
|
SCRIPT_TYPE_BACKGROUND,
|
||||||
|
SCRIPT_TYPE_CRONTAB,
|
||||||
|
ScriptRunResouce,
|
||||||
|
} from "@App/app/repo/scripts";
|
||||||
|
import { CronJob } from "cron";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import ExecScript from "./exec_script";
|
||||||
|
import { BgExecScriptWarp, CATRetryError } from "./exec_warp";
|
||||||
|
|
||||||
|
type SandboxEvent = "enable" | "disable" | "start" | "stop";
|
||||||
|
|
||||||
|
type Handler = (data: any) => Promise<any>;
|
||||||
|
|
||||||
|
// 沙盒运行环境, 后台脚本与定时脚本的运行环境
|
||||||
|
@IoC.Singleton(MessageSandbox)
|
||||||
|
export default class SandboxRuntime {
|
||||||
|
message: MessageSandbox;
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
cronJob: Map<number, Array<CronJob>> = new Map();
|
||||||
|
|
||||||
|
execScripts: Map<number, ExecScript> = new Map();
|
||||||
|
|
||||||
|
retryList: {
|
||||||
|
script: ScriptRunResouce;
|
||||||
|
retryTime: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
constructor(message: MessageSandbox) {
|
||||||
|
this.message = message;
|
||||||
|
this.logger = LoggerCore.getInstance().logger({ component: "sandbox" });
|
||||||
|
// 重试队列,5s检查一次
|
||||||
|
setInterval(() => {
|
||||||
|
if (!this.retryList.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
const retryList = [];
|
||||||
|
for (let i = 0; i < this.retryList.length; i += 1) {
|
||||||
|
const item = this.retryList[i];
|
||||||
|
if (item.retryTime < now) {
|
||||||
|
this.retryList.splice(i, 1);
|
||||||
|
i -= 1;
|
||||||
|
retryList.push(item.script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retryList.forEach((script) => {
|
||||||
|
script.nextruntime = 0;
|
||||||
|
this.execScript(script);
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
joinRetryList(script: ScriptRunResouce) {
|
||||||
|
if (script.nextruntime) {
|
||||||
|
this.retryList.push({
|
||||||
|
script,
|
||||||
|
retryTime: script.nextruntime,
|
||||||
|
});
|
||||||
|
this.retryList.sort((a, b) => a.retryTime - b.retryTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRetryList(scriptId: number) {
|
||||||
|
for (let i = 0; i < this.retryList.length; i += 1) {
|
||||||
|
if (this.retryList[i].script.id === scriptId) {
|
||||||
|
this.retryList.splice(i, 1);
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenEvent(event: SandboxEvent, handler: Handler) {
|
||||||
|
this.message.setHandler(event, (_action, data) => {
|
||||||
|
return handler.bind(this)(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启沙盒运行环境,监听background来的请求
|
||||||
|
init() {
|
||||||
|
this.listenEvent("enable", this.enable);
|
||||||
|
this.listenEvent("disable", this.disable);
|
||||||
|
this.listenEvent("start", this.start);
|
||||||
|
this.listenEvent("stop", this.stop);
|
||||||
|
// 监听值更新
|
||||||
|
this.message.setHandler("valueUpdate", (action, data) => {
|
||||||
|
this.execScripts.forEach((val) => {
|
||||||
|
val.valueUpdate(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接运行脚本
|
||||||
|
start(script: ScriptRunResouce): Promise<boolean> {
|
||||||
|
return this.execScript(script, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(scriptId: number): Promise<boolean> {
|
||||||
|
const exec = this.execScripts.get(scriptId);
|
||||||
|
if (!exec) {
|
||||||
|
this.message.send("scriptRunStatus", [
|
||||||
|
scriptId,
|
||||||
|
SCRIPT_RUN_STATUS_COMPLETE,
|
||||||
|
]);
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
this.execStop(exec);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(script: ScriptRunResouce): Promise<boolean> {
|
||||||
|
// 如果正在运行,先释放
|
||||||
|
if (this.execScripts.has(script.id)) {
|
||||||
|
this.disable(script.id);
|
||||||
|
}
|
||||||
|
// 开启脚本在沙盒环境中运行
|
||||||
|
switch (script.type) {
|
||||||
|
case SCRIPT_TYPE_CRONTAB:
|
||||||
|
// 定时脚本
|
||||||
|
this.stopCronJob(script.id);
|
||||||
|
return this.crontabScript(script);
|
||||||
|
case SCRIPT_TYPE_BACKGROUND:
|
||||||
|
// 后台脚本, 直接执行脚本
|
||||||
|
return this.execScript(script);
|
||||||
|
default:
|
||||||
|
throw new Error("不支持的脚本类型");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disable(id: number): Promise<boolean> {
|
||||||
|
// 停止脚本运行,主要是停止定时器
|
||||||
|
// 后续考虑停止正在运行的脚本的方法
|
||||||
|
// 现期对于正在运行的脚本仅仅是在background中判断是否运行
|
||||||
|
// 未运行的脚本不处理GMApi的请求
|
||||||
|
this.stopCronJob(id);
|
||||||
|
// 移除重试队列
|
||||||
|
this.removeRetryList(id);
|
||||||
|
return this.stop(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止计时器
|
||||||
|
stopCronJob(id: number) {
|
||||||
|
const list = this.cronJob.get(id);
|
||||||
|
if (list) {
|
||||||
|
list.forEach((val) => {
|
||||||
|
val.stop();
|
||||||
|
});
|
||||||
|
this.cronJob.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行脚本
|
||||||
|
execScript(script: ScriptRunResouce, execOnce?: boolean) {
|
||||||
|
const logger = this.logger.with({ scriptId: script.id, name: script.name });
|
||||||
|
if (this.execScripts.has(script.id)) {
|
||||||
|
// 释放掉资源
|
||||||
|
// 暂未实现执行完成后立马释放,会在下一次执行时释放
|
||||||
|
this.stop(script.id);
|
||||||
|
}
|
||||||
|
const exec = new BgExecScriptWarp(script, this.message);
|
||||||
|
this.execScripts.set(script.id, exec);
|
||||||
|
this.message.send("scriptRunStatus", [
|
||||||
|
exec.scriptRes.id,
|
||||||
|
SCRIPT_RUN_STATUS_RUNNING,
|
||||||
|
]);
|
||||||
|
// 修改掉脚本掉最后运行时间, 数据库也需要修改
|
||||||
|
script.lastruntime = new Date().getTime();
|
||||||
|
const ret = exec.exec();
|
||||||
|
if (ret instanceof Promise) {
|
||||||
|
ret
|
||||||
|
.then((resp) => {
|
||||||
|
// 发送执行完成消息
|
||||||
|
this.message.send("scriptRunStatus", [
|
||||||
|
exec.scriptRes.id,
|
||||||
|
SCRIPT_RUN_STATUS_COMPLETE,
|
||||||
|
]);
|
||||||
|
logger.info("exec script complete", {
|
||||||
|
value: resp,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// 发送执行完成+错误消息
|
||||||
|
let errMsg;
|
||||||
|
let nextruntime = 0;
|
||||||
|
if (err instanceof CATRetryError) {
|
||||||
|
errMsg = { error: err.msg };
|
||||||
|
if (!execOnce) {
|
||||||
|
// 下一次执行时间
|
||||||
|
nextruntime = err.time.getTime();
|
||||||
|
script.nextruntime = nextruntime;
|
||||||
|
this.joinRetryList(script);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errMsg = Logger.E(err);
|
||||||
|
}
|
||||||
|
logger.error("exec script error", errMsg);
|
||||||
|
this.message.send("scriptRunStatus", [
|
||||||
|
exec.scriptRes.id,
|
||||||
|
SCRIPT_RUN_STATUS_ERROR,
|
||||||
|
errMsg,
|
||||||
|
nextruntime,
|
||||||
|
]);
|
||||||
|
// 错误还是抛出,方便排查
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn("backscript return not promise");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
crontabScript(script: ScriptRunResouce) {
|
||||||
|
// 执行定时脚本 运行表达式
|
||||||
|
if (!script.metadata.crontab) {
|
||||||
|
throw new Error("错误的crontab表达式");
|
||||||
|
}
|
||||||
|
// 如果有nextruntime,则加入重试队列
|
||||||
|
this.joinRetryList(script);
|
||||||
|
let flag = false;
|
||||||
|
const cronJobList: Array<CronJob> = [];
|
||||||
|
script.metadata.crontab.forEach((val) => {
|
||||||
|
let oncePos = 0;
|
||||||
|
let crontab = val;
|
||||||
|
if (crontab.indexOf("once") !== -1) {
|
||||||
|
const vals = crontab.split(" ");
|
||||||
|
vals.forEach((item, index) => {
|
||||||
|
if (item === "once") {
|
||||||
|
oncePos = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (vals.length === 5) {
|
||||||
|
oncePos += 1;
|
||||||
|
}
|
||||||
|
crontab = crontab.replace(/once/g, "*");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const cron = new CronJob(crontab, this.crontabExec(script, oncePos));
|
||||||
|
cron.start();
|
||||||
|
cronJobList.push(cron);
|
||||||
|
} catch (e) {
|
||||||
|
flag = true;
|
||||||
|
this.logger.error("create cronjob failed", {
|
||||||
|
script: script.id,
|
||||||
|
crontab: val,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (cronJobList.length !== script.metadata.crontab.length) {
|
||||||
|
// 有表达式失败了
|
||||||
|
cronJobList.forEach((crontab) => {
|
||||||
|
crontab.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.cronJob.set(script.id, cronJobList);
|
||||||
|
}
|
||||||
|
return Promise.resolve(!flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
crontabExec(script: ScriptRunResouce, oncePos: number) {
|
||||||
|
if (oncePos) {
|
||||||
|
return () => {
|
||||||
|
// 没有最后一次执行时间表示之前都没执行过,直接执行
|
||||||
|
if (!script.lastruntime) {
|
||||||
|
this.execScript(script);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const last = new Date(script.lastruntime);
|
||||||
|
let flag = false;
|
||||||
|
// 根据once所在的位置去判断执行
|
||||||
|
switch (oncePos) {
|
||||||
|
case 1: // 每分钟
|
||||||
|
flag = last.getMinutes() !== now.getMinutes();
|
||||||
|
break;
|
||||||
|
case 2: // 每小时
|
||||||
|
flag = last.getHours() !== now.getHours();
|
||||||
|
break;
|
||||||
|
case 3: // 每天
|
||||||
|
flag = last.getDay() !== now.getDay();
|
||||||
|
break;
|
||||||
|
case 4: // 每月
|
||||||
|
flag = last.getMonth() !== now.getMonth();
|
||||||
|
break;
|
||||||
|
case 5: // 每周
|
||||||
|
flag = this.getWeek(last) !== this.getWeek(now);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if (flag) {
|
||||||
|
this.execScript(script);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
this.execScript(script);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
execStop(exec: ExecScript) {
|
||||||
|
exec.stop();
|
||||||
|
this.execScripts.delete(exec.scriptRes.id);
|
||||||
|
this.message.send("scriptRunStatus", [
|
||||||
|
exec.scriptRes.id,
|
||||||
|
SCRIPT_RUN_STATUS_COMPLETE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本周是第几周
|
||||||
|
getWeek(date: Date) {
|
||||||
|
const nowDate = new Date(date);
|
||||||
|
const firstDay = new Date(date);
|
||||||
|
firstDay.setMonth(0); // 设置1月
|
||||||
|
firstDay.setDate(1); // 设置1号
|
||||||
|
const diffDays = Math.ceil(
|
||||||
|
(nowDate.getTime() - firstDay.getTime()) / (24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
|
const week = Math.ceil(diffDays / 7);
|
||||||
|
return week === 0 ? 1 : week;
|
||||||
|
}
|
||||||
|
}
|
101
src/runtime/content/utils.test.ts
Normal file
101
src/runtime/content/utils.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { init, proxyContext, writables } from "./utils";
|
||||||
|
|
||||||
|
describe("proxy context", () => {
|
||||||
|
const context: any = {};
|
||||||
|
const global: any = {
|
||||||
|
gbok: "gbok",
|
||||||
|
onload: null,
|
||||||
|
eval: () => {
|
||||||
|
console.log("eval");
|
||||||
|
},
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
location: "ok",
|
||||||
|
};
|
||||||
|
init.set("onload", true);
|
||||||
|
init.set("location", true);
|
||||||
|
const _this = proxyContext(global, context);
|
||||||
|
|
||||||
|
it("set contenxt", () => {
|
||||||
|
_this["md5"] = "ok";
|
||||||
|
expect(_this["md5"]).toEqual("ok");
|
||||||
|
expect(global["md5"]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("set window null", () => {
|
||||||
|
_this["onload"] = "ok";
|
||||||
|
expect(_this["onload"]).toEqual("ok");
|
||||||
|
expect(global["onload"]).toEqual(null);
|
||||||
|
_this["onload"] = undefined;
|
||||||
|
expect(_this["onload"]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("update", () => {
|
||||||
|
_this["okk"] = "ok";
|
||||||
|
expect(_this["okk"]).toEqual("ok");
|
||||||
|
expect(global["okk"]).toEqual(undefined);
|
||||||
|
_this["okk"] = "ok2";
|
||||||
|
expect(_this["okk"]).toEqual("ok2");
|
||||||
|
expect(global["okk"]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("禁止穿透global对象", () => {
|
||||||
|
expect(_this["gbok"]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("禁止修改window", () => {
|
||||||
|
expect(() => (_this["window"] = "ok")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("访问location", () => {
|
||||||
|
expect(_this.location).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只允许访问onxxxxx
|
||||||
|
describe("window", () => {
|
||||||
|
const _this = proxyContext({ onanimationstart: null }, {});
|
||||||
|
it("window", () => {
|
||||||
|
expect(_this.onanimationstart).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("兼容问题", () => {
|
||||||
|
const _this = proxyContext({}, {});
|
||||||
|
// https://github.com/xcanwin/KeepChatGPT 环境隔离得不够干净导致的
|
||||||
|
it("Uncaught TypeError: Illegal invocation #189", () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log(_this.setTimeout.prototype);
|
||||||
|
_this.setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// AC-baidu-重定向优化百度搜狗谷歌必应搜索_favicon_双列
|
||||||
|
it("TypeError: Object.freeze is not a function #116", () => {
|
||||||
|
expect(() => _this.Object.freeze({})).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Symbol", () => {
|
||||||
|
const _this = proxyContext({}, {});
|
||||||
|
// 允许往global写入Symbol属性,影响内容: https://bbs.tampermonkey.net.cn/thread-5509-1-1.html
|
||||||
|
it("Symbol", () => {
|
||||||
|
const s = Symbol("test");
|
||||||
|
_this[s] = "ok";
|
||||||
|
expect(_this[s]).toEqual("ok");
|
||||||
|
});
|
||||||
|
// toString.call(window)返回的是'[object Object]'而不是'[object Window]',影响内容: https://github.com/scriptscat/scriptcat/issues/260
|
||||||
|
it("Window", () => {
|
||||||
|
expect(toString.call(_this)).toEqual("[object Window]");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object.hasOwnProperty穿透 https://github.com/scriptscat/scriptcat/issues/272
|
||||||
|
describe("Object", () => {
|
||||||
|
const _this = proxyContext({}, {});
|
||||||
|
it("hasOwnProperty", () => {
|
||||||
|
expect(_this.hasOwnProperty("test1")).toEqual(false);
|
||||||
|
_this.test1 = "ok";
|
||||||
|
expect(_this.hasOwnProperty("test1")).toEqual(true);
|
||||||
|
expect(_this.hasOwnProperty("test")).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
349
src/runtime/content/utils.ts
Normal file
349
src/runtime/content/utils.ts
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import GMApi, { ApiValue, GMContext } from "./gm_api";
|
||||||
|
|
||||||
|
// 构建脚本运行代码
|
||||||
|
export function compileScriptCode(scriptRes: ScriptRunResouce): string {
|
||||||
|
let { code } = scriptRes;
|
||||||
|
let require = "";
|
||||||
|
if (scriptRes.metadata.require) {
|
||||||
|
scriptRes.metadata.require.forEach((val) => {
|
||||||
|
const res = scriptRes.resource[val];
|
||||||
|
if (res) {
|
||||||
|
require = `${require}\n${res.content}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
code = require + code;
|
||||||
|
return `with (context) return (async ()=>{\n${code}\n//# sourceURL=${chrome.runtime.getURL(
|
||||||
|
`/${encodeURI(scriptRes.name)}.user.js`
|
||||||
|
)}\n})()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptFunc = (context: any, GM_info: any) => any;
|
||||||
|
|
||||||
|
// 通过脚本代码编译脚本函数
|
||||||
|
export function compileScript(code: string): ScriptFunc {
|
||||||
|
return <ScriptFunc>new Function("context", "GM_info", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileInjectScript(script: ScriptRunResouce): string {
|
||||||
|
return `window['${script.flag}']=function(context,GM_info){\n${script.code}\n}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置api依赖
|
||||||
|
function setDepend(context: { [key: string]: any }, apiVal: ApiValue) {
|
||||||
|
if (apiVal.param.depend) {
|
||||||
|
for (let i = 0; i < apiVal.param.depend.length; i += 1) {
|
||||||
|
const value = apiVal.param.depend[i];
|
||||||
|
const dependApi = GMContext.apis.get(value);
|
||||||
|
if (!dependApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.startsWith("GM.")) {
|
||||||
|
const [, t] = value.split(".");
|
||||||
|
(<{ [key: string]: any }>context.GM)[t] = dependApi.api.bind(context);
|
||||||
|
} else {
|
||||||
|
context[value] = dependApi.api.bind(context);
|
||||||
|
}
|
||||||
|
setDepend(context, dependApi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建沙盒上下文
|
||||||
|
export function createContext(scriptRes: ScriptRunResouce, GMInfo: any, message: MessageManager): GMApi {
|
||||||
|
// 按照GMApi构建
|
||||||
|
const context: { [key: string]: any } = {
|
||||||
|
scriptRes,
|
||||||
|
message,
|
||||||
|
valueChangeListener: new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>(),
|
||||||
|
sendMessage: GMApi.prototype.sendMessage,
|
||||||
|
connect: GMApi.prototype.connect,
|
||||||
|
runFlag: uuidv4(),
|
||||||
|
valueUpdate: GMApi.prototype.valueUpdate,
|
||||||
|
GM: { Info: GMInfo },
|
||||||
|
GM_info: GMInfo,
|
||||||
|
};
|
||||||
|
if (scriptRes.metadata.grant) {
|
||||||
|
scriptRes.metadata.grant.forEach((val) => {
|
||||||
|
const api = GMContext.apis.get(val);
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (val.startsWith("GM.")) {
|
||||||
|
const [, t] = val.split(".");
|
||||||
|
(<{ [key: string]: any }>context.GM)[t] = api.api.bind(context);
|
||||||
|
} else if (val === "GM_cookie") {
|
||||||
|
// 特殊处理GM_cookie.list之类
|
||||||
|
context[val] = api.api.bind(context);
|
||||||
|
// eslint-disable-next-line func-names, camelcase
|
||||||
|
const GM_cookie = function (action: string) {
|
||||||
|
return (
|
||||||
|
details: GMTypes.CookieDetails,
|
||||||
|
done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void
|
||||||
|
) => {
|
||||||
|
return context[val](action, details, done);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
context[val].list = GM_cookie("list");
|
||||||
|
context[val].delete = GM_cookie("delete");
|
||||||
|
context[val].set = GM_cookie("set");
|
||||||
|
} else {
|
||||||
|
context[val] = api.api.bind(context);
|
||||||
|
}
|
||||||
|
setDepend(context, api);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
context.unsafeWindow = window;
|
||||||
|
return <GMApi>context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writables: { [key: string]: any } = {
|
||||||
|
addEventListener: global.addEventListener.bind(global),
|
||||||
|
removeEventListener: global.removeEventListener.bind(global),
|
||||||
|
dispatchEvent: global.dispatchEvent.bind(global),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录初始的window字段
|
||||||
|
export const init = new Map<string, boolean>();
|
||||||
|
|
||||||
|
// 需要用到全局的
|
||||||
|
export const unscopables: { [key: string]: boolean } = {
|
||||||
|
NodeFilter: true,
|
||||||
|
RegExp: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制原有的,防止被前端网页复写
|
||||||
|
const descs = Object.getOwnPropertyDescriptors(global);
|
||||||
|
Object.keys(descs).forEach((key) => {
|
||||||
|
const desc = descs[key];
|
||||||
|
// 可写但不在特殊配置writables中
|
||||||
|
if (desc && desc.writable && !writables[key]) {
|
||||||
|
if (typeof desc.value === "function") {
|
||||||
|
// 判断是否需要bind,例如Object、Function这些就不需要bind
|
||||||
|
if (desc.value.prototype) {
|
||||||
|
writables[key] = desc.value;
|
||||||
|
} else {
|
||||||
|
writables[key] = desc.value.bind(global);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writables[key] = desc.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
init.set(key, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function warpObject(thisContext: Object, ...context: Object[]) {
|
||||||
|
// 处理Object上的方法
|
||||||
|
thisContext.hasOwnProperty = (name: PropertyKey) => {
|
||||||
|
return (
|
||||||
|
Object.hasOwnProperty.call(thisContext, name) || context.some((val) => Object.hasOwnProperty.call(val, name))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
thisContext.isPrototypeOf = (name: Object) => {
|
||||||
|
return Object.isPrototypeOf.call(thisContext, name) || context.some((val) => Object.isPrototypeOf.call(val, name));
|
||||||
|
};
|
||||||
|
thisContext.propertyIsEnumerable = (name: PropertyKey) => {
|
||||||
|
return (
|
||||||
|
Object.propertyIsEnumerable.call(thisContext, name) ||
|
||||||
|
context.some((val) => Object.propertyIsEnumerable.call(val, name))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拦截上下文
|
||||||
|
export function proxyContext(global: any, context: any, thisContext?: { [key: string]: any }) {
|
||||||
|
const special = Object.assign(writables);
|
||||||
|
// 处理某些特殊的属性
|
||||||
|
// 后台脚本要不要考虑不能使用eval?
|
||||||
|
if (!thisContext) {
|
||||||
|
thisContext = {};
|
||||||
|
}
|
||||||
|
thisContext.eval = global.eval;
|
||||||
|
thisContext.define = undefined;
|
||||||
|
warpObject(thisContext, special, global, context);
|
||||||
|
// keyword是与createContext时同步的,避免访问到context的内部变量
|
||||||
|
const contextKeyword: { [key: string]: any } = {
|
||||||
|
message: 1,
|
||||||
|
valueChangeListener: 1,
|
||||||
|
connect: 1,
|
||||||
|
runFlag: 1,
|
||||||
|
valueUpdate: 1,
|
||||||
|
sendMessage: 1,
|
||||||
|
scriptRes: 1,
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
const proxy = new Proxy(context, {
|
||||||
|
defineProperty(_, name, desc) {
|
||||||
|
if (Object.defineProperty(thisContext, name, desc)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
get(_, name): any {
|
||||||
|
switch (name) {
|
||||||
|
case "window":
|
||||||
|
case "self":
|
||||||
|
case "globalThis":
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return proxy;
|
||||||
|
case "top":
|
||||||
|
case "parent":
|
||||||
|
if (global[name] === global.self) {
|
||||||
|
return special.global || proxy;
|
||||||
|
}
|
||||||
|
return global.top;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (name !== "undefined") {
|
||||||
|
if (has(thisContext, name)) {
|
||||||
|
// @ts-ignore
|
||||||
|
return thisContext[name];
|
||||||
|
}
|
||||||
|
if (typeof name === "string") {
|
||||||
|
if (has(context, name)) {
|
||||||
|
if (has(contextKeyword, name)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return context[name];
|
||||||
|
}
|
||||||
|
if (has(special, name)) {
|
||||||
|
if (typeof special[name] === "function" && !(<{ prototype: any }>special[name]).prototype) {
|
||||||
|
return (<{ bind: any }>special[name]).bind(global);
|
||||||
|
}
|
||||||
|
return special[name];
|
||||||
|
}
|
||||||
|
if (has(global, name)) {
|
||||||
|
// 特殊处理onxxxx的事件
|
||||||
|
if (name.startsWith("on")) {
|
||||||
|
if (typeof global[name] === "function" && !(<{ prototype: any }>global[name]).prototype) {
|
||||||
|
return (<{ bind: any }>global[name]).bind(global);
|
||||||
|
}
|
||||||
|
return global[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (init.has(name)) {
|
||||||
|
const val = global[name];
|
||||||
|
if (typeof val === "function" && !(<{ prototype: any }>val).prototype) {
|
||||||
|
return (<{ bind: any }>val).bind(global);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
} else if (name === Symbol.unscopables) {
|
||||||
|
return unscopables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
has(_, name) {
|
||||||
|
switch (name) {
|
||||||
|
case "window":
|
||||||
|
case "self":
|
||||||
|
case "globalThis":
|
||||||
|
return true;
|
||||||
|
case "top":
|
||||||
|
case "parent":
|
||||||
|
if (global[name] === global.self) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (name !== "undefined") {
|
||||||
|
if (typeof name === "string") {
|
||||||
|
if (has(unscopables, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (has(thisContext, name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (has(context, name)) {
|
||||||
|
if (has(contextKeyword, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (has(special, name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 只处理onxxxx的事件
|
||||||
|
if (has(global[name], name)) {
|
||||||
|
if (name.startsWith("on")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof name === "symbol") {
|
||||||
|
return has(thisContext, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
set(_, name: string, val) {
|
||||||
|
switch (name) {
|
||||||
|
case "window":
|
||||||
|
case "self":
|
||||||
|
case "globalThis":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if (has(special, name)) {
|
||||||
|
special[name] = val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (init.has(name)) {
|
||||||
|
const des = Object.getOwnPropertyDescriptor(global, name);
|
||||||
|
// 只读的return
|
||||||
|
if (des && des.get && !des.set && des.configurable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 只处理onxxxx的事件
|
||||||
|
if (has(global, name) && name.startsWith("on")) {
|
||||||
|
if (val === undefined) {
|
||||||
|
global.removeEventListener(name.slice(2), thisContext[name]);
|
||||||
|
} else {
|
||||||
|
if (thisContext[name]) {
|
||||||
|
global.removeEventListener(name.slice(2), thisContext[name]);
|
||||||
|
}
|
||||||
|
global.addEventListener(name.slice(2), val);
|
||||||
|
}
|
||||||
|
thisContext[name] = val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
thisContext[name] = val;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(_, name) {
|
||||||
|
try {
|
||||||
|
let ret = Object.getOwnPropertyDescriptor(thisContext, name);
|
||||||
|
if (ret) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret = Object.getOwnPropertyDescriptor(context, name);
|
||||||
|
if (ret) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret = Object.getOwnPropertyDescriptor(global, name);
|
||||||
|
return ret;
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
proxy[Symbol.toStringTag] = "Window";
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addStyle(css: string): HTMLElement {
|
||||||
|
const dom = document.createElement("style");
|
||||||
|
dom.innerHTML = css;
|
||||||
|
if (document.head) {
|
||||||
|
return document.head.appendChild(dom);
|
||||||
|
}
|
||||||
|
return document.documentElement.appendChild(dom);
|
||||||
|
}
|
@ -48,9 +48,9 @@ async function main() {
|
|||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
debug: process.env.NODE_ENV === "development",
|
debug: process.env.NODE_ENV === "development",
|
||||||
writer: new DBWriter(new LoggerDAO()),
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
labels: { env: "background" },
|
labels: { env: "service_worker" },
|
||||||
});
|
});
|
||||||
loggerCore.logger().debug("background start");
|
loggerCore.logger().debug("service worker start");
|
||||||
// 初始化管理器
|
// 初始化管理器
|
||||||
const manager = new ServiceWorkerManager();
|
const manager = new ServiceWorkerManager();
|
||||||
manager.initManager();
|
manager.initManager();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user