diff --git a/packages/tsurlfilter/CHANGELOG.md b/packages/tsurlfilter/CHANGELOG.md index 78395ab17..03a9b2696 100644 --- a/packages/tsurlfilter/CHANGELOG.md +++ b/packages/tsurlfilter/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 In addition to that, it solves the problem of leaking links to the original leaking strings that in turn was leading to higher memory usage. +[2.2.14]: https://github.com/AdguardTeam/tsurlfilter/releases/tag/tsurlfilter-v2.2.14 + ## [2.2.13] - 2024-02-13 ### Changed diff --git a/packages/tswebextension/CHANGELOG.md b/packages/tswebextension/CHANGELOG.md index ac698d929..4cb8876fe 100644 --- a/packages/tswebextension/CHANGELOG.md +++ b/packages/tswebextension/CHANGELOG.md @@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [1.0.15] - 2024-02-22 + +### Fixed + +- `$popup` modifier block other types of resources [#2723]. + +[1.0.15]: https://github.com/AdguardTeam/tsurlfilter/releases/tag/tswebextension-v1.0.15 +[#2723]: https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2723 + ## [1.0.14] - 2024-02-13 ### Changed @@ -15,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Filtering engine now uses the new `BufferRuleList` provided by `@adguard/tsurlfilter` to improve performance and memory usage. +[1.0.14]: https://github.com/AdguardTeam/tsurlfilter/releases/tag/tswebextension-v1.0.14 + ## [1.0.13] - 2024-02-13 ### Added diff --git a/packages/tswebextension/package.json b/packages/tswebextension/package.json index f9bec6259..e794e2f1f 100644 --- a/packages/tswebextension/package.json +++ b/packages/tswebextension/package.json @@ -1,6 +1,6 @@ { "name": "@adguard/tswebextension", - "version": "1.0.14", + "version": "1.0.15", "description": "This is a TypeScript library that implements AdGuard's extension API", "main": "dist/index.js", "typings": "dist/types/src/lib/mv2/background/index.d.ts", diff --git a/packages/tswebextension/src/lib/mv2/background/request/request-blocking-api.ts b/packages/tswebextension/src/lib/mv2/background/request/request-blocking-api.ts index ee7dbcb5e..830246187 100644 --- a/packages/tswebextension/src/lib/mv2/background/request/request-blocking-api.ts +++ b/packages/tswebextension/src/lib/mv2/background/request/request-blocking-api.ts @@ -13,7 +13,7 @@ import { ContentType } from '../../../common/request-type'; /** * Params for {@link RequestBlockingApi.getBlockingResponse}. */ -type GetBlockingResponseParams = { +export type GetBlockingResponseParams = { tabId: number, eventId: string, rule: NetworkRule | null, @@ -104,45 +104,38 @@ export class RequestBlockingApi { return undefined; } - // If the request is a document request. - if (requestType === RequestType.Document) { - // Blocking rule can be with $popup modifier - in this case we need - // to close the tab as soon as possible. - // https://adguard.com/kb/ru/general/ad-filtering/create-own-filters/#popup-modifier - if (rule.isOptionEnabled(NetworkRuleOption.Popup)) { - const isNewTab = tabsApi.isNewPopupTab(tabId); - - if (isNewTab) { - // the tab is considered as a popup and should be closed - RequestBlockingApi.logRuleApplying(data); - browser.tabs.remove(tabId); - return { cancel: true }; - } - - // $popup modifier can be used as a single modifier in the rule - // so there should be no document type, and: - // 1. new tab should be handled as a popup earlier (isNewTab check) - // 2. tab loading on direct url navigation should not be blocked - // https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2449 - // - // Note: for both rules `||example.com^$document,popup` and `||example.com^$all` - // there will be document type set (so we can safely return undefined here) - // and this function execution shall proceed to the final return statement - // and blocking page should be shown - if ((rule.getPermittedRequestTypes() & RequestType.Document) !== RequestType.Document) { - return undefined; - } + // Blocking rule can be with $popup modifier - in this case we need + // to close the tab as soon as possible. + // https://adguard.com/kb/ru/general/ad-filtering/create-own-filters/#popup-modifier + if (rule.isOptionEnabled(NetworkRuleOption.Popup)) { + const isNewTab = tabsApi.isNewPopupTab(tabId); + + if (isNewTab) { + // the tab is considered as a popup and should be closed + RequestBlockingApi.logRuleApplying(data); + browser.tabs.remove(tabId); + return { cancel: true }; } - // For all other blocking rules, we return our dummy page with the - // option to temporarily disable blocking for the specified domain. - return documentBlockingService.getDocumentBlockingResponse({ - eventId, - requestUrl, - referrerUrl, - rule, - tabId, - }); + // $popup modifier can be used as a single modifier in the rule + // so there should be no document type, and: + // 1. new tab should be handled as a popup earlier (isNewTab check) + // 2. tab loading on direct url navigation should not be blocked + // https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2449 + // + // Note: for both rules `||example.com^$document,popup` and `||example.com^$all` + // there will be document type set so blocking page should be shown + if ((rule.getPermittedRequestTypes() & RequestType.Document) === RequestType.Document) { + return documentBlockingService.getDocumentBlockingResponse({ + eventId, + requestUrl, + referrerUrl, + rule, + tabId, + }); + } + + return undefined; } if (rule.isOptionEnabled(NetworkRuleOption.Redirect)) { @@ -157,6 +150,25 @@ export class RequestBlockingApi { } } + // Basic rules for blocking requests are applied only to sub-requests + // so `||example.com^` will not block the main page + // https://adguard.com/kb/general/ad-filtering/create-own-filters/#basic-rules + if (requestType === RequestType.Document) { + // but if the blocking rule has $document modifier, blocking page should be shown + // e.g. `||example.com^$document` + if (rule.getPermittedRequestTypes() === RequestType.Document) { + return documentBlockingService.getDocumentBlockingResponse({ + eventId, + requestUrl, + referrerUrl, + rule, + tabId, + }); + } + + return undefined; + } + RequestBlockingApi.logRuleApplying(data); return { cancel: true }; } diff --git a/packages/tswebextension/test/lib/mv2/background/request/request-blocking-api.test.ts b/packages/tswebextension/test/lib/mv2/background/request/request-blocking-api.test.ts index ca8826b16..fdfa35308 100644 --- a/packages/tswebextension/test/lib/mv2/background/request/request-blocking-api.test.ts +++ b/packages/tswebextension/test/lib/mv2/background/request/request-blocking-api.test.ts @@ -1,10 +1,44 @@ import { MatchingResult, NetworkRule, RequestType } from '@adguard/tsurlfilter'; -import { RequestBlockingApi } from '@lib/mv2/background/request/request-blocking-api'; -import { engineApi } from '@lib/mv2/background/api'; +import { type GetBlockingResponseParams, RequestBlockingApi } from '@lib/mv2/background/request/request-blocking-api'; +import { documentBlockingService, engineApi, tabsApi } from '@lib/mv2/background/api'; +import { ContentType } from '@lib/common'; jest.mock('@lib/mv2/background/api'); -describe('Request Blocking Api', () => { +/** + * Returns simple data object for {@link RequestBlockingApi.getBlockingResponse} method + * with hardcoded values: + * - `tabId: 1`; + * - `eventId: '1'`; + * - `referrerUrl: ''`. + * + * Other parameters are passed as arguments. + * + * @param ruleText Rule text. + * @param requestUrl Request url. + * @param requestType Request type. + * @param contentType Content type. + * + * @returns Data for getBlockingResponse() method. + */ +const getGetBlockingResponseParamsData = ( + ruleText: string, + requestUrl: string, + requestType: RequestType, + contentType: ContentType, +): GetBlockingResponseParams => { + return { + tabId: 1, + eventId: '1', + rule: new NetworkRule(ruleText, 0), + referrerUrl: '', + requestUrl, + requestType, + contentType, + }; +}; + +describe('Request Blocking Api - shouldCollapseElement', () => { const mockMatchingResult = (ruleText?: string): void => { let matchingResult = null; @@ -56,3 +90,269 @@ describe('Request Blocking Api', () => { ).toBe(false); }); }); + +describe('Request Blocking Api - getBlockingResponse', () => { + const BLOCKING_PAGE_RESPONSE_MARKER = 'BLOCKING_PAGE'; + const mockedBlockingPageResponse = { + cancel: true, + redirectUrl: BLOCKING_PAGE_RESPONSE_MARKER, + }; + + beforeEach(() => { + jest.spyOn(documentBlockingService, 'getDocumentBlockingResponse') + .mockReturnValue(mockedBlockingPageResponse); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('tab is new', () => { + beforeEach(() => { + jest.spyOn(tabsApi, 'isNewPopupTab').mockReturnValue(true); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('the popup modifier, document request - close tab', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$popup', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('the all modifier, document request - close tab', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$all', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + // $all is actually a synonym for $popup,document + // so a new tab should be cancelled due to 'Popup' option enabled + expect(response).toEqual({ cancel: true }); + }); + + it('explicit modifiers popup and document, document request - close tab', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$popup,document', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + // a new tab should be cancelled due to 'Popup' option enabled + expect(response).toEqual({ cancel: true }); + }); + + it('blocking rule, document modifier, document request - blocking page', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$document', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(mockedBlockingPageResponse); + }); + + it('the popup modifier, image request - cancel request', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$popup', + 'http://example.com/image.png', + RequestType.Image, + ContentType.Image, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('wide popup modifier rule, image request - close tab', () => { + const data = getGetBlockingResponseParamsData( + '|http*://$popup', + 'http://example.com/image.png', + RequestType.Image, + ContentType.Image, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('basic rule, main document request - should NOT be cancelled', () => { + // Basic rules for blocking requests are applied only to sub-requests, + // not to main frame which is loaded as document request type. + // https://adguard.com/kb/general/ad-filtering/create-own-filters/#basic-rules + const data = getGetBlockingResponseParamsData( + '||example.com^', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(undefined); + }); + + it('basic rule, subdocument - should be cancelled', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^', + 'http://example.com', + RequestType.SubDocument, + ContentType.Subdocument, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('basic rule, subrequest - should be cancelled', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^', + 'http://example.com', + RequestType.XmlHttpRequest, + ContentType.XmlHttpRequest, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('basic rule, script request - should be cancelled', () => { + const data = getGetBlockingResponseParamsData( + '||example.com/script.js', + 'http://example.com/script.js', + RequestType.Script, + ContentType.Script, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + }); + + describe('tab is not new', () => { + beforeEach(() => { + jest.spyOn(tabsApi, 'isNewPopupTab').mockReturnValue(false); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('just popup modifier, document request - do not close tab, undefined is returned', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$popup', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(undefined); + }); + + it('the all modifier, document request - blocking page', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$all', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(mockedBlockingPageResponse); + }); + + it('explicit popup with document, document request - blocking page', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$popup,document', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(mockedBlockingPageResponse); + }); + + it('blocking rule, document modifier, document request - blocking page', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$document', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(mockedBlockingPageResponse); + }); + + it('the popup modifier, image request - do nothing', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^$popup', + 'http://example.com/image.png', + RequestType.Image, + ContentType.Image, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(undefined); + }); + + it('wide popup modifier rule, image request - do nothing', () => { + const data = getGetBlockingResponseParamsData( + '|http*://$popup', + 'http://example.com/image.png', + RequestType.Image, + ContentType.Image, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(undefined); + }); + + it('basic rule, main document request - should NOT be cancelled', () => { + // Basic rules for blocking requests are applied only to sub-requests, + // not to main frame which is loaded as document request type. + // https://adguard.com/kb/general/ad-filtering/create-own-filters/#basic-rules + const data = getGetBlockingResponseParamsData( + '||example.com^', + 'http://example.com', + RequestType.Document, + ContentType.Document, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual(undefined); + }); + + it('basic rule, subdocument - should be cancelled', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^', + 'http://example.com', + RequestType.SubDocument, + ContentType.Subdocument, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('basic rule, subrequest - should be cancelled', () => { + const data = getGetBlockingResponseParamsData( + '||example.com^', + 'http://example.com', + RequestType.XmlHttpRequest, + ContentType.XmlHttpRequest, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + + it('simple basic rule, script request - should be cancelled', () => { + const data = getGetBlockingResponseParamsData( + '||example.com/script.js', + 'http://example.com/script.js', + RequestType.Script, + ContentType.Script, + ); + const response = RequestBlockingApi.getBlockingResponse(data); + expect(response).toEqual({ cancel: true }); + }); + }); +});