├── .env-example ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── release.yml │ └── test.yaml ├── .gitignore ├── .markdownlint.json ├── .markdownlintignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── bamboo-specs ├── bamboo.yaml ├── build.yaml ├── deploy.yaml ├── increment.yaml ├── permissions.yaml └── test.yaml ├── browserstack.js ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── scripts ├── build-compatibility-table.js ├── build-corelibs.js ├── build-docs.js ├── build-funcs.js ├── build-redirects-map.js ├── build-redirects.js ├── build-scriptlets.js ├── build-tests.js ├── build-txt.js ├── build.js ├── check-corelibs-dist.js ├── check-sources-updates.js ├── compatibility-table.json ├── constants.js ├── helpers.js ├── rollup-runners.js └── test.js ├── src ├── converters │ ├── converters.ts │ └── index.ts ├── helpers │ ├── add-event-listener-utils.ts │ ├── adjust-set-utils.ts │ ├── array-utils.ts │ ├── attribute-utils.ts │ ├── cookie-utils.ts │ ├── create-on-error-handler.ts │ ├── get-descriptor-addon.ts │ ├── get-error-message.ts │ ├── get-property-in-chain.ts │ ├── get-wildcard-property-in-chain.ts │ ├── hit.ts │ ├── index.ts │ ├── injector.ts │ ├── log-message.ts │ ├── match-request-props.ts │ ├── match-stack.ts │ ├── node-text-utils.ts │ ├── noop-utils.ts │ ├── number-utils.ts │ ├── object-utils.ts │ ├── observer.ts │ ├── open-shadow-dom-utils.ts │ ├── parse-flags.ts │ ├── parse-keyword-value.ts │ ├── prevent-utils.ts │ ├── prevent-window-open-utils.ts │ ├── prune-utils.ts │ ├── random-id.ts │ ├── regexp-utils.ts │ ├── request-utils.ts │ ├── response-utils.ts │ ├── rule-helpers.ts │ ├── script-source-utils.ts │ ├── shadow-dom-utils.ts │ ├── storage-utils.ts │ ├── string-utils.ts │ ├── throttle.ts │ ├── trusted-types-utils.ts │ └── value-matchers.ts ├── index.ts ├── redirects │ ├── amazon-apstag.js │ ├── ati-smarttag.js │ ├── blocking-redirects.yml │ ├── blocking-redirects │ │ ├── click2load.html │ │ └── click2load.js │ ├── didomi-loader.js │ ├── fingerprintjs2.js │ ├── fingerprintjs3.js │ ├── gemius.js │ ├── google-analytics-ga.js │ ├── google-analytics.js │ ├── google-ima3.js │ ├── googlesyndication-adsbygoogle.js │ ├── googletagservices-gpt.js │ ├── index.js │ ├── matomo.js │ ├── metrika-yandex-tag.js │ ├── metrika-yandex-watch.js │ ├── naver-wcslog.js │ ├── noeval.js │ ├── pardot-1.0.js │ ├── prebid-ads.js │ ├── prebid.js │ ├── prevent-bab.js │ ├── prevent-bab2.js │ ├── prevent-fab-3.2.0.js │ ├── prevent-popads-net.js │ ├── redirects-list.js │ ├── redirects-names-list.js │ ├── redirects.ts │ ├── scorecardresearch-beacon.js │ ├── set-popads-dummy.js │ └── static-redirects.yml ├── scriptlets │ ├── abort-current-inline-script.js │ ├── abort-on-property-read.js │ ├── abort-on-property-write.js │ ├── abort-on-stack-trace.js │ ├── adjust-setInterval.js │ ├── adjust-setTimeout.js │ ├── amazon-apstag.ts │ ├── call-nothrow.js │ ├── close-window.js │ ├── debug-current-inline-script.js │ ├── debug-on-property-read.js │ ├── debug-on-property-write.js │ ├── didomi-loader.ts │ ├── dir-string.js │ ├── disable-newtab-links.js │ ├── evaldata-prune.js │ ├── fingerprintjs2.ts │ ├── fingerprintjs3.ts │ ├── gemius.ts │ ├── google-analytics-ga.ts │ ├── google-analytics.ts │ ├── google-ima3.ts │ ├── googlesyndication-adsbygoogle.ts │ ├── googletagservices-gpt.ts │ ├── hide-in-shadow-dom.js │ ├── href-sanitizer.ts │ ├── index.ts │ ├── inject-css-in-shadow-dom.js │ ├── json-prune-fetch-response.ts │ ├── json-prune-xhr-response.ts │ ├── json-prune.js │ ├── log-addEventListener.js │ ├── log-eval.js │ ├── log-on-stack-trace.js │ ├── log.js │ ├── m3u-prune.js │ ├── matomo.ts │ ├── metrika-yandex-tag.ts │ ├── metrika-yandex-watch.ts │ ├── naver-wcslog.ts │ ├── no-protected-audience.ts │ ├── no-topics.js │ ├── noeval.js │ ├── nowebrtc.js │ ├── pardot-1.0.ts │ ├── prebid.ts │ ├── prevent-addEventListener.js │ ├── prevent-adfly.js │ ├── prevent-bab.js │ ├── prevent-canvas.ts │ ├── prevent-element-src-loading.js │ ├── prevent-eval-if.js │ ├── prevent-fab-3.2.0.js │ ├── prevent-fetch.js │ ├── prevent-popads-net.js │ ├── prevent-refresh.js │ ├── prevent-requestAnimationFrame.js │ ├── prevent-setInterval.js │ ├── prevent-setTimeout.js │ ├── prevent-window-open.js │ ├── prevent-xhr.js │ ├── remove-attr.js │ ├── remove-class.js │ ├── remove-cookie.js │ ├── remove-in-shadow-dom.js │ ├── remove-node-text.js │ ├── scorecardresearch-beacon.ts │ ├── scriptlets-list.ts │ ├── scriptlets-names-list.ts │ ├── scriptlets.ts │ ├── set-attr.js │ ├── set-constant.js │ ├── set-cookie-reload.js │ ├── set-cookie.js │ ├── set-local-storage-item.js │ ├── set-popads-dummy.js │ ├── set-session-storage-item.js │ ├── spoof-css.js │ ├── trusted-click-element.ts │ ├── trusted-create-element.ts │ ├── trusted-dispatch-event.ts │ ├── trusted-prune-inbound-object.js │ ├── trusted-replace-fetch-response.js │ ├── trusted-replace-node-text.js │ ├── trusted-replace-outbound-text.ts │ ├── trusted-replace-xhr-response.js │ ├── trusted-set-attr.js │ ├── trusted-set-constant.js │ ├── trusted-set-cookie-reload.js │ ├── trusted-set-cookie.js │ ├── trusted-set-local-storage-item.js │ ├── trusted-set-session-storage-item.ts │ ├── trusted-suppress-native-method.ts │ └── xml-prune.js └── validators │ ├── index.ts │ └── validators.ts ├── tests ├── .eslintrc.cjs ├── api │ ├── converters.spec.ts │ └── validators.spec.ts ├── helpers.js ├── helpers │ ├── attribute-utils.spec.js │ ├── cookie-utils.spec.js │ ├── fetch-utils.spec.js │ ├── index.test.js │ ├── log-message.spec.js │ ├── match-stack.spec.js │ ├── noop.test.js │ ├── number-utils.spec.js │ ├── object-utils.spec.js │ ├── prevent-utils.spec.js │ ├── prune-utils.spec.js │ ├── regexp-utils.spec.js │ ├── string-utils.spec.js │ ├── trusted-types-utils.spec.ts │ └── value-matchers.spec.js ├── index.js ├── index.test.js ├── redirects │ ├── amazon-apstag.test.js │ ├── ati-smarttag.test.js │ ├── didomi-loader.test.js │ ├── fingerprintjs2.test.js │ ├── fingerprintjs3.test.js │ ├── gemius.test.js │ ├── google-analytics-ga.test.js │ ├── google-analytics.test.js │ ├── google-ima3.test.js │ ├── googlesyndication-adsbygoogle.test.js │ ├── googletagservices-gpt.test.js │ ├── index.test.js │ ├── matomo.test.js │ ├── metrika-yandex-tag.test.js │ ├── metrika-yandex-watch.test.js │ ├── naver-wcslog.test.js │ ├── noeval.test.js │ ├── pardot-1.0.test.js │ ├── prebid-ads.test.js │ ├── prebid.test.js │ ├── prevent-bab2.test.js │ └── scorecardresearch-beacon.test.js ├── scriptlets-entrypoint.js ├── scriptlets │ ├── abort-current-inline-script.test.js │ ├── abort-on-property-read.test.js │ ├── abort-on-property-write.test.js │ ├── abort-on-stack-trace.test.js │ ├── adjust-setInterval.test.js │ ├── adjust-setTimeout.test.js │ ├── call-nothrow.test.js │ ├── close-window.test.js │ ├── debug-current-inline-script.test.js │ ├── debug-on-property-read.test.js │ ├── debug-on-property-write.test.js │ ├── dir-string.test.js │ ├── disable-newtab-links.test.js │ ├── evaldata-prune.test.js │ ├── fetch-objects │ │ ├── test01.json │ │ ├── test02.json │ │ ├── test03.json │ │ ├── test04.json │ │ ├── test05.json │ │ └── test06.json │ ├── hide-in-shadow-dom.test.js │ ├── href-sanitizer.test.js │ ├── index.test.js │ ├── inject-css-in-shadow-dom.test.js │ ├── json-prune-fetch-response.test.js │ ├── json-prune-xhr-response.test.js │ ├── json-prune.test.js │ ├── log-addEventListener.test.js │ ├── log-eval.test.js │ ├── log-on-stack-trace.test.js │ ├── log.test.js │ ├── m3u-prune.test.js │ ├── no-protected-audience.test.js │ ├── no-topics.test.js │ ├── noeval.test.js │ ├── nowebrtc.test.js │ ├── prevent-addEventListener.test.js │ ├── prevent-adfly.test.js │ ├── prevent-bab.test.js │ ├── prevent-canvas.test.js │ ├── prevent-element-src-loading.test.js │ ├── prevent-eval-if.test.js │ ├── prevent-fab-3.2.0.test.js │ ├── prevent-fetch.test.js │ ├── prevent-popads-net.test.js │ ├── prevent-refresh.test.js │ ├── prevent-requestAnimationFrame.test.js │ ├── prevent-setInterval.test.js │ ├── prevent-setTimeout.test.js │ ├── prevent-window-open.test.js │ ├── prevent-xhr.test.js │ ├── remove-attr.test.js │ ├── remove-class.test.js │ ├── remove-cookie.test.js │ ├── remove-in-shadow-dom.test.js │ ├── remove-node-text.test.js │ ├── set-attr.test.js │ ├── set-constant.test.js │ ├── set-cookie-reload.test.js │ ├── set-cookie.test.js │ ├── set-local-storage-item.test.js │ ├── set-popads-dummy.test.js │ ├── set-session-storage-item.test.js │ ├── spoof-css.test.js │ ├── test-files │ │ ├── empty.html │ │ ├── manifestM3U8-01.m3u8 │ │ ├── manifestM3U8-02.m3u8 │ │ ├── manifestM3U8-03.m3u8 │ │ ├── manifestM3U8-04.m3u8 │ │ ├── manifestM3U8-carriage-return.m3u8 │ │ ├── manifestMPD.mpd │ │ ├── test-image.jpeg │ │ ├── test-script01.js │ │ ├── test-script02.js │ │ ├── test01.json │ │ ├── test02.json │ │ ├── test03.json │ │ ├── test04.json │ │ ├── test05.json │ │ └── test06.json │ ├── trusted-click-element.spec.js │ ├── trusted-click-element.test.js │ ├── trusted-create-element.test.js │ ├── trusted-dispatch-event.test.js │ ├── trusted-prune-inbound-object.test.js │ ├── trusted-replace-fetch-response.test.js │ ├── trusted-replace-node-text.test.js │ ├── trusted-replace-outbound-text.test.js │ ├── trusted-replace-xhr-response.test.js │ ├── trusted-set-attr.test.js │ ├── trusted-set-constant.test.js │ ├── trusted-set-cookie-reload.spec.js │ ├── trusted-set-cookie.test.js │ ├── trusted-set-local-storage-item.test.js │ ├── trusted-set-session-storage-item.test.js │ ├── trusted-suppress-native-method.test.js │ └── xml-prune.test.js ├── server.js ├── smoke │ └── exports │ │ ├── package.json │ │ ├── root-exports.ts │ │ ├── specific-exports.ts │ │ ├── test.sh │ │ └── tsconfig.json ├── styles.css └── template.html ├── tsconfig.build.json ├── tsconfig.json ├── types └── types.d.ts ├── vitest.config.ts └── wiki ├── about-redirects.md ├── about-scriptlets.md ├── about-trusted-scriptlets.md └── compatibility-table.md /.env-example: -------------------------------------------------------------------------------- 1 | BROWSERSTACK_USER= 2 | BROWSERSTACK_KEY= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | tmp -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | 3 | env: 4 | NODE_VERSION: 22.14.0 5 | PNPM_VERSION: 10.7.1 6 | 7 | # Workflow need write access to the repository to create a GitHub release 8 | permissions: 9 | contents: write 10 | 11 | on: 12 | push: 13 | tags: 14 | - v* 15 | # Make possible to run manually 16 | workflow_dispatch: 17 | inputs: 18 | # warn before running manually 19 | warning: 20 | description: 'Should be run only for tags like `v*`' 21 | required: false 22 | type: boolean 23 | 24 | # Make sure that only one release workflow runs at a time. 25 | concurrency: 26 | group: release 27 | 28 | jobs: 29 | release: 30 | name: Create GitHub release 31 | runs-on: ubuntu-latest 32 | # Only run this job for v* tags 33 | if: startsWith(github.ref, 'refs/tags/v') 34 | steps: 35 | - name: Check out the repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Setup pnpm 39 | uses: pnpm/action-setup@v4 40 | with: 41 | version: ${{ env.PNPM_VERSION }} 42 | run_install: false 43 | 44 | - name: Set up Node.js 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ env.NODE_VERSION }} 48 | registry-url: https://registry.npmjs.org 49 | cache: pnpm 50 | 51 | - name: Install dependencies 52 | run: pnpm install 53 | 54 | - name: Run ESLint 55 | run: pnpm lint 56 | 57 | - name: Run tests 58 | run: pnpm test 59 | 60 | - name: Run build 61 | run: pnpm build 62 | 63 | - name: Release on GitHub 64 | uses: softprops/action-gh-release@v1 65 | with: 66 | draft: false 67 | prerelease: false 68 | body: See [CHANGELOG.md](../master/CHANGELOG.md) for the list of changes. 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | 72 | notify: 73 | name: Send Slack notification 74 | needs: release 75 | # Note: 'always()' is needed to run the notify job even if the test job was failed 76 | if: 77 | ${{ 78 | always() && 79 | github.repository == 'AdguardTeam/Scriptlets' && 80 | (github.event_name == 'push' || github.event_name == 'workflow_dispatch') 81 | }} 82 | runs-on: ubuntu-latest 83 | steps: 84 | - name: Send Slack notification 85 | uses: 8398a7/action-slack@v3 86 | with: 87 | status: ${{ needs.release.result }} 88 | fields: workflow, repo, message, commit, author, eventName, ref, job 89 | job_name: release 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 93 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test Scriptlets 2 | 3 | env: 4 | NODE_VERSION: 22.14.0 5 | PNPM_VERSION: 10.7.1 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | tags: 12 | - "v*.*.*" 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup pnpm 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: ${{ env.PNPM_VERSION }} 25 | run_install: false 26 | 27 | - name: Use Node.jobs 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ env.NODE_VERSION }} 31 | 32 | - name: pnpm install 33 | run: pnpm install 34 | 35 | - name: pnpm lint 36 | run: pnpm lint 37 | 38 | - name: pnpm test 39 | run: pnpm test 40 | 41 | - name: pnpm build 42 | run: pnpm build 43 | 44 | notify: 45 | name: Send Slack notification on failure 46 | needs: build 47 | # Run this job only if the previous job failed and the event was triggered by the 'AdguardTeam/Scriptlets' repository 48 | # Note: 'always()' is needed to run the notify job even if the test job was failed 49 | if: 50 | ${{ 51 | always() && 52 | needs.check_code.result == 'failure' && 53 | github.repository == 'AdguardTeam/Scriptlets' && 54 | ( 55 | github.event_name == 'push' || 56 | github.event_name == 'workflow_dispatch' || 57 | github.event.pull_request.head.repo.full_name == github.repository 58 | ) 59 | }} 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Send Slack notification 63 | uses: 8398a7/action-slack@v3 64 | with: 65 | status: failure 66 | fields: workflow, repo, message, commit, author, eventName, ref, job 67 | job_name: check_code 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .idea/ 4 | .vscode/ 5 | *.iml 6 | node_modules/ 7 | private 8 | tmp/ 9 | output.md 10 | tests/dist/ 11 | browserstack.err 12 | local.log 13 | 14 | # linters cache 15 | .eslintcache 16 | 17 | tsconfig.tsbuildinfo 18 | .pnpm-store/ 19 | dist/ 20 | 21 | .npmrc 22 | *.tgz 23 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "ul-indent": { "indent": 4 }, 3 | "line-length": { 4 | "stern": true, 5 | "line_length": 120 6 | }, 7 | "no-multiple-blanks": { "maximum": 2 }, 8 | "no-inline-html": { "allowed_elements": ["a"] }, 9 | "no-duplicate-heading": { "siblings_only": true }, 10 | "no-blanks-blockquote": false, 11 | "no-bare-urls": false, 12 | "ul-style": { "style": "dash" } 13 | } 14 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(false); 3 | const config = { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | exclude: [ 9 | '@babel/plugin-transform-async-to-generator', 10 | '@babel/plugin-transform-regenerator', 11 | '@babel/plugin-transform-typeof-symbol', 12 | '@babel/plugin-transform-computed-properties', 13 | '@babel/plugin-transform-destructuring', 14 | ], 15 | targets: [ 16 | 'last 1 version', 17 | '> 1%', 18 | // ie 11 is dead and no longer supported 19 | 'not dead', 20 | 'chrome >= 55', 21 | 'firefox >= 52', 22 | 'edge >= 15', 23 | 'opera >= 42', 24 | 'safari >= 13', 25 | ], 26 | }, 27 | ], 28 | [ 29 | '@babel/preset-typescript', 30 | { 31 | optimizeConstEnums: true, 32 | }, 33 | ], 34 | ], 35 | plugins: [ 36 | '@babel/plugin-transform-runtime', 37 | '@babel/plugin-transform-arrow-functions', 38 | '@babel/plugin-transform-function-name', 39 | ], 40 | }; 41 | 42 | return config; 43 | }; 44 | -------------------------------------------------------------------------------- /bamboo-specs/bamboo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | !include 'build.yaml' 3 | 4 | --- 5 | !include 'deploy.yaml' 6 | 7 | --- 8 | !include 'increment.yaml' 9 | 10 | --- 11 | !include 'permissions.yaml' 12 | 13 | --- 14 | !include 'test.yaml' -------------------------------------------------------------------------------- /bamboo-specs/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | plan: 4 | project-key: AJL 5 | key: SCRIPTLETSBUILD 6 | name: scriptlets - build 7 | variables: 8 | dockerContainer: adguard/node-ssh:22.14--0 9 | 10 | stages: 11 | - Build: 12 | manual: false 13 | final: false 14 | jobs: 15 | - Build 16 | 17 | Build: 18 | key: BUILD 19 | other: 20 | clean-working-dir: true 21 | docker: 22 | image: "${bamboo.dockerContainer}" 23 | volumes: 24 | ${system.PNPM_DIR}: "${bamboo.cachePnpm}" 25 | tasks: 26 | - checkout: 27 | force-clean-build: true 28 | - script: 29 | interpreter: SHELL 30 | scripts: 31 | - |- 32 | set -e 33 | set -x 34 | 35 | # Fix mixed logs 36 | exec 2>&1 37 | 38 | ls -laht 39 | 40 | # Set cache directory 41 | pnpm config set store-dir ${bamboo.cachePnpm} 42 | 43 | pnpm install ${bamboo.varsPnpm} 44 | 45 | pnpm build 46 | 47 | pnpm pack --out scriptlets.tgz 48 | - inject-variables: 49 | file: dist/build.txt 50 | scope: RESULT 51 | namespace: inject 52 | final-tasks: 53 | - script: 54 | interpreter: SHELL 55 | scripts: 56 | - |- 57 | set -x 58 | set -e 59 | 60 | # Fix mixed logs 61 | exec 2>&1 62 | 63 | ls -la 64 | 65 | echo "Size before cleanup:" && du -h | tail -n 1 66 | rm -rf node_modules 67 | echo "Size after cleanup:" && du -h | tail -n 1 68 | artifacts: 69 | - name: scriptlets.tgz 70 | location: . 71 | pattern: scriptlets.tgz 72 | shared: true 73 | required: true 74 | requirements: 75 | - adg-docker: 'true' 76 | 77 | triggers: [] 78 | 79 | branches: 80 | create: manually 81 | delete: never 82 | link-to-jira: true 83 | 84 | notifications: 85 | - events: 86 | - plan-status-changed 87 | recipients: 88 | - webhook: 89 | name: Build webhook 90 | url: http://prod.jirahub.service.eu.consul/v1/webhook/bamboo 91 | 92 | labels: [] 93 | 94 | other: 95 | concurrent-build-plugin: system-default 96 | -------------------------------------------------------------------------------- /bamboo-specs/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | deployment: 4 | name: scriptlets - deploy 5 | source-plan: AJL-SCRIPTLETSBUILD 6 | release-naming: ${bamboo.inject.version} 7 | 8 | environments: 9 | - npmjs 10 | - npmjs • alpha 11 | - npmjs • beta 12 | 13 | npmjs: &npmjs 14 | docker: 15 | image: adguard/node-ssh:22.14--0 16 | volumes: 17 | ${system.PNPM_DIR}: "${bamboo.cachePnpm}" 18 | variables: &npmjs-variables 19 | publishArgs: --access public 20 | # This ID is visible on the page with linked repositories. Ask the admin to provide it to you. 21 | selectedRepository: 26050988 22 | triggers: [ ] 23 | tasks: 24 | - checkout: 25 | force-clean-build: true 26 | - artifact-download: 27 | artifacts: 28 | - name: scriptlets.tgz 29 | - script: 30 | interpreter: SHELL 31 | scripts: 32 | - |- 33 | set -e 34 | set -x 35 | 36 | # Fix mixed logs 37 | exec 2>&1 38 | 39 | ls -alt 40 | 41 | export NPM_TOKEN=${bamboo.npmSecretToken} 42 | 43 | # Store NPM token in .npmrc file to be used during publishing the package 44 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc 45 | 46 | npm publish scriptlets.tgz ${bamboo_publishArgs} 47 | 48 | # After successful publish and before updated docs committing, 49 | # tgz and npmrc files should be removed 50 | rm -f scriptlets.tgz 51 | rm -f .npmrc 52 | 53 | # Set cache directory 54 | pnpm config set store-dir ${bamboo.cachePnpm} 55 | 56 | pnpm install ${bamboo.varsPnpm} 57 | 58 | pnpm wiki 59 | - any-task: 60 | plugin-key: com.atlassian.bamboo.plugins.vcs:task.vcs.tagging 61 | configuration: 62 | # This ID is visible on the page with linked repositories. Ask the admin to provide it to you. 63 | selectedRepository: '26050988' 64 | tagName: v${bamboo.inject.version} 65 | - any-task: 66 | plugin-key: com.atlassian.bamboo.plugins.vcs:task.vcs.commit 67 | configuration: 68 | # This ID is visible on the page with linked repositories. Ask the admin to provide it to you. 69 | selectedRepository: '26050988' 70 | # do not change commitMessage 71 | commitMessage: 'deploy: update docs for v${bamboo.inject.version}' 72 | requirements: 73 | - adg-docker: 'true' 74 | notifications: 75 | - events: 76 | - deployment-started-and-finished 77 | recipients: 78 | - webhook: 79 | name: Deploy webhook 80 | url: http://prod.jirahub.service.eu.consul/v1/webhook/bamboo 81 | 82 | npmjs • alpha: 83 | <<: *npmjs 84 | variables: 85 | <<: *npmjs-variables 86 | publishArgs: --tag alpha --access public 87 | 88 | npmjs • beta: 89 | <<: *npmjs 90 | variables: 91 | <<: *npmjs-variables 92 | publishArgs: --tag beta --access public 93 | -------------------------------------------------------------------------------- /bamboo-specs/increment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | plan: 4 | project-key: AJL 5 | key: SCRIPTLETSINCR 6 | name: scriptlets - increment 7 | variables: 8 | dockerContainer: adguard/node-ssh:22.14--0 9 | 10 | stages: 11 | - Increment: 12 | manual: true 13 | final: false 14 | jobs: 15 | - Increment 16 | 17 | Increment: 18 | key: INCR 19 | docker: 20 | image: "${bamboo.dockerContainer}" 21 | volumes: 22 | ${system.PNPM_DIR}: "${bamboo.cachePnpm}" 23 | other: 24 | clean-working-dir: true 25 | tasks: 26 | - checkout: 27 | force-clean-build: true 28 | - script: 29 | interpreter: SHELL 30 | scripts: 31 | - |- 32 | #!/bin/bash 33 | set -e -f -u -x 34 | 35 | # Explicitly checkout the revision that we need. 36 | git checkout "${bamboo.repository.revision.number}" 37 | 38 | # do not increment for dist deploy after release build 39 | if ! [[ `git log -1 --pretty=%s` =~ "deploy: update dist" ]];then 40 | git checkout master 41 | set -e 42 | set -x 43 | ls -alt 44 | pnpm increment 45 | fi 46 | - any-task: 47 | plugin-key: com.atlassian.bamboo.plugins.vcs:task.vcs.commit 48 | configuration: 49 | commitMessage: 'skipci: Automatic increment build number' 50 | selectedRepository: defaultRepository 51 | requirements: 52 | - adg-docker: 'true' 53 | 54 | branches: 55 | create: manually 56 | delete: never 57 | link-to-jira: true 58 | 59 | labels: [] 60 | other: 61 | concurrent-build-plugin: system-default 62 | -------------------------------------------------------------------------------- /bamboo-specs/permissions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | deployment: 4 | name: scriptlets - deploy 5 | deployment-permissions: 6 | - groups: 7 | - extensions-developers 8 | permissions: 9 | - view 10 | environment-permissions: 11 | - npmjs: 12 | - groups: 13 | - extensions-developers 14 | permissions: 15 | - view 16 | - deploy 17 | - npmjs • alpha: 18 | - groups: 19 | - extensions-developers 20 | permissions: 21 | - view 22 | - deploy 23 | - npmjs • beta: 24 | - groups: 25 | - extensions-developers 26 | permissions: 27 | - view 28 | - deploy 29 | 30 | -------------------------------------------------------------------------------- /bamboo-specs/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | plan: 4 | project-key: AJL 5 | key: SCRIPTLETSTEST 6 | name: scriptlets - test new 7 | variables: 8 | dockerPuppeteer: adguard/puppeteer-runner:22.14--24.5--1 9 | 10 | stages: 11 | - Build: 12 | manual: false 13 | final: false 14 | jobs: 15 | - Build 16 | 17 | Build: 18 | key: BUILD 19 | docker: 20 | image: "${bamboo.dockerPuppeteer}" 21 | volumes: 22 | ${system.PNPM_DIR}: "${bamboo.cachePnpm}" 23 | tasks: 24 | - checkout: 25 | force-clean-build: true 26 | - script: 27 | interpreter: SHELL 28 | scripts: 29 | - |- 30 | set -e 31 | set -x 32 | 33 | # Fix mixed logs 34 | exec 2>&1 35 | 36 | ls -laht 37 | 38 | # Exclude '--ignore-scripts' from pnpm arguments 39 | # since it prevents postinstall scripts from running 40 | # so Chrome is not installed which is crucial for tests 41 | originalPnpmArgs="$bamboo_varsPnpm" 42 | modifiedPnpmArgs=$(echo "$originalPnpmArgs" | sed 's/--ignore-scripts//g') 43 | 44 | # Install common dependencies 45 | pnpm install ${modifiedPnpmArgs} 46 | 47 | # build docs to lint them later 48 | # check compatibility table updates and build it and build wiki docs 49 | pnpm wiki 50 | 51 | pnpm test 52 | pnpm build 53 | 54 | # lint code and docs. it should be run after `pnpm build` 55 | pnpm lint 56 | final-tasks: 57 | - script: 58 | interpreter: SHELL 59 | scripts: 60 | - |- 61 | set -x 62 | set -e 63 | 64 | # Fix mixed logs 65 | exec 2>&1 66 | 67 | ls -la 68 | 69 | echo "Size before cleanup:" && du -h | tail -n 1 70 | rm -rf node_modules 71 | echo "Size after cleanup:" && du -h | tail -n 1 72 | artifacts: 73 | - name: scriptlets.corelibs.json 74 | location: dist 75 | pattern: scriptlets.corelibs.json 76 | required: true 77 | - name: redirects.json 78 | location: dist 79 | pattern: redirects.json 80 | required: true 81 | requirements: 82 | - adg-docker: 'true' 83 | 84 | branches: 85 | create: for-pull-request 86 | delete: 87 | after-deleted-days: '1' 88 | after-inactive-days: '5' 89 | link-to-jira: true 90 | 91 | notifications: 92 | - events: 93 | - plan-status-changed 94 | recipients: 95 | - webhook: 96 | name: Build webhook 97 | url: http://prod.jirahub.service.eu.consul/v1/webhook/bamboo 98 | 99 | labels: [ ] 100 | other: 101 | concurrent-build-plugin: system-default 102 | -------------------------------------------------------------------------------- /scripts/build-corelibs.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { minify } from 'terser'; 5 | 6 | import * as scriptletNamesList from '../src/scriptlets/scriptlets-names-list'; 7 | import { version } from '../package.json'; 8 | import { writeFile } from './helpers'; 9 | import { DIST_DIR_NAME, CORELIBS_SCRIPTLETS_FILE_NAME } from './constants'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | const corelibsScriptletsPath = path.join(__dirname, '../', DIST_DIR_NAME, CORELIBS_SCRIPTLETS_FILE_NAME); 15 | 16 | const buildCorelibsJson = async () => { 17 | // eslint-disable-next-line import/no-unresolved 18 | const { getScriptletFunction } = await import('../tmp/scriptlets-func'); 19 | const scriptlets = await Promise.all(Object 20 | .values(scriptletNamesList) 21 | .map(async (names) => { 22 | const scriptlet = getScriptletFunction(names[0]).toString(); 23 | const result = await minify(scriptlet, { 24 | mangle: false, 25 | format: { comments: false }, 26 | // needed for "debug-" scriptlets 27 | // https://github.com/AdguardTeam/Scriptlets/issues/218 28 | compress: { drop_debugger: false }, 29 | }); 30 | return { 31 | names, 32 | scriptlet: result.code, 33 | }; 34 | })); 35 | 36 | return JSON.stringify({ 37 | version, 38 | scriptlets, 39 | }, null, 4); 40 | }; 41 | 42 | export const buildScriptletsForCorelibs = async () => { 43 | console.log('Start building corelibs...'); 44 | const json = await buildCorelibsJson(); 45 | await writeFile(corelibsScriptletsPath, json); 46 | console.log('Corelibs built'); 47 | }; 48 | -------------------------------------------------------------------------------- /scripts/build-redirects-map.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { minify } from 'terser'; 3 | import path from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | 6 | import { getPreparedRedirects } from './build-redirects'; 7 | import { writeFile } from './helpers'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | const createRedirectsMap = (redirects) => { 13 | const map = {}; 14 | 15 | redirects.forEach((item) => { 16 | const { title, aliases, file } = item; 17 | 18 | if (title) { 19 | map[title] = file; 20 | } 21 | 22 | if (aliases) { 23 | aliases.forEach((alias) => { 24 | map[alias] = file; 25 | }); 26 | } 27 | }); 28 | 29 | return `export const redirectsMap = ${JSON.stringify(map)}`; 30 | }; 31 | 32 | const getRedirectsMap = async () => { 33 | const { mergedRedirects } = await getPreparedRedirects({ code: false }); 34 | return createRedirectsMap(mergedRedirects); 35 | }; 36 | 37 | export const buildRedirectsMap = async () => { 38 | console.log('Start building redirects map...'); 39 | 40 | const redirectsMap = await getRedirectsMap(); 41 | 42 | const beautifiedScriptletFunctions = await minify(redirectsMap, { 43 | mangle: false, 44 | compress: false, 45 | format: { beautify: true }, 46 | }); 47 | 48 | await writeFile(path.resolve(__dirname, '../tmp/redirects-map.js'), beautifiedScriptletFunctions.code); 49 | 50 | console.log('Finish building redirect map'); 51 | }; 52 | -------------------------------------------------------------------------------- /scripts/build-scriptlets.js: -------------------------------------------------------------------------------- 1 | import { rollupStandard } from './rollup-runners'; 2 | import { scriptlets, scriptletsListConfig } from '../rollup.config'; 3 | 4 | export const buildScriptletsList = async () => rollupStandard(scriptletsListConfig); 5 | 6 | export const buildScriptlets = async () => rollupStandard(scriptlets); 7 | -------------------------------------------------------------------------------- /scripts/build-txt.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import { writeFile } from './helpers'; 5 | import { version } from '../package.json'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | const PATH = '../dist'; 11 | const FILENAME = 'build.txt'; 12 | 13 | export const buildTxt = async () => { 14 | const content = `version=${version}`; 15 | await writeFile(path.resolve(__dirname, PATH, FILENAME), content); 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import { program } from 'commander'; 2 | 3 | import { buildScriptletsFunc } from './build-funcs'; 4 | import { 5 | buildClick2Load, 6 | buildRedirectsFiles, 7 | buildRedirectsForCorelibs, 8 | buildRedirectsList, 9 | prebuildRedirects, 10 | } from './build-redirects'; 11 | import { buildScriptletsForCorelibs } from './build-corelibs'; 12 | import { buildScriptlets, buildScriptletsList } from './build-scriptlets'; 13 | import { buildTxt } from './build-txt'; 14 | import { buildRedirectsMap } from './build-redirects-map'; 15 | import { runTasks } from './helpers'; 16 | 17 | const buildScriptletsAndRedirectsLists = async () => { 18 | await Promise.all([buildRedirectsList(), buildScriptletsList()]); 19 | }; 20 | 21 | const tasks = [ 22 | buildScriptletsAndRedirectsLists, 23 | buildScriptletsFunc, 24 | buildClick2Load, 25 | buildRedirectsMap, 26 | prebuildRedirects, 27 | buildRedirectsFiles, 28 | buildRedirectsForCorelibs, 29 | buildScriptlets, 30 | buildScriptletsForCorelibs, 31 | buildTxt, 32 | ]; 33 | 34 | program 35 | .description('Builds scriptlets and redirects') 36 | .action(async () => { 37 | await runTasks(tasks); 38 | }); 39 | 40 | program.parse(process.argv); 41 | -------------------------------------------------------------------------------- /scripts/constants.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import { getFilesList } from './helpers'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | /** 10 | * Rules which were removed from the list should be marked with it 11 | */ 12 | const REMOVED_MARKER = '(removed)'; 13 | 14 | const COMPATIBILITY_TABLE_INPUT_FILENAME = 'compatibility-table.json'; 15 | 16 | /** 17 | * Path to **input** compatibility data source json 18 | */ 19 | const COMPATIBILITY_TABLE_DATA_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_INPUT_FILENAME); 20 | 21 | const WIKI_DIR_PATH = '../wiki'; 22 | 23 | const SRC_RELATIVE_DIR = '../src'; 24 | const SRC_SCRIPTLETS_SUB_DIR = 'scriptlets'; 25 | const SRC_REDIRECTS_SUB_DIR = 'redirects'; 26 | 27 | const SCRIPTLETS_SRC_RELATIVE_DIR_PATH = `${SRC_RELATIVE_DIR}/${SRC_SCRIPTLETS_SUB_DIR}`; 28 | const REDIRECTS_SRC_RELATIVE_DIR_PATH = `${SRC_RELATIVE_DIR}/${SRC_REDIRECTS_SUB_DIR}`; 29 | 30 | const TRUSTED_SCRIPTLETS_PREFIX = 'trusted-'; 31 | 32 | // files which are not scriptlets in the source directory 33 | const NON_SCRIPTLETS_FILES = [ 34 | 'index.ts', 35 | 'scriptlets.ts', 36 | 'scriptlets-list.ts', 37 | 'scriptlets-names-list.ts', 38 | ]; 39 | 40 | const isUtilityFileName = (filename) => NON_SCRIPTLETS_FILES.includes(filename); 41 | const isTrustedScriptletsFilename = (filename) => filename.startsWith(TRUSTED_SCRIPTLETS_PREFIX); 42 | 43 | const scriptletsFilenames = getFilesList(SCRIPTLETS_SRC_RELATIVE_DIR_PATH) 44 | .filter((el) => !isUtilityFileName(el) && !isTrustedScriptletsFilename(el)); 45 | 46 | const trustedScriptletsFilenames = getFilesList(SCRIPTLETS_SRC_RELATIVE_DIR_PATH) 47 | .filter((el) => isTrustedScriptletsFilename(el)); 48 | 49 | // files which are not redirects in the source directory 50 | const NON_REDIRECTS_FILES = [ 51 | 'index.js', 52 | 'redirects.ts', 53 | 'redirects-list.js', 54 | 'redirects-names-list.js', 55 | ]; 56 | const redirectsFilenames = getFilesList(REDIRECTS_SRC_RELATIVE_DIR_PATH) 57 | .filter((el) => !NON_REDIRECTS_FILES.includes(el)); 58 | 59 | const DIST_DIR_NAME = 'dist'; 60 | const CORELIBS_SCRIPTLETS_FILE_NAME = 'scriptlets.corelibs.json'; 61 | const CORELIBS_REDIRECTS_FILE_NAME = 'redirects.json'; 62 | 63 | export { 64 | REMOVED_MARKER, 65 | COMPATIBILITY_TABLE_DATA_PATH, 66 | WIKI_DIR_PATH, 67 | SCRIPTLETS_SRC_RELATIVE_DIR_PATH, 68 | REDIRECTS_SRC_RELATIVE_DIR_PATH, 69 | scriptletsFilenames, 70 | trustedScriptletsFilenames, 71 | redirectsFilenames, 72 | DIST_DIR_NAME, 73 | CORELIBS_SCRIPTLETS_FILE_NAME, 74 | CORELIBS_REDIRECTS_FILE_NAME, 75 | }; 76 | -------------------------------------------------------------------------------- /scripts/rollup-runners.js: -------------------------------------------------------------------------------- 1 | import * as rollup from 'rollup'; 2 | import chalk from 'chalk'; 3 | 4 | const { log } = console; 5 | 6 | /** 7 | * Builds scriptlets 8 | * 9 | * @param {object|object[]} config config may be list of configs or one config 10 | */ 11 | export const rollupStandard = async (config) => { 12 | const runOneConfig = async (config) => { 13 | log('Start building...', config.input); 14 | const bundle = await rollup.rollup(config); 15 | if (Array.isArray(config.output)) { 16 | for (const outputOptions of config.output) { 17 | await bundle.write(outputOptions); 18 | } 19 | } else { 20 | await bundle.write(config.output); 21 | } 22 | log(chalk.greenBright('Successfully built'), config.input); 23 | }; 24 | 25 | if (Array.isArray(config)) { 26 | for (const oneConfig of config) { 27 | await runOneConfig(oneConfig); 28 | } 29 | } else { 30 | await runOneConfig(config); 31 | } 32 | }; 33 | 34 | /** 35 | * Builds scriptlets in the watch mode 36 | * 37 | * @param {object|object[]} config - config may be list of configs or one config 38 | */ 39 | export const rollupWatch = (config) => { 40 | const watcher = rollup.watch(config); 41 | watcher.on('event', (event) => { 42 | if (event.code === 'BUNDLE_START') { 43 | log('Start building...', event.input); 44 | } 45 | if (event.result) { 46 | event.result.close(); 47 | log(chalk.yellowBright('Waiting for changes...')); 48 | } 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/converters/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | convertAbpSnippetToAdg as convertAbpToAdg, 3 | convertAdgScriptletToUbo as convertAdgToUbo, 4 | convertScriptletToAdg, 5 | convertUboScriptletToAdg as convertUboToAdg, 6 | convertAdgRedirectToUbo, 7 | } from './converters'; 8 | -------------------------------------------------------------------------------- /src/helpers/add-event-listener-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates event type 3 | * 4 | * @param type event type 5 | * @returns true if type is valid 6 | */ 7 | export const validateType = (type: unknown): boolean => { 8 | // https://github.com/AdguardTeam/Scriptlets/issues/125 9 | return typeof type !== 'undefined'; 10 | }; 11 | 12 | /** 13 | * Validates event listener 14 | * 15 | * @param listener event listener 16 | * @returns true if listener callback is valid 17 | */ 18 | export const validateListener = (listener: unknown): boolean => { 19 | // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters 20 | return typeof listener !== 'undefined' 21 | && (typeof listener === 'function' 22 | || (typeof listener === 'object' 23 | // https://github.com/AdguardTeam/Scriptlets/issues/76 24 | && listener !== null 25 | && 'handleEvent' in listener 26 | && typeof listener.handleEvent === 'function')); 27 | }; 28 | 29 | /** 30 | * Serialize valid event listener 31 | * https://developer.mozilla.org/en-US/docs/Web/API/EventListener 32 | * 33 | * @param listener valid listener 34 | * @returns listener string 35 | */ 36 | export const listenerToString = (listener: EventListener | EventListenerObject): string => { 37 | return typeof listener === 'function' 38 | ? listener.toString() 39 | : listener.handleEvent.toString(); 40 | }; 41 | -------------------------------------------------------------------------------- /src/helpers/adjust-set-utils.ts: -------------------------------------------------------------------------------- 1 | import { nativeIsNaN, nativeIsFinite } from './number-utils'; 2 | 3 | export const shouldMatchAnyDelay = (delay: string) => delay === '*'; 4 | 5 | /** 6 | * Handles input delay value 7 | * 8 | * @param delay matchDelay argument of adjust-* scriptlets 9 | * @returns proper number delay value 10 | */ 11 | export const getMatchDelay = (delay: string): number => { 12 | const DEFAULT_DELAY = 1000; 13 | const parsedDelay = parseInt(delay, 10); 14 | const delayMatch = nativeIsNaN(parsedDelay) 15 | ? DEFAULT_DELAY // default scriptlet value 16 | : parsedDelay; 17 | return delayMatch; 18 | }; 19 | 20 | /** 21 | * Checks delay match condition 22 | * 23 | * @param inputDelay matchDelay argument of adjust-* scriptlets 24 | * @param realDelay delay argument of setTimeout/setInterval 25 | * @returns if given delays match 26 | */ 27 | export const isDelayMatched = (inputDelay: string, realDelay: number): boolean => { 28 | return shouldMatchAnyDelay(inputDelay) 29 | || realDelay === getMatchDelay(inputDelay); 30 | }; 31 | 32 | /** 33 | * Handles input boost value 34 | * 35 | * @param boost boost argument of adjust-* scriptlets 36 | * @returns proper number boost multiplier value 37 | */ 38 | export const getBoostMultiplier = (boost: string): number => { 39 | const DEFAULT_MULTIPLIER = 0.05; 40 | // https://github.com/AdguardTeam/Scriptlets/issues/262 41 | const MIN_MULTIPLIER = 0.001; 42 | const MAX_MULTIPLIER = 50; 43 | const parsedBoost = parseFloat(boost); 44 | let boostMultiplier = nativeIsNaN(parsedBoost) || !nativeIsFinite(parsedBoost) 45 | ? DEFAULT_MULTIPLIER // default scriptlet value 46 | : parsedBoost; 47 | if (boostMultiplier < MIN_MULTIPLIER) { 48 | boostMultiplier = MIN_MULTIPLIER; 49 | } 50 | if (boostMultiplier > MAX_MULTIPLIER) { 51 | boostMultiplier = MAX_MULTIPLIER; 52 | } 53 | return boostMultiplier; 54 | }; 55 | -------------------------------------------------------------------------------- /src/helpers/array-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Some browsers do not support Array.prototype.flat() 3 | * for example, Opera 42 which is used for browserstack tests 4 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat 5 | * 6 | * @param input arbitrary array 7 | * @returns flattened array 8 | */ 9 | export const flatten = (input: Array): T[] => { 10 | const stack: Array = []; 11 | input.forEach((el) => stack.push(el)); 12 | const res: T[] = []; 13 | while (stack.length) { 14 | // pop value from stack 15 | const next = stack.pop(); 16 | if (Array.isArray(next)) { 17 | // push back array items, won't modify the original input 18 | next.forEach((el) => stack.push(el)); 19 | } else { 20 | res.push(next as T); 21 | } 22 | } 23 | // reverse to restore input order 24 | return res.reverse(); 25 | }; 26 | 27 | /** 28 | * Predicate method to check if the array item exists 29 | * 30 | * @param item arbitrary 31 | * @returns if item is truthy or not 32 | */ 33 | export const isExisting = (item: unknown): boolean => !!item; 34 | 35 | /** 36 | * Converts NodeList to array 37 | * 38 | * @param {NodeList} nodeList arbitrary NodeList 39 | * @returns {Node[Array]} array of nodes 40 | */ 41 | export const nodeListToArray = (nodeList: NodeList): Node[] => { 42 | const nodes = []; 43 | for (let i = 0; i < nodeList.length; i += 1) { 44 | nodes.push(nodeList[i]); 45 | } 46 | return nodes; 47 | }; 48 | -------------------------------------------------------------------------------- /src/helpers/create-on-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { randomId } from './random-id'; 2 | 3 | /** 4 | * Generates function which silents global errors on page generated by scriptlet 5 | * If error doesn't belong to our error we transfer it to the native onError handler 6 | * 7 | * @param rid - unique identifier of scriptlet 8 | * @returns window.onerror handler 9 | */ 10 | export function createOnErrorHandler(rid: string): OnErrorEventHandlerNonNull { 11 | // eslint-disable-next-line consistent-return 12 | const nativeOnError = window.onerror; 13 | return function onError(error, ...args) { 14 | if (typeof error === 'string' && error.includes(rid)) { 15 | return true; 16 | } 17 | if (nativeOnError instanceof Function) { 18 | return nativeOnError.apply(window, [error, ...args]); 19 | } 20 | return false; 21 | }; 22 | } 23 | 24 | /** 25 | * Silently aborts currently running script 26 | * TODO use this for other abort scriptlets 27 | * 28 | * @returns abort function 29 | */ 30 | export function getAbortFunc() { 31 | const rid = randomId(); 32 | let isErrorHandlerSet = false; 33 | return function abort() { 34 | if (!isErrorHandlerSet) { 35 | window.onerror = createOnErrorHandler(rid); 36 | isErrorHandlerSet = true; 37 | } 38 | throw new ReferenceError(rid); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/helpers/get-descriptor-addon.ts: -------------------------------------------------------------------------------- 1 | import { randomId } from './random-id'; 2 | import { type ArbitraryFunction } from '../../types/types'; 3 | 4 | interface DescriptorAddon { 5 | isAbortingSuspended: boolean; 6 | isolateCallback: (cb: ArbitraryFunction, ...args: unknown[]) => void; 7 | } 8 | 9 | /** 10 | * Prevents infinite loops when trapping props that could be used by scriptlet's own helpers 11 | * Example: window.RegExp, that is used by matchStackTrace > toRegExp 12 | * 13 | * https://github.com/AdguardTeam/Scriptlets/issues/251 14 | * https://github.com/AdguardTeam/Scriptlets/issues/226 15 | * https://github.com/AdguardTeam/Scriptlets/issues/232 16 | * 17 | * @returns descriptor addon 18 | */ 19 | export function getDescriptorAddon(): DescriptorAddon { 20 | return { 21 | isAbortingSuspended: false, 22 | isolateCallback(cb, ...args) { 23 | this.isAbortingSuspended = true; 24 | // try...catch is required in case there are more than one inline scripts 25 | // which should be aborted, 26 | // so after the first successful abortion, `cb(...args);` will throw error, 27 | // and we should not stop on that and continue to abort other scripts 28 | try { 29 | const result = cb(...args); 30 | this.isAbortingSuspended = false; 31 | return result; 32 | } catch { 33 | const rid = randomId(); 34 | this.isAbortingSuspended = false; 35 | // It's necessary to throw error 36 | // otherwise script will be not aborted 37 | throw new ReferenceError(rid); 38 | } 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/get-error-message.ts: -------------------------------------------------------------------------------- 1 | type ErrorWithMessage = { 2 | message: string; 3 | }; 4 | 5 | /** 6 | * Converts error object to error with message. This method might be helpful to handle thrown errors. 7 | * 8 | * @param error Error object. 9 | * 10 | * @returns Message of the error. 11 | */ 12 | export const getErrorMessage = (error: unknown): string => { 13 | /** 14 | * Checks if error has message. 15 | * 16 | * @param e Error object. 17 | * 18 | * @returns True if error has message, false otherwise. 19 | */ 20 | const isErrorWithMessage = (e: unknown): e is ErrorWithMessage => ( 21 | typeof e === 'object' 22 | && e !== null 23 | && 'message' in e 24 | && typeof (e as Record).message === 'string' 25 | ); 26 | 27 | if (isErrorWithMessage(error)) { 28 | return error.message; 29 | } 30 | 31 | try { 32 | return (new Error(JSON.stringify(error))).message; 33 | } catch { 34 | // fallback in case there's an error stringifying the error 35 | // like with circular references for example. 36 | return (new Error(String(error))).message; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/helpers/get-property-in-chain.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyObject } from './object-utils'; 2 | import { type ChainBase, type ChainInfo } from '../../types/types'; 3 | 4 | /** 5 | * Check if the property exists in the base object (recursively) 6 | * 7 | * If property doesn't exist in base object, 8 | * defines this property as 'undefined' 9 | * and returns base, property name and remaining part of property chain 10 | * 11 | * @param base object that owns chain 12 | * @param chain chain of owner properties 13 | * @returns chain info object 14 | */ 15 | export function getPropertyInChain(base: ChainBase, chain: string): ChainInfo { 16 | const pos = chain.indexOf('.'); 17 | if (pos === -1) { 18 | return { base, prop: chain }; 19 | } 20 | const prop = chain.slice(0, pos); 21 | 22 | // https://github.com/AdguardTeam/Scriptlets/issues/128 23 | if (base === null) { 24 | // if base is null, return 'null' as base. 25 | // it's needed for triggering the reason logging while debugging 26 | return { base, prop, chain }; 27 | } 28 | 29 | const nextBase = base[prop]; 30 | chain = chain.slice(pos + 1); 31 | 32 | if ((base instanceof Object || typeof base === 'object') && isEmptyObject(base)) { 33 | // for empty objects in chain 34 | return { base, prop, chain }; 35 | } 36 | 37 | if (nextBase === null) { 38 | return { base, prop, chain }; 39 | } 40 | 41 | if (nextBase !== undefined) { 42 | return getPropertyInChain(nextBase, chain); 43 | } 44 | 45 | Object.defineProperty(base, prop, { configurable: true }); 46 | return { base, prop, chain }; 47 | } 48 | -------------------------------------------------------------------------------- /src/helpers/hit.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-underscore-dangle */ 2 | 3 | import { type ArbitraryFunction } from '../../types/types'; 4 | import { type Source } from '../scriptlets'; 5 | 6 | declare global { 7 | interface Window { 8 | __debug?: ArbitraryFunction; 9 | } 10 | } 11 | 12 | /** 13 | * Hit used only for debug purposes now 14 | * 15 | * @param source scriptlet properties 16 | * use LOG_MARKER = 'log: ' at the start of a message 17 | * for logging scriptlets 18 | */ 19 | export const hit = (source: Source) => { 20 | const ADGUARD_PREFIX = '[AdGuard]'; 21 | if (!source.verbose) { 22 | return; 23 | } 24 | 25 | try { 26 | const trace = console.trace.bind(console); 27 | 28 | let label = `${ADGUARD_PREFIX} `; 29 | if (source.engine === 'corelibs') { 30 | // rule text will be available for corelibs 31 | label += source.ruleText; 32 | } else { 33 | if (source.domainName) { 34 | label += `${source.domainName}`; 35 | } 36 | if (source.args) { 37 | label += `#%#//scriptlet('${source.name}', '${source.args.join("', '")}')`; 38 | } else { 39 | label += `#%#//scriptlet('${source.name}')`; 40 | } 41 | } 42 | 43 | if (trace) { 44 | trace(label); 45 | } 46 | } catch (e) { 47 | // try catch for Edge 15 48 | // In according to this issue https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14495220/ 49 | // console.log throws an error 50 | } 51 | 52 | // This is necessary for unit-tests only! 53 | if (typeof window.__debug === 'function') { 54 | window.__debug(source); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file must export all used dependencies 3 | */ 4 | 5 | export * from './add-event-listener-utils'; 6 | export * from './adjust-set-utils'; 7 | export * from './array-utils'; 8 | export * from './attribute-utils'; 9 | export * from './cookie-utils'; 10 | export * from './noop-utils'; 11 | export * from './number-utils'; 12 | export * from './object-utils'; 13 | export * from './script-source-utils'; 14 | export * from './open-shadow-dom-utils'; 15 | export * from './prevent-utils'; 16 | export * from './prevent-window-open-utils'; 17 | export * from './prune-utils'; 18 | export * from './regexp-utils'; 19 | export * from './response-utils'; 20 | export * from './request-utils'; 21 | export * from './storage-utils'; 22 | export * from './string-utils'; 23 | 24 | export * from './log-message'; 25 | export * from './create-on-error-handler'; 26 | export * from './get-descriptor-addon'; 27 | export * from './get-error-message'; 28 | export * from './get-property-in-chain'; 29 | export * from './get-wildcard-property-in-chain'; 30 | export * from './hit'; 31 | export * from './match-request-props'; 32 | export * from './match-stack'; 33 | export * from './observer'; 34 | export * from './parse-flags'; 35 | export * from './parse-keyword-value'; 36 | export * from './random-id'; 37 | export * from './throttle'; 38 | export * from './shadow-dom-utils'; 39 | export * from './node-text-utils'; 40 | export * from './trusted-types-utils'; 41 | export * from './value-matchers'; 42 | -------------------------------------------------------------------------------- /src/helpers/log-message.ts: -------------------------------------------------------------------------------- 1 | import { type Source } from '../scriptlets'; 2 | 3 | /** 4 | * Conditionally logs message to console. 5 | * Convention is to log messages by source.verbose if such log 6 | * is not a part of scriptlet's functionality, eg on invalid input, 7 | * and use 'forced' argument otherwise. 8 | * 9 | * @param source required, scriptlet properties 10 | * @param message required, message to log 11 | * @param forced to log message unconditionally 12 | * @param convertMessageToString to convert message to string 13 | */ 14 | export const logMessage = ( 15 | source: Source, 16 | message: unknown, 17 | forced = false, 18 | convertMessageToString = true, 19 | ) => { 20 | const { 21 | name, 22 | verbose, 23 | } = source; 24 | 25 | if (!forced && !verbose) { 26 | return; 27 | } 28 | 29 | // eslint-disable-next-line no-console 30 | const nativeConsole = console.log; 31 | 32 | if (!convertMessageToString) { 33 | // Template literals convert object to string, 34 | // so 'message' should not be passed to template literals 35 | // as it will not be logged correctly 36 | nativeConsole(`${name}:`, message); 37 | return; 38 | } 39 | 40 | nativeConsole(`${name}: ${message}`); 41 | }; 42 | -------------------------------------------------------------------------------- /src/helpers/match-request-props.ts: -------------------------------------------------------------------------------- 1 | import { getMatchPropsData, isValidParsedData, parseMatchProps } from './request-utils'; 2 | import type { LegalRequestProp, MatchPropsData } from './request-utils'; 3 | import { logMessage } from './log-message'; 4 | import { type Source } from '../scriptlets'; 5 | 6 | /** 7 | * Checks if given propsToMatch string matches with given request data 8 | * This is used by prevent-xhr, prevent-fetch, trusted-replace-xhr-response 9 | * and trusted-replace-fetch-response scriptlets 10 | * 11 | * @param source scriptlet properties 12 | * @param propsToMatch string of space-separated request properties to match 13 | * @param requestData object with standard properties of fetch/xhr like url, method etc 14 | * @returns if request properties match 15 | */ 16 | export const matchRequestProps = ( 17 | source: Source, 18 | propsToMatch: string, 19 | requestData: MatchPropsData, 20 | ): boolean => { 21 | if (propsToMatch === '' || propsToMatch === '*') { 22 | return true; 23 | } 24 | 25 | let isMatched: boolean; 26 | 27 | const parsedData = parseMatchProps(propsToMatch); 28 | if (!isValidParsedData(parsedData)) { 29 | logMessage(source, `Invalid parameter: ${propsToMatch}`); 30 | isMatched = false; 31 | } else { 32 | const matchData = getMatchPropsData(parsedData); 33 | const matchKeys = Object.keys(matchData) as LegalRequestProp[]; 34 | // prevent only if all props match 35 | isMatched = matchKeys.every((matchKey) => { 36 | const matchValue = matchData[matchKey]; 37 | const dataValue = requestData[matchKey]; 38 | return Object.prototype.hasOwnProperty.call(requestData, matchKey) 39 | && typeof dataValue === 'string' 40 | && matchValue?.test(dataValue); 41 | }); 42 | } 43 | 44 | return isMatched; 45 | }; 46 | -------------------------------------------------------------------------------- /src/helpers/match-stack.ts: -------------------------------------------------------------------------------- 1 | import { toRegExp } from './string-utils'; 2 | import { shouldAbortInlineOrInjectedScript } from './script-source-utils'; 3 | import { getNativeRegexpTest, backupRegExpValues, restoreRegExpValues } from './regexp-utils'; 4 | 5 | /** 6 | * Checks if the stackTrace contains stackRegexp 7 | * https://github.com/AdguardTeam/Scriptlets/issues/82 8 | * 9 | * @param stackMatch - input stack value to match 10 | * @param stackTrace - script error stack trace 11 | * @returns if the stackTrace contains stackRegexp 12 | */ 13 | export const matchStackTrace = (stackMatch: string | undefined, stackTrace: string): boolean => { 14 | if (!stackMatch || stackMatch === '') { 15 | return true; 16 | } 17 | 18 | const regExpValues = backupRegExpValues(); 19 | 20 | if (shouldAbortInlineOrInjectedScript(stackMatch, stackTrace)) { 21 | if (regExpValues.length && regExpValues[0] !== RegExp.$1) { 22 | restoreRegExpValues(regExpValues); 23 | } 24 | return true; 25 | } 26 | 27 | const stackRegexp = toRegExp(stackMatch); 28 | const refinedStackTrace = stackTrace 29 | .split('\n') 30 | .slice(2) // get rid of our own functions in the stack trace 31 | .map((line) => line.trim()) // trim the lines 32 | .join('\n'); 33 | 34 | if (regExpValues.length && regExpValues[0] !== RegExp.$1) { 35 | restoreRegExpValues(regExpValues); 36 | } 37 | return getNativeRegexpTest().call(stackRegexp, refinedStackTrace); 38 | }; 39 | -------------------------------------------------------------------------------- /src/helpers/number-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines whether the passed value is NaN 3 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN 4 | * 5 | * @param num arbitrary value 6 | * @returns if provided value is NaN 7 | */ 8 | export const nativeIsNaN = (num: unknown): boolean => { 9 | // eslint-disable-next-line no-restricted-properties 10 | const native = Number.isNaN || window.isNaN; 11 | return native(num); 12 | }; 13 | /** 14 | * Determines whether the passed value is a finite number 15 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite 16 | * 17 | * @param num arbitrary value 18 | * @returns if provided value is finite 19 | */ 20 | export const nativeIsFinite = (num: unknown): num is number => { 21 | // eslint-disable-next-line no-restricted-properties 22 | const native = Number.isFinite || window.isFinite; 23 | return native(num); 24 | }; 25 | 26 | /** 27 | * Parses string for a number, if possible, otherwise returns null. 28 | * 29 | * @param rawString arbitrary string 30 | * @returns number or null if string not parsable 31 | */ 32 | export const getNumberFromString = (rawString: string): number | null => { 33 | const parsedDelay = parseInt(rawString, 10); 34 | const validDelay = nativeIsNaN(parsedDelay) 35 | ? null 36 | : parsedDelay; 37 | return validDelay; 38 | }; 39 | 40 | /** 41 | * Generate a random integer between two values, inclusive 42 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive 43 | * 44 | * @param min range minimum 45 | * @param max range maximum 46 | * @returns random number 47 | */ 48 | export function getRandomIntInclusive(min: number, max: number): number { 49 | min = Math.ceil(min); 50 | max = Math.floor(max); 51 | return Math.floor(Math.random() * (max - min + 1) + min); 52 | } 53 | -------------------------------------------------------------------------------- /src/helpers/object-utils.ts: -------------------------------------------------------------------------------- 1 | import { type ArbitraryObject } from '../../types/types'; 2 | 3 | /** 4 | * Checks whether the obj is an empty object 5 | * 6 | * @param obj arbitrary object 7 | * @returns if object is empty 8 | */ 9 | export const isEmptyObject = (obj: Record): boolean => { 10 | return Object.keys(obj).length === 0 && !obj.prototype; 11 | }; 12 | 13 | /** 14 | * Safely retrieve property descriptor 15 | * 16 | * @param obj target object 17 | * @param prop target property 18 | * @returns descriptor or null if it's not available or non-configurable 19 | */ 20 | export const safeGetDescriptor = (obj: PropertyDescriptorMap, prop: string): PropertyDescriptor | null => { 21 | const descriptor = Object.getOwnPropertyDescriptor(obj, prop); 22 | if (descriptor && descriptor.configurable) { 23 | return descriptor; 24 | } 25 | return null; 26 | }; 27 | 28 | /** 29 | * Set getter and setter to property if it's configurable 30 | * 31 | * @param object target object with property 32 | * @param property property name 33 | * @param descriptor contains getter and setter functions 34 | * @returns is operation successful 35 | */ 36 | export function setPropertyAccess( 37 | object: ArbitraryObject, 38 | property: string, 39 | descriptor: PropertyDescriptor, 40 | ): boolean { 41 | const currentDescriptor = Object.getOwnPropertyDescriptor(object, property); 42 | if (currentDescriptor && !currentDescriptor.configurable) { 43 | return false; 44 | } 45 | Object.defineProperty(object, property, descriptor); 46 | return true; 47 | } 48 | 49 | /** 50 | * Checks whether the value is an arbitrary object 51 | * 52 | * @param value arbitrary value 53 | * @returns true, if value is an arbitrary object 54 | */ 55 | export function isArbitraryObject(value: unknown): value is ArbitraryObject { 56 | return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof RegExp); 57 | } 58 | -------------------------------------------------------------------------------- /src/helpers/parse-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface representing the structure of flags data. 3 | */ 4 | export interface FlagsData { 5 | /** 6 | * Represents the 'asap' flag, indicating immediate action. 7 | */ 8 | ASAP: string; 9 | 10 | /** 11 | * Represents the 'complete' flag, indicating action upon completion. 12 | */ 13 | COMPLETE: string; 14 | 15 | /** 16 | * Represents the 'stay' flag, indicating persistent action. 17 | */ 18 | STAY: string; 19 | 20 | /** 21 | * Checks if a specific flag is present. 22 | * @param flag The flag to check for. 23 | * @returns True if the flag is present, otherwise false. 24 | */ 25 | hasFlag(flag: string): boolean; 26 | } 27 | 28 | /** 29 | * Behaviour flags string parser 30 | * 31 | * @param flags required, 'applying' argument string 32 | * @returns object with parsed flags 33 | */ 34 | export const parseFlags = (flags: string): FlagsData => { 35 | // !IMPORTANT: Do not move constants outside of this function 36 | const FLAGS_DIVIDER = ' '; 37 | const ASAP_FLAG = 'asap'; 38 | const COMPLETE_FLAG = 'complete'; 39 | const STAY_FLAG = 'stay'; 40 | const VALID_FLAGS = new Set([ASAP_FLAG, COMPLETE_FLAG, STAY_FLAG]); 41 | 42 | const passedFlags = new Set( 43 | flags 44 | .trim() 45 | .split(FLAGS_DIVIDER) 46 | .filter((flag) => VALID_FLAGS.has(flag)), 47 | ); 48 | 49 | return { 50 | ASAP: ASAP_FLAG, 51 | COMPLETE: COMPLETE_FLAG, 52 | STAY: STAY_FLAG, 53 | hasFlag: (flag: string): boolean => passedFlags.has(flag), 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/helpers/parse-keyword-value.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modifies passed keyword value according to its purpose. 3 | * Returns initial value if it's not a keyword. 4 | * 5 | * Supported keywords: 6 | * - '$now$' - returns current time in ms, e.g 1667915146503 7 | * - '$currentDate$' - returns current date e.g 'Tue Nov 08 2022 13:53:19 GMT+0300' 8 | * 9 | * @param rawValue keyword 10 | * @returns parsed value 11 | */ 12 | export const parseKeywordValue = (rawValue: string): string => { 13 | // !IMPORTANT: Do not move constants outside of this function 14 | const NOW_VALUE_KEYWORD = '$now$'; 15 | const CURRENT_DATE_KEYWORD = '$currentDate$'; 16 | const CURRENT_ISO_DATE_KEYWORD = '$currentISODate$'; 17 | 18 | let parsedValue = rawValue; 19 | 20 | if (rawValue === NOW_VALUE_KEYWORD) { 21 | // Set to current time in ms, e.g 1667915146503 22 | parsedValue = Date.now().toString(); 23 | } else if (rawValue === CURRENT_DATE_KEYWORD) { 24 | // Set to current date e.g 'Tue Nov 08 2022 13:53:19 GMT+0300' 25 | parsedValue = Date(); 26 | } else if (rawValue === CURRENT_ISO_DATE_KEYWORD) { 27 | // Set to current date e.g '2022-11-08T13:53:19.650Z' 28 | parsedValue = new Date().toISOString(); 29 | } 30 | 31 | return parsedValue; 32 | }; 33 | -------------------------------------------------------------------------------- /src/helpers/random-id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate random seven symbols id 3 | * 4 | * @returns randomized id 5 | */ 6 | export function randomId(): string { 7 | return Math.random().toString(36).slice(2, 9); 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/regexp-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the native `RegExp.prototype.test` method if it exists. 3 | * 4 | * @returns The native `RegExp.prototype.test` method. 5 | * @throws If `RegExp.prototype.test` is not a function. 6 | */ 7 | export const getNativeRegexpTest = (): (string: string) => boolean => { 8 | const descriptor = Object.getOwnPropertyDescriptor(RegExp.prototype, 'test'); 9 | const nativeRegexTest = descriptor?.value; 10 | if (descriptor && typeof descriptor.value === 'function') { 11 | return nativeRegexTest; 12 | } 13 | throw new Error('RegExp.prototype.test is not a function'); 14 | }; 15 | 16 | /** 17 | * Retrieves the values of the global RegExp.$1, …, RegExp.$9 properties 18 | * The problem is that RegExp.$1 is modified by scriptlet and according 19 | * to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/n#description 20 | * the values of $1, …, $9 update whenever a RegExp instance makes a successful match 21 | * so we need to save these values and then change them back. 22 | * Related issue - https://github.com/AdguardTeam/Scriptlets/issues/384 23 | * 24 | * @returns {Array} An array containing the values of the RegExp.$1, …, RegExp.$9 properties. 25 | */ 26 | export const backupRegExpValues = (): Array => { 27 | try { 28 | const arrayOfRegexpValues = []; 29 | for (let index = 1; index < 10; index += 1) { 30 | const value = `$${index}`; 31 | if (!(RegExp as any)[value]) { 32 | break; 33 | } 34 | arrayOfRegexpValues.push((RegExp as any)[value]); 35 | } 36 | return arrayOfRegexpValues; 37 | } catch (error) { 38 | return []; 39 | } 40 | }; 41 | 42 | /** 43 | * Sets previous values of the RegExp.$1, …, RegExp.$9 properties. 44 | * 45 | * @param {Array} array 46 | * @returns {void} 47 | */ 48 | export const restoreRegExpValues = (array: Array): void => { 49 | if (!array.length) { 50 | return; 51 | } 52 | try { 53 | let stringPattern = ''; 54 | if (array.length === 1) { 55 | stringPattern = `(${array[0]})`; 56 | } else { 57 | // Create a string pattern with a capturing group from passed array, 58 | // e.g. ['foo', 'bar', 'baz'] will create '(foo),(bar),(baz)' string 59 | stringPattern = array.reduce((accumulator, currentValue, currentIndex) => { 60 | if (currentIndex === 1) { 61 | return `(${accumulator}),(${currentValue})`; 62 | } 63 | return `${accumulator},(${currentValue})`; 64 | }); 65 | } 66 | const regExpGroup = new RegExp(stringPattern); 67 | array.toString().replace(regExpGroup, ''); 68 | } catch (error) { 69 | const message = `Failed to restore RegExp values: ${error}`; 70 | // eslint-disable-next-line no-console 71 | console.log(message); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/helpers/response-utils.ts: -------------------------------------------------------------------------------- 1 | type ReplacementData = { 2 | body: string; 3 | type?: string; 4 | }; 5 | 6 | /** 7 | * Modifies original response with the given replacement data. 8 | * 9 | * @param origResponse Original response. 10 | * @param replacement Replacement data for response with possible keys: 11 | * - `body`: optional, string, default to '{}'; 12 | * - `type`: optional, string, original response type is used if not specified. 13 | * 14 | * @returns Modified response. 15 | */ 16 | export const modifyResponse = ( 17 | origResponse: Response, 18 | replacement: ReplacementData = { 19 | body: '{}', 20 | }, 21 | ): Response => { 22 | const headers: HeadersInit = {}; 23 | origResponse?.headers?.forEach((value, key) => { 24 | headers[key] = value; 25 | }); 26 | 27 | const modifiedResponse = new Response(replacement.body, { 28 | status: origResponse.status, 29 | statusText: origResponse.statusText, 30 | headers, 31 | }); 32 | 33 | // Mock response url and type to avoid adblocker detection 34 | // https://github.com/AdguardTeam/Scriptlets/issues/216 35 | Object.defineProperties(modifiedResponse, { 36 | url: { 37 | value: origResponse.url, 38 | }, 39 | type: { 40 | value: replacement.type || origResponse.type, 41 | }, 42 | }); 43 | 44 | return modifiedResponse; 45 | }; 46 | 47 | /** 48 | * Create new Response object using original response' properties 49 | * and given text as body content 50 | * 51 | * @param response original response to copy properties from 52 | * @param textContent text to set as body content 53 | */ 54 | export const forgeResponse = (response: Response, textContent: string): Response => { 55 | const { 56 | bodyUsed, 57 | headers, 58 | ok, 59 | redirected, 60 | status, 61 | statusText, 62 | type, 63 | url, 64 | } = response; 65 | 66 | const forgedResponse = new Response(textContent, { 67 | status, 68 | statusText, 69 | headers, 70 | }); 71 | 72 | // Manually set properties which can't be set by Response constructor 73 | Object.defineProperties(forgedResponse, { 74 | url: { value: url }, 75 | type: { value: type }, 76 | ok: { value: ok }, 77 | bodyUsed: { value: bodyUsed }, 78 | redirected: { value: redirected }, 79 | }); 80 | 81 | return forgedResponse; 82 | }; 83 | -------------------------------------------------------------------------------- /src/helpers/rule-helpers.ts: -------------------------------------------------------------------------------- 1 | import { type AnyRule } from '@adguard/agtree'; 2 | import { RuleParser, defaultParserOptions } from '@adguard/agtree/parser'; 3 | import { RuleGenerator } from '@adguard/agtree/generator'; 4 | 5 | /** 6 | * Get rule node from string or rule node 7 | * 8 | * @param rule Rule in string or rule node format 9 | * @returns Rule node 10 | * 11 | * @throws If the rule is in string format and cannot be parsed 12 | */ 13 | export const getRuleNode = (rule: string | AnyRule): AnyRule => { 14 | return typeof rule === 'string' 15 | // Note: AGTree does not support legacy script:inject syntax 16 | ? RuleParser.parse( 17 | rule, 18 | Object.assign( 19 | {}, 20 | defaultParserOptions, 21 | { includeRaws: false, isLocIncluded: false }, 22 | ), 23 | ) 24 | : rule; 25 | }; 26 | 27 | /** 28 | * Get rule text from string or rule node 29 | * 30 | * @param rule Rule in string or rule node format 31 | * @returns Rule text 32 | */ 33 | export const getRuleText = (rule: string | AnyRule): string => { 34 | return typeof rule === 'string' 35 | ? rule 36 | : RuleGenerator.generate(rule); 37 | }; 38 | -------------------------------------------------------------------------------- /src/helpers/shadow-dom-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes arbitrary operations on shadow root element, 3 | * to be passed as callback to hijackAttachShadow 4 | */ 5 | type AttachShadowCallback = (shadowRoot: ShadowRoot) => void; 6 | 7 | type AttachShadow = (init: ShadowRootInit) => ShadowRoot; 8 | 9 | /** 10 | * Overrides attachShadow method of Element API on a given context 11 | * to pass retrieved shadowRoots to callback 12 | * 13 | * @param context e.g global window object or contentWindow of an iframe 14 | * @param hostSelector selector to determine if callback should be called on current shadow subtree 15 | * @param callback callback to call on shadow root 16 | */ 17 | export const hijackAttachShadow = ( 18 | context: typeof globalThis, 19 | hostSelector: string, 20 | callback: AttachShadowCallback, 21 | ): void => { 22 | const handlerWrapper = (target: AttachShadow, thisArg: Element, args: unknown[]): ShadowRoot => { 23 | const shadowRoot: ShadowRoot = Reflect.apply(target, thisArg, args); 24 | 25 | if (thisArg && thisArg.matches(hostSelector || '*')) { 26 | callback(shadowRoot); 27 | } 28 | 29 | return shadowRoot; 30 | }; 31 | 32 | const attachShadowHandler: ProxyHandler = { 33 | apply: handlerWrapper, 34 | }; 35 | 36 | context.Element.prototype.attachShadow = new Proxy( 37 | context.Element.prototype.attachShadow, 38 | attachShadowHandler, 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/helpers/throttle.ts: -------------------------------------------------------------------------------- 1 | import { type ArbitraryFunction } from '../../types/types'; 2 | 3 | /** 4 | * Returns a wrapper, passing the call to 'method' at maximum once per 'delay' milliseconds. 5 | * Those calls that fall into the "cooldown" period, are ignored 6 | * 7 | * @param cb callback 8 | * @param delay - milliseconds 9 | * @returns throttled callback 10 | */ 11 | export const throttle = ( 12 | cb: ArbitraryFunction, 13 | delay: number, 14 | ): (...params: unknown[]) => void => { 15 | let wait = false; 16 | let savedArgs: unknown[] | null; 17 | const wrapper = (...args: unknown[]) => { 18 | if (wait) { 19 | savedArgs = args; 20 | return; 21 | } 22 | 23 | cb(...args); 24 | wait = true; 25 | 26 | setTimeout(() => { 27 | wait = false; 28 | if (savedArgs) { 29 | // "savedArgs" might contains few arguments, so it's necessary to use spread operator 30 | // https://github.com/AdguardTeam/Scriptlets/issues/284#issuecomment-1419464354 31 | wrapper(...savedArgs); 32 | savedArgs = null; 33 | } 34 | }, delay); 35 | }; 36 | return wrapper; 37 | }; 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json'; 2 | 3 | export { scriptlets, type Source } from './scriptlets'; 4 | 5 | const SCRIPTLETS_VERSION = version; 6 | export { SCRIPTLETS_VERSION }; 7 | -------------------------------------------------------------------------------- /src/redirects/amazon-apstag.js: -------------------------------------------------------------------------------- 1 | import { hit, noopFunc } from '../helpers'; 2 | 3 | /** 4 | * @redirect amazon-apstag 5 | * 6 | * @description 7 | * Mocks Amazon's apstag.js 8 | * 9 | * Related UBO redirect resource: 10 | * https://github.com/gorhill/uBlock/blob/master/src/web_accessible_resources/amazon_apstag.js 11 | * 12 | * ### Examples 13 | * 14 | * ```adblock 15 | * ||amazon-adsystem.com/aax2/apstag.js$script,redirect=amazon-apstag 16 | * ``` 17 | * 18 | * @added v1.2.3. 19 | */ 20 | export function AmazonApstag(source) { 21 | const apstagWrapper = { 22 | fetchBids(a, b) { 23 | if (typeof b === 'function') { 24 | b([]); 25 | } 26 | }, 27 | init: noopFunc, 28 | setDisplayBids: noopFunc, 29 | targetingKeys: noopFunc, 30 | }; 31 | 32 | window.apstag = apstagWrapper; 33 | 34 | hit(source); 35 | } 36 | 37 | export const AmazonApstagNames = [ 38 | 'amazon-apstag', 39 | 'ubo-amazon_apstag.js', 40 | 'amazon_apstag.js', 41 | ]; 42 | 43 | // eslint-disable-next-line prefer-destructuring 44 | AmazonApstag.primaryName = AmazonApstagNames[0]; 45 | 46 | AmazonApstag.injections = [hit, noopFunc]; 47 | -------------------------------------------------------------------------------- /src/redirects/blocking-redirects.yml: -------------------------------------------------------------------------------- 1 | # used for new type of redirects, like click2load.html 2 | # 3 | # use "|-" in order to keep indentation 4 | # use ">" if property contains long string 5 | 6 | - title: click2load.html 7 | added: v1.5.0 8 | description: |- 9 | Redirects resource and replaces supposed content by decoy frame with button for original content recovering 10 | 11 | Related UBO redirect resource: 12 | https://github.com/gorhill/uBlock/blob/1.31.0/src/web_accessible_resources/click2load.html 13 | 14 | ### Example 15 | 16 | ```adblock 17 | ||youtube.com/embed/$frame,third-party,redirect=click2load.html 18 | ``` 19 | aliases: 20 | - click2load.html 21 | - ubo-click2load.html 22 | file: click2load.html 23 | contentType: text/html 24 | scriptPath: ./click2load.js 25 | # 'content' will be bundled later 26 | # 'isBlocking' and 'sha' will be added later as well 27 | -------------------------------------------------------------------------------- /src/redirects/fingerprintjs2.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { hit } from '../helpers'; 3 | 4 | /** 5 | * @redirect fingerprintjs2 6 | * 7 | * @description 8 | * Mocks FingerprintJS v2 9 | * https://github.com/fingerprintjs 10 | * 11 | * Related UBO redirect resource: 12 | * https://github.com/gorhill/uBlock/blob/master/src/web_accessible_resources/fingerprint2.js 13 | * 14 | * ### Examples 15 | * 16 | * ```adblock 17 | * ||example.com/modules/js/lib/fgp/fingerprint2.js$script,redirect=fingerprintjs2 18 | * ``` 19 | * 20 | * @added v1.5.0. 21 | */ 22 | export function Fingerprintjs2(source) { 23 | let browserId = ''; 24 | for (let i = 0; i < 8; i += 1) { 25 | browserId += (Math.random() * 0x10000 + 0x1000).toString(16).slice(-4); 26 | } 27 | 28 | const Fingerprint2 = function () { }; 29 | 30 | Fingerprint2.get = function (options, callback) { 31 | if (!callback) { 32 | callback = options; 33 | } 34 | setTimeout(() => { 35 | if (callback) { 36 | callback(browserId, []); 37 | } 38 | }, 1); 39 | }; 40 | 41 | Fingerprint2.prototype = { 42 | get: Fingerprint2.get, 43 | }; 44 | 45 | window.Fingerprint2 = Fingerprint2; 46 | 47 | hit(source); 48 | } 49 | 50 | export const Fingerprintjs2Names = [ 51 | 'fingerprintjs2', 52 | // redirect aliases are needed for conversion: 53 | // prefixed for us 54 | 'ubo-fingerprint2.js', 55 | // original ubo name 56 | 'fingerprint2.js', 57 | ]; 58 | 59 | // eslint-disable-next-line prefer-destructuring 60 | Fingerprintjs2.primaryName = Fingerprintjs2Names[0]; 61 | 62 | Fingerprintjs2.injections = [hit]; 63 | -------------------------------------------------------------------------------- /src/redirects/fingerprintjs3.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { hit, noopStr } from '../helpers'; 3 | 4 | /** 5 | * @redirect fingerprintjs3 6 | * 7 | * @description 8 | * Mocks FingerprintJS v3 9 | * https://github.com/fingerprintjs 10 | * 11 | * Related UBO redirect resource: 12 | * https://github.com/gorhill/uBlock/blob/master/src/web_accessible_resources/fingerprint3.js 13 | * 14 | * ### Examples 15 | * 16 | * ```adblock 17 | * ||example.com/js/ufe/isomorphic/thirdparty/fp.min.js$script,redirect=fingerprintjs3 18 | * ``` 19 | * 20 | * @added v1.6.2. 21 | */ 22 | export function Fingerprintjs3(source) { 23 | const visitorId = (() => { 24 | let id = ''; 25 | for (let i = 0; i < 8; i += 1) { 26 | id += (Math.random() * 0x10000 + 0x1000).toString(16).slice(-4); 27 | } 28 | return id; 29 | })(); 30 | 31 | const FingerprintJS = function () { }; 32 | FingerprintJS.prototype = { 33 | load() { 34 | return Promise.resolve(new FingerprintJS()); 35 | }, 36 | get() { 37 | return Promise.resolve({ 38 | visitorId, 39 | }); 40 | }, 41 | hashComponents: noopStr, 42 | }; 43 | 44 | window.FingerprintJS = new FingerprintJS(); 45 | 46 | hit(source); 47 | } 48 | 49 | export const Fingerprintjs3Names = [ 50 | 'fingerprintjs3', 51 | // redirect aliases are needed for conversion: 52 | // prefixed for us 53 | 'ubo-fingerprint3.js', 54 | // original ubo name 55 | 'fingerprint3.js', 56 | ]; 57 | 58 | // eslint-disable-next-line prefer-destructuring 59 | Fingerprintjs3.primaryName = Fingerprintjs3Names[0]; 60 | 61 | Fingerprintjs3.injections = [hit, noopStr]; 62 | -------------------------------------------------------------------------------- /src/redirects/gemius.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { hit, noopFunc } from '../helpers'; 3 | 4 | /** 5 | * @redirect gemius 6 | * 7 | * @description 8 | * Mocks Gemius Analytics. 9 | * https://flowplayer.com/developers/plugins/gemius 10 | * 11 | * ### Examples 12 | * 13 | * ```adblock 14 | * ||example.org/gplayer.js$script,redirect=gemius 15 | * ``` 16 | * 17 | * @added v1.5.0. 18 | */ 19 | export function Gemius(source) { 20 | const GemiusPlayer = function () {}; 21 | GemiusPlayer.prototype = { 22 | setVideoObject: noopFunc, 23 | newProgram: noopFunc, 24 | programEvent: noopFunc, 25 | newAd: noopFunc, 26 | adEvent: noopFunc, 27 | }; 28 | 29 | window.GemiusPlayer = GemiusPlayer; 30 | 31 | hit(source); 32 | } 33 | 34 | export const GemiusNames = [ 35 | 'gemius', 36 | ]; 37 | 38 | // eslint-disable-next-line prefer-destructuring 39 | Gemius.primaryName = GemiusNames[0]; 40 | 41 | Gemius.injections = [hit, noopFunc]; 42 | -------------------------------------------------------------------------------- /src/redirects/index.js: -------------------------------------------------------------------------------- 1 | import { Redirects } from './redirects'; 2 | // eslint-disable-next-line import/no-unresolved 3 | import { redirectsMap } from '../../tmp/redirects-map'; 4 | 5 | /** 6 | * For a given name or alias of redirect returns the corresponding filename 7 | * @param {string} name — name or alias of redirect 8 | * @returns {string} — Redirect's filename with extension 9 | */ 10 | const getRedirectFilename = (name) => { 11 | return redirectsMap[name]; 12 | }; 13 | 14 | export { 15 | Redirects, 16 | getRedirectFilename, 17 | }; 18 | -------------------------------------------------------------------------------- /src/redirects/matomo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { hit, noopFunc } from '../helpers'; 3 | 4 | /** 5 | * @redirect matomo 6 | * 7 | * @description 8 | * Mocks the piwik.js file of Matomo (formerly Piwik). 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * ||example.org/piwik.js$script,redirect=matomo 14 | * ``` 15 | * 16 | * @added v1.5.0. 17 | */ 18 | 19 | export function Matomo(source) { 20 | const Tracker = function () {}; 21 | Tracker.prototype.setDoNotTrack = noopFunc; 22 | Tracker.prototype.setDomains = noopFunc; 23 | Tracker.prototype.setCustomDimension = noopFunc; 24 | Tracker.prototype.trackPageView = noopFunc; 25 | const AsyncTracker = function () {}; 26 | AsyncTracker.prototype.addListener = noopFunc; 27 | 28 | const matomoWrapper = { 29 | getTracker: Tracker, 30 | getAsyncTracker: AsyncTracker, 31 | }; 32 | 33 | window.Piwik = matomoWrapper; 34 | 35 | hit(source); 36 | } 37 | 38 | export const MatomoNames = ['matomo']; 39 | 40 | // eslint-disable-next-line prefer-destructuring 41 | Matomo.primaryName = MatomoNames[0]; 42 | 43 | Matomo.injections = [hit, noopFunc]; 44 | -------------------------------------------------------------------------------- /src/redirects/metrika-yandex-watch.js: -------------------------------------------------------------------------------- 1 | import { hit, noopFunc, noopArray } from '../helpers'; 2 | 3 | /** 4 | * @redirect metrika-yandex-watch 5 | * 6 | * @description 7 | * Mocks the old Yandex Metrika API. 8 | * https://yandex.ru/support/metrica/objects/_method-reference.html 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * ||mc.yandex.ru/metrika/watch.js$script,redirect=metrika-yandex-watch 14 | * ``` 15 | * 16 | * @added v1.0.10. 17 | */ 18 | export function metrikaYandexWatch(source) { 19 | const cbName = 'yandex_metrika_callbacks'; 20 | 21 | /** 22 | * Gets callback and its context from options and call it in async way 23 | * 24 | * @param {object} options Yandex Metrika API options 25 | */ 26 | const asyncCallbackFromOptions = (options = {}) => { 27 | let { callback } = options; 28 | const { ctx } = options; 29 | if (typeof callback === 'function') { 30 | callback = ctx !== undefined ? callback.bind(ctx) : callback; 31 | setTimeout(() => callback()); 32 | } 33 | }; 34 | 35 | function Metrika() { } // constructor 36 | Metrika.counters = noopArray; 37 | // Methods without options 38 | Metrika.prototype.addFileExtension = noopFunc; 39 | Metrika.prototype.getClientID = noopFunc; 40 | Metrika.prototype.setUserID = noopFunc; 41 | Metrika.prototype.userParams = noopFunc; 42 | Metrika.prototype.params = noopFunc; 43 | Metrika.prototype.counters = noopArray; 44 | 45 | // Methods with options 46 | // The order of arguments should be kept in according to API 47 | Metrika.prototype.extLink = (url, options) => { 48 | asyncCallbackFromOptions(options); 49 | }; 50 | Metrika.prototype.file = (url, options) => { 51 | asyncCallbackFromOptions(options); 52 | }; 53 | Metrika.prototype.hit = (url, options) => { 54 | asyncCallbackFromOptions(options); 55 | }; 56 | Metrika.prototype.reachGoal = (target, params, cb, ctx) => { 57 | asyncCallbackFromOptions({ callback: cb, ctx }); 58 | }; 59 | Metrika.prototype.notBounce = asyncCallbackFromOptions; 60 | 61 | if (window.Ya) { 62 | window.Ya.Metrika = Metrika; 63 | } else { 64 | window.Ya = { Metrika }; 65 | } 66 | 67 | if (window[cbName] && Array.isArray(window[cbName])) { 68 | window[cbName].forEach((func) => { 69 | if (typeof func === 'function') { 70 | func(); 71 | } 72 | }); 73 | } 74 | 75 | hit(source); 76 | } 77 | 78 | export const metrikaYandexWatchNames = [ 79 | 'metrika-yandex-watch', 80 | ]; 81 | 82 | // eslint-disable-next-line prefer-destructuring 83 | metrikaYandexWatch.primaryName = metrikaYandexWatchNames[0]; 84 | 85 | metrikaYandexWatch.injections = [hit, noopFunc, noopArray]; 86 | -------------------------------------------------------------------------------- /src/redirects/naver-wcslog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { hit, noopFunc } from '../helpers'; 3 | 4 | /** 5 | * @redirect naver-wcslog 6 | * 7 | * @description 8 | * Mocks wcslog.js of Naver Analytics. 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * ||wcs.naver.net/wcslog.js$script,redirect=naver-wcslog 14 | * ``` 15 | * 16 | * @added v1.6.2. 17 | */ 18 | 19 | export function NaverWcslog(source) { 20 | window.wcs_add = {}; 21 | window.wcs_do = noopFunc; 22 | window.wcs = { 23 | inflow: noopFunc, 24 | }; 25 | 26 | hit(source); 27 | } 28 | 29 | export const NaverWcslogNames = ['naver-wcslog']; 30 | 31 | // eslint-disable-next-line prefer-destructuring 32 | NaverWcslog.primaryName = NaverWcslogNames[0]; 33 | 34 | NaverWcslog.injections = [hit, noopFunc]; 35 | -------------------------------------------------------------------------------- /src/redirects/noeval.js: -------------------------------------------------------------------------------- 1 | import { noeval, noevalNames } from '../scriptlets/noeval'; 2 | 3 | /** 4 | * @redirect noeval 5 | * 6 | * @description 7 | * Redirects request to the source which sets static properties to PopAds and popns objects. 8 | * 9 | * Prevents page to use eval. 10 | * Notifies about attempts in the console 11 | * 12 | * Mostly it is used as `scriptlet`. 13 | * See [scriptlet description](../wiki/about-scriptlets.md#noeval). 14 | * 15 | * Related UBO redirect resource: 16 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#noeval-silentjs- 17 | * 18 | * ### Examples 19 | * 20 | * ```adblock 21 | * ||example.org/index.js$script,redirect=noeval 22 | * ``` 23 | * 24 | * @added v1.0.4. 25 | */ 26 | export { noeval, noevalNames }; 27 | -------------------------------------------------------------------------------- /src/redirects/pardot-1.0.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { 3 | hit, 4 | noopFunc, 5 | noopStr, 6 | noopNull, 7 | } from '../helpers'; 8 | 9 | /** 10 | * @redirect pardot-1.0 11 | * 12 | * @description 13 | * Mocks the pd.js file of Salesforce. 14 | * https://pi.pardot.com/pd.js 15 | * https://developer.salesforce.com/docs/marketing/pardot/overview 16 | * 17 | * ### Examples 18 | * 19 | * ```adblock 20 | * ||pi.pardot.com/pd.js$script,redirect=pardot 21 | * ||pacedg.com.au/pd.js$redirect=pardot 22 | * ``` 23 | * 24 | * @added v1.6.55. 25 | */ 26 | 27 | export function Pardot(source) { 28 | window.piVersion = '1.0.2'; 29 | window.piScriptNum = 0; 30 | window.piScriptObj = []; 31 | 32 | window.checkNamespace = noopFunc; 33 | window.getPardotUrl = noopStr; 34 | window.piGetParameter = noopNull; 35 | window.piSetCookie = noopFunc; 36 | window.piGetCookie = noopStr; 37 | 38 | function piTracker() { 39 | window.pi = { 40 | tracker: { 41 | visitor_id: '', 42 | visitor_id_sign: '', 43 | pi_opt_in: '', 44 | campaign_id: '', 45 | }, 46 | }; 47 | 48 | window.piScriptNum += 1; 49 | } 50 | 51 | window.piResponse = noopFunc; 52 | window.piTracker = piTracker; 53 | piTracker(); 54 | 55 | hit(source); 56 | } 57 | 58 | export const PardotNames = ['pardot-1.0']; 59 | 60 | // eslint-disable-next-line prefer-destructuring 61 | Pardot.primaryName = PardotNames[0]; 62 | 63 | Pardot.injections = [ 64 | hit, 65 | noopFunc, 66 | noopStr, 67 | noopNull, 68 | ]; 69 | -------------------------------------------------------------------------------- /src/redirects/prebid-ads.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { hit } from '../helpers'; 3 | 4 | /** 5 | * @redirect prebid-ads 6 | * 7 | * @description 8 | * Sets predefined constants on a page: 9 | * 10 | * - `canRunAds`: `true` 11 | * - `isAdBlockActive`: `false` 12 | * 13 | * ### Examples 14 | * 15 | * ```adblock 16 | * ||example.org/assets/js/prebid-ads.js$script,redirect=prebid-ads 17 | * ``` 18 | * 19 | * @added v1.6.2. 20 | */ 21 | export function prebidAds(source) { 22 | window.canRunAds = true; 23 | window.isAdBlockActive = false; 24 | 25 | hit(source); 26 | } 27 | 28 | export const prebidAdsNames = [ 29 | 'prebid-ads', 30 | 'ubo-prebid-ads.js', 31 | 'prebid-ads.js', 32 | ]; 33 | 34 | // eslint-disable-next-line prefer-destructuring 35 | prebidAds.primaryName = prebidAdsNames[0]; 36 | 37 | prebidAds.injections = [hit]; 38 | -------------------------------------------------------------------------------- /src/redirects/prebid.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import { 3 | hit, 4 | noopFunc, 5 | noopStr, 6 | noopArray, 7 | } from '../helpers'; 8 | 9 | /** 10 | * @redirect prebid 11 | * 12 | * @description 13 | * Mocks the prebid.js header bidding suit. 14 | * https://docs.prebid.org/ 15 | * 16 | * ### Examples 17 | * 18 | * ```adblock 19 | * ||example.org/bd/hb/prebid.js$script,redirect=prebid 20 | * ``` 21 | * 22 | * @added v1.6.2. 23 | */ 24 | 25 | export function Prebid(source) { 26 | const pushFunction = function (arg) { 27 | if (typeof arg === 'function') { 28 | try { 29 | arg.call(); 30 | } catch (ex) { 31 | /* empty */ 32 | } 33 | } 34 | }; 35 | 36 | const pbjsWrapper = { 37 | addAdUnits() { }, 38 | adServers: { 39 | dfp: { 40 | // https://docs.prebid.org/dev-docs/publisher-api-reference/adServers.dfp.buildVideoUrl.html 41 | // returns ad URL 42 | buildVideoUrl: noopStr, 43 | }, 44 | }, 45 | adUnits: [], 46 | aliasBidder() { }, 47 | cmd: [], 48 | enableAnalytics() { }, 49 | getHighestCpmBids: noopArray, 50 | libLoaded: true, 51 | que: [], 52 | requestBids(arg) { 53 | if (arg instanceof Object && arg.bidsBackHandler) { 54 | try { 55 | arg.bidsBackHandler.call(); // https://docs.prebid.org/dev-docs/publisher-api-reference/requestBids.html 56 | } catch (ex) { 57 | /* empty */ 58 | } 59 | } 60 | }, 61 | removeAdUnit() { }, 62 | setBidderConfig() { }, 63 | setConfig() { }, 64 | setTargetingForGPTAsync() { }, 65 | }; 66 | pbjsWrapper.cmd.push = pushFunction; 67 | pbjsWrapper.que.push = pushFunction; 68 | 69 | window.pbjs = pbjsWrapper; 70 | 71 | hit(source); 72 | } 73 | 74 | export const PrebidNames = ['prebid']; 75 | 76 | // eslint-disable-next-line prefer-destructuring 77 | Prebid.primaryName = PrebidNames[0]; 78 | 79 | Prebid.injections = [hit, noopFunc, noopStr, noopArray]; 80 | -------------------------------------------------------------------------------- /src/redirects/prevent-bab.js: -------------------------------------------------------------------------------- 1 | import { preventBab as preventBabScriptlet } from '../scriptlets/prevent-bab'; 2 | 3 | /** 4 | * @redirect prevent-bab 5 | * 6 | * @description 7 | * Prevents BlockAdblock script from detecting an ad blocker. 8 | * 9 | * Mostly it is used as `scriptlet`. 10 | * See [scriptlet description](../wiki/about-scriptlets.md#prevent-bab). 11 | * 12 | * Related UBO redirect resource: 13 | * https://github.com/gorhill/uBlock/blob/master/src/web_accessible_resources/nobab.js 14 | * 15 | * ### Examples 16 | * 17 | * ```adblock 18 | * /blockadblock.$script,redirect=prevent-bab 19 | * ``` 20 | * 21 | * @added v1.3.19. 22 | */ 23 | const preventBab = preventBabScriptlet; 24 | 25 | export const preventBabNames = [ 26 | 'prevent-bab', 27 | // list of prevent-bab redirect aliases 28 | 'nobab.js', 29 | 'ubo-nobab.js', 30 | 'bab-defuser.js', 31 | 'ubo-bab-defuser.js', 32 | 'ubo-nobab', 33 | 'ubo-bab-defuser', 34 | ]; 35 | 36 | export { preventBab }; 37 | -------------------------------------------------------------------------------- /src/redirects/prevent-bab2.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return, no-eval */ 2 | import { hit } from '../helpers'; 3 | 4 | /** 5 | * @redirect prevent-bab2 6 | * 7 | * @description 8 | * Prevents BlockAdblock script from detecting an ad blocker. 9 | * 10 | * Related UBO redirect: 11 | * https://github.com/gorhill/uBlock/blob/master/src/web_accessible_resources/nobab2.js 12 | * 13 | * See [redirect description](../wiki/about-redirects.md#prevent-bab2). 14 | * 15 | * ### Examples 16 | * 17 | * ```adblock 18 | * /blockadblock.$script,redirect=prevent-bab2 19 | * ``` 20 | * 21 | * @added v1.5.0. 22 | */ 23 | export function preventBab2(source) { 24 | const script = document.currentScript; 25 | if (script === null) { 26 | return; 27 | } 28 | 29 | const url = script.src; 30 | if (typeof url !== 'string') { 31 | return; 32 | } 33 | 34 | const domainsStr = [ 35 | 'adclixx\\.net', 36 | 'adnetasia\\.com', 37 | 'adtrackers\\.net', 38 | 'bannertrack\\.net', 39 | ].join('|'); 40 | const matchStr = `^https?://[\\w-]+\\.(${domainsStr})/.`; 41 | const domainsRegex = new RegExp(matchStr); 42 | if (domainsRegex.test(url) === false) { 43 | return; 44 | } 45 | 46 | window.nH7eXzOsG = 858; 47 | hit(source); 48 | } 49 | 50 | export const preventBab2Names = [ 51 | 'prevent-bab2', 52 | // aliases are needed for matching the related scriptlet converted into our syntax 53 | 'nobab2.js', 54 | ]; 55 | 56 | // eslint-disable-next-line prefer-destructuring 57 | preventBab2.primaryName = preventBab2Names[0]; 58 | 59 | preventBab2.injections = [hit]; 60 | -------------------------------------------------------------------------------- /src/redirects/prevent-fab-3.2.0.js: -------------------------------------------------------------------------------- 1 | import { preventFab, preventFabNames } from '../scriptlets/prevent-fab-3.2.0'; 2 | 3 | /** 4 | * @redirect prevent-fab-3.2.0 5 | * 6 | * @description 7 | * Redirects fuckadblock script to the source js file. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * \*\/fuckadblock-$script,redirect=prevent-fab-3.2.0 13 | * ``` 14 | * 15 | * @added v1.0.4. 16 | */ 17 | export { preventFab, preventFabNames }; 18 | -------------------------------------------------------------------------------- /src/redirects/prevent-popads-net.js: -------------------------------------------------------------------------------- 1 | import { preventPopadsNet, preventPopadsNetNames } from '../scriptlets/prevent-popads-net'; 2 | 3 | /** 4 | * @redirect prevent-popads-net 5 | * 6 | * @description 7 | * Redirects request to the source which sets static properties to PopAds and popns objects. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * ||popads.net/pop.js$script,redirect=prevent-popads-net 13 | * ``` 14 | * 15 | * @added v1.0.4. 16 | */ 17 | export { preventPopadsNet, preventPopadsNetNames }; 18 | -------------------------------------------------------------------------------- /src/redirects/redirects-list.js: -------------------------------------------------------------------------------- 1 | export { noeval } from './noeval'; 2 | export { GoogleAnalytics } from './google-analytics'; 3 | export { GoogleAnalyticsGa } from './google-analytics-ga'; 4 | export { GoogleSyndicationAdsByGoogle } from './googlesyndication-adsbygoogle'; 5 | export { GoogleTagServicesGpt } from './googletagservices-gpt'; 6 | export { ScoreCardResearchBeacon } from './scorecardresearch-beacon'; 7 | export { metrikaYandexTag } from './metrika-yandex-tag'; 8 | export { metrikaYandexWatch } from './metrika-yandex-watch'; 9 | export { Pardot } from './pardot-1.0'; 10 | export { preventFab } from './prevent-fab-3.2.0'; 11 | export { preventBab } from './prevent-bab'; 12 | export { setPopadsDummy } from './set-popads-dummy'; 13 | export { preventPopadsNet } from './prevent-popads-net'; 14 | export { AmazonApstag } from './amazon-apstag'; 15 | export { Matomo } from './matomo'; 16 | export { Fingerprintjs2 } from './fingerprintjs2'; 17 | export { Fingerprintjs3 } from './fingerprintjs3'; 18 | export { Gemius } from './gemius'; 19 | export { ATInternetSmartTag } from './ati-smarttag'; 20 | export { preventBab2 } from './prevent-bab2'; 21 | export { GoogleIma3 } from './google-ima3'; 22 | export { DidomiLoader } from './didomi-loader'; 23 | export { Prebid } from './prebid'; 24 | export { prebidAds } from './prebid-ads'; 25 | export { NaverWcslog } from './naver-wcslog'; 26 | -------------------------------------------------------------------------------- /src/redirects/redirects-names-list.js: -------------------------------------------------------------------------------- 1 | export { noevalNames } from './noeval'; 2 | export { GoogleAnalyticsNames } from './google-analytics'; 3 | export { GoogleAnalyticsGaNames } from './google-analytics-ga'; 4 | export { GoogleSyndicationAdsByGoogleNames } from './googlesyndication-adsbygoogle'; 5 | export { GoogleTagServicesGptNames } from './googletagservices-gpt'; 6 | export { ScoreCardResearchBeaconNames } from './scorecardresearch-beacon'; 7 | export { metrikaYandexTagNames } from './metrika-yandex-tag'; 8 | export { metrikaYandexWatchNames } from './metrika-yandex-watch'; 9 | export { PardotNames } from './pardot-1.0'; 10 | export { preventFabNames } from './prevent-fab-3.2.0'; 11 | export { preventBabNames } from './prevent-bab'; 12 | export { setPopadsDummyNames } from './set-popads-dummy'; 13 | export { preventPopadsNetNames } from './prevent-popads-net'; 14 | export { AmazonApstagNames } from './amazon-apstag'; 15 | export { MatomoNames } from './matomo'; 16 | export { Fingerprintjs2Names } from './fingerprintjs2'; 17 | export { Fingerprintjs3Names } from './fingerprintjs3'; 18 | export { GemiusNames } from './gemius'; 19 | export { ATInternetSmartTagNames } from './ati-smarttag'; 20 | export { preventBab2Names } from './prevent-bab2'; 21 | export { GoogleIma3Names } from './google-ima3'; 22 | export { DidomiLoaderNames } from './didomi-loader'; 23 | export { PrebidNames } from './prebid'; 24 | export { prebidAdsNames } from './prebid-ads'; 25 | export { NaverWcslogNames } from './naver-wcslog'; 26 | -------------------------------------------------------------------------------- /src/redirects/scorecardresearch-beacon.js: -------------------------------------------------------------------------------- 1 | import { hit } from '../helpers'; 2 | 3 | /** 4 | * @redirect scorecardresearch-beacon 5 | * 6 | * @description 7 | * Mocks Scorecard Research API. 8 | * 9 | * Related UBO redirect resource: 10 | * https://github.com/gorhill/uBlock/blob/master/src/web_accessible_resources/scorecardresearch_beacon.js 11 | * 12 | * ### Examples 13 | * 14 | * ```adblock 15 | * ||sb.scorecardresearch.com/beacon.js$script,redirect=scorecardresearch-beacon 16 | * ``` 17 | * 18 | * @added v1.0.10. 19 | */ 20 | export function ScoreCardResearchBeacon(source) { 21 | window.COMSCORE = { 22 | purge() { 23 | // eslint-disable-next-line no-underscore-dangle 24 | window._comscore = []; 25 | }, 26 | beacon() {}, 27 | }; 28 | hit(source); 29 | } 30 | 31 | export const ScoreCardResearchBeaconNames = [ 32 | 'scorecardresearch-beacon', 33 | 'ubo-scorecardresearch_beacon.js', 34 | 'scorecardresearch_beacon.js', 35 | ]; 36 | 37 | // eslint-disable-next-line prefer-destructuring 38 | ScoreCardResearchBeacon.primaryName = ScoreCardResearchBeaconNames[0]; 39 | 40 | ScoreCardResearchBeacon.injections = [ 41 | hit, 42 | ]; 43 | -------------------------------------------------------------------------------- /src/redirects/set-popads-dummy.js: -------------------------------------------------------------------------------- 1 | import { setPopadsDummy, setPopadsDummyNames } from '../scriptlets/set-popads-dummy'; 2 | 3 | /** 4 | * @redirect set-popads-dummy 5 | * 6 | * @description 7 | * Redirects request to the source which sets static properties to PopAds and popns objects. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * ||popads.net^$script,redirect=set-popads-dummy,domain=example.org 13 | * ``` 14 | * 15 | * @added v1.0.4. 16 | */ 17 | export { setPopadsDummy, setPopadsDummyNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/abort-on-property-write.js: -------------------------------------------------------------------------------- 1 | import { 2 | randomId, 3 | setPropertyAccess, 4 | getPropertyInChain, 5 | createOnErrorHandler, 6 | hit, 7 | isEmptyObject, 8 | } from '../helpers'; 9 | 10 | /* eslint-disable max-len */ 11 | /** 12 | * @scriptlet abort-on-property-write 13 | * 14 | * @description 15 | * Aborts a script when it attempts to **write** the specified property. 16 | * 17 | * Related UBO scriptlet: 18 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#abort-on-property-writejs- 19 | * 20 | * Related ABP source: 21 | * https://gitlab.com/eyeo/snippets/-/blob/main/source/behavioral/abort-on-property-write.js 22 | * 23 | * ### Syntax 24 | * 25 | * ```text 26 | * example.org#%#//scriptlet('abort-on-property-write', property) 27 | * ``` 28 | * 29 | * - `property` — required, path to a property (joined with `.` if needed). 30 | * The property must be attached to `window` 31 | * 32 | * ### Examples 33 | * 34 | * ```adblock 35 | * ! Aborts script when it tries to set `window.adblock` value 36 | * example.org#%#//scriptlet('abort-on-property-write', 'adblock') 37 | * ``` 38 | * 39 | * @added v1.0.4. 40 | */ 41 | /* eslint-enable max-len */ 42 | export function abortOnPropertyWrite(source, property) { 43 | if (!property) { 44 | return; 45 | } 46 | 47 | const rid = randomId(); 48 | const abort = () => { 49 | hit(source); 50 | throw new ReferenceError(rid); 51 | }; 52 | const setChainPropAccess = (owner, property) => { 53 | const chainInfo = getPropertyInChain(owner, property); 54 | let { base } = chainInfo; 55 | const { prop, chain } = chainInfo; 56 | if (chain) { 57 | const setter = (a) => { 58 | base = a; 59 | if (a instanceof Object) { 60 | setChainPropAccess(a, chain); 61 | } 62 | }; 63 | Object.defineProperty(owner, prop, { 64 | get: () => base, 65 | set: setter, 66 | }); 67 | return; 68 | } 69 | 70 | setPropertyAccess(base, prop, { set: abort }); 71 | }; 72 | 73 | setChainPropAccess(window, property); 74 | 75 | window.onerror = createOnErrorHandler(rid).bind(); 76 | } 77 | 78 | export const abortOnPropertyWriteNames = [ 79 | 'abort-on-property-write', 80 | // aliases are needed for matching the related scriptlet converted into our syntax 81 | 'abort-on-property-write.js', 82 | 'ubo-abort-on-property-write.js', 83 | 'aopw.js', 84 | 'ubo-aopw.js', 85 | 'ubo-abort-on-property-write', 86 | 'ubo-aopw', 87 | 'abp-abort-on-property-write', 88 | ]; 89 | 90 | // eslint-disable-next-line prefer-destructuring 91 | abortOnPropertyWrite.primaryName = abortOnPropertyWriteNames[0]; 92 | 93 | abortOnPropertyWrite.injections = [ 94 | randomId, 95 | setPropertyAccess, 96 | getPropertyInChain, 97 | createOnErrorHandler, 98 | hit, 99 | isEmptyObject, 100 | ]; 101 | -------------------------------------------------------------------------------- /src/scriptlets/amazon-apstag.ts: -------------------------------------------------------------------------------- 1 | import { AmazonApstag, AmazonApstagNames } from '../redirects/amazon-apstag'; 2 | 3 | /** 4 | * @scriptlet amazon-apstag 5 | * 6 | * @description 7 | * Mocks Amazon's apstag.js 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('amazon-apstag') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { AmazonApstag, AmazonApstagNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/debug-on-property-read.js: -------------------------------------------------------------------------------- 1 | import { 2 | randomId, 3 | setPropertyAccess, 4 | getPropertyInChain, 5 | createOnErrorHandler, 6 | hit, 7 | noopFunc, 8 | isEmptyObject, 9 | } from '../helpers'; 10 | 11 | /* eslint-disable max-len */ 12 | /** 13 | * @scriptlet debug-on-property-read 14 | * 15 | * @description 16 | * This scriptlet is basically the same as [abort-on-property-read](#abort-on-property-read), 17 | * but instead of aborting it starts the debugger. 18 | * 19 | * > It is not allowed for prod versions of filter lists. 20 | * 21 | * ### Examples 22 | * 23 | * ```adblock 24 | * ! Debug script if it tries to access `window.alert` 25 | * example.org#%#//scriptlet('debug-on-property-read', 'alert') 26 | * 27 | * ! or `window.open` 28 | * example.org#%#//scriptlet('debug-on-property-read', 'open') 29 | * ``` 30 | * 31 | * @added v1.0.4. 32 | */ 33 | /* eslint-enable max-len */ 34 | export function debugOnPropertyRead(source, property) { 35 | if (!property) { 36 | return; 37 | } 38 | const rid = randomId(); 39 | const abort = () => { 40 | hit(source); 41 | debugger; // eslint-disable-line no-debugger 42 | }; 43 | const setChainPropAccess = (owner, property) => { 44 | const chainInfo = getPropertyInChain(owner, property); 45 | let { base } = chainInfo; 46 | const { prop, chain } = chainInfo; 47 | if (chain) { 48 | const setter = (a) => { 49 | base = a; 50 | if (a instanceof Object) { 51 | setChainPropAccess(a, chain); 52 | } 53 | }; 54 | Object.defineProperty(owner, prop, { 55 | get: () => base, 56 | set: setter, 57 | }); 58 | return; 59 | } 60 | 61 | setPropertyAccess(base, prop, { 62 | get: abort, 63 | set: noopFunc, 64 | }); 65 | }; 66 | 67 | setChainPropAccess(window, property); 68 | 69 | window.onerror = createOnErrorHandler(rid).bind(); 70 | } 71 | 72 | export const debugOnPropertyReadNames = [ 73 | 'debug-on-property-read', 74 | ]; 75 | 76 | // eslint-disable-next-line prefer-destructuring 77 | debugOnPropertyRead.primaryName = debugOnPropertyReadNames[0]; 78 | 79 | debugOnPropertyRead.injections = [ 80 | randomId, 81 | setPropertyAccess, 82 | getPropertyInChain, 83 | createOnErrorHandler, 84 | hit, 85 | noopFunc, 86 | isEmptyObject, 87 | ]; 88 | -------------------------------------------------------------------------------- /src/scriptlets/debug-on-property-write.js: -------------------------------------------------------------------------------- 1 | import { 2 | randomId, 3 | setPropertyAccess, 4 | getPropertyInChain, 5 | createOnErrorHandler, 6 | hit, 7 | isEmptyObject, 8 | } from '../helpers'; 9 | 10 | /* eslint-disable max-len */ 11 | /** 12 | * @scriptlet debug-on-property-write 13 | * 14 | * @description 15 | * This scriptlet is basically the same as [abort-on-property-write](#abort-on-property-write), 16 | * but instead of aborting it starts the debugger. 17 | * 18 | * > It is not allowed for prod versions of filter lists. 19 | * 20 | * ### Examples 21 | * 22 | * ```adblock 23 | * ! Aborts script when it tries to write in property `window.test` 24 | * example.org#%#//scriptlet('debug-on-property-write', 'test') 25 | * ``` 26 | * 27 | * @added v1.0.4. 28 | */ 29 | /* eslint-enable max-len */ 30 | export function debugOnPropertyWrite(source, property) { 31 | if (!property) { 32 | return; 33 | } 34 | const rid = randomId(); 35 | const abort = () => { 36 | hit(source); 37 | debugger; // eslint-disable-line no-debugger 38 | }; 39 | const setChainPropAccess = (owner, property) => { 40 | const chainInfo = getPropertyInChain(owner, property); 41 | let { base } = chainInfo; 42 | const { prop, chain } = chainInfo; 43 | if (chain) { 44 | const setter = (a) => { 45 | base = a; 46 | if (a instanceof Object) { 47 | setChainPropAccess(a, chain); 48 | } 49 | }; 50 | Object.defineProperty(owner, prop, { 51 | get: () => base, 52 | set: setter, 53 | }); 54 | return; 55 | } 56 | 57 | setPropertyAccess(base, prop, { set: abort }); 58 | }; 59 | 60 | setChainPropAccess(window, property); 61 | 62 | window.onerror = createOnErrorHandler(rid).bind(); 63 | } 64 | 65 | export const debugOnPropertyWriteNames = [ 66 | 'debug-on-property-write', 67 | ]; 68 | 69 | // eslint-disable-next-line prefer-destructuring 70 | debugOnPropertyWrite.primaryName = debugOnPropertyWriteNames[0]; 71 | 72 | debugOnPropertyWrite.injections = [ 73 | randomId, 74 | setPropertyAccess, 75 | getPropertyInChain, 76 | createOnErrorHandler, 77 | hit, 78 | isEmptyObject, 79 | ]; 80 | -------------------------------------------------------------------------------- /src/scriptlets/didomi-loader.ts: -------------------------------------------------------------------------------- 1 | import { DidomiLoader, DidomiLoaderNames } from '../redirects/didomi-loader'; 2 | 3 | /** 4 | * @scriptlet didomi-loader 5 | * 6 | * @description 7 | * Mocks Didomi's CMP loader script. 8 | * https://developers.didomi.io/ 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('didomi-loader') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { DidomiLoader, DidomiLoaderNames }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/dir-string.js: -------------------------------------------------------------------------------- 1 | import { hit } from '../helpers'; 2 | 3 | /* eslint-disable max-len */ 4 | /** 5 | * @scriptlet dir-string 6 | * 7 | * @description 8 | * Wraps the `console.dir` API to call the `toString` method of the argument. 9 | * There are several adblock circumvention systems that detect browser devtools 10 | * and hide themselves. Therefore, if we force them to think 11 | * that devtools are open (using this scriptlet), 12 | * it will automatically disable the adblock circumvention script. 13 | * 14 | * ### Syntax 15 | * 16 | * ```text 17 | * example.org#%#//scriptlet('dir-string'[, times]) 18 | * ``` 19 | * 20 | * - `times` — optional, the number of times to call the `toString` method of the argument to `console.dir` 21 | * 22 | * ### Examples 23 | * 24 | * ```adblock 25 | * ! Run 2 times 26 | * example.org#%#//scriptlet('dir-string', '2') 27 | * ``` 28 | * 29 | * @added v1.0.4. 30 | */ 31 | /* eslint-enable max-len */ 32 | export function dirString(source, times) { 33 | const { dir } = console; 34 | times = parseInt(times, 10); 35 | 36 | function dirWrapper(object) { 37 | // eslint-disable-next-line no-unused-vars 38 | let temp; 39 | for (let i = 0; i < times; i += 1) { 40 | // eslint-disable-next-line no-unused-expressions 41 | temp = `${object}`; 42 | } 43 | if (typeof dir === 'function') { 44 | dir.call(this, object); 45 | } 46 | hit(source, temp); 47 | } 48 | // eslint-disable-next-line no-console 49 | console.dir = dirWrapper; 50 | } 51 | 52 | export const dirStringNames = [ 53 | 'dir-string', 54 | ]; 55 | 56 | // eslint-disable-next-line prefer-destructuring 57 | dirString.primaryName = dirStringNames[0]; 58 | 59 | dirString.injections = [hit]; 60 | -------------------------------------------------------------------------------- /src/scriptlets/disable-newtab-links.js: -------------------------------------------------------------------------------- 1 | import { hit } from '../helpers'; 2 | 3 | /** 4 | * @scriptlet disable-newtab-links 5 | * 6 | * @description 7 | * Prevents opening new tabs and windows if there is `target` attribute in element. 8 | * 9 | * Related UBO scriptlet: 10 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#disable-newtab-linksjs- 11 | * 12 | * ### Syntax 13 | * 14 | * ```adblock 15 | * example.org#%#//scriptlet('disable-newtab-links') 16 | * ``` 17 | * 18 | * @added v1.0.4. 19 | */ 20 | export function disableNewtabLinks(source) { 21 | document.addEventListener('click', (ev) => { 22 | let { target } = ev; 23 | while (target !== null) { 24 | if (target.localName === 'a' && target.hasAttribute('target')) { 25 | ev.stopPropagation(); 26 | ev.preventDefault(); 27 | hit(source); 28 | break; 29 | } 30 | target = target.parentNode; 31 | } 32 | }); 33 | } 34 | 35 | export const disableNewtabLinksNames = [ 36 | 'disable-newtab-links', 37 | // aliases are needed for matching the related scriptlet converted into our syntax 38 | 'disable-newtab-links.js', 39 | 'ubo-disable-newtab-links.js', 40 | 'ubo-disable-newtab-links', 41 | ]; 42 | 43 | // eslint-disable-next-line prefer-destructuring 44 | disableNewtabLinks.primaryName = disableNewtabLinksNames[0]; 45 | 46 | disableNewtabLinks.injections = [ 47 | hit, 48 | ]; 49 | -------------------------------------------------------------------------------- /src/scriptlets/fingerprintjs2.ts: -------------------------------------------------------------------------------- 1 | import { Fingerprintjs2, Fingerprintjs2Names } from '../redirects/fingerprintjs2'; 2 | 3 | /** 4 | * @scriptlet fingerprintjs2 5 | * 6 | * @description 7 | * Mocks FingerprintJS v2 8 | * https://github.com/fingerprintjs 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('fingerprintjs2') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { Fingerprintjs2, Fingerprintjs2Names }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/fingerprintjs3.ts: -------------------------------------------------------------------------------- 1 | import { Fingerprintjs3, Fingerprintjs3Names } from '../redirects/fingerprintjs3'; 2 | 3 | /** 4 | * @scriptlet fingerprintjs3 5 | * 6 | * @description 7 | * Mocks FingerprintJS v3 8 | * https://github.com/fingerprintjs 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('fingerprintjs3') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { Fingerprintjs3, Fingerprintjs3Names }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/gemius.ts: -------------------------------------------------------------------------------- 1 | import { Gemius, GemiusNames } from '../redirects/gemius'; 2 | 3 | /** 4 | * @scriptlet gemius 5 | * 6 | * @description 7 | * Mocks Gemius Analytics. 8 | * https://flowplayer.com/developers/plugins/gemius 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('gemius') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { Gemius, GemiusNames }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/google-analytics-ga.ts: -------------------------------------------------------------------------------- 1 | import { GoogleAnalyticsGa, GoogleAnalyticsGaNames } from '../redirects/google-analytics-ga'; 2 | 3 | /** 4 | * @scriptlet google-analytics-ga 5 | * 6 | * @description 7 | * Mocks old Google Analytics API. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('google-analytics-ga') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { GoogleAnalyticsGa, GoogleAnalyticsGaNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/google-analytics.ts: -------------------------------------------------------------------------------- 1 | import { GoogleAnalytics, GoogleAnalyticsNames } from '../redirects/google-analytics'; 2 | 3 | /** 4 | * @scriptlet google-analytics 5 | * 6 | * @description 7 | * Mocks Google's Analytics and Tag Manager APIs. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('google-analytics') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { GoogleAnalytics, GoogleAnalyticsNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/google-ima3.ts: -------------------------------------------------------------------------------- 1 | import { GoogleIma3, GoogleIma3Names } from '../redirects/google-ima3'; 2 | 3 | /** 4 | * @scriptlet google-ima3 5 | * 6 | * @description 7 | * Mocks the IMA SDK of Google. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('google-ima3') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { GoogleIma3, GoogleIma3Names }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/googlesyndication-adsbygoogle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GoogleSyndicationAdsByGoogle, 3 | GoogleSyndicationAdsByGoogleNames, 4 | } from '../redirects/googlesyndication-adsbygoogle'; 5 | 6 | /** 7 | * @scriptlet googlesyndication-adsbygoogle 8 | * 9 | * @description 10 | * Mocks Google AdSense API. 11 | * 12 | * ### Examples 13 | * 14 | * ```adblock 15 | * example.org#%#//scriptlet('googlesyndication-adsbygoogle') 16 | * ``` 17 | * 18 | * @added v1.10.25. 19 | */ 20 | export { GoogleSyndicationAdsByGoogle, GoogleSyndicationAdsByGoogleNames }; 21 | -------------------------------------------------------------------------------- /src/scriptlets/googletagservices-gpt.ts: -------------------------------------------------------------------------------- 1 | import { GoogleTagServicesGpt, GoogleTagServicesGptNames } from '../redirects/googletagservices-gpt'; 2 | 3 | /** 4 | * @scriptlet googletagservices-gpt 5 | * 6 | * @description 7 | * Mocks Google Publisher Tag API. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('googletagservices-gpt') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { GoogleTagServicesGpt, GoogleTagServicesGptNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/index.ts: -------------------------------------------------------------------------------- 1 | export { scriptlets, type Source } from './scriptlets'; 2 | -------------------------------------------------------------------------------- /src/scriptlets/log-eval.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval */ 2 | import { hit, logMessage } from '../helpers'; 3 | 4 | /** 5 | * @scriptlet log-eval 6 | * 7 | * @description 8 | * Logs all `eval()` or `new Function()` calls to the console. 9 | * 10 | * ### Syntax 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('log-eval') 14 | * ``` 15 | * 16 | * @added v1.0.4. 17 | */ 18 | export function logEval(source) { 19 | // wrap eval function 20 | const nativeEval = window.eval; 21 | function evalWrapper(str) { 22 | hit(source); 23 | logMessage(source, `eval("${str}")`, true); 24 | return nativeEval(str); 25 | } 26 | window.eval = evalWrapper; 27 | 28 | // wrap new Function 29 | const nativeFunction = window.Function; 30 | 31 | function FunctionWrapper(...args) { 32 | hit(source); 33 | logMessage(source, `new Function(${args.join(', ')})`, true); 34 | return nativeFunction.apply(this, [...args]); 35 | } 36 | 37 | FunctionWrapper.prototype = Object.create(nativeFunction.prototype); 38 | FunctionWrapper.prototype.constructor = FunctionWrapper; 39 | 40 | window.Function = FunctionWrapper; 41 | } 42 | 43 | export const logEvalNames = [ 44 | 'log-eval', 45 | ]; 46 | 47 | // eslint-disable-next-line prefer-destructuring 48 | logEval.primaryName = logEvalNames[0]; 49 | 50 | logEval.injections = [hit, logMessage]; 51 | -------------------------------------------------------------------------------- /src/scriptlets/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @scriptlet log 3 | * 4 | * @description 5 | * A simple scriptlet which only purpose is to print arguments to console. 6 | * This scriptlet can be helpful for debugging and troubleshooting other scriptlets. 7 | * 8 | * Related ABP source: 9 | * https://gitlab.com/eyeo/snippets/-/blob/main/source/introspection/log.js 10 | * 11 | * ### Examples 12 | * 13 | * ```adblock 14 | * example.org#%#//scriptlet('log', 'arg1', 'arg2') 15 | * ``` 16 | * 17 | * @added v1.0.4. 18 | */ 19 | export function log(...args) { 20 | console.log(args); // eslint-disable-line no-console 21 | } 22 | 23 | export const logNames = [ 24 | 'log', 25 | 'abp-log', 26 | ]; 27 | 28 | // eslint-disable-next-line prefer-destructuring 29 | log.primaryName = logNames[0]; 30 | -------------------------------------------------------------------------------- /src/scriptlets/matomo.ts: -------------------------------------------------------------------------------- 1 | import { Matomo, MatomoNames } from '../redirects/matomo'; 2 | 3 | /** 4 | * @scriptlet matomo 5 | * 6 | * @description 7 | * Mocks the piwik.js file of Matomo (formerly Piwik). 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('matomo') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { Matomo, MatomoNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/metrika-yandex-tag.ts: -------------------------------------------------------------------------------- 1 | import { metrikaYandexTag, metrikaYandexTagNames } from '../redirects/metrika-yandex-tag'; 2 | 3 | /** 4 | * @scriptlet metrika-yandex-tag 5 | * 6 | * @description 7 | * Mocks Yandex Metrika API. 8 | * https://yandex.ru/support/metrica/objects/method-reference.html 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('metrika-yandex-tag') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { metrikaYandexTag, metrikaYandexTagNames }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/metrika-yandex-watch.ts: -------------------------------------------------------------------------------- 1 | import { metrikaYandexWatch, metrikaYandexWatchNames } from '../redirects/metrika-yandex-watch'; 2 | 3 | /** 4 | * @scriptlet metrika-yandex-watch 5 | * 6 | * @description 7 | * Mocks the old Yandex Metrika API. 8 | * https://yandex.ru/support/metrica/objects/_method-reference.html 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('metrika-yandex-watch') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { metrikaYandexWatch, metrikaYandexWatchNames }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/naver-wcslog.ts: -------------------------------------------------------------------------------- 1 | import { NaverWcslog, NaverWcslogNames } from '../redirects/naver-wcslog'; 2 | 3 | /** 4 | * @scriptlet naver-wcslog 5 | * 6 | * @description 7 | * Mocks wcslog.js of Naver Analytics. 8 | * 9 | * ### Examples 10 | * 11 | * ```adblock 12 | * example.org#%#//scriptlet('naver-wcslog') 13 | * ``` 14 | * 15 | * @added v1.10.25. 16 | */ 17 | export { NaverWcslog, NaverWcslogNames }; 18 | -------------------------------------------------------------------------------- /src/scriptlets/no-protected-audience.ts: -------------------------------------------------------------------------------- 1 | import { 2 | hit, 3 | noopStr, 4 | noopFunc, 5 | noopResolveVoid, 6 | noopResolveNull, 7 | } from '../helpers'; 8 | import { type Source } from './scriptlets'; 9 | 10 | /** 11 | * @scriptlet no-protected-audience 12 | * 13 | * @description 14 | * Prevents using the Protected Audience API. 15 | * https://wicg.github.io/turtledove/ 16 | * 17 | * ### Syntax 18 | * 19 | * ```adblock 20 | * example.org#%#//scriptlet('no-protected-audience') 21 | * ``` 22 | * 23 | * @added v1.10.25. 24 | */ 25 | export function noProtectedAudience(source: Source) { 26 | // Prevent XMLDocuments from being tampered with generic scriptlet rule 27 | if (Document instanceof Object === false) { 28 | return; 29 | } 30 | 31 | // This is not a complete list of methods, but rather a minimal set to suppress the API 32 | const protectedAudienceMethods = { 33 | joinAdInterestGroup: noopResolveVoid, 34 | runAdAuction: noopResolveNull, 35 | leaveAdInterestGroup: noopResolveVoid, 36 | clearOriginJoinedAdInterestGroups: noopResolveVoid, 37 | createAuctionNonce: noopStr, 38 | updateAdInterestGroups: noopFunc, 39 | }; 40 | 41 | for (const key of Object.keys(protectedAudienceMethods)) { 42 | /** 43 | * TODO Remove type castings when Protected Audience API types become available on DOM definitions. 44 | * https://github.com/WICG/turtledove/issues/759 45 | */ 46 | const methodName = key as keyof typeof protectedAudienceMethods; 47 | const prototype = Navigator.prototype as unknown as Record; 48 | 49 | if (!Object.prototype.hasOwnProperty.call(prototype, methodName) 50 | || prototype[methodName] instanceof Function === false) { 51 | continue; 52 | } 53 | 54 | prototype[methodName] = protectedAudienceMethods[methodName]; 55 | } 56 | 57 | hit(source); 58 | } 59 | 60 | export const noProtectedAudienceNames = [ 61 | 'no-protected-audience', 62 | ]; 63 | 64 | // eslint-disable-next-line prefer-destructuring 65 | noProtectedAudience.primaryName = noProtectedAudienceNames[0]; 66 | 67 | noProtectedAudience.injections = [ 68 | hit, 69 | noopStr, 70 | noopFunc, 71 | noopResolveVoid, 72 | noopResolveNull, 73 | ]; 74 | -------------------------------------------------------------------------------- /src/scriptlets/no-topics.js: -------------------------------------------------------------------------------- 1 | import { hit, noopPromiseResolve } from '../helpers'; 2 | 3 | /** 4 | * @scriptlet no-topics 5 | * 6 | * @description 7 | * Prevents using the Topics API. 8 | * https://developer.chrome.com/docs/privacy-sandbox/topics/ 9 | * 10 | * ### Syntax 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('no-topics') 14 | * ``` 15 | * 16 | * @added v1.6.18. 17 | */ 18 | export function noTopics(source) { 19 | const TOPICS_PROPERTY_NAME = 'browsingTopics'; 20 | 21 | if (Document instanceof Object === false) { 22 | return; 23 | } 24 | if (!Object.prototype.hasOwnProperty.call(Document.prototype, TOPICS_PROPERTY_NAME) 25 | || Document.prototype[TOPICS_PROPERTY_NAME] instanceof Function === false) { 26 | return; 27 | } 28 | 29 | // document.browsingTopics() is async function so it's better to return noopPromiseResolve() 30 | // https://github.com/patcg-individual-drafts/topics#the-api-and-how-it-works 31 | Document.prototype[TOPICS_PROPERTY_NAME] = () => noopPromiseResolve('[]'); 32 | hit(source); 33 | } 34 | 35 | export const noTopicsNames = [ 36 | 'no-topics', 37 | ]; 38 | 39 | // eslint-disable-next-line prefer-destructuring 40 | noTopics.primaryName = noTopicsNames[0]; 41 | 42 | noTopics.injections = [ 43 | hit, 44 | noopPromiseResolve, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/scriptlets/noeval.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-extra-bind */ 2 | import { hit, logMessage } from '../helpers'; 3 | 4 | /** 5 | * @scriptlet noeval 6 | * 7 | * @description 8 | * Prevents page to use eval. 9 | * Notifies about attempts in the console 10 | * 11 | * Related UBO scriptlet: 12 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#noevaljs- 13 | * 14 | * It also can be used as `$redirect` rules sometimes. 15 | * See [redirect description](../wiki/about-redirects.md#noeval). 16 | * 17 | * ### Syntax 18 | * 19 | * ```adblock 20 | * example.org#%#//scriptlet('noeval') 21 | * ``` 22 | * 23 | * @added v1.0.4. 24 | */ 25 | export function noeval(source) { 26 | window.eval = function evalWrapper(s) { 27 | hit(source); 28 | logMessage(source, `AdGuard has prevented eval:\n${s}`, true); 29 | }.bind(); 30 | } 31 | 32 | export const noevalNames = [ 33 | 'noeval', 34 | // aliases are needed for matching the related scriptlet converted into our syntax 35 | 'noeval.js', 36 | 'silent-noeval.js', 37 | 'ubo-noeval.js', 38 | 'ubo-silent-noeval.js', 39 | 'ubo-noeval', 40 | 'ubo-silent-noeval', 41 | ]; 42 | 43 | // eslint-disable-next-line prefer-destructuring 44 | noeval.primaryName = noevalNames[0]; 45 | 46 | noeval.injections = [hit, logMessage]; 47 | -------------------------------------------------------------------------------- /src/scriptlets/nowebrtc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars, no-extra-bind, func-names */ 2 | import { 3 | hit, 4 | noopFunc, 5 | logMessage, 6 | convertRtcConfigToString, 7 | } from '../helpers'; 8 | 9 | /* eslint-disable max-len */ 10 | /** 11 | * @scriptlet nowebrtc 12 | * 13 | * @description 14 | * Disables WebRTC by overriding `RTCPeerConnection`. 15 | * The overridden function will log every attempt to create a new connection. 16 | * 17 | * Related UBO scriptlet: 18 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#nowebrtcjs- 19 | * 20 | * ### Syntax 21 | * 22 | * ```adblock 23 | * example.org#%#//scriptlet('nowebrtc') 24 | * ``` 25 | * 26 | * @added v1.0.4. 27 | */ 28 | /* eslint-enable max-len */ 29 | export function nowebrtc(source) { 30 | let propertyName = ''; 31 | if (window.RTCPeerConnection) { 32 | propertyName = 'RTCPeerConnection'; 33 | } else if (window.webkitRTCPeerConnection) { 34 | propertyName = 'webkitRTCPeerConnection'; 35 | } 36 | 37 | if (propertyName === '') { 38 | return; 39 | } 40 | 41 | const rtcReplacement = (config) => { 42 | // eslint-disable-next-line max-len 43 | const message = `Document tried to create an RTCPeerConnection: ${convertRtcConfigToString(config)}`; 44 | logMessage(source, message); 45 | hit(source); 46 | }; 47 | rtcReplacement.prototype = { 48 | close: noopFunc, 49 | createDataChannel: noopFunc, 50 | createOffer: noopFunc, 51 | setRemoteDescription: noopFunc, 52 | }; 53 | const rtc = window[propertyName]; 54 | window[propertyName] = rtcReplacement; 55 | if (rtc.prototype) { 56 | rtc.prototype.createDataChannel = function (a, b) { 57 | return { 58 | close: noopFunc, 59 | send: noopFunc, 60 | }; 61 | }.bind(null); 62 | } 63 | } 64 | 65 | export const nowebrtcNames = [ 66 | 'nowebrtc', 67 | // aliases are needed for matching the related scriptlet converted into our syntax 68 | 'nowebrtc.js', 69 | 'ubo-nowebrtc.js', 70 | 'ubo-nowebrtc', 71 | ]; 72 | 73 | // eslint-disable-next-line prefer-destructuring 74 | nowebrtc.primaryName = nowebrtcNames[0]; 75 | 76 | nowebrtc.injections = [ 77 | hit, 78 | noopFunc, 79 | logMessage, 80 | convertRtcConfigToString, 81 | ]; 82 | -------------------------------------------------------------------------------- /src/scriptlets/pardot-1.0.ts: -------------------------------------------------------------------------------- 1 | import { Pardot, PardotNames } from '../redirects/pardot-1.0'; 2 | 3 | /** 4 | * @scriptlet pardot-1.0 5 | * 6 | * @description 7 | * Mocks the pd.js file of Salesforce. 8 | * https://pi.pardot.com/pd.js 9 | * https://developer.salesforce.com/docs/marketing/pardot/overview 10 | * 11 | * ### Examples 12 | * 13 | * ```adblock 14 | * example.org#%#//scriptlet('pardot-1.0') 15 | * ``` 16 | * 17 | * @added v1.10.25. 18 | */ 19 | export { Pardot, PardotNames }; 20 | -------------------------------------------------------------------------------- /src/scriptlets/prebid.ts: -------------------------------------------------------------------------------- 1 | import { Prebid, PrebidNames } from '../redirects/prebid'; 2 | 3 | /** 4 | * @scriptlet prebid 5 | * 6 | * @description 7 | * Mocks the prebid.js header bidding suit. 8 | * https://docs.prebid.org/ 9 | * 10 | * ### Examples 11 | * 12 | * ```adblock 13 | * example.org#%#//scriptlet('prebid') 14 | * ``` 15 | * 16 | * @added v1.10.25. 17 | */ 18 | export { Prebid, PrebidNames }; 19 | -------------------------------------------------------------------------------- /src/scriptlets/prevent-eval-if.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-extra-bind, func-names */ 2 | 3 | import { toRegExp, hit } from '../helpers'; 4 | 5 | /** 6 | * @scriptlet prevent-eval-if 7 | * 8 | * @description 9 | * Prevents page to use eval matching payload. 10 | * 11 | * Related UBO scriptlet: 12 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#noeval-ifjs- 13 | * 14 | * ### Syntax 15 | * 16 | * ```text 17 | * example.org#%#//scriptlet('prevent-eval-if'[, search]) 18 | * ``` 19 | * 20 | * - `search` — optional, string or regular expression matching the stringified eval payload; 21 | * defaults to match all stringified eval payloads; 22 | * invalid regular expression will cause exit and rule will not work 23 | * 24 | * ### Examples 25 | * 26 | * ```adblock 27 | * ! Prevents eval if it matches 'test' 28 | * example.org#%#//scriptlet('prevent-eval-if', 'test') 29 | * ``` 30 | * 31 | * @added v1.0.4. 32 | */ 33 | export function preventEvalIf(source, search) { 34 | const searchRegexp = toRegExp(search); 35 | 36 | const nativeEval = window.eval; 37 | window.eval = function (payload) { 38 | if (!searchRegexp.test(payload.toString())) { 39 | return nativeEval.call(window, payload); 40 | } 41 | hit(source, payload); 42 | return undefined; 43 | }.bind(window); 44 | 45 | // Protect window.eval from native code check 46 | window.eval.toString = nativeEval.toString.bind(nativeEval); 47 | } 48 | 49 | export const preventEvalIfNames = [ 50 | 'prevent-eval-if', 51 | // aliases are needed for matching the related scriptlet converted into our syntax 52 | 'noeval-if.js', 53 | 'ubo-noeval-if.js', 54 | 'ubo-noeval-if', 55 | ]; 56 | 57 | // eslint-disable-next-line prefer-destructuring 58 | preventEvalIf.primaryName = preventEvalIfNames[0]; 59 | 60 | preventEvalIf.injections = [toRegExp, hit]; 61 | -------------------------------------------------------------------------------- /src/scriptlets/prevent-popads-net.js: -------------------------------------------------------------------------------- 1 | import { hit, createOnErrorHandler, randomId } from '../helpers'; 2 | 3 | /** 4 | * @scriptlet prevent-popads-net 5 | * 6 | * @description 7 | * Aborts on property write (PopAds, popns), throws reference error with random id. 8 | * 9 | * Related UBO scriptlet: 10 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#popadsnetjs- 11 | * 12 | * ### Syntax 13 | * 14 | * ```adblock 15 | * example.org#%#//scriptlet('prevent-popads-net') 16 | * ``` 17 | * 18 | * @added v1.0.4. 19 | */ 20 | export function preventPopadsNet(source) { 21 | const rid = randomId(); 22 | 23 | const throwError = () => { 24 | throw new ReferenceError(rid); 25 | }; 26 | 27 | delete window.PopAds; 28 | delete window.popns; 29 | Object.defineProperties(window, { 30 | PopAds: { set: throwError }, 31 | popns: { set: throwError }, 32 | }); 33 | 34 | window.onerror = createOnErrorHandler(rid).bind(); 35 | hit(source); 36 | } 37 | 38 | export const preventPopadsNetNames = [ 39 | 'prevent-popads-net', 40 | // aliases are needed for matching the related scriptlet converted into our syntax 41 | 'popads.net.js', 42 | 'ubo-popads.net.js', 43 | 'ubo-popads.net', 44 | ]; 45 | 46 | // eslint-disable-next-line prefer-destructuring 47 | preventPopadsNet.primaryName = preventPopadsNetNames[0]; 48 | 49 | preventPopadsNet.injections = [createOnErrorHandler, randomId, hit]; 50 | -------------------------------------------------------------------------------- /src/scriptlets/scorecardresearch-beacon.ts: -------------------------------------------------------------------------------- 1 | import { ScoreCardResearchBeacon, ScoreCardResearchBeaconNames } from '../redirects/scorecardresearch-beacon'; 2 | 3 | /** 4 | * @scriptlet scorecardresearch-beacon 5 | * 6 | * @description 7 | * 8 | * ### Examples 9 | * 10 | * ```adblock 11 | * example.org#%#//scriptlet('scorecardresearch-beacon') 12 | * ``` 13 | * 14 | * @added v1.10.25. 15 | */ 16 | export { ScoreCardResearchBeacon, ScoreCardResearchBeaconNames }; 17 | -------------------------------------------------------------------------------- /src/scriptlets/set-popads-dummy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, no-multi-assign */ 2 | import { hit } from '../helpers'; 3 | 4 | /** 5 | * @scriptlet set-popads-dummy 6 | * 7 | * @description 8 | * Sets static properties PopAds and popns. 9 | * 10 | * Related UBO scriptlet: 11 | * https://github.com/gorhill/uBlock/wiki/Resources-Library#popads-dummyjs- 12 | * 13 | * ### Syntax 14 | * 15 | * ```adblock 16 | * example.org#%#//scriptlet('set-popads-dummy') 17 | * ``` 18 | * 19 | * @added v1.0.4. 20 | */ 21 | export function setPopadsDummy(source) { 22 | delete window.PopAds; 23 | delete window.popns; 24 | Object.defineProperties(window, { 25 | PopAds: { 26 | get: () => { 27 | hit(source); 28 | return {}; 29 | }, 30 | }, 31 | popns: { 32 | get: () => { 33 | hit(source); 34 | return {}; 35 | }, 36 | }, 37 | }); 38 | } 39 | 40 | export const setPopadsDummyNames = [ 41 | 'set-popads-dummy', 42 | // aliases are needed for matching the related scriptlet converted into our syntax 43 | 'popads-dummy.js', 44 | 'ubo-popads-dummy.js', 45 | 'ubo-popads-dummy', 46 | ]; 47 | 48 | // eslint-disable-next-line prefer-destructuring 49 | setPopadsDummy.primaryName = setPopadsDummyNames[0]; 50 | 51 | setPopadsDummy.injections = [hit]; 52 | -------------------------------------------------------------------------------- /src/validators/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isAbpSnippetRule, 3 | isAdgScriptletRule, 4 | isRedirectResourceCompatibleWithAdg, 5 | isUboScriptletRule, 6 | isValidAdgRedirectRule, 7 | isValidScriptletName, 8 | isValidScriptletRule, 9 | } from './validators'; 10 | -------------------------------------------------------------------------------- /tests/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '../.eslintrc.cjs', 4 | ], 5 | rules: { 6 | 'jsdoc/require-jsdoc': 0, 7 | 'function-paren-newline': 'off', 8 | 'import/no-extraneous-dependencies': 'off', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /tests/helpers/index.test.js: -------------------------------------------------------------------------------- 1 | // tests for QUnit 2 | import './noop.test'; 3 | -------------------------------------------------------------------------------- /tests/helpers/log-message.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { 3 | afterEach, 4 | describe, 5 | vi, 6 | test, 7 | expect, 8 | } from 'vitest'; 9 | 10 | import { logMessage } from '../../src/helpers'; 11 | 12 | const nativeConsole = console.log; 13 | 14 | const RULE_TEXT = 'example.org#%#//scriptlet(\'set-cookie\', \'name\', \'value\')'; 15 | const SCRIPTLET_NAME = 'set-cookie'; 16 | const MESSAGE = 'arbitrary text message'; 17 | 18 | describe('Test logMessage', () => { 19 | afterEach(() => { 20 | console.log = nativeConsole; 21 | }); 22 | 23 | test('Logs message conditionally', async () => { 24 | console.log = vi.fn(); 25 | 26 | let forced; 27 | let source; 28 | 29 | // Log forced message 30 | forced = true; 31 | source = { 32 | name: SCRIPTLET_NAME, 33 | ruleText: RULE_TEXT, 34 | verbose: false, 35 | }; 36 | logMessage(source, MESSAGE, forced); 37 | expect(console.log.mock.calls[0][0].includes(`${SCRIPTLET_NAME}: ${MESSAGE}`)).toBeTruthy(); 38 | 39 | // Log message on verbose 40 | forced = false; 41 | source = { 42 | name: SCRIPTLET_NAME, 43 | ruleText: RULE_TEXT, 44 | verbose: true, 45 | }; 46 | logMessage(source, MESSAGE, forced); 47 | expect(console.log.mock.calls[0][0].includes(`${SCRIPTLET_NAME}: ${MESSAGE}`)).toBeTruthy(); 48 | 49 | // Message should not be logged this time, 50 | // so after the first call, there should be 2 calls in total 51 | forced = false; 52 | source = { 53 | name: SCRIPTLET_NAME, 54 | ruleText: RULE_TEXT, 55 | verbose: false, 56 | }; 57 | logMessage(source, MESSAGE, forced); 58 | 59 | expect(console.log).toHaveBeenCalledTimes(2); 60 | }); 61 | 62 | test('Logs message without ruleText', async () => { 63 | console.log = vi.fn(); 64 | 65 | const FORCED = true; 66 | const source = { 67 | name: SCRIPTLET_NAME, 68 | verbose: false, 69 | }; 70 | logMessage(source, MESSAGE, FORCED); 71 | 72 | expect(console.log.mock.calls[0][0].includes(`${SCRIPTLET_NAME}: ${MESSAGE}`)).toBeTruthy(); 73 | expect(console.log).toHaveBeenCalledTimes(1); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tests/helpers/match-stack.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | import { matchStackTrace } from '../../src/helpers'; 4 | 5 | test('matchStackTrace() for working with getNativeRegexpTest() helper', async () => { 6 | expect(matchStackTrace('vitest', new Error().stack)).toBeTruthy(); 7 | expect(matchStackTrace('not_present', new Error().stack)).toBeFalsy(); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/helpers/noop.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tests for noop helper functions should be run by QUint, 3 | * not by Jest because jsdom does not support Request which is needed for noopPromiseResolve() 4 | */ 5 | 6 | import { 7 | noopPromiseResolve, 8 | noopCallbackFunc, 9 | noopFunc, 10 | throwFunc, 11 | } from '../../src/helpers'; 12 | 13 | const { test, module } = QUnit; 14 | const name = 'scriptlets-redirects noop helpers'; 15 | 16 | module(name); 17 | 18 | test('Test noopPromiseResolve for valid response props', async (assert) => { 19 | const TEST_URL = 'url'; 20 | const TEST_TYPE = 'opaque'; 21 | const objResponse = await noopPromiseResolve('{}'); 22 | const objBody = await objResponse.json(); 23 | 24 | const arrResponse = await noopPromiseResolve('[]'); 25 | const arrBody = await arrResponse.json(); 26 | 27 | const responseWithUrl = await noopPromiseResolve('{}', TEST_URL); 28 | const responseWithType = await noopPromiseResolve('{}', '', TEST_TYPE); 29 | 30 | assert.ok(responseWithUrl.url === TEST_URL); 31 | assert.ok(typeof objBody === 'object' && !objBody.length); 32 | assert.ok(Array.isArray(arrBody) && !arrBody.length); 33 | assert.strictEqual(responseWithType.type, TEST_TYPE); 34 | }); 35 | 36 | test('noopCallbackFunc returns noopFunc', async (assert) => { 37 | const func = noopCallbackFunc(); 38 | assert.ok(typeof func === 'function', 'returns function'); 39 | assert.strictEqual(func.toString(), noopFunc.toString(), 'returns empty function'); 40 | assert.strictEqual(func(), undefined, 'function returns undefined'); 41 | }); 42 | 43 | test('throwFunc throws an error', async (assert) => { 44 | assert.throws(() => throwFunc(), 'throwFunc throws an error'); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/helpers/number-utils.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | 3 | import { getNumberFromString } from '../../src/helpers'; 4 | 5 | describe('Number utils tests', () => { 6 | describe('check getNumberFromString for all data types inputs', () => { 7 | const testCases = [ 8 | { 9 | actual: 123, 10 | expected: 123, 11 | }, 12 | { 13 | actual: '123parsable', 14 | expected: 123, 15 | }, 16 | { 17 | actual: true, 18 | expected: null, 19 | }, 20 | { 21 | actual: null, 22 | expected: null, 23 | }, 24 | { 25 | actual: undefined, 26 | expected: null, 27 | }, 28 | { 29 | actual: undefined, 30 | expected: null, 31 | }, 32 | { 33 | actual: 'not parsable 123', 34 | expected: null, 35 | }, 36 | { 37 | actual: { test: 'test' }, 38 | expected: null, 39 | }, 40 | { 41 | actual: ['test'], 42 | expected: null, 43 | }, 44 | { 45 | actual: ['test'], 46 | expected: null, 47 | }, 48 | ]; 49 | test.each(testCases)('$actual -> $expected', ({ actual, expected }) => { 50 | expect(getNumberFromString(actual)).toStrictEqual(expected); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/helpers/object-utils.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | import { isEmptyObject } from '../../src/helpers'; 4 | 5 | test('isEmptyObject() for different inputs', async () => { 6 | const emptyObj = {}; 7 | const obj = { a: 1 }; 8 | const emptyArr = []; 9 | const arr = [1, 2, 3]; 10 | function func() {} 11 | 12 | expect(isEmptyObject(emptyObj)).toBeTruthy(); 13 | expect(isEmptyObject(emptyArr)).toBeTruthy(); 14 | 15 | expect(isEmptyObject(obj)).toBeFalsy(); 16 | expect(isEmptyObject(arr)).toBeFalsy(); 17 | expect(isEmptyObject(func)).toBeFalsy(); 18 | 19 | expect(isEmptyObject(EventTarget)).toBeFalsy(); 20 | expect(isEmptyObject(Array)).toBeFalsy(); 21 | expect(isEmptyObject(Object)).toBeFalsy(); 22 | expect(isEmptyObject(Function)).toBeFalsy(); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/helpers/prevent-utils.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | 3 | import { parseRawDelay } from '../../src/helpers'; 4 | 5 | describe('Test parseRawDelay', () => { 6 | const testCases = [ 7 | { 8 | actual: 0, 9 | expected: 0, 10 | }, 11 | { 12 | actual: 10, 13 | expected: 10, 14 | }, 15 | { 16 | actual: 10.123, 17 | expected: 10, 18 | }, 19 | { 20 | actual: '0', 21 | expected: 0, 22 | }, 23 | { 24 | actual: '10', 25 | expected: 10, 26 | }, 27 | { 28 | actual: '10.123', 29 | expected: 10, 30 | }, 31 | { 32 | actual: 'string', 33 | expected: 'string', 34 | }, 35 | { 36 | actual: null, 37 | expected: null, 38 | }, 39 | { 40 | actual: undefined, 41 | expected: undefined, 42 | }, 43 | { 44 | actual: false, 45 | expected: false, 46 | }, 47 | ]; 48 | test.each(testCases)('$actual -> $expected', ({ actual, expected }) => { 49 | expect(parseRawDelay(actual)).toStrictEqual(expected); 50 | }); 51 | 52 | test('parsing NaN', () => { 53 | const actual = NaN; 54 | expect(parseRawDelay(actual).toString()).toStrictEqual('NaN'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/helpers/regexp-utils.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | import { restoreRegExpValues, backupRegExpValues } from '../../src/helpers'; 4 | 5 | test('restoreRegExpValues() check if correct value have been set', async () => { 6 | restoreRegExpValues(['foo']); 7 | expect(RegExp.$1).toBe('foo'); 8 | expect(RegExp.$2).toBe(''); 9 | }); 10 | 11 | test('restoreRegExpValues() check if correct values have been set', async () => { 12 | restoreRegExpValues(['test', 'abc', 'xyz', 'aaaa', '123']); 13 | expect(RegExp.$1).toBe('test'); 14 | expect(RegExp.$2).toBe('abc'); 15 | expect(RegExp.$3).toBe('xyz'); 16 | expect(RegExp.$4).toBe('aaaa'); 17 | expect(RegExp.$5).toBe('123'); 18 | expect(RegExp.$6).toBe(''); 19 | }); 20 | 21 | test('backupRegExpValues() and restoreRegExpValues(), modify values and restore them', async () => { 22 | const regExp = /(\w+)\s(\w+)/; 23 | const string = 'div a'; 24 | string.replace(regExp, '$2, $1'); 25 | 26 | expect(RegExp.$1).toBe('div'); 27 | expect(RegExp.$2).toBe('a'); 28 | 29 | const backupRegexp = backupRegExpValues(); 30 | 31 | const regExp2 = /(\w+)\s(\w+)/; 32 | const string2 = 'qwerty zxcvbn'; 33 | string2.replace(regExp2, '$2, $1'); 34 | 35 | expect(RegExp.$1).toBe('qwerty'); 36 | expect(RegExp.$2).toBe('zxcvbn'); 37 | 38 | restoreRegExpValues(backupRegexp); 39 | 40 | expect(RegExp.$1).toBe('div'); 41 | expect(RegExp.$2).toBe('a'); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | import { runQunitPuppeteer, printFailedTests, printResultSummary } from 'node-qunit-puppeteer'; 5 | import { fileURLToPath } from 'node:url'; 6 | 7 | import { 8 | server, 9 | port, 10 | start, 11 | stop, 12 | } from './server'; 13 | 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | 17 | const TESTS_RUN_TIMEOUT = 30000; 18 | const TESTS_DIST = './dist'; 19 | const TEST_FILE_NAME_MARKER = '.html'; 20 | 21 | /** 22 | * Returns false if test failed and true if test passed 23 | * 24 | * @param {string} indexFile 25 | * @returns {Promise} 26 | */ 27 | const runQunit = async (indexFile) => { 28 | const qunitArgs = { 29 | targetUrl: `http://localhost:${port}/${indexFile}?test`, 30 | timeout: TESTS_RUN_TIMEOUT, 31 | // needed for logging to console while testing run via `pnpm test` 32 | // redirectConsole: true, 33 | puppeteerArgs: ['--no-sandbox', '--allow-file-access-from-files'], 34 | }; 35 | 36 | const result = await runQunitPuppeteer(qunitArgs); 37 | printResultSummary(result, console); 38 | if (result.stats.failed > 0) { 39 | printFailedTests(result, console); 40 | return false; 41 | } 42 | return true; 43 | }; 44 | 45 | const runQunitTests = async () => { 46 | const testServer = server.init(); 47 | 48 | await start(testServer, port); 49 | 50 | const dirPath = path.resolve(__dirname, TESTS_DIST); 51 | const testFiles = fs.readdirSync(dirPath, { encoding: 'utf8' }) 52 | .filter((el) => el.includes(TEST_FILE_NAME_MARKER)); 53 | 54 | let errorOccurred = false; 55 | let testsPassed = true; 56 | 57 | try { 58 | console.log('Running tests..'); 59 | 60 | // eslint-disable-next-line no-restricted-syntax 61 | for (const fileName of testFiles) { 62 | // \n is needed to divide logging 63 | console.log(`\nTesting ${fileName}:`); 64 | // eslint-disable-next-line no-await-in-loop 65 | const testPassed = await runQunit(fileName); 66 | testsPassed = testsPassed && testPassed; 67 | } 68 | } catch (e) { 69 | console.log(e); 70 | await stop(testServer); 71 | // do not fail all tests run if some test failed 72 | errorOccurred = true; 73 | } 74 | 75 | if (errorOccurred || !testsPassed) { 76 | process.exit(1); 77 | } 78 | 79 | await stop(testServer); 80 | }; 81 | 82 | export { 83 | runQunitTests, 84 | }; 85 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import './scriptlets/index.test'; 2 | import './redirects/index.test'; 3 | import './helpers/index.test'; 4 | -------------------------------------------------------------------------------- /tests/redirects/amazon-apstag.test.js: -------------------------------------------------------------------------------- 1 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'amazon-apstag'; 5 | 6 | let redirects; 7 | const before = async () => { 8 | redirects = await getRedirectsInstance(); 9 | }; 10 | 11 | module(name, { before }); 12 | 13 | test('Checking if alias name works', async (assert) => { 14 | const codeByAdgParams = redirects.getRedirect(name).content; 15 | const codeByUboParams = redirects.getRedirect('ubo-amazon_apstag.js').content; 16 | 17 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 18 | }); 19 | 20 | test('amazon-apstag: works', (assert) => { 21 | evalWrapper(redirects.getRedirect(name).content); 22 | 23 | assert.ok(window.apstag, 'window.apstag exists'); 24 | assert.ok(window.apstag.fetchBids, 'apstag.fetchBids exists'); 25 | assert.equal(window.apstag.init(), undefined, 'apstag.init() is mocked'); 26 | assert.equal(window.apstag.setDisplayBids(), undefined, 'apstag.setDisplayBids() is mocked'); 27 | assert.equal(window.apstag.targetingKeys(), undefined, 'apstag.targetingKeys() is mocked'); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/redirects/didomi-loader.test.js: -------------------------------------------------------------------------------- 1 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'didomi-loader'; 5 | 6 | let redirects; 7 | const before = async () => { 8 | redirects = await getRedirectsInstance(); 9 | }; 10 | 11 | module(name, { before }); 12 | 13 | test('Didomi-loader works', (assert) => { 14 | evalWrapper(redirects.getRedirect(name).content); 15 | 16 | assert.expect(9); 17 | const done1 = assert.async(); 18 | const done2 = assert.async(); 19 | 20 | const { 21 | Didomi, didomiState, didomiEventListeners, didomiOnReady, __tcfapi, 22 | } = window; 23 | // Main wrappers 24 | assert.strictEqual(typeof Didomi, 'object', 'Didomi mocked'); 25 | assert.strictEqual(typeof Didomi.getObservableOnUserConsentStatusForVendor(), 'object', 'Didomi mocked'); 26 | assert.strictEqual(typeof didomiState, 'object', 'didomiState mocked'); 27 | assert.strictEqual(typeof didomiEventListeners, 'object', 'didomiEventListeners mocked'); 28 | assert.strictEqual(typeof didomiOnReady, 'object', 'didomiOnReady mocked'); 29 | assert.strictEqual(typeof __tcfapi, 'function', '__tcfapi mocked'); 30 | // Callbacks 31 | assert.strictEqual(typeof didomiOnReady.push, 'function', 'didomiOnReady.push mocked'); 32 | didomiOnReady.push(() => { 33 | assert.ok(Didomi, 'callback is called'); 34 | done1(); 35 | }); 36 | __tcfapi('addEventListener', 1, (tcData) => { 37 | assert.ok(typeof tcData, 'object', 'callback is called'); 38 | done2(); 39 | }); 40 | __tcfapi('removeEventListener', 1, () => { 41 | throw new Error(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/redirects/fingerprintjs2.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'fingerprintjs2'; 6 | 7 | let redirects; 8 | const before = async () => { 9 | redirects = await getRedirectsInstance(); 10 | }; 11 | 12 | module(name, { before }); 13 | 14 | test('Checking if alias name works', (assert) => { 15 | const codeByAdgParams = redirects.getRedirect(name).content; 16 | const codeByUboParams = redirects.getRedirect('fingerprint2.js').content; 17 | 18 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 19 | }); 20 | 21 | test('Fingerprint2 works', (assert) => { 22 | evalWrapper(redirects.getRedirect(name).content); 23 | 24 | const done = assert.async(); 25 | 26 | assert.ok(window.Fingerprint2, 'Fingerprint2 object was created'); 27 | assert.notOk(window.Fingerprint2.get(), 'getter returns nothing'); 28 | 29 | const cb = () => { 30 | assert.ok(true, 'callback was executed'); 31 | done(); 32 | }; 33 | window.Fingerprint2.get([], cb); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/redirects/fingerprintjs3.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'fingerprintjs3'; 6 | 7 | let redirects; 8 | const before = async () => { 9 | redirects = await getRedirectsInstance(); 10 | }; 11 | 12 | module(name, { before }); 13 | 14 | test('Checking if alias name works', (assert) => { 15 | const codeByAdgParams = redirects.getRedirect(name).content; 16 | const codeByUboParams = redirects.getRedirect('ubo-fingerprint3.js').content; 17 | 18 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 19 | }); 20 | 21 | test('Fingerprint3 works', (assert) => { 22 | evalWrapper(redirects.getRedirect(name).content); 23 | 24 | assert.expect(6); 25 | 26 | const { FingerprintJS } = window; 27 | 28 | assert.ok(FingerprintJS, 'FingerprintJS object was created'); 29 | assert.strictEqual(FingerprintJS.hashComponents(), '', 'hashComponents() mocked'); 30 | FingerprintJS.load().then((response) => { 31 | assert.ok(response, 'load() request fulfilled'); 32 | }); 33 | FingerprintJS.get().then((response) => { 34 | assert.ok(response, 'get() request fulfilled'); 35 | assert.ok(response.visitorId, 'visitorId mocked'); 36 | assert.strictEqual(typeof response.visitorId, 'string', 'visitorId is a string'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/redirects/gemius.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'gemius'; 6 | 7 | let redirects; 8 | const before = async () => { 9 | redirects = await getRedirectsInstance(); 10 | }; 11 | 12 | module(name, { before }); 13 | 14 | test('Gemius works', (assert) => { 15 | evalWrapper(redirects.getRedirect(name).content); 16 | 17 | const gemius = new window.GemiusPlayer(); 18 | assert.ok(gemius, 'gemiusPlayer was created'); 19 | 20 | assert.notOk(gemius.setVideoObject(), 'setVideoObject function mocked'); 21 | assert.notOk(gemius.newProgram(), 'newProgram function mocked'); 22 | assert.notOk(gemius.programEvent(), 'programEvent function mocked'); 23 | assert.notOk(gemius.newAd(), 'newAd function mocked'); 24 | assert.notOk(gemius.adEvent(), 'adEvent function mocked'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/redirects/google-analytics-ga.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { clearGlobalProps, getRedirectsInstance, evalWrapper } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'google-analytics-ga'; 6 | 7 | const changingProps = ['_gat', '_gaq']; 8 | 9 | const afterEach = () => { 10 | clearGlobalProps(...changingProps); 11 | }; 12 | 13 | let redirects; 14 | const before = async () => { 15 | redirects = await getRedirectsInstance(); 16 | }; 17 | 18 | module(name, { afterEach, before }); 19 | 20 | test('Checking if alias name works', (assert) => { 21 | const codeByAdgParams = redirects.getRedirect(name).content; 22 | const codeByUboParams = redirects.getRedirect('ubo-google-analytics_ga.js').content; 23 | 24 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 25 | }); 26 | 27 | test('AdGuard Syntax _gat', (assert) => { 28 | evalWrapper(redirects.getRedirect(name).content); 29 | 30 | assert.ok(window._gat, '_gat object was created'); 31 | assert.notOk(window._gat._createTracker(), '_createTracker returns nothing'); 32 | 33 | const tracker = window._gat._getTracker(); 34 | assert.ok(typeof tracker === 'object', '_getTracker returns tracker object'); 35 | assert.notOk(tracker._addIgnoredOrganic(), 'checks _addIgnoredOrganic tracker method'); 36 | assert.notOk(tracker._setCookiePersistence(), 'checks _setCookiePersistence tracker method'); 37 | }); 38 | 39 | test('AdGuard Syntax _gaq', (assert) => { 40 | evalWrapper(redirects.getRedirect(name).content); 41 | 42 | assert.expect(5); 43 | 44 | assert.ok(window._gaq, '_gaq object was created'); 45 | assert.notOk(window._gaq._createAsyncTracker(), '_createAsyncTracker returns nothing'); 46 | window._gaq.push(() => assert.ok(true, 'push with cb runs it')); 47 | assert.notOk(window._gaq.push([]), 'push with array returns nothing'); 48 | 49 | // https://github.com/gorhill/uBlock/issues/2162 50 | const cb = () => assert.ok(true, 'hitCallback was executed'); 51 | window._gaq.push(['_set', 'hitCallback', cb]); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/redirects/index.test.js: -------------------------------------------------------------------------------- 1 | import './googlesyndication-adsbygoogle.test'; 2 | import './google-analytics.test'; 3 | import './google-analytics-ga.test'; 4 | import './googletagservices-gpt.test'; 5 | import './google-ima3.test'; 6 | import './gemius.test'; 7 | import './scorecardresearch-beacon.test'; 8 | import './metrika-yandex-watch.test'; 9 | import './metrika-yandex-tag.test'; 10 | import './amazon-apstag.test'; 11 | import './matomo.test'; 12 | import './fingerprintjs2.test'; 13 | import './fingerprintjs3.test'; 14 | import './ati-smarttag.test'; 15 | import './prevent-bab2.test'; 16 | import './prebid.test'; 17 | import './didomi-loader.test'; 18 | import './prebid-ads.test'; 19 | import './naver-wcslog.test'; 20 | import './pardot-1.0.test'; 21 | -------------------------------------------------------------------------------- /tests/redirects/matomo.test.js: -------------------------------------------------------------------------------- 1 | import { getRedirectsInstance, evalWrapper } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'matomo'; 5 | 6 | let redirects; 7 | const before = async () => { 8 | redirects = await getRedirectsInstance(); 9 | }; 10 | 11 | module(name, { before }); 12 | 13 | test('matomo works', (assert) => { 14 | evalWrapper(redirects.getRedirect(name).content); 15 | 16 | assert.ok(window.Piwik, 'window.Piwik exists'); 17 | assert.ok(window.Piwik.getTracker, 'Piwik.getTracker exists'); 18 | assert.ok(window.Piwik.getAsyncTracker, 'Piwik.getTracker exists'); 19 | 20 | // eslint-disable-next-line new-cap 21 | const tracker = new window.Piwik.getTracker(); 22 | assert.equal(tracker.setDoNotTrack(), undefined, 'getTracker.setDoNotTrack() is mocked.'); 23 | assert.equal(tracker.setDomains(), undefined, 'getTracker.setDomains() is mocked.'); 24 | assert.equal(tracker.setCustomDimension(), undefined, 'getTracker.setCustomDimension() is mocked.'); 25 | assert.equal(tracker.trackPageView(), undefined, 'getTracker.trackPageView() is mocked.'); 26 | 27 | // eslint-disable-next-line new-cap 28 | const asyncTracker = new window.Piwik.getAsyncTracker(); 29 | assert.equal(asyncTracker.addListener(), undefined, 'getAsyncTracker.addListener() is mocked.'); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/redirects/metrika-yandex-watch.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { getRedirectsInstance, evalWrapper } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'metrika-yandex-watch'; 6 | 7 | let redirects; 8 | const before = async () => { 9 | redirects = await getRedirectsInstance(); 10 | }; 11 | 12 | module(name, { before }); 13 | 14 | test('AdGuard: yandex metrika watch.js', (assert) => { 15 | assert.expect(13); 16 | 17 | // yandex_metrika_callbacks: these callbacks needed for 18 | // creating an instance of Ya.Metrika after script loading 19 | window.yandex_metrika_callbacks = [ 20 | () => assert.ok(true, 'yandex_metrika_callbacks were executed'), 21 | ]; 22 | 23 | evalWrapper(redirects.getRedirect(name).content); 24 | 25 | const { Metrika } = window.Ya; 26 | assert.ok(Metrika, 'Metrika function was created'); 27 | assert.ok(Metrika.counters().length === 0, 'Metrika.counters returns empty array'); 28 | 29 | const ya = new Metrika(); 30 | assert.notOk(ya.addFileExtension(), 'addFileExtension function created and executed'); 31 | assert.ok(ya.counters().length === 0, 'ya.counters returns empty array'); 32 | 33 | // no-options methods test 34 | assert.notOk(ya.addFileExtension(), 'addFileExtension function created and executed'); 35 | assert.notOk(ya.getClientID(), 'getClientID function created and executed'); 36 | assert.notOk(ya.setUserID(), 'setUserID function created and executed'); 37 | assert.notOk(ya.userParams(), 'userParams function created and executed'); 38 | assert.notOk(ya.params(), 'params function created and executed'); 39 | 40 | // reachGoal method test 41 | const done = assert.async(); 42 | function reachGoalCb() { 43 | // eslint-disable-next-line eqeqeq 44 | assert.ok(this == 123, 'context was changed'); 45 | assert.ok(true, 'callback passed in reachGoal method was executed'); 46 | done(); 47 | } 48 | ya.reachGoal('some target', 'some param', reachGoalCb, 123); 49 | 50 | const done1 = assert.async(); 51 | function extLinkCb() { 52 | // eslint-disable-next-line eqeqeq 53 | assert.ok(this == 123, 'extLinkCb was executed and context was changed'); 54 | done1(); 55 | } 56 | ya.extLink('some url', { callback: extLinkCb, ctx: 123 }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/redirects/naver-wcslog.test.js: -------------------------------------------------------------------------------- 1 | import { getRedirectsInstance, evalWrapper } from '../helpers'; 2 | import { isEmptyObject } from '../../src/helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'naver-wcslog'; 6 | 7 | let redirects; 8 | const before = async () => { 9 | redirects = await getRedirectsInstance(); 10 | }; 11 | 12 | module(name, { before }); 13 | 14 | test('wcslog works', (assert) => { 15 | evalWrapper(redirects.getRedirect(name).content); 16 | 17 | assert.ok(window.wcs, 'wcs created'); 18 | assert.ok(window.wcs.inflow() === undefined, 'wcs.inflow mocked'); 19 | assert.ok(isEmptyObject(window.wcs_add), 'wcs_add mocked'); 20 | assert.ok(window.wcs_do() === undefined, 'wcs_do mocked'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/redirects/noeval.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-console, no-underscore-dangle */ 2 | 3 | import { getRedirectsInstance, evalWrapper } from '../helpers'; 4 | 5 | const { test, module } = QUnit; 6 | const name = 'noeval'; 7 | 8 | const nativeEval = window.eval; 9 | const nativeConsole = console.log; 10 | 11 | const afterEach = () => { 12 | window.eval = nativeEval; 13 | console.log = nativeConsole; 14 | }; 15 | 16 | let redirects; 17 | const before = async () => { 18 | redirects = await getRedirectsInstance(); 19 | }; 20 | 21 | module(name, { afterEach, before }); 22 | 23 | test('Checking if alias name works', (assert) => { 24 | const codeByAdgParams = redirects.getRedirect(name).content; 25 | const codeByUboParams = redirects.getRedirect('ubo-silent-noeval.js').content; 26 | 27 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 28 | }); 29 | 30 | test('AG noeval alias', (assert) => { 31 | evalWrapper(redirects.getRedirect(name).content); 32 | 33 | const evalStr = '2'; 34 | 35 | // set assertions amount 36 | assert.expect(2); 37 | 38 | console.log = function log(input) { 39 | if (input.includes('trace')) { 40 | return; 41 | } 42 | assert.ok(input.includes(`${name}: AdGuard has prevented eval:`), 'console.hit should print info'); 43 | }; 44 | 45 | const innerEvalWrapper = eval; 46 | const actual = innerEvalWrapper(evalStr); 47 | 48 | assert.strictEqual(actual, undefined, 'result of eval evaluation should be undefined'); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/redirects/pardot-1.0.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { clearGlobalProps, getRedirectsInstance, evalWrapper } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'pardot-1.0'; 6 | 7 | let redirects; 8 | const before = async () => { 9 | redirects = await getRedirectsInstance(); 10 | }; 11 | 12 | module(name, { before }); 13 | 14 | test('pardot works', (assert) => { 15 | evalWrapper(redirects.getRedirect(name).content); 16 | 17 | const { 18 | piVersion, 19 | piScriptNum, 20 | piScriptObj, 21 | checkNamespace, 22 | getPardotUrl, 23 | piGetParameter, 24 | piSetCookie, 25 | piGetCookie, 26 | piResponse, 27 | piTracker, 28 | } = window; 29 | 30 | assert.strictEqual(piTracker(), undefined, 'piTracker mocked'); 31 | assert.strictEqual(piResponse(), undefined, 'piResponse mocked'); 32 | 33 | assert.strictEqual(checkNamespace(), undefined, 'checkNamespace mocked'); 34 | assert.strictEqual(getPardotUrl(), '', 'getPardotUrl mocked'); 35 | assert.strictEqual(piGetParameter(), null, 'piGetParameter mocked'); 36 | assert.strictEqual(piSetCookie(), undefined, 'piSetCookie mocked'); 37 | assert.strictEqual(piGetCookie(), '', 'piGetCookie mocked'); 38 | 39 | assert.ok(piVersion); 40 | assert.ok(piScriptObj); 41 | assert.strictEqual(piScriptNum, 1); 42 | 43 | clearGlobalProps('GemiusPlayer'); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/redirects/prebid-ads.test.js: -------------------------------------------------------------------------------- 1 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'prebid-ads'; 5 | 6 | let redirects; 7 | const before = async () => { 8 | redirects = await getRedirectsInstance(); 9 | }; 10 | 11 | module(name, { before }); 12 | 13 | test('constants are set', (assert) => { 14 | evalWrapper(redirects.getRedirect(name).content); 15 | 16 | assert.true(window.canRunAds, 'window.canRunAds created'); 17 | assert.false(window.isAdBlockActive, 'Piwik.isAdBlockActive created'); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/redirects/prebid.test.js: -------------------------------------------------------------------------------- 1 | import { evalWrapper, getRedirectsInstance } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'prebid'; 5 | 6 | let redirects; 7 | const before = async () => { 8 | redirects = await getRedirectsInstance(); 9 | }; 10 | 11 | module(name, { before }); 12 | 13 | test('Prebid mocked', (assert) => { 14 | evalWrapper(redirects.getRedirect(name).content); 15 | 16 | const { pbjs } = window; 17 | 18 | assert.ok(pbjs, 'window.pbjs exists'); 19 | assert.ok(pbjs.cmd, 'pbjs.cmd exists'); 20 | assert.ok(pbjs.que, 'pbjs.que exists'); 21 | 22 | assert.strictEqual(typeof pbjs.addAdUnits, 'function', 'addAdUnits mocked'); 23 | assert.strictEqual(pbjs.adServers.dfp.buildVideoUrl(), '', 'buildVideoUrl mocked'); 24 | assert.ok(Array.isArray(pbjs.adUnits), 'adUnits mocked'); 25 | assert.strictEqual(typeof pbjs.aliasBidder, 'function', 'aliasBidder mocked'); 26 | assert.ok(Array.isArray(pbjs.cmd), 'cmd mocked'); 27 | assert.strictEqual(typeof pbjs.cmd.push, 'function', 'cmd.push mocked'); 28 | assert.strictEqual(typeof pbjs.enableAnalytics, 'function', 'enableAnalytics mocked'); 29 | assert.ok(Array.isArray(pbjs.getHighestCpmBids()), 'getHighestCpmBids mocked'); 30 | assert.true(pbjs.libLoaded, 'libLoaded mocked'); 31 | assert.ok(Array.isArray(pbjs.que), 'que mocked'); 32 | assert.strictEqual(typeof pbjs.que.push, 'function', 'que.push mocked'); 33 | assert.strictEqual(typeof pbjs.requestBids, 'function', 'requestBids mocked'); 34 | assert.strictEqual(typeof pbjs.removeAdUnit, 'function', 'removeAdUnit mocked'); 35 | assert.strictEqual(typeof pbjs.setBidderConfig, 'function', 'setBidderConfig mocked'); 36 | assert.strictEqual(typeof pbjs.setConfig, 'function', 'setConfig mocked'); 37 | assert.strictEqual(typeof pbjs.setTargetingForGPTAsync, 'function', 'setTargetingForGPTAsync mocked'); 38 | 39 | const bid = { 40 | bidsBackHandler() { 41 | assert.true(true, 'requestBids callback mocked'); 42 | }, 43 | }; 44 | pbjs.requestBids(bid); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/redirects/prevent-bab2.test.js: -------------------------------------------------------------------------------- 1 | import { getRedirectsInstance } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'prevent-bab2'; 5 | 6 | let redirects; 7 | const before = async () => { 8 | redirects = await getRedirectsInstance(); 9 | }; 10 | 11 | module(name, { before }); 12 | 13 | test('Checking if alias name works', (assert) => { 14 | const codeByAdgParams = redirects.getRedirect(name).content; 15 | const codeByUboParams = redirects.getRedirect('nobab2.js').content; 16 | 17 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/redirects/scorecardresearch-beacon.test.js: -------------------------------------------------------------------------------- 1 | import { clearGlobalProps, getRedirectsInstance, evalWrapper } from '../helpers'; 2 | 3 | const { test, module } = QUnit; 4 | const name = 'scorecardresearch-beacon'; 5 | 6 | const changingProps = ['COMSCORE', '_comscore']; 7 | 8 | const afterEach = () => { 9 | clearGlobalProps(...changingProps); 10 | }; 11 | 12 | let redirects; 13 | const before = async () => { 14 | redirects = await getRedirectsInstance(); 15 | }; 16 | 17 | module(name, { before, afterEach }); 18 | 19 | test('Checking if alias name works', (assert) => { 20 | const codeByAdgParams = redirects.getRedirect(name).content; 21 | const codeByUboParams = redirects.getRedirect('ubo-scorecardresearch_beacon.js').content; 22 | 23 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 24 | }); 25 | 26 | test('AdGuard Syntax', (assert) => { 27 | evalWrapper(redirects.getRedirect(name).content); 28 | 29 | assert.ok(window.COMSCORE, 'COMSCORE object was created'); 30 | window.COMSCORE.purge(); 31 | // eslint-disable-next-line no-underscore-dangle 32 | assert.strictEqual(window._comscore.length, 0, 'purge function reset _compscore var to []'); 33 | assert.notOk(window.COMSCORE.beacon(), 'becacon function was mocked'); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/scriptlets-entrypoint.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | 3 | // Dist directory will be available after scriptlets build 4 | import { scriptlets } from '../dist/index'; 5 | import { Redirects } from '../dist/redirects/index'; 6 | 7 | window.scriptlets = scriptlets; 8 | window.Redirects = Redirects; 9 | -------------------------------------------------------------------------------- /tests/scriptlets/call-nothrow.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle, no-console */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'call-nothrow'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('hit', '__debug'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | test('Checking if alias name works', (assert) => { 20 | const adgParams = { 21 | name, 22 | engine: 'test', 23 | verbose: true, 24 | }; 25 | const uboParams = { 26 | name: 'ubo-call-nothrow.js', 27 | engine: 'test', 28 | verbose: true, 29 | }; 30 | 31 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 32 | const codeByUboParams = window.scriptlets.invoke(uboParams); 33 | 34 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 35 | }); 36 | 37 | test('call-nothrow - JSON.parse', (assert) => { 38 | let testPassed; 39 | 40 | runScriptlet(name, ['JSON.parse']); 41 | 42 | // JSON.parse('foo') throws an error, 43 | // so scriptlet should catch it and testPassed should be true 44 | try { 45 | JSON.parse('foo'); 46 | testPassed = true; 47 | } catch (e) { 48 | testPassed = false; 49 | } 50 | assert.strictEqual(testPassed, true, 'testPassed set to true'); 51 | assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); 52 | }); 53 | 54 | test('call-nothrow - Object.defineProperty', (assert) => { 55 | let testPassed; 56 | const foo = {}; 57 | Object.defineProperty(foo, 'bar', { value: true }); 58 | 59 | runScriptlet(name, ['Object.defineProperty']); 60 | 61 | // Redefining foo.bar should throw an error, 62 | // so scriptlet should catch it and testPassed should be true 63 | try { 64 | Object.defineProperty(foo, 'bar', { value: false }); 65 | testPassed = true; 66 | } catch (e) { 67 | testPassed = false; 68 | } 69 | assert.strictEqual(testPassed, true, 'testPassed set to true'); 70 | assert.strictEqual(foo.bar, true, 'foo.bar set to true'); 71 | assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/scriptlets/close-window.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'close-window'; 6 | 7 | const nativeWindowClose = window.close; 8 | 9 | const TEST_PROP = 'run'; 10 | 11 | const mockedWindowClose = () => { 12 | window[TEST_PROP] = true; 13 | }; 14 | 15 | const beforeEach = () => { 16 | window.__debug = () => { 17 | window.hit = 'value'; 18 | }; 19 | window.close = mockedWindowClose; 20 | window[TEST_PROP] = false; 21 | }; 22 | 23 | const afterEach = () => { 24 | window.close = nativeWindowClose; 25 | clearGlobalProps('hit', '__debug', TEST_PROP); 26 | }; 27 | 28 | module(name, { beforeEach, afterEach }); 29 | 30 | test('works: no args', (assert) => { 31 | assert.equal(window.hit, undefined, 'Hit function not executed yet'); 32 | 33 | const scriptletArgs = ['']; 34 | runScriptlet(name, scriptletArgs); 35 | 36 | assert.equal(window.hit, 'value', 'Hit function was executed'); 37 | // scriptlet calls window.close which is mocked for test purposes 38 | assert.strictEqual(window[TEST_PROP], true, 'mocked window.close() has been called'); 39 | }); 40 | 41 | // TODO fix test running in browserstack 42 | // test('works: string path', (assert) => { 43 | // assert.equal(window.hit, undefined, 'Hit function not executed yet'); 44 | // 45 | // const scriptletArgs = ['test']; 46 | // runScriptlet(name, scriptletArgs); 47 | // 48 | // assert.equal(window.hit, 'value', 'Hit function was executed'); 49 | // // scriptlet calls window.close which is mocked for test purposes 50 | // assert.strictEqual(window[TEST_PROP], true, 'mocked window.close() has been called'); 51 | // }); 52 | 53 | test('does not work: window.close is not a function', (assert) => { 54 | assert.equal(window.hit, undefined, 'Hit function not executed yet'); 55 | 56 | window.close = { 57 | isFunction: false, 58 | }; 59 | 60 | const scriptletArgs = ['']; 61 | runScriptlet(name, scriptletArgs); 62 | 63 | assert.equal(window.hit, undefined, 'Hit should not be executed'); 64 | assert.strictEqual(window[TEST_PROP], false, 'mockedWindowClose() was not called'); 65 | }); 66 | 67 | test('does not work: invalid regexp pattern', (assert) => { 68 | assert.equal(window.hit, undefined, 'Hit function not executed yet'); 69 | 70 | const scriptletArgs = ['/*/']; 71 | runScriptlet(name, scriptletArgs); 72 | 73 | assert.equal(window.hit, undefined, 'Hit should not be executed'); 74 | assert.strictEqual(window[TEST_PROP], false, 'mocked window.close() was not called'); 75 | }); 76 | -------------------------------------------------------------------------------- /tests/scriptlets/debug-on-property-read.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle, no-console */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'debug-on-property-read'; 6 | const PROPERTY = 'aaa'; 7 | const CHAIN_PROPERTY = 'aaa.bbb'; 8 | 9 | const changingProps = [PROPERTY, 'hit', '__debug']; 10 | 11 | const beforeEach = () => { 12 | window.__debug = () => { 13 | window.hit = 'FIRED'; 14 | }; 15 | }; 16 | 17 | const afterEach = () => { 18 | clearGlobalProps(...changingProps); 19 | }; 20 | 21 | module(name, { beforeEach, afterEach }); 22 | 23 | test('simple', (assert) => { 24 | window[PROPERTY] = 'value'; 25 | const scriptletArgs = [PROPERTY]; 26 | runScriptlet(name, scriptletArgs); 27 | 28 | console.log(window[PROPERTY]); 29 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 30 | }); 31 | 32 | test('dot notation', (assert) => { 33 | window.aaa = { 34 | bbb: 'value', 35 | }; 36 | const scriptletArgs = [CHAIN_PROPERTY]; 37 | runScriptlet(name, scriptletArgs); 38 | 39 | console.log(window.aaa.bbb); 40 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 41 | }); 42 | 43 | test('dot notation deferred defenition', (assert) => { 44 | window.aaa = {}; 45 | window.aaa.bbb = 'value'; 46 | const scriptletArgs = [CHAIN_PROPERTY]; 47 | runScriptlet(name, scriptletArgs); 48 | 49 | console.log(window.aaa.bbb); 50 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 51 | }); 52 | 53 | test('works with an empty object in chain', (assert) => { 54 | const scriptletArgs = [CHAIN_PROPERTY]; 55 | 56 | window.aaa = {}; 57 | runScriptlet(name, scriptletArgs); 58 | window.aaa.bbb = 'value'; 59 | 60 | console.log(window.aaa.bbb); 61 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/scriptlets/debug-on-property-write.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'debug-on-property-write'; 6 | const PROPERTY = 'aaa'; 7 | const CHAIN_PROPERTY = 'aaa.bbb'; 8 | 9 | const changingProps = [PROPERTY, 'hit', '__debug']; 10 | 11 | const beforeEach = () => { 12 | window.__debug = () => { 13 | window.hit = 'FIRED'; 14 | }; 15 | }; 16 | 17 | const afterEach = () => { 18 | clearGlobalProps(...changingProps); 19 | }; 20 | 21 | module(name, { beforeEach, afterEach }); 22 | 23 | test('set prop for existed prop', (assert) => { 24 | window[PROPERTY] = 'value'; 25 | const scriptletArgs = [PROPERTY]; 26 | runScriptlet(name, scriptletArgs); 27 | 28 | window[PROPERTY] = 'new value'; 29 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 30 | }); 31 | 32 | test('dot notation', (assert) => { 33 | window.aaa = { 34 | bbb: 'value', 35 | }; 36 | const scriptletArgs = [CHAIN_PROPERTY]; 37 | runScriptlet(name, scriptletArgs); 38 | 39 | window.aaa.bbb = 'new value'; 40 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 41 | }); 42 | 43 | test('dot notation deferred defenition', (assert) => { 44 | const scriptletArgs = [CHAIN_PROPERTY]; 45 | runScriptlet(name, scriptletArgs); 46 | 47 | window.aaa = {}; 48 | window.aaa.bbb = 'new value'; 49 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 50 | }); 51 | 52 | test('works with an empty object in chain', (assert) => { 53 | const scriptletArgs = [CHAIN_PROPERTY]; 54 | 55 | window.aaa = {}; 56 | runScriptlet(name, scriptletArgs); 57 | window.aaa.bbb = 'value'; 58 | 59 | window.aaa.bbb = 'new value'; 60 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/scriptlets/dir-string.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'dir-string'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('hit', '__debug'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | test('Adg rule times = 2', (assert) => { 20 | const scriptletArgs = [2]; 21 | runScriptlet(name, scriptletArgs); 22 | 23 | // eslint-disable-next-line no-console 24 | console.dir({}); 25 | assert.strictEqual(window.hit, 'FIRED', 'Console dir was updated and invoked'); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/scriptlets/disable-newtab-links.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'disable-newtab-links'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('hit', '__debug'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | const createLink = () => { 20 | const elem = document.createElement('a'); 21 | elem.setAttribute('target', '_blank'); 22 | document.body.appendChild(elem); 23 | 24 | return elem; 25 | }; 26 | 27 | test('Checking if alias name works', (assert) => { 28 | const adgParams = { 29 | name, 30 | engine: 'test', 31 | verbose: true, 32 | }; 33 | const uboParams = { 34 | name: 'ubo-disable-newtab-links.js', 35 | engine: 'test', 36 | verbose: true, 37 | }; 38 | 39 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 40 | const codeByUboParams = window.scriptlets.invoke(uboParams); 41 | 42 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 43 | }); 44 | 45 | test('adg works', (assert) => { 46 | runScriptlet(name); 47 | 48 | const elem = createLink(); 49 | elem.click(); 50 | 51 | const done = assert.async(); 52 | setTimeout(() => { 53 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 54 | done(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/scriptlets/fetch-objects/test01.json: -------------------------------------------------------------------------------- 1 | { 2 | "a1": 1, 3 | "b2": "test", 4 | "c3": 3 5 | } -------------------------------------------------------------------------------- /tests/scriptlets/fetch-objects/test02.json: -------------------------------------------------------------------------------- 1 | { 2 | "ad": 1, 3 | "src": "test", 4 | "count": 3 5 | } -------------------------------------------------------------------------------- /tests/scriptlets/fetch-objects/test03.json: -------------------------------------------------------------------------------- 1 | { 2 | "aa": 1, 3 | "bb": "test", 4 | "cc": { 5 | "id": 0, 6 | "src": "example.org", 7 | "arr": [ 8 | { "inner1": 123 }, 9 | { "inner2": 456 } 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /tests/scriptlets/fetch-objects/test04.json: -------------------------------------------------------------------------------- 1 | { 2 | "ab": "test", 3 | "bc": 123, 4 | "cc1": { 5 | "id": 0, 6 | "src": "example.org", 7 | "arr": [ 8 | { "inner1": 123 }, 9 | { "inner2": 456 } 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /tests/scriptlets/fetch-objects/test05.json: -------------------------------------------------------------------------------- 1 | { 2 | "ax": 1, 3 | "xx": { 4 | "id": 1, 5 | "nested": { 6 | "inner1": 123 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/scriptlets/fetch-objects/test06.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "content": [ 4 | { 5 | "id": 0, 6 | "source": "example.com" 7 | }, 8 | { 9 | "id": 1, 10 | "source": "example.org" 11 | } 12 | ], 13 | "state": { 14 | "ready": true 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /tests/scriptlets/index.test.js: -------------------------------------------------------------------------------- 1 | import './abort-on-property-read.test'; 2 | import './abort-on-property-write.test'; 3 | import './prevent-setTimeout.test'; 4 | import './prevent-setInterval.test'; 5 | import './prevent-window-open.test'; 6 | import './abort-current-inline-script.test'; 7 | import './set-constant.test'; 8 | import './set-attr.test'; 9 | import './prevent-addEventListener.test'; 10 | import './prevent-bab.test'; 11 | import './nowebrtc.test'; 12 | import './log-addEventListener.test'; 13 | import './log-eval.test'; 14 | import './log.test'; 15 | import './noeval.test'; 16 | import './prevent-eval-if.test'; 17 | import './prevent-fab-3.2.0.test'; 18 | import './set-popads-dummy.test'; 19 | import './prevent-popads-net.test'; 20 | import './prevent-adfly.test'; 21 | import './debug-on-property-read.test'; 22 | import './debug-on-property-write.test'; 23 | import './debug-current-inline-script.test'; 24 | import './remove-attr.test'; 25 | import './remove-class.test'; 26 | import './disable-newtab-links.test'; 27 | import './adjust-setInterval.test'; 28 | import './adjust-setTimeout.test'; 29 | import './dir-string.test'; 30 | import './json-prune.test'; 31 | import './prevent-requestAnimationFrame.test'; 32 | import './set-cookie.test'; 33 | import './remove-cookie.test'; 34 | import './hide-in-shadow-dom.test'; 35 | import './remove-in-shadow-dom.test'; 36 | import './prevent-fetch.test'; 37 | import './prevent-xhr.test'; 38 | import './set-local-storage-item.test'; 39 | import './set-session-storage-item.test'; 40 | import './abort-on-stack-trace.test'; 41 | import './log-on-stack-trace.test'; 42 | import './close-window.test'; 43 | import './prevent-refresh.test'; 44 | import './prevent-element-src-loading.test'; 45 | import './no-topics.test'; 46 | import './trusted-replace-xhr-response.test'; 47 | import './xml-prune.test'; 48 | import './m3u-prune.test'; 49 | import './trusted-click-element.test'; 50 | import './trusted-click-element.spec'; 51 | import './trusted-set-cookie.test'; 52 | import './trusted-replace-fetch-response.test'; 53 | import './trusted-set-local-storage-item.test'; 54 | import './trusted-set-session-storage-item.test'; 55 | import './trusted-set-constant.test'; 56 | import './inject-css-in-shadow-dom.test'; 57 | import './remove-node-text.test'; 58 | import './trusted-replace-node-text.test'; 59 | import './trusted-prune-inbound-object.test'; 60 | import './trusted-suppress-native-method.test'; 61 | import './prevent-canvas.test'; 62 | -------------------------------------------------------------------------------- /tests/scriptlets/log-eval.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-console, no-new-func, no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'log-eval'; 6 | 7 | const nativeEval = window.eval; 8 | const nativeFunction = window.Function; 9 | const nativeConsole = console.log; 10 | 11 | const afterEach = () => { 12 | window.Function = nativeFunction; 13 | console.log = nativeConsole; 14 | window.eval = nativeEval; 15 | clearGlobalProps('hit', '__debug'); 16 | }; 17 | 18 | const beforeEach = () => { 19 | window.__debug = () => { 20 | window.hit = 'FIRED'; 21 | }; 22 | }; 23 | 24 | module(name, { afterEach, beforeEach }); 25 | 26 | test('logs eval calls', (assert) => { 27 | const agLogEval = 'agLogEval'; 28 | 29 | const evalStr = `(function () {window.${agLogEval} = 'changed';})()`; 30 | 31 | console.log = function log(input) { 32 | if (input.includes('trace')) { 33 | return; 34 | } 35 | assert.strictEqual(input, `${name}: eval("${evalStr}")`, 'console.hit input should be equal'); 36 | }; 37 | runScriptlet(name); 38 | const evalWrap = eval; 39 | evalWrap(evalStr); 40 | assert.strictEqual(window[agLogEval], 'changed', 'function in eval executed as expected'); 41 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 42 | 43 | clearGlobalProps(agLogEval); 44 | }); 45 | 46 | test('logs new Function() calls', (assert) => { 47 | const agLogFunction = 'agLogFunction'; 48 | 49 | const args = ['propName', 'propValue', 'window[propName] = propValue']; 50 | 51 | console.log = function log(input) { 52 | if (input.includes('trace')) { 53 | return; 54 | } 55 | assert.strictEqual(input, `${name}: new Function(${args.join(', ')})`, 'console.hit input should be equal'); 56 | }; 57 | 58 | runScriptlet(name); 59 | const func = new Function(...args); 60 | func(agLogFunction, 'changed'); 61 | 62 | assert.strictEqual(window[agLogFunction], 'changed', 'function in function call executed as expected'); 63 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 64 | clearGlobalProps(agLogFunction); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/scriptlets/log.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-new-func, no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'log'; 6 | 7 | const nativeConsole = console.log; 8 | 9 | const afterEach = () => { 10 | console.log = nativeConsole; 11 | clearGlobalProps('hit', '__debug'); 12 | }; 13 | 14 | const beforeEach = () => { }; 15 | 16 | module(name, { afterEach, beforeEach }); 17 | 18 | test('log scriptlet', (assert) => { 19 | const TEST_ARGS = ['arg1', 'arg2']; 20 | 21 | console.log = function log(input) { 22 | assert.ok(input instanceof Array); 23 | TEST_ARGS.forEach((el) => assert.ok(input.includes(el))); 24 | }; 25 | 26 | runScriptlet(name, TEST_ARGS); 27 | }); 28 | 29 | test('checking if alias name works', (assert) => { 30 | const adgParams = { 31 | name, 32 | engine: 'test', 33 | verbose: true, 34 | }; 35 | 36 | const abpParams = { 37 | name: 'abp-log', 38 | engine: 'test', 39 | verbose: true, 40 | }; 41 | 42 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 43 | const codeByAbpParams = window.scriptlets.invoke(abpParams); 44 | 45 | assert.strictEqual(codeByAdgParams, codeByAbpParams, 'abp name - ok'); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/scriptlets/no-protected-audience.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | import { 4 | noopStr, 5 | noopFunc, 6 | noopResolveVoid, 7 | noopResolveNull, 8 | } from '../../src/helpers/index'; 9 | 10 | const { test, module } = QUnit; 11 | const name = 'no-protected-audience'; 12 | 13 | const protectedAudienceMethods = { 14 | joinAdInterestGroup: noopResolveVoid, 15 | runAdAuction: noopResolveNull, 16 | leaveAdInterestGroup: noopResolveVoid, 17 | clearOriginJoinedAdInterestGroups: noopResolveVoid, 18 | createAuctionNonce: noopStr, 19 | updateAdInterestGroups: noopFunc, 20 | }; 21 | 22 | const methodNames = Object.keys(protectedAudienceMethods); 23 | 24 | const beforeEach = () => { 25 | window.__debug = () => { 26 | window.hit = 'FIRED'; 27 | }; 28 | }; 29 | 30 | const afterEach = () => { 31 | clearGlobalProps('hit', '__debug'); 32 | }; 33 | 34 | module(name, { beforeEach, afterEach }); 35 | 36 | test('no-protected-audience', async (assert) => { 37 | runScriptlet(name); 38 | 39 | /** 40 | * TODO remove this check when Protected Audience API gets to puppeteer/playwright. 41 | * This works with --gui option though. 42 | */ 43 | if (Navigator.prototype.joinAdInterestGroup instanceof Function) { 44 | const done = assert.async(methodNames.length); 45 | for (const methodName of methodNames) { 46 | const mockMethod = protectedAudienceMethods[methodName]; 47 | const navigatorMethod = Navigator.prototype[methodName]; 48 | 49 | if (mockMethod() instanceof Promise) { 50 | const originalValue = await mockMethod(); 51 | const navigatorValue = await navigatorMethod(); 52 | 53 | assert.strictEqual(navigatorValue, originalValue, `async ${methodName} was successfully stubbed`); 54 | done(); 55 | } else { 56 | const originalValue = mockMethod(); 57 | const navigatorValue = navigatorMethod(); 58 | 59 | assert.strictEqual(navigatorValue, originalValue, `${methodName} was successfully stubbed`); 60 | done(); 61 | } 62 | } 63 | } 64 | 65 | assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/scriptlets/no-topics.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'no-topics'; 6 | const TOPICS_PROPERTY_NAME = 'browsingTopics'; 7 | 8 | const beforeEach = () => { 9 | window.__debug = () => { 10 | window.hit = 'FIRED'; 11 | }; 12 | }; 13 | 14 | const clearTopicsProp = () => { 15 | try { 16 | delete Document.prototype[TOPICS_PROPERTY_NAME]; 17 | } catch (e) { 18 | // Safari does not allow to delete property 19 | Document.prototype[TOPICS_PROPERTY_NAME] = null; 20 | } 21 | }; 22 | 23 | const afterEach = () => { 24 | clearGlobalProps('hit', '__debug'); 25 | clearTopicsProp(); 26 | }; 27 | 28 | module(name, { beforeEach, afterEach }); 29 | 30 | test('no-topics - works', async (assert) => { 31 | Document.prototype[TOPICS_PROPERTY_NAME] = () => ['value1', 'value2']; 32 | const mockedDocument = new Document(); 33 | 34 | const done = assert.async(); 35 | runScriptlet(name); 36 | 37 | const response = await mockedDocument[TOPICS_PROPERTY_NAME](); 38 | const body = await response.json(); 39 | 40 | assert.ok(Array.isArray(body) && body.length === 0, 'mocked browsingTopics() returns []'); 41 | 42 | assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); 43 | done(); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/scriptlets/noeval.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-console, no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'noeval'; 6 | 7 | const nativeEval = window.eval; 8 | const nativeConsole = console.log; 9 | 10 | const beforeEach = () => { 11 | window.__debug = () => { 12 | window.hit = 'FIRED'; 13 | }; 14 | }; 15 | 16 | const afterEach = () => { 17 | clearGlobalProps('hit', '__debug'); 18 | window.eval = nativeEval; 19 | console.log = nativeConsole; 20 | }; 21 | 22 | module(name, { beforeEach, afterEach }); 23 | 24 | test('Checking if alias name works', (assert) => { 25 | const adgParams = { 26 | name, 27 | engine: 'test', 28 | verbose: true, 29 | }; 30 | const uboParams = { 31 | name: 'ubo-noeval.js', 32 | engine: 'test', 33 | verbose: true, 34 | }; 35 | 36 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 37 | const codeByUboParams = window.scriptlets.invoke(uboParams); 38 | 39 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 40 | }); 41 | 42 | test('AG noeval alias', (assert) => { 43 | runScriptlet(name); 44 | 45 | const evalStr = '2'; 46 | 47 | // set assertions amount 48 | assert.expect(3); 49 | 50 | console.log = function log(input) { 51 | if (input.includes('trace')) { 52 | return; 53 | } 54 | assert.ok(input.includes(`${name}: AdGuard has prevented eval:`), 'console.hit should print info'); 55 | }; 56 | 57 | const evalWrapper = eval; 58 | const actual = evalWrapper(evalStr); 59 | 60 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 61 | assert.strictEqual(actual, undefined, 'result of eval evaluation should be undefined'); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/scriptlets/prevent-adfly.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'prevent-adfly'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('__debug', 'hit'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | test('ag works', (assert) => { 20 | assert.expect(2); 21 | const nativeDefineProperty = Object.defineProperty; 22 | Object.defineProperty = (arg1, arg2) => { 23 | assert.strictEqual(arg1, window, 'Object.defineProperty should be called with window'); 24 | assert.strictEqual(arg2, 'ysmm', 'Object.defineProperty should be called with "ysmm"'); 25 | }; 26 | 27 | runScriptlet(name); 28 | Object.defineProperty = nativeDefineProperty; 29 | }); 30 | -------------------------------------------------------------------------------- /tests/scriptlets/prevent-bab.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'prevent-bab'; 6 | 7 | const testProp = 'evalProp'; 8 | 9 | const nativeEval = window.eval; 10 | 11 | const beforeEach = () => { 12 | window.__debug = () => { 13 | window.hit = 'FIRED'; 14 | }; 15 | }; 16 | 17 | const afterEach = () => { 18 | clearGlobalProps(testProp, 'hit', '__debug'); 19 | window.eval = nativeEval; 20 | }; 21 | 22 | module(name, { beforeEach, afterEach }); 23 | 24 | test('works eval with AdblockBlock', (assert) => { 25 | const originalEvalString = window.eval.toString(); 26 | 27 | runScriptlet(name); 28 | 29 | const evalWrap = eval; 30 | evalWrap(`(function test() { const temp = 'babasbm'; window.${testProp} = 'test';})()`); 31 | 32 | assert.strictEqual(window[testProp], undefined); 33 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 34 | assert.strictEqual(window.eval.toString(), originalEvalString, 'eval.toString() should return original value'); 35 | }); 36 | 37 | test('sample eval script works', (assert) => { 38 | runScriptlet(name); 39 | 40 | const evalWrap = eval; 41 | evalWrap(`(function test() { const temp = 'temp'; window.${testProp} = 'test';})()`); 42 | 43 | assert.strictEqual(window[testProp], 'test'); 44 | assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); 45 | }); 46 | 47 | test('non-string eval works', (assert) => { 48 | runScriptlet(name); 49 | 50 | const evalWrap = eval; 51 | 52 | assert.strictEqual(evalWrap(2), 2); 53 | assert.strictEqual(evalWrap(2 + 2), 4); 54 | assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); 55 | }); 56 | 57 | test('prevents set timeout with AdblockBlock', (assert) => { 58 | runScriptlet(name); 59 | 60 | const func = `(function test(id) {window.${testProp} = 'test'})(test.bab_elementid)`; 61 | setTimeout(func); // eslint-disable-line no-implied-eval 62 | 63 | const done = assert.async(); 64 | 65 | setTimeout(() => { 66 | assert.strictEqual(window.hit, 'FIRED', 'hit fired'); 67 | assert.strictEqual(window[testProp], undefined); 68 | clearGlobalProps(testProp); 69 | done(); 70 | }, 20); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/scriptlets/prevent-fab-3.2.0.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-multi-assign, func-names, no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'prevent-fab-3.2.0'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('hit', 'FuckAdBlock', 'BlockAdBlock', 'fuckAdBlock', 'blockAdBlock', '__debug'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | const createFuckAdBlockSample = () => { 20 | function FuckAdBlock() {} 21 | FuckAdBlock.prototype.onDetected = function (fn) { 22 | fn(); 23 | return this; 24 | }; 25 | FuckAdBlock.prototype.check = function () { 26 | return 'true'; 27 | }; 28 | window.FuckAdBlock = window.BlockAdBlock = FuckAdBlock; 29 | window.fuckAdBlock = window.blockAdBlock = new FuckAdBlock(); 30 | }; 31 | 32 | test('Checking if alias name works', (assert) => { 33 | const adgParams = { 34 | name, 35 | engine: 'test', 36 | verbose: true, 37 | }; 38 | const uboParams = { 39 | name: 'ubo-nofab.js', 40 | engine: 'test', 41 | verbose: true, 42 | }; 43 | 44 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 45 | const codeByUboParams = window.scriptlets.invoke(uboParams); 46 | 47 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 48 | }); 49 | 50 | test('ag works', (assert) => { 51 | const agFuckAdBlock = 'agFuckAdBlock'; 52 | createFuckAdBlockSample(); 53 | assert.ok(window.fuckAdBlock.check()); 54 | window.fuckAdBlock.onDetected(() => { 55 | window[agFuckAdBlock] = agFuckAdBlock; 56 | }); 57 | assert.strictEqual(window[agFuckAdBlock], agFuckAdBlock, 'callback should apply'); 58 | assert.notOk(window.fuckAdBlock.options); 59 | 60 | clearGlobalProps(agFuckAdBlock); 61 | runScriptlet(name); 62 | 63 | assert.notOk(window.fuckAdBlock.check(), 'should be undefined'); 64 | window.fuckAdBlock.onDetected(() => { 65 | window[agFuckAdBlock] = agFuckAdBlock; 66 | }); 67 | assert.strictEqual(window[agFuckAdBlock], undefined, 'callback should not be applied'); 68 | assert.ok(window.fuckAdBlock.options); 69 | 70 | assert.strictEqual(window.hit, 'FIRED'); 71 | clearGlobalProps(agFuckAdBlock); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/scriptlets/prevent-popads-net.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon */ 2 | /* eslint-disable no-underscore-dangle */ 3 | 4 | import { runScriptlet, clearGlobalProps } from '../helpers'; 5 | 6 | const { test, module } = QUnit; 7 | const name = 'prevent-popads-net'; 8 | 9 | const beforeEach = () => { 10 | window.__debug = () => { 11 | window.hit = 'FIRED'; 12 | }; 13 | }; 14 | 15 | const afterEach = () => { 16 | clearGlobalProps('hit', '__debug'); 17 | }; 18 | 19 | module(name, { beforeEach, afterEach }); 20 | 21 | test('Checking if alias name works', (assert) => { 22 | const adgParams = { 23 | name, 24 | engine: 'test', 25 | verbose: true, 26 | }; 27 | const uboParams = { 28 | name: 'ubo-popads.net.js', 29 | engine: 'test', 30 | verbose: true, 31 | }; 32 | 33 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 34 | const codeByUboParams = window.scriptlets.invoke(uboParams); 35 | 36 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 37 | }); 38 | 39 | test('ag works', (assert) => { 40 | const stub = sinon.stub(Object, 'defineProperties').callsFake((obj, props) => props); 41 | runScriptlet(name); 42 | assert.ok(stub.calledOnce, 'Object.defineProperties called once'); 43 | assert.ok(stub.calledWith(window), 'Object.defineProperties called with window object'); 44 | 45 | assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); 46 | 47 | stub.restore(); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/scriptlets/remove-cookie.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'remove-cookie'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('hit', '__debug'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | test('Checking if alias name works', (assert) => { 20 | const adgParams = { 21 | name, 22 | engine: 'test', 23 | verbose: true, 24 | }; 25 | const uboParams = { 26 | name: 'ubo-cookie-remover.js', 27 | engine: 'test', 28 | verbose: true, 29 | }; 30 | 31 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 32 | const codeByUboParams = window.scriptlets.invoke(uboParams); 33 | 34 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 35 | }); 36 | 37 | test('Cookie works fine', (assert) => { 38 | const cName = '__test-cookie-name__'; 39 | document.cookie = `${cName}=cookie`; 40 | assert.strictEqual(document.cookie.includes(cName), true, 'cookie was set'); 41 | }); 42 | 43 | test('Remove cookie by match', (assert) => { 44 | const cName = '__test-cookie-name__1'; 45 | const cName1 = '__test-cookie-name__3'; 46 | document.cookie = `${cName}=cookie`; 47 | document.cookie = `${cName1}=cookie`; 48 | runScriptlet(name, [cName]); 49 | 50 | assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); 51 | assert.strictEqual(document.cookie.includes(cName), false, 'Cookie by match was removed'); 52 | assert.strictEqual(document.cookie.includes(cName1), true, 'Another cookies works'); 53 | }); 54 | 55 | test('Remove all cookies', (assert) => { 56 | const cName = '__test-cookie-name__2'; 57 | document.cookie = `${cName}=cookie`; 58 | runScriptlet(name); 59 | 60 | assert.strictEqual(window.hit, 'FIRED'); 61 | assert.strictEqual(document.cookie.includes(cName), false, 'If no match delete all cookies for domain'); 62 | }); 63 | 64 | test('Do not remove cookie - invalid regexp pattern', (assert) => { 65 | const cName = '__test-cookie-name__1'; 66 | document.cookie = `${cName}=cookie`; 67 | 68 | runScriptlet(name, ['/\\/']); 69 | 70 | assert.strictEqual(window.hit, undefined, 'Hit should not be fired'); 71 | assert.strictEqual(document.cookie.includes(cName), true, 'Cookie was not removed'); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/scriptlets/set-cookie-reload.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'set-cookie-reload'; 6 | 7 | const beforeEach = () => { 8 | window.__debug = () => { 9 | window.hit = 'FIRED'; 10 | }; 11 | }; 12 | 13 | const afterEach = () => { 14 | clearGlobalProps('hit', '__debug'); 15 | }; 16 | 17 | module(name, { beforeEach, afterEach }); 18 | 19 | test('Checking if alias name works', (assert) => { 20 | const adgParams = { 21 | name, 22 | engine: 'test', 23 | verbose: true, 24 | }; 25 | const uboParams = { 26 | name: 'ubo-set-cookie-reload.js', 27 | engine: 'test', 28 | verbose: true, 29 | }; 30 | 31 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 32 | const codeByUboParams = window.scriptlets.invoke(uboParams); 33 | 34 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/scriptlets/set-popads-dummy.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon */ 2 | /* eslint-disable no-underscore-dangle */ 3 | 4 | import { runScriptlet, clearGlobalProps } from '../helpers'; 5 | 6 | const { test, module } = QUnit; 7 | const name = 'set-popads-dummy'; 8 | const popAdsProp = 'PopAds'; 9 | const popnsProp = 'popns'; 10 | 11 | const beforeEach = () => { 12 | window.__debug = () => { 13 | window.hit = 'FIRED'; 14 | }; 15 | }; 16 | 17 | const afterEach = () => { 18 | clearGlobalProps('hit', '__debug', popAdsProp, popnsProp); 19 | }; 20 | module(name, { beforeEach, afterEach }); 21 | 22 | const fillPopAdsWithValues = () => { 23 | window[popAdsProp] = popAdsProp; 24 | window[popnsProp] = popnsProp; 25 | }; 26 | 27 | const isEmpty = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object; 28 | 29 | test('Checking if alias name works', (assert) => { 30 | const adgParams = { 31 | name, 32 | engine: 'test', 33 | verbose: true, 34 | }; 35 | const uboParams = { 36 | name: 'ubo-popads-dummy.js', 37 | engine: 'test', 38 | verbose: true, 39 | }; 40 | 41 | const codeByAdgParams = window.scriptlets.invoke(adgParams); 42 | const codeByUboParams = window.scriptlets.invoke(uboParams); 43 | 44 | assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); 45 | }); 46 | 47 | test('set-popads-dummy: works', (assert) => { 48 | assert.expect(5); 49 | fillPopAdsWithValues(); 50 | assert.strictEqual(window[popAdsProp], popAdsProp); 51 | assert.strictEqual(window[popnsProp], popnsProp); 52 | runScriptlet(name); 53 | assert.ok(isEmpty(window[popAdsProp]), 'should be empty'); 54 | assert.ok(isEmpty(window[popnsProp]), 'should be empty'); 55 | assert.strictEqual(window.hit, 'FIRED'); 56 | }); 57 | 58 | test('set-popads-dummy: ag works', (assert) => { 59 | assert.expect(2); 60 | const stub = sinon.stub(Object, 'defineProperties').callsFake((obj, props) => props); 61 | runScriptlet(name); 62 | 63 | assert.ok(stub.calledOnce, 'Object.defineProperties called once'); 64 | assert.ok(stub.calledWith(window), 'Object.defineProperties called with window object'); 65 | 66 | stub.restore(); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/scriptlets/test-files/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/scriptlets/test-files/manifestM3U8-carriage-return.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-TARGETDURATION:6 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXT-X-PLAYLIST-TYPE:VOD 6 | #EXTINF:5.005, 7 | https://advert.com/foo.ts 8 | #EXT-X-DISCONTINUITY 9 | #EXTINF:6, 10 | https://example.com/segment1.ts 11 | #EXTINF:6, 12 | https://example.com/segment2.ts 13 | #EXTINF:6, 14 | https://example.com/segment3.ts 15 | #EXT-X-DISCONTINUITY 16 | -------------------------------------------------------------------------------- /tests/scriptlets/test-files/manifestMPD.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://vod-gcs-cedexis.cbsaavideo.com/intl_vms/2017/02/17/879659075884/609941_cenc_precon_dash/ 4 | 5 | https://dai.google.com/segments/redirect/c/ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/Scriptlets/c0c8be5aa2d4c6e08a369049b241f5d337701459/tests/scriptlets/test-files/test-image.jpeg -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test-script01.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/Scriptlets/c0c8be5aa2d4c6e08a369049b241f5d337701459/tests/scriptlets/test-files/test-script01.js -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test-script02.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/Scriptlets/c0c8be5aa2d4c6e08a369049b241f5d337701459/tests/scriptlets/test-files/test-script02.js -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test01.json: -------------------------------------------------------------------------------- 1 | { 2 | "a1": 1, 3 | "b2": "test", 4 | "c3": 3 5 | } -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test02.json: -------------------------------------------------------------------------------- 1 | { 2 | "ad": 1, 3 | "src": "test", 4 | "count": 3 5 | } 6 | -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test03.json: -------------------------------------------------------------------------------- 1 | { 2 | "aa": 1, 3 | "bb": "test", 4 | "cc": { 5 | "id": 0, 6 | "src": "example.org", 7 | "arr": [ 8 | { "inner1": 123 }, 9 | { "inner2": 456 } 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test04.json: -------------------------------------------------------------------------------- 1 | { 2 | "ab": "test", 3 | "bc": 123, 4 | "cc1": { 5 | "id": 0, 6 | "src": "example.org", 7 | "arr": [ 8 | { "inner1": 123 }, 9 | { "inner2": 456 } 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test05.json: -------------------------------------------------------------------------------- 1 | { 2 | "ax": 1, 3 | "xx": { 4 | "id": 1, 5 | "nested": { 6 | "inner1": 123 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/scriptlets/test-files/test06.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "content": [ 4 | { 5 | "id": 0, 6 | "source": "example.com" 7 | }, 8 | { 9 | "id": 1, 10 | "source": "example.org" 11 | } 12 | ], 13 | "state": { 14 | "ready": true 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /tests/scriptlets/trusted-dispatch-event.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle, no-console */ 2 | import { runScriptlet, clearGlobalProps } from '../helpers'; 3 | 4 | const { test, module } = QUnit; 5 | const name = 'trusted-dispatch-event'; 6 | 7 | const createElem = () => { 8 | const div = document.createElement('div'); 9 | div.setAttribute('id', 'testElem'); 10 | 11 | document.body.appendChild(div); 12 | return div; 13 | }; 14 | 15 | const removeElem = () => { 16 | const elem = document.getElementById('testElem'); 17 | if (elem) { 18 | elem.remove(); 19 | } 20 | }; 21 | 22 | const beforeEach = () => { 23 | window.__debug = () => { 24 | window.hit = 'FIRED'; 25 | }; 26 | }; 27 | 28 | const afterEach = () => { 29 | clearGlobalProps('hit', '__debug'); 30 | removeElem(); 31 | }; 32 | 33 | module(name, { beforeEach, afterEach }); 34 | 35 | test('Dispatch event - document', (assert) => { 36 | const event = 'testEvent1'; 37 | 38 | let eventFired = false; 39 | 40 | const scriptletArgs = [event]; 41 | runScriptlet(name, scriptletArgs); 42 | 43 | assert.strictEqual(eventFired, false, 'Event not fired yet'); 44 | 45 | document.addEventListener('testEvent1', () => { 46 | eventFired = true; 47 | }); 48 | 49 | const done = assert.async(); 50 | setTimeout(() => { 51 | assert.strictEqual(eventFired, true, 'Event fired'); 52 | assert.strictEqual(window.hit, 'FIRED'); 53 | done(); 54 | }, 10); 55 | }); 56 | 57 | test('Dispatch event - specific element', (assert) => { 58 | const event = 'testEvent2'; 59 | const selector = '#testElem'; 60 | 61 | let eventFired = false; 62 | 63 | const scriptletArgs = [event, selector]; 64 | runScriptlet(name, scriptletArgs); 65 | 66 | const elem = createElem(); 67 | 68 | assert.strictEqual(eventFired, false, 'Event not fired yet'); 69 | 70 | elem.addEventListener('testEvent2', () => { 71 | eventFired = true; 72 | }); 73 | 74 | const done = assert.async(); 75 | setTimeout(() => { 76 | assert.strictEqual(eventFired, true, 'Event fired'); 77 | assert.strictEqual(window.hit, 'FIRED'); 78 | done(); 79 | }, 10); 80 | }); 81 | 82 | test('Dispatch event - window object', (assert) => { 83 | const event = 'windowObj'; 84 | const selector = 'window'; 85 | 86 | let eventFired = false; 87 | 88 | const scriptletArgs = [event, selector]; 89 | runScriptlet(name, scriptletArgs); 90 | 91 | assert.strictEqual(eventFired, false, 'Event not fired yet'); 92 | 93 | window.addEventListener('windowObj', () => { 94 | eventFired = true; 95 | }); 96 | 97 | const done = assert.async(); 98 | setTimeout(() => { 99 | assert.strictEqual(eventFired, true, 'Event fired'); 100 | assert.strictEqual(window.hit, 'FIRED'); 101 | done(); 102 | }, 10); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import http from 'http'; 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | const TEST_QUERY_MARKER = '?test'; 11 | 12 | const PORT = 54136; 13 | 14 | const mimeTypes = { 15 | '.html': 'text/html', 16 | '.js': 'application/javascript', 17 | '.css': 'text/css', 18 | '.json': 'application/json', 19 | '.yml': 'text/yaml', 20 | '.yaml': 'text/yaml', 21 | '.png': 'image/png', 22 | '.jpg': 'image/jpeg', 23 | '.gif': 'image/gif', 24 | }; 25 | 26 | const getContentType = (filePath) => { 27 | const extname = path.extname(filePath).toLowerCase(); 28 | return mimeTypes[extname] || 'application/octet-stream'; 29 | }; 30 | 31 | const server = { 32 | init() { 33 | return http.createServer((req, res) => { 34 | let filename = req.url; 35 | const queryPosition = filename.indexOf(TEST_QUERY_MARKER); 36 | if (queryPosition > -1) { 37 | filename = req.url.slice(0, queryPosition); 38 | } 39 | 40 | const fullPath = path.join(__dirname, 'dist', filename); 41 | 42 | fs.stat(fullPath, (err, stats) => { 43 | if (err) { 44 | console.log(err.message); 45 | res.writeHead(404); 46 | res.end(JSON.stringify(err)); 47 | return; 48 | } 49 | 50 | if (stats.isFile()) { 51 | // It's a file, serve it 52 | fs.readFile(fullPath, (err, data) => { 53 | if (err) { 54 | console.log(err.message); 55 | res.writeHead(500); 56 | res.end(JSON.stringify(err)); 57 | return; 58 | } 59 | res.writeHead(200, { 'Content-Type': getContentType(fullPath) }); 60 | res.end(data); 61 | }); 62 | } else { 63 | // Neither a file nor a directory 64 | res.writeHead(404); 65 | res.end(); 66 | } 67 | }); 68 | }); 69 | }, 70 | }; 71 | 72 | const start = (server, port) => { 73 | return new Promise((resolve) => { 74 | server.listen(port, () => { 75 | console.log(`Server running at port: ${port}`); 76 | resolve(); 77 | }); 78 | }); 79 | }; 80 | 81 | const stop = (server) => { 82 | return new Promise((resolve) => { 83 | server.close(() => { 84 | resolve(); 85 | }); 86 | }); 87 | }; 88 | 89 | export { 90 | server, 91 | PORT as port, 92 | start, 93 | stop, 94 | }; 95 | -------------------------------------------------------------------------------- /tests/smoke/exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "author": "Adguard Software Ltd.", 7 | "type": "module", 8 | "scripts": { 9 | "start": "tsc --noEmit && tsx root-exports.ts && tsx specific-exports.ts", 10 | "test": "test.sh" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/smoke/exports/root-exports.ts: -------------------------------------------------------------------------------- 1 | import { scriptlets, type Source } from '@adguard/scriptlets'; 2 | import { ok } from 'assert'; 3 | 4 | ok(scriptlets); 5 | 6 | const config: Source = { 7 | name: 'log', 8 | args: [], 9 | engine: 'test', 10 | version: '1.0', 11 | verbose: true, 12 | }; 13 | 14 | ok(scriptlets.invoke(config)); 15 | -------------------------------------------------------------------------------- /tests/smoke/exports/specific-exports.ts: -------------------------------------------------------------------------------- 1 | import { Redirects } from '@adguard/scriptlets/redirects'; 2 | import { isValidAdgRedirectRule } from '@adguard/scriptlets/validators'; 3 | import fs from 'node:fs/promises'; 4 | import assert from 'node:assert'; 5 | 6 | (async () => { 7 | const ymlFilePath = 'node_modules/@adguard/scriptlets/dist/redirects.yml'; 8 | const ymlFile = await fs.readFile(ymlFilePath, 'utf-8'); 9 | const redirects = new Redirects(ymlFile); 10 | const redirect = redirects.getRedirect('noopjs'); 11 | assert.ok(redirect); 12 | assert.ok(redirect.title === 'noopjs'); 13 | })(); 14 | 15 | assert.ok(isValidAdgRedirectRule('example.org$redirect=noopjs')); 16 | -------------------------------------------------------------------------------- /tests/smoke/exports/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on error 4 | 5 | curr_path="tests/smoke/exports" 6 | scriptlets="scriptlets.tgz" 7 | nm_path="node_modules" 8 | 9 | # Define cleanup function 10 | cleanup() { 11 | echo "Performing cleanup..." 12 | rm -f $scriptlets && rm -rf $nm_path 13 | echo "Cleanup complete" 14 | } 15 | 16 | # Set trap to execute the cleanup function on script exit 17 | trap cleanup EXIT 18 | 19 | (cd ../../.. && pnpm build && pnpm pack && mv adguard-scriptlets-*.tgz "$curr_path/$scriptlets") 20 | 21 | # unzip to @adguard/tsurlfilter to node_modules 22 | scriptlets_node_modules=$nm_path"/@adguard/scriptlets" 23 | mkdir -p $scriptlets_node_modules 24 | tar -xzf $scriptlets --strip-components=1 -C $scriptlets_node_modules 25 | 26 | pnpm start 27 | #echo "Test successfully built." 28 | -------------------------------------------------------------------------------- /tests/smoke/exports/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scriptlets tests 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/types", 5 | "rootDir": ".", 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "noEmit": false, 9 | "incremental": false, 10 | }, 11 | "include": [ 12 | "src/**/*", 13 | "tmp/**/*" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "dist", 18 | "**/*.test.ts", 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "strict": true, 10 | "lib": ["DOM"], 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "paths": { 14 | "scriptlets-func": [ 15 | "./tmp/scriptlets-func" 16 | ] 17 | } 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | "dist", 22 | "tests/smoke" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /types/types.d.ts: -------------------------------------------------------------------------------- 1 | import { type Source } from '../src/scriptlets'; 2 | 3 | export type ArbitraryObject = { [key: string | symbol | number]: unknown }; 4 | export type ArbitraryFunction = (...args: any[]) => unknown; 5 | export type NoopFunc = () => void; 6 | export type TrueFunc = () => true; 7 | 8 | interface PageFunction { 9 | names: string[]; 10 | injections?: Helper[]; 11 | } 12 | 13 | export interface Scriptlet extends PageFunction { 14 | (source: Source, ...args: any[]): void; 15 | } 16 | 17 | export interface Redirect extends PageFunction { 18 | (source: Source): void; 19 | } 20 | 21 | export type ChainBase = { 22 | [key: string]: ChainBase; 23 | }; 24 | 25 | export type ChainInfo = { 26 | base: ChainBase; 27 | prop: string; 28 | chain?: string; 29 | }; 30 | 31 | export type StorageItemValue = string | number | undefined | boolean; 32 | 33 | /** 34 | * Utility type to replace return type of methods in object by its key. 35 | * 36 | * @example 37 | * ```ts 38 | * type Test = { 39 | * a: () => string; 40 | * b: () => number; 41 | * c: string; 42 | * }; 43 | * 44 | * type Test2 = ReplaceReturnTypeOfObject = { 45 | * a: () => boolean; 46 | * b: () => boolean; 47 | * c: string; 48 | * }; 49 | * ``` 50 | */ 51 | export type ChangeMethodReturnType = { 52 | [TKey in keyof TObj]: TKey extends TReplaceKey 53 | ? TObj[TKey] extends (...args: infer TArgs) => any 54 | ? (...args: TArgs) => TNewReturn 55 | : TObj[TKey] 56 | : TObj[TKey]; 57 | }; 58 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | // Include test files matching .spec.js or .spec.ts 7 | include: ['**/*.spec.[jt]s'], 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------