/* * Copyright (C) 2017 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "HTTPServer.h" #import "PlatformUtilities.h" #import "Test.h" #import "TestNavigationDelegate.h" #import "TestUIDelegate.h" #import "TestURLSchemeHandler.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import class WKContentRuleListStoreTest : public testing::Test { public: virtual void SetUp() { [[WKContentRuleListStore defaultStore] _removeAllContentRuleLists]; } }; static NSString *basicFilter = @"[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\".*webkit.org\"}}]"; TEST_F(WKContentRuleListStoreTest, Compile) { __block bool doneCompiling = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestRuleList" encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); } static NSString *invalidFilter = @"["; static void checkDomain(NSError *error) { EXPECT_STREQ([[error domain] UTF8String], [WKErrorDomain UTF8String]); } TEST_F(WKContentRuleListStoreTest, InvalidRuleList) { __block bool doneCompiling = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestRuleList" encodedContentRuleList:invalidFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NULL(filter); EXPECT_NOT_NULL(error); checkDomain(error); EXPECT_EQ(error.code, WKErrorContentRuleListStoreCompileFailed); EXPECT_STREQ("Rule list compilation failed: Failed to parse the JSON String.", [[error helpAnchor] UTF8String]); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); } TEST_F(WKContentRuleListStoreTest, Lookup) { __block bool doneCompiling = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestRuleList" encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); __block bool doneLookingUp = false; [[WKContentRuleListStore defaultStore] lookUpContentRuleListForIdentifier:@"TestRuleList" completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_STREQ(filter.identifier.UTF8String, "TestRuleList"); EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneLookingUp = true; }]; TestWebKitAPI::Util::run(&doneLookingUp); } TEST_F(WKContentRuleListStoreTest, EncodedIdentifier) { // FIXME: U+00C4 causes problems here. Using the output of encodeForFileName with // the filesystem changes it to U+0041 followed by U+0308 NSString *identifier = @":;%25%+25И😍"; __block bool done = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:identifier encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_STREQ(filter.identifier.UTF8String, identifier.UTF8String); [[WKContentRuleListStore defaultStore] getAvailableContentRuleListIdentifiers:^(NSArray *identifiers) { EXPECT_EQ(identifiers.count, 1u); EXPECT_EQ(identifiers[0].length, identifier.length); EXPECT_STREQ(identifiers[0].UTF8String, identifier.UTF8String); done = true; }]; }]; TestWebKitAPI::Util::run(&done); } TEST_F(WKContentRuleListStoreTest, NonExistingIdentifierLookup) { __block bool doneLookingUp = false; [[WKContentRuleListStore defaultStore] lookUpContentRuleListForIdentifier:@"DoesNotExist" completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NULL(filter); EXPECT_NOT_NULL(error); checkDomain(error); EXPECT_EQ(error.code, WKErrorContentRuleListStoreLookUpFailed); EXPECT_STREQ("Rule list lookup failed: Unspecified error during lookup.", [[error helpAnchor] UTF8String]); doneLookingUp = true; }]; TestWebKitAPI::Util::run(&doneLookingUp); } TEST_F(WKContentRuleListStoreTest, VersionMismatch) { __block bool doneCompiling = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestRuleList" encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); [[WKContentRuleListStore defaultStore] _invalidateContentRuleListVersionForIdentifier:@"TestRuleList"]; __block bool doneLookingUp = false; [[WKContentRuleListStore defaultStore] lookUpContentRuleListForIdentifier:@"TestRuleList" completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NULL(filter); EXPECT_NOT_NULL(error); checkDomain(error); EXPECT_EQ(error.code, WKErrorContentRuleListStoreVersionMismatch); EXPECT_STREQ("Rule list lookup failed: Version of file does not match version of interpreter.", [[error helpAnchor] UTF8String]); doneLookingUp = true; }]; TestWebKitAPI::Util::run(&doneLookingUp); __block bool doneGettingSource = false; [[WKContentRuleListStore defaultStore] _getContentRuleListSourceForIdentifier:@"TestRuleList" completionHandler:^(NSString* source) { EXPECT_NULL(source); doneGettingSource = true; }]; TestWebKitAPI::Util::run(&doneGettingSource); } TEST_F(WKContentRuleListStoreTest, Removal) { __block bool doneCompiling = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestRuleList" encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); __block bool doneRemoving = false; [[WKContentRuleListStore defaultStore] removeContentRuleListForIdentifier:@"TestRuleList" completionHandler:^(NSError *error) { EXPECT_NULL(error); doneRemoving = true; }]; TestWebKitAPI::Util::run(&doneRemoving); } TEST_F(WKContentRuleListStoreTest, CrossOriginCookieBlocking) { using namespace TestWebKitAPI; auto cookiePresentWhenBlocking = [] (bool blockCookies) { std::optional requestHadCookieResult; HTTPServer server(HTTPServer::UseCoroutines::Yes, [&] (Connection connection) -> Task { while (true) { auto request = co_await connection.awaitableReceiveHTTPRequest(); auto path = HTTPServer::parsePath(request); auto response = [&] { if (path == "/com"_s) { #if CPU(ARM64E) && defined(NDEBUG) // FIXME: This is exactly equivalent code, but the code below crashes on release builds on arm64e. // See rdar://95391568 HashMap headerFields; headerFields.reserveInitialCapacity(1); headerFields.add("Set-Cookie"_s, "testCookie=42; Path=/; SameSite=None; Secure"_s); return HTTPResponse(WTFMove(headerFields), ""_s); #else return HTTPResponse({ { "Set-Cookie"_s, "testCookie=42; Path=/; SameSite=None; Secure"_s } }, ""_s); #endif } if (path == "/org"_s) return HTTPResponse(""_s); if (path == "/cookie-check"_s) { auto cookieHeader = "Cookie: testCookie=42"; requestHadCookieResult = memmem(request.data(), request.size(), cookieHeader, strlen(cookieHeader)); return HTTPResponse("hi"_s); } RELEASE_ASSERT_NOT_REACHED(); }(); co_await connection.awaitableSend(response.serialize()); } }, HTTPServer::Protocol::HttpsProxy); auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]); [storeConfiguration setAllowsServerPreconnect:NO]; [storeConfiguration setProxyConfiguration:@{ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1", (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port()) }]; auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]); [dataStore _setResourceLoadStatisticsEnabled:NO]; __block bool setPolicy { false }; [dataStore.get().httpCookieStore _setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways completionHandler:^{ setPolicy = true; }]; Util::run(&setPolicy); auto viewConfiguration = adoptNS([WKWebViewConfiguration new]); [viewConfiguration setWebsiteDataStore:dataStore.get()]; if (blockCookies) { __block bool doneCompiling { false }; NSString *json = @"[{\"action\":{\"type\":\"block-cookies\"},\"trigger\":{\"url-filter\":\"cookie-check\"}}]"; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestBlockCookies" encodedContentRuleList:json completionHandler:^(WKContentRuleList *compiledRuleList, NSError *error) { EXPECT_FALSE(error); [[viewConfiguration userContentController] addContentRuleList:compiledRuleList]; doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); } auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:viewConfiguration.get()]); auto delegate = adoptNS([TestNavigationDelegate new]); delegate.get().didReceiveAuthenticationChallenge = ^(WKWebView *, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) { completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); }; webView.get().navigationDelegate = delegate.get(); [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/com"]]]; [delegate waitForDidFinishNavigation]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.org/org"]]]; while (!requestHadCookieResult) Util::spinRunLoop(); return *requestHadCookieResult; }; EXPECT_FALSE(cookiePresentWhenBlocking(true)); EXPECT_TRUE(cookiePresentWhenBlocking(false)); } TEST_F(WKContentRuleListStoreTest, NonExistingIdentifierRemove) { __block bool doneRemoving = false; [[WKContentRuleListStore defaultStore] removeContentRuleListForIdentifier:@"DoesNotExist" completionHandler:^(NSError *error) { EXPECT_NOT_NULL(error); checkDomain(error); EXPECT_EQ(error.code, WKErrorContentRuleListStoreRemoveFailed); EXPECT_STREQ("Rule list removal failed: Unspecified error during remove.", [[error helpAnchor] UTF8String]); doneRemoving = true; }]; TestWebKitAPI::Util::run(&doneRemoving); } TEST_F(WKContentRuleListStoreTest, NonDefaultStore) { NSURL *tempDir = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"ContentRuleListTest"] isDirectory:YES]; WKContentRuleListStore *store = [WKContentRuleListStore storeWithURL:tempDir]; NSString *identifier = @"TestRuleList"; NSString *fileName = @"ContentRuleList-TestRuleList"; __block bool doneGettingAvailableIdentifiers = false; [store getAvailableContentRuleListIdentifiers:^(NSArray *identifiers) { EXPECT_NOT_NULL(identifiers); EXPECT_EQ(identifiers.count, 0u); doneGettingAvailableIdentifiers = true; }]; TestWebKitAPI::Util::run(&doneGettingAvailableIdentifiers); __block bool doneCompiling = false; [store compileContentRuleListForIdentifier:identifier encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); doneGettingAvailableIdentifiers = false; [store getAvailableContentRuleListIdentifiers:^(NSArray *identifiers) { EXPECT_NOT_NULL(identifiers); EXPECT_EQ(identifiers.count, 1u); EXPECT_STREQ(identifiers[0].UTF8String, "TestRuleList"); doneGettingAvailableIdentifiers = true; }]; TestWebKitAPI::Util::run(&doneGettingAvailableIdentifiers); NSData *data = [NSData dataWithContentsOfURL:[tempDir URLByAppendingPathComponent:fileName]]; EXPECT_NOT_NULL(data); EXPECT_EQ(data.length, 241u); __block bool doneCheckingSource = false; [store _getContentRuleListSourceForIdentifier:identifier completionHandler:^(NSString *source) { EXPECT_NOT_NULL(source); EXPECT_STREQ(basicFilter.UTF8String, source.UTF8String); doneCheckingSource = true; }]; TestWebKitAPI::Util::run(&doneCheckingSource); __block bool doneRemoving = false; [store removeContentRuleListForIdentifier:identifier completionHandler:^(NSError *error) { EXPECT_NULL(error); doneRemoving = true; }]; TestWebKitAPI::Util::run(&doneRemoving); NSData *dataAfterRemoving = [NSData dataWithContentsOfURL:[tempDir URLByAppendingPathComponent:fileName]]; EXPECT_NULL(dataAfterRemoving); } TEST_F(WKContentRuleListStoreTest, MultipleRuleLists) { __block bool done = false; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"FirstRuleList" encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"SecondRuleList" encodedContentRuleList:basicFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); [[WKContentRuleListStore defaultStore] getAvailableContentRuleListIdentifiers:^(NSArray *identifiers) { EXPECT_NOT_NULL(identifiers); EXPECT_EQ(identifiers.count, 2u); EXPECT_STREQ(identifiers[0].UTF8String, "FirstRuleList"); EXPECT_STREQ(identifiers[1].UTF8String, "SecondRuleList"); done = true; }]; }]; }]; TestWebKitAPI::Util::run(&done); } TEST_F(WKContentRuleListStoreTest, NonASCIISource) { static NSString *nonASCIIFilter = @"[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\".*webkit.org\"}, \"unused\":\"💩\"}]"; NSURL *tempDir = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"ContentRuleListTest"] isDirectory:YES]; WKContentRuleListStore *store = [WKContentRuleListStore storeWithURL:tempDir]; NSString *identifier = @"TestRuleList"; NSString *fileName = @"ContentRuleList-TestRuleList"; __block bool done = false; [store compileContentRuleListForIdentifier:identifier encodedContentRuleList:nonASCIIFilter completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); [store _getContentRuleListSourceForIdentifier:identifier completionHandler:^(NSString *source) { EXPECT_NOT_NULL(source); EXPECT_STREQ(nonASCIIFilter.UTF8String, source.UTF8String); [store _removeAllContentRuleLists]; NSData *dataAfterRemoving = [NSData dataWithContentsOfURL:[tempDir URLByAppendingPathComponent:fileName]]; EXPECT_NULL(dataAfterRemoving); done = true; }]; }]; TestWebKitAPI::Util::run(&done); } static size_t alertCount { 0 }; static bool receivedAlert { false }; @interface ContentRuleListDelegate : NSObject @end @implementation ContentRuleListDelegate - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { switch (alertCount++) { case 0: // Default behavior. EXPECT_STREQ("content blockers enabled", message.UTF8String); break; case 1: // After having removed the content RuleList. EXPECT_STREQ("content blockers disabled", message.UTF8String); break; default: EXPECT_TRUE(false); } receivedAlert = true; completionHandler(); } @end TEST_F(WKContentRuleListStoreTest, AddRemove) { [[WKContentRuleListStore defaultStore] _removeAllContentRuleLists]; __block bool doneCompiling = false; NSString* contentBlocker = @"[{\"action\":{\"type\":\"css-display-none\",\"selector\":\".hidden\"},\"trigger\":{\"url-filter\":\".*\"}}]"; __block RetainPtr ruleList; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"TestAddRemove" encodedContentRuleList:contentBlocker completionHandler:^(WKContentRuleList *compiledRuleList, NSError *error) { EXPECT_TRUE(error == nil); ruleList = compiledRuleList; doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); EXPECT_NOT_NULL(ruleList); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); [[configuration userContentController] addContentRuleList:ruleList.get()]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); auto delegate = adoptNS([[ContentRuleListDelegate alloc] init]); [webView setUIDelegate:delegate.get()]; NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"contentBlockerCheck" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]; alertCount = 0; receivedAlert = false; [webView loadRequest:request]; TestWebKitAPI::Util::run(&receivedAlert); [[configuration userContentController] removeContentRuleList:ruleList.get()]; receivedAlert = false; [webView reload]; TestWebKitAPI::Util::run(&receivedAlert); } #if PLATFORM(IOS_FAMILY) TEST_F(WKContentRuleListStoreTest, UnsafeMMap) { RetainPtr tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"UnsafeMMapTest"]; RetainPtr store = [WKContentRuleListStore storeWithURL:[NSURL fileURLWithPath:tempDir.get() isDirectory:YES]]; static NSString *compiledIdentifier = @"CompiledRuleList"; static NSString *copiedIdentifier = @"CopiedRuleList"; static NSString *ruleListSourceString = @"[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"blockedsubresource\"}}]"; RetainPtr compiledFilePath = [tempDir stringByAppendingPathComponent:@"ContentRuleList-CompiledRuleList"]; RetainPtr copiedFilePath = [tempDir stringByAppendingPathComponent:@"ContentRuleList-CopiedRuleList"]; __block bool doneCompiling = false; [store compileContentRuleListForIdentifier:compiledIdentifier encodedContentRuleList:ruleListSourceString completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); auto hasCompleteProtection = [] (const RetainPtr& path) { NSError *error = nil; NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.get() error:&error]; EXPECT_NULL(error); return [[attributes objectForKey:NSFileProtectionKey] isEqualToString:NSFileProtectionComplete]; }; NSError *error = nil; [[NSFileManager defaultManager] copyItemAtPath:compiledFilePath.get() toPath:copiedFilePath.get() error:&error]; EXPECT_NULL(error); EXPECT_FALSE(hasCompleteProtection(copiedFilePath)); [[NSFileManager defaultManager] setAttributes:@{ NSFileProtectionKey: NSFileProtectionComplete } ofItemAtPath:copiedFilePath.get() error:&error]; EXPECT_NULL(error); #if !PLATFORM(IOS_FAMILY_SIMULATOR) EXPECT_TRUE(hasCompleteProtection(copiedFilePath)); #endif __block bool doneLookingUp = false; [store lookUpContentRuleListForIdentifier:copiedIdentifier completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NOT_NULL(filter); EXPECT_NULL(error); doneLookingUp = true; }]; TestWebKitAPI::Util::run(&doneLookingUp); EXPECT_FALSE(hasCompleteProtection(copiedFilePath)); __block bool doneRemoving = false; [store removeContentRuleListForIdentifier:compiledIdentifier completionHandler:^(NSError *error) { EXPECT_NULL(error); [store removeContentRuleListForIdentifier:copiedIdentifier completionHandler:^(NSError *error) { EXPECT_NULL(error); doneRemoving = true; }]; }]; TestWebKitAPI::Util::run(&doneRemoving); } #endif // PLATFORM(IOS_FAMILY) TEST_F(WKContentRuleListStoreTest, _WKUserContentExtensionStoreSelectors) { [[WKContentRuleListStore defaultStore] _removeAllContentRuleLists]; NSString *identifier = @"TestOldSPIMixup"; __block bool doneCompiling = false; [(_WKUserContentExtensionStore *)[WKContentRuleListStore defaultStore] compileContentExtensionForIdentifier:identifier encodedContentExtension:basicFilter completionHandler:^(_WKUserContentFilter *filter, NSError *error) { EXPECT_NULL(error); EXPECT_NOT_NULL(filter); doneCompiling = true; }]; TestWebKitAPI::Util::run(&doneCompiling); __block bool doneLookingUp = false; [(_WKUserContentExtensionStore *)[WKContentRuleListStore defaultStore] lookupContentExtensionForIdentifier:identifier completionHandler:^(_WKUserContentFilter *filter, NSError *error) { EXPECT_NULL(error); EXPECT_NOT_NULL(filter); doneLookingUp = true; }]; TestWebKitAPI::Util::run(&doneLookingUp); __block bool domeRemoving = false; [(_WKUserContentExtensionStore *)[WKContentRuleListStore defaultStore] removeContentExtensionForIdentifier:identifier completionHandler:^(NSError *error) { EXPECT_NULL(error); domeRemoving = true; }]; TestWebKitAPI::Util::run(&domeRemoving); doneLookingUp = false; [(_WKUserContentExtensionStore *)[WKContentRuleListStore defaultStore] lookupContentExtensionForIdentifier:identifier completionHandler:^(_WKUserContentFilter *filter, NSError *error) { EXPECT_WK_STREQ(error.domain, WKErrorDomain); EXPECT_EQ(error.code, WKErrorContentRuleListStoreLookUpFailed); EXPECT_NULL(filter); doneLookingUp = true; }]; TestWebKitAPI::Util::run(&doneLookingUp); } static RetainPtr compileContentRuleList(const char* json, NSString *identifier = @"testidentifier") { [[WKContentRuleListStore defaultStore] _removeAllContentRuleLists]; __block RetainPtr list; [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"testidentifier" encodedContentRuleList:[NSString stringWithFormat:@"%s", json] completionHandler:^(WKContentRuleList *filter, NSError *error) { EXPECT_NULL(error); list = filter; }]; while (!list) TestWebKitAPI::Util::spinRunLoop(); return list; } static RetainPtr navigationDelegateAllowingActiveActionsOnTestHost() { static auto delegate = adoptNS([TestNavigationDelegate new]); delegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) { preferences._activeContentRuleListActionPatterns = [NSDictionary dictionaryWithObject:[NSSet setWithObject:@"*://testhost/*"] forKey:@"testidentifier"]; decisionHandler(WKNavigationActionPolicyAllow, preferences); }; return delegate; } static void checkURLs(const Vector& actual, const Vector& expected) { EXPECT_EQ(actual.size(), expected.size()); for (size_t i = 0; i < std::min(actual.size(), expected.size()); i++) EXPECT_WK_STREQ(actual[i], expected[i]); } TEST_F(WKContentRuleListStoreTest, ModifyHeaders) { auto list = compileContentRuleList(R"JSON( [ { "action": { "type": "modify-headers", "request-headers": [ { "operation": "set", "header": "testkey", "value": "testvalue" } ] }, "trigger": { "url-filter": "testscheme" } }, { "action": { "type": "modify-headers", "request-headers": [ { "operation": "remove", "header": "Accept" } ] }, "trigger": { "url-filter": "testscheme" } }, { "action": { "type": "modify-headers", "request-headers": [ { "operation": "append", "header": "Content-Type", "value": "Modified-by-test" } ] }, "trigger": { "url-filter": "testscheme" } } ] )JSON"); __block bool receivedAllRequests { false }; auto handler = adoptNS([TestURLSchemeHandler new]); handler.get().startURLSchemeTaskHandler = ^(WKWebView *, id task) { NSString *path = task.request.URL.path; NSDictionary *fields = [task.request allHTTPHeaderFields]; if ([path isEqualToString:@"/main.html"]) { EXPECT_WK_STREQ(fields[@"Content-Type"], "Modified-by-test"); EXPECT_WK_STREQ(fields[@"testkey"], "testvalue"); EXPECT_NULL(fields[@"Accept"]); return respond(task, ""); } if ([path isEqualToString:@"/frame.html"]) { EXPECT_NULL(fields[@"Content-Type"]); EXPECT_NULL(fields[@"testkey"]); EXPECT_NOT_NULL(fields[@"Accept"]); return respond(task, ""); } if ([path isEqualToString:@"/fetch.txt"]) { EXPECT_WK_STREQ(fields[@"Content-Type"], "application/json; Modified-by-test"); EXPECT_WK_STREQ(fields[@"testkey"], "testvalue"); EXPECT_NULL(fields[@"Accept"]); receivedAllRequests = true; return respond(task, ""); } ASSERT_NOT_REACHED(); }; auto configuration = adoptNS([WKWebViewConfiguration new]); [[configuration userContentController] addContentRuleList:list.get()]; [configuration setURLSchemeHandler:handler.get() forURLScheme:@"testscheme"]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration.get()]); auto delegate = navigationDelegateAllowingActiveActionsOnTestHost().get(); webView.get().navigationDelegate = delegate; __block bool receivedActionNotification { false }; __block Vector urls; delegate.contentRuleListPerformedAction = ^(WKWebView *, NSString *identifier, _WKContentRuleListAction *action, NSURL *url) { urls.append(url.absoluteString); EXPECT_TRUE(action.modifiedHeaders); receivedActionNotification = true; }; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"testscheme://testhost/main.html"]]]; TestWebKitAPI::Util::run(&receivedAllRequests); TestWebKitAPI::Util::run(&receivedActionNotification); checkURLs(urls, { "testscheme://testhost/main.html"_s, "testscheme://testhost/fetch.txt"_s }); // FIXME: Appending to the User-Agent replaces the user agent because we haven't added the user agent yet when processing the request. } TEST_F(WKContentRuleListStoreTest, Redirect) { auto list = compileContentRuleList(R"JSON( [ { "action": { "type": "redirect", "redirect": { "url": "othertestscheme://not-testhost/1-redirected.txt" } }, "trigger": { "url-filter": "1.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "port": "123", "ignored-comment": "123 is a forbidden port, so this URL is not seen by the WKURLSchemeHandler." } } }, "trigger": { "url-filter": "2.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "host": "newhost", "username": "newusername", "port": "443" } } }, "trigger": { "url-filter": "3.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "port": "", "scheme": "othertestscheme", "query": "?key=value" } } }, "trigger": { "url-filter": "4.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "password": "testpassword", "query": "", "fragment": "#fragment-to-be-added" } } }, "trigger": { "url-filter": "5.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "password": "testpassword", "fragment": "" } } }, "trigger": { "url-filter": "6.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "query": "?", "fragment": "#" } } }, "trigger": { "url-filter": "7.txt" } }, { "action": { "type": "redirect", "redirect": { "transform": { "query-transform": { "remove-parameters": [ "parameter-to-remove" ], "add-or-replace-parameters": [ { "key": "key-to-add", "value": "value-to-add" }, { "key": "key-to-replace-only", "value": "value-to-replace-only", "replace-only": true }, { "key": "key-to-replace-only-but-not-found", "value": "value-to-replace-only-but-not-found", "replace-only": true } ] } } } }, "trigger": { "url-filter": "8.txt" } }, { "action": { "type": "redirect", "redirect": { "regex-substitution": "testscheme://replaced-by-regex/\\1.txt" } }, "trigger": { "url-filter": "(9).txt" } } ] )JSON"); __block bool receivedAllRequests { false }; __block auto urls = adoptNS([NSMutableArray new]); auto handler = adoptNS([TestURLSchemeHandler new]); handler.get().startURLSchemeTaskHandler = ^(WKWebView *, id task) { NSURL *url = task.request.URL; [urls addObject:url]; NSString *path = url.path; if ([path isEqualToString:@"/main.html"]) { return respond(task, R"HTML( )HTML"); } if ([path isEqualToString:@"/9.txt"]) receivedAllRequests = true; respond(task, ""); }; auto configuration = adoptNS([WKWebViewConfiguration new]); [[configuration userContentController] addContentRuleList:list.get()]; [configuration setURLSchemeHandler:handler.get() forURLScheme:@"testscheme"]; [configuration setURLSchemeHandler:handler.get() forURLScheme:@"othertestscheme"]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration.get()]); auto delegate = navigationDelegateAllowingActiveActionsOnTestHost().get(); webView.get().navigationDelegate = delegate; __block bool receivedActionNotification { false }; __block Vector urlsFromCallback; delegate.contentRuleListPerformedAction = ^(WKWebView *, NSString *identifier, _WKContentRuleListAction *action, NSURL *url) { urlsFromCallback.append(url.absoluteString); EXPECT_TRUE(action.redirected); receivedActionNotification = true; }; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"testscheme://testhost/main.html"]]]; TestWebKitAPI::Util::run(&receivedAllRequests); TestWebKitAPI::Util::run(&receivedActionNotification); Vector expectedRequestedURLs { "testscheme://testhost/main.html"_s, "othertestscheme://not-testhost/1-redirected.txt"_s, "testscheme://not-testhost-should-not-trigger-action/2.txt"_s, "testscheme://newusername@newhost:443/3.txt?query-should-not-be-removed"_s, "othertestscheme://testhost/4.txt?key=value#fragment-should-not-be-removed"_s, "testscheme://:testpassword@testhost/5.txt#fragment-to-be-added"_s, "testscheme://:testpassword@testhost/6.txt"_s, "testscheme://testhost/7.txt?#"_s, "testscheme://testhost/8.txt?key-to-replace-only=value-to-replace-only&key-to-add=value-to-add"_s, "testscheme://replaced-by-regex/9.txt"_s, }; EXPECT_EQ(expectedRequestedURLs.size(), [urls count]); for (size_t i = 0; i < expectedRequestedURLs.size(); i++) EXPECT_WK_STREQ(expectedRequestedURLs[i], [[urls objectAtIndex:i] absoluteString]); expectedRequestedURLs.remove(0); expectedRequestedURLs[1] = "testscheme://testhost:123/2.txt"_s; checkURLs(urlsFromCallback, expectedRequestedURLs); } TEST_F(WKContentRuleListStoreTest, NullPatternSet) { auto list = compileContentRuleList(R"JSON( [ { "action": { "type": "redirect", "redirect": { "url": "testscheme://testhost/main-redirected.html" } }, "trigger": { "url-filter": "main.html" } } ] )JSON"); __block std::optional redirectedResult; auto handler = adoptNS([TestURLSchemeHandler new]); handler.get().startURLSchemeTaskHandler = ^(WKWebView *, id task) { NSString *path = task.request.URL.path; if ([path isEqualToString:@"/main.html"]) redirectedResult = false; else { EXPECT_WK_STREQ(path, "/main-redirected.html"); redirectedResult = true; } respond(task, ""); }; __block enum class DelegateAction : uint8_t { AllowAll, AllowNone, AllowTestHost, } delegateAction; auto delegate = adoptNS([TestNavigationDelegate new]); delegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) { EXPECT_NOT_NULL(preferences._activeContentRuleListActionPatterns); EXPECT_EQ(preferences._activeContentRuleListActionPatterns.count, 0u); switch (delegateAction) { case DelegateAction::AllowAll: preferences._activeContentRuleListActionPatterns = [NSDictionary dictionaryWithObject:[NSSet setWithObject:@"*://*/*"] forKey:@"testidentifier"]; break; case DelegateAction::AllowNone: preferences._activeContentRuleListActionPatterns = [NSDictionary dictionary]; break; case DelegateAction::AllowTestHost: preferences._activeContentRuleListActionPatterns = [NSDictionary dictionaryWithObject:[NSSet setWithObject:@"*://testhost/*"] forKey:@"testidentifier"]; break; } decisionHandler(WKNavigationActionPolicyAllow, preferences); }; auto configuration = adoptNS([WKWebViewConfiguration new]); [[configuration userContentController] addContentRuleList:list.get()]; [configuration setURLSchemeHandler:handler.get() forURLScheme:@"testscheme"]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration.get()]); webView.get().navigationDelegate = delegate.get(); auto getRedirectResult = ^(DelegateAction action) { delegateAction = action; redirectedResult = std::nullopt; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"testscheme://testhost/main.html"]]]; while (!redirectedResult) TestWebKitAPI::Util::spinRunLoop(); return *redirectedResult; }; EXPECT_TRUE(getRedirectResult(DelegateAction::AllowAll)); EXPECT_FALSE(getRedirectResult(DelegateAction::AllowNone)); EXPECT_TRUE(getRedirectResult(DelegateAction::AllowTestHost)); } TEST_F(WKContentRuleListStoreTest, ExtensionPath) { auto list = compileContentRuleList(R"JSON( [ { "action": { "type": "redirect", "redirect": { "extension-path": "/redirected-to-extension?no-query#no-fragment" } }, "trigger": { "url-filter": "main.html" } } ] )JSON"); __block RetainPtr redirectedURL; auto handler = adoptNS([TestURLSchemeHandler new]); handler.get().startURLSchemeTaskHandler = ^(WKWebView *, id task) { redirectedURL = task.request.URL; respond(task, ""); }; auto delegate = adoptNS([TestNavigationDelegate new]); delegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) { preferences._activeContentRuleListActionPatterns = [NSDictionary dictionaryWithObject:[NSSet setWithObject:@"*://testhost/*"] forKey:@"testidentifier"]; decisionHandler(WKNavigationActionPolicyAllow, preferences); }; auto configuration = adoptNS([WKWebViewConfiguration new]); [[configuration userContentController] _addContentRuleList:list.get() extensionBaseURL:[NSURL URLWithString:@"extension-scheme://extension-host/"]]; [configuration setURLSchemeHandler:handler.get() forURLScheme:@"testscheme"]; [configuration setURLSchemeHandler:handler.get() forURLScheme:@"extension-scheme"]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration.get()]); webView.get().navigationDelegate = delegate.get(); [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"testscheme://testhost/main.html"]]]; while (!redirectedURL) TestWebKitAPI::Util::spinRunLoop(); EXPECT_WK_STREQ([redirectedURL absoluteString], "extension-scheme://extension-host/redirected-to-extension%3Fno-query%23no-fragment"); }