Skip to content

Commit

Permalink
Support web accessible resources in Web Extensions.
Browse files Browse the repository at this point in the history
https://webkit.org/b/246489
rdar://problem/114823315

Reviewed by Brent Fulgham.

* Source/WebCore/page/UserContentURLPattern.cpp:
(WebCore::matchesWildcardPattern): Added.
* Source/WebCore/page/UserContentURLPattern.h:
* Source/WebKit/Platform/cocoa/CocoaHelpers.mm:
(WebKit::filterObjects<NSArray>): Return nil if the input is nil.
(WebKit::filterObjects<NSDictionary>): Ditto.
(WebKit::filterObjects<NSSet>): Ditto.
(WebKit::mapObjects<NSArray>): Ditto.
(WebKit::mapObjects<NSDictionary>): Ditto.
(WebKit::mapObjects<NSSet>): Ditto.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm:
(WebKit::WebExtension::isWebAccessibleResource): Added.
(WebKit::WebExtension::populateWebAccessibleResourcesIfNeeded): Added.
(WebKit::WebExtension::errors): Call populateWebAccessibleResourcesIfNeeded().
(WebKit::WebExtension::isAccessibleResourcePath): Deleted.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionTabCocoa.mm:
(WebKit::WebExtensionTab::matches const): Use matchesWildcardPattern instead of NSPredicate.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionURLSchemeHandlerCocoa.mm:
(WebKit::WebExtensionURLSchemeHandler::platformStartTask): Call isWebAccessibleResource.
* Source/WebKit/UIProcess/Extensions/WebExtension.h:
* Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtension.mm:
(TestWebKitAPI::TEST):
* Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionController.mm:
(TestWebKitAPI::TEST):

Canonical link: https://commits.webkit.org/270298@main
  • Loading branch information
xeenon committed Nov 7, 2023
1 parent 7bd9ef4 commit dc77e1b
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 8 deletions.
5 changes: 5 additions & 0 deletions Source/WebCore/page/UserContentURLPattern.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,9 @@ bool UserContentURLPattern::matchesPath(const String& path) const
return MatchTester(m_path, path).test();
}

bool matchesWildcardPattern(const String& pattern, const String& testString)
{
return MatchTester(pattern, testString).test();
}

} // namespace WebCore
2 changes: 2 additions & 0 deletions Source/WebCore/page/UserContentURLPattern.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,6 @@ class UserContentURLPattern {
bool m_matchSubdomains { false };
};

WEBCORE_EXPORT bool matchesWildcardPattern(const String& pattern, const String& testString);

} // namespace WebCore
18 changes: 18 additions & 0 deletions Source/WebKit/Platform/cocoa/CocoaHelpers.mm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
template<>
NSArray *filterObjects<NSArray>(NSArray *array, bool NS_NOESCAPE (^block)(__kindof id key, __kindof id value))
{
if (!array)
return nil;

switch (array.count) {
case 0:
return @[ ];
Expand All @@ -56,6 +59,9 @@
template<>
NSDictionary *filterObjects<NSDictionary>(NSDictionary *dictionary, bool NS_NOESCAPE (^block)(__kindof id key, __kindof id value))
{
if (!dictionary)
return nil;

if (!dictionary.count)
return @{ };

Expand All @@ -72,6 +78,9 @@
template<>
NSSet *filterObjects<NSSet>(NSSet *set, bool NS_NOESCAPE (^block)(__kindof id key, __kindof id value))
{
if (!set)
return nil;

if (!set.count)
return [NSSet set];

Expand All @@ -83,6 +92,9 @@
template<>
NSArray *mapObjects<NSArray>(NSArray *array, __kindof id NS_NOESCAPE (^block)(__kindof id key, __kindof id value))
{
if (!array)
return nil;

switch (array.count) {
case 0:
return @[ ];
Expand All @@ -108,6 +120,9 @@
template<>
NSDictionary *mapObjects<NSDictionary>(NSDictionary *dictionary, __kindof id NS_NOESCAPE (^block)(__kindof id key, __kindof id value))
{
if (!dictionary)
return nil;

if (!dictionary.count)
return @{ };

Expand All @@ -124,6 +139,9 @@
template<>
NSSet *mapObjects<NSSet>(NSSet *set, __kindof id NS_NOESCAPE (^block)(__kindof id key, __kindof id value))
{
if (!set)
return nil;

switch (set.count) {
case 0:
return [NSSet set];
Expand Down
109 changes: 106 additions & 3 deletions Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#import <wtf/BlockPtr.h>
#import <wtf/HashSet.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/cocoa/VectorCocoa.h>
#import <wtf/text/WTFString.h>

#if PLATFORM(MAC)
Expand Down Expand Up @@ -122,6 +123,10 @@
static NSString * const contentSecurityPolicyManifestKey = @"content_security_policy";
static NSString * const contentSecurityPolicyExtensionPagesManifestKey = @"extension_pages";

static NSString * const webAccessibleResourcesManifestKey = @"web_accessible_resources";
static NSString * const webAccessibleResourcesResourcesManifestKey = @"resources";
static NSString * const webAccessibleResourcesMatchesManifestKey = @"matches";

WebExtension::WebExtension(NSBundle *appExtensionBundle, NSError **outError)
: m_bundle(appExtensionBundle)
, m_resourceBaseURL(appExtensionBundle.resourceURL.URLByStandardizingPath.absoluteURL)
Expand Down Expand Up @@ -293,10 +298,107 @@

#endif // PLATFORM(MAC)

bool WebExtension::isAccessibleResourcePath(NSString *, NSURL *pageURL)
bool WebExtension::isWebAccessibleResource(const URL& resourceURL, const URL& pageURL)
{
// FIXME: <https://webkit.org/b/246489> Implement web accessible resources.
return true;
populateWebAccessibleResourcesIfNeeded();

auto resourcePath = resourceURL.path().toString();

// The path is expected to match without the prefix slash.
ASSERT(resourcePath.startsWith('/'));
resourcePath = resourcePath.substring(1);

for (auto& data : m_webAccessibleResources) {
bool allowed = false;
for (auto& matchPattern : data.matchPatterns) {
if (matchPattern->matchesURL(pageURL)) {
allowed = true;
break;
}
}

if (!allowed)
continue;

for (auto& pathPattern : data.resourcePathPatterns) {
if (WebCore::matchesWildcardPattern(pathPattern, resourcePath))
return true;
}
}

return false;
}

void WebExtension::populateWebAccessibleResourcesIfNeeded()
{
if (!manifestParsedSuccessfully())
return;

if (m_parsedManifestWebAccessibleResources)
return;

m_parsedManifestWebAccessibleResources = true;

// Documentation: https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/manifest.json/web_accessible_resources

if (supportsManifestVersion(3)) {
if (auto *resourcesArray = objectForKey<NSArray>(m_manifest, webAccessibleResourcesManifestKey, false, NSDictionary.class)) {
bool errorOccurred = false;
for (NSDictionary *resourcesDictionary in resourcesArray) {
auto *pathsArray = objectForKey<NSArray>(resourcesDictionary, webAccessibleResourcesResourcesManifestKey, false, NSString.class);
auto *matchesArray = objectForKey<NSArray>(resourcesDictionary, webAccessibleResourcesMatchesManifestKey, false, NSString.class);

pathsArray = filterObjects(pathsArray, ^(id key, NSString *string) {
return !!string.length;
});

matchesArray = filterObjects(matchesArray, ^(id key, NSString *string) {
return !!string.length;
});

if (!pathsArray || !matchesArray) {
errorOccurred = true;
continue;
}

if (!pathsArray.count || !matchesArray.count)
continue;

MatchPatternSet matchPatterns;
for (NSString *matchPatternString in matchesArray) {
if (auto matchPattern = WebExtensionMatchPattern::getOrCreate(matchPatternString)) {
if (matchPattern->isSupported())
matchPatterns.add(matchPattern.releaseNonNull());
else
errorOccurred = true;
}
}

if (matchPatterns.isEmpty()) {
errorOccurred = true;
continue;
}

m_webAccessibleResources.append({ WTFMove(matchPatterns), makeVector<String>(pathsArray) });
}

if (errorOccurred)
recordError(createError(Error::InvalidWebAccessibleResources));
} else if ([m_manifest objectForKey:webAccessibleResourcesManifestKey])
recordError(createError(Error::InvalidWebAccessibleResources));
} else {
if (auto *resourcesArray = objectForKey<NSArray>(m_manifest, webAccessibleResourcesManifestKey, false, NSString.class)) {
resourcesArray = filterObjects(resourcesArray, ^(id key, NSString *string) {
return !!string.length;
});

if (resourcesArray.count) {
MatchPatternSet matchPatterns { WebExtensionMatchPattern::allHostsAndSchemesMatchPattern() };
m_webAccessibleResources.append({ WTFMove(matchPatterns), makeVector<String>(resourcesArray) });
}
} else if ([m_manifest objectForKey:webAccessibleResourcesManifestKey])
recordError(createError(Error::InvalidWebAccessibleResources));
}
}

NSURL *WebExtension::resourceFileURLForPath(NSString *path)
Expand Down Expand Up @@ -665,6 +767,7 @@ static _WKWebExtensionError toAPI(WebExtension::Error error)
populatePermissionsPropertiesIfNeeded();
populatePagePropertiesIfNeeded();
populateContentSecurityPolicyStringsIfNeeded();
populateWebAccessibleResourcesIfNeeded();

return [m_errors copy] ?: @[ ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,7 @@
}

if (parameters.titlePattern) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF LIKE %@", (NSString *)parameters.titlePattern.value()];
if (![predicate evaluateWithObject:title()])
if (!WebCore::matchesWildcardPattern(parameters.titlePattern.value(), title()))
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

void WebExtensionURLSchemeHandler::platformStartTask(WebPageProxy& page, WebURLSchemeTask& task)
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:makeBlockPtr([this, &task, &page, protectedThis = Ref { *this }, protectedTask = Ref { task }, protectedPage = Ref { page }]() {
auto *operation = [NSBlockOperation blockOperationWithBlock:makeBlockPtr([this, &task, &page, protectedThis = Ref { *this }, protectedTask = Ref { task }, protectedPage = Ref { page }]() {
// If a frame is loading, the frame request URL will be an empty string, since the request is actually the frame URL being loaded.
// In this case, consider the firstPartyForCookies() to be the document including the frame. This fails for nested frames, since
// it is always the main frame URL, not the immediate parent frame.
Expand All @@ -84,7 +84,7 @@
// FIXME: <https://webkit.org/b/246485> Support devtools' exception to web accessible resources.

if (!protocolHostAndPortAreEqual(frameDocumentURL, requestURL)) {
if (!extensionContext->extension().isAccessibleResourcePath(requestURL.path().toString(), frameDocumentURL)) {
if (!extensionContext->extension().isWebAccessibleResource(requestURL, frameDocumentURL)) {
task.didComplete([NSError errorWithDomain:NSURLErrorDomain code:noPermissionErrorCode userInfo:nil]);
return;
}
Expand Down
11 changes: 10 additions & 1 deletion Source/WebKit/UIProcess/Extensions/WebExtension.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,13 @@ class WebExtension : public API::ObjectImpl<API::Object::Type::WebExtension>, pu
NSArray *expandedExcludeMatchPatternStrings() const;
};

struct WebAccessibleResourceData {
MatchPatternSet matchPatterns;
Vector<String> resourcePathPatterns;
};

using InjectedContentVector = Vector<InjectedContentData>;
using WebAccessibleResourcesVector = Vector<WebAccessibleResourceData>;

static const PermissionsSet& supportedPermissions();

Expand All @@ -159,7 +165,7 @@ class WebExtension : public API::ObjectImpl<API::Object::Type::WebExtension>, pu
bool validateResourceData(NSURL *, NSData *, NSError **);
#endif

bool isAccessibleResourcePath(NSString *, NSURL *frameDocumentURL);
bool isWebAccessibleResource(const URL& resourceURL, const URL& pageURL);

NSURL *resourceFileURLForPath(NSString *);

Expand Down Expand Up @@ -246,8 +252,10 @@ class WebExtension : public API::ObjectImpl<API::Object::Type::WebExtension>, pu
void populatePermissionsPropertiesIfNeeded();
void populatePagePropertiesIfNeeded();
void populateContentSecurityPolicyStringsIfNeeded();
void populateWebAccessibleResourcesIfNeeded();

InjectedContentVector m_staticInjectedContents;
WebAccessibleResourcesVector m_webAccessibleResources;

MatchPatternSet m_permissionMatchPatterns;
MatchPatternSet m_optionalPermissionMatchPatterns;
Expand Down Expand Up @@ -302,6 +310,7 @@ class WebExtension : public API::ObjectImpl<API::Object::Type::WebExtension>, pu
bool m_parsedManifestContentScriptProperties : 1 { false };
bool m_parsedManifestPermissionProperties : 1 { false };
bool m_parsedManifestPageProperties : 1 { false };
bool m_parsedManifestWebAccessibleResources : 1 { false };
};

#ifdef __OBJC__
Expand Down
85 changes: 85 additions & 0 deletions Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtension.mm
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,91 @@
EXPECT_EQ(testExtension.errors.count, 1ul);
}

TEST(WKWebExtension, WebAccessibleResourcesV2)
{
NSMutableDictionary *testManifestDictionary = [@{
@"manifest_version": @2,
@"name": @"Test",
@"description": @"Test",
@"version": @"1.0",
@"web_accessible_resources": @[ @"images/*.png", @"styles/*.css" ]
} mutableCopy];

auto *testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_NS_EQUAL(testExtension.errors, @[ ]);

testManifestDictionary[@"web_accessible_resources"] = @[ ];
testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_NS_EQUAL(testExtension.errors, @[ ]);

testManifestDictionary[@"web_accessible_resources"] = @"bad";
testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_EQ(testExtension.errors.count, 1ul);

testManifestDictionary[@"web_accessible_resources"] = @{ };
testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_EQ(testExtension.errors.count, 1ul);
}

TEST(WKWebExtension, WebAccessibleResourcesV3)
{
NSMutableDictionary *testManifestDictionary = [@{
@"manifest_version": @3,
@"name": @"Test",
@"description": @"Test",
@"version": @"1.0",
@"web_accessible_resources": @[ @{
@"resources": @[ @"images/*.png", @"styles/*.css" ],
@"matches": @[ @"<all_urls>" ]
},
@{
@"resources": @[ @"scripts/*.js" ],
@"matches": @[ @"*://localhost/*" ]
} ]
} mutableCopy];

auto *testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_NS_EQUAL(testExtension.errors, @[ ]);

testManifestDictionary[@"web_accessible_resources"] = @[ @{
@"resources": @[ ],
@"matches": @[ ]
} ];

testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_NS_EQUAL(testExtension.errors, @[ ]);

testManifestDictionary[@"web_accessible_resources"] = @[ @{
@"resources": @"bad",
@"matches": @[ @"<all_urls>" ]
} ];

testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_EQ(testExtension.errors.count, 1ul);

testManifestDictionary[@"web_accessible_resources"] = @[ @{
@"resources": @[ @"images/*.png" ],
@"matches": @"bad"
} ];

testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_EQ(testExtension.errors.count, 1ul);

testManifestDictionary[@"web_accessible_resources"] = @[ @{
@"matches": @[ @"<all_urls>" ]
} ];

testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_EQ(testExtension.errors.count, 1ul);

testManifestDictionary[@"web_accessible_resources"] = @[ @{
@"resources": @[ ]
} ];

testExtension = [[_WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary];
EXPECT_EQ(testExtension.errors.count, 1ul);
}

} // namespace TestWebKitAPI

#endif // ENABLE(WK_WEB_EXTENSIONS)
Loading

0 comments on commit dc77e1b

Please sign in to comment.