364 lines
13 KiB
Objective-C
364 lines
13 KiB
Objective-C
//
|
|
// SBRProcessBundleBridge.m
|
|
// SBrowser
|
|
//
|
|
// Created by James Magahern on 7/22/20.
|
|
//
|
|
|
|
#import "SBRProcessBundleBridge.h"
|
|
|
|
#import "SBRScriptPolicy.h"
|
|
#import "Hacks.h"
|
|
|
|
#import <OSLog/OSLog.h>
|
|
|
|
#import <WebKit/_WKRemoteObjectInterface.h>
|
|
#import <WebKit/_WKRemoteObjectRegistry.h>
|
|
#import <WebKit/_WKProcessPoolConfiguration.h>
|
|
|
|
#import <WebKit/WKProcessPoolPrivate.h>
|
|
#import <WebKit/WKWebViewPrivate.h>
|
|
#import <WebKit/WKWebViewConfigurationPrivate.h>
|
|
|
|
#define WKUserStyleSheet id
|
|
#define WKUserStyleSheetEncodedClassName "X1dLVXNlclN0eWxlU2hlZXQ="
|
|
|
|
@interface StyleSheet : NSObject <NSCopying>
|
|
@property (nonatomic, readonly, copy) NSString *source;
|
|
@property (nonatomic, readonly, copy) NSURL *baseURL;
|
|
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOL forMainFrameOnly;
|
|
|
|
- (instancetype)initWithSource:(NSString *)source forMainFrameOnly:(BOOL)forMainFrameOnly;
|
|
@end
|
|
|
|
@interface WKUserContentController (Private)
|
|
- (void)__addUserStyleSheet:(WKUserStyleSheet)userStyleSheet;
|
|
- (void)__removeUserStyleSheet:(WKUserStyleSheet)userStyleSheet;
|
|
- (void)__addUserScriptImmediately:(WKUserScript *)userScript;
|
|
@end
|
|
|
|
@implementation WKUserContentController (Private)
|
|
|
|
- (void)__addUserStyleSheet:(id)userStyleSheet
|
|
{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
[self performSelector:DecodedSelector("X2FkZFVzZXJTdHlsZVNoZWV0Og==") withObject:userStyleSheet];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
- (void)__removeUserStyleSheet:(id)userStyleSheet
|
|
{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
[self performSelector:DecodedSelector("X3JlbW92ZVVzZXJTdHlsZVNoZWV0Og==") withObject:userStyleSheet];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
- (void)__addUserScriptImmediately:(WKUserScript *)userScript
|
|
{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
[self performSelector:DecodedSelector("X2FkZFVzZXJTY3JpcHRJbW1lZGlhdGVseTo=") withObject:userScript];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
@end
|
|
|
|
#define LOG_DEBUG(format, ...) os_log_debug(_log, format, ##__VA_ARGS__)
|
|
#define LOG_ERROR(format, ...) os_log_error(_log, format, ##__VA_ARGS__)
|
|
|
|
@interface NSURLResponse (BridgeAdditions)
|
|
@property (nonatomic, readonly) BOOL isJavascriptResponse;
|
|
@end
|
|
|
|
@implementation NSURLResponse (BridgeAdditions)
|
|
|
|
- (BOOL)isJavascriptResponse
|
|
{
|
|
NSString *extension = [[self URL] pathExtension];
|
|
if ([[extension lowercaseString] isEqualToString:@"js"]) {
|
|
return YES;
|
|
}
|
|
|
|
NSString *MIMEType = [self MIMEType];
|
|
if ([[MIMEType lowercaseString] containsString:@"javascript"]) {
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface SBRProcessBundleBridge () <WKURLSchemeHandler>
|
|
|
|
@end
|
|
|
|
@implementation SBRProcessBundleBridge {
|
|
os_log_t _log;
|
|
|
|
WKWebView *_webView;
|
|
WKWebViewConfiguration *_webViewConfiguration;
|
|
WKProcessPool *_processPool;
|
|
|
|
WKUserStyleSheet _darkModeStyleSheet;
|
|
WKUserScript *_readabilityScript;
|
|
|
|
NSArray<WKUserScript *> *_userScripts;
|
|
|
|
dispatch_queue_t _dataTasksAccessQueue;
|
|
NSMutableDictionary<NSURLRequest *, NSURLSessionDataTask *> *_dataTasks;
|
|
|
|
// These come from settings.
|
|
WKUserStyleSheet _customizedUserStylesheet;
|
|
WKUserScript *_customizedUserScript;
|
|
}
|
|
|
|
- (void)tearDown
|
|
{
|
|
// This was used to unregister the delegate with the web process.
|
|
}
|
|
|
|
- (instancetype)initWithWebViewConfiguration:(WKWebViewConfiguration *)webViewConfiguration
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
if (!webViewConfiguration) {
|
|
_log = os_log_create("net.buzzert.attractor.webview", "bridge");
|
|
|
|
webViewConfiguration = [[WKWebViewConfiguration alloc] init];
|
|
|
|
// Set up process pool
|
|
_processPool = [[WKProcessPool alloc] init];
|
|
webViewConfiguration.processPool = _processPool;
|
|
|
|
webViewConfiguration._waitsForPaintAfterViewDidMoveToWindow = NO;
|
|
webViewConfiguration._applePayEnabled = YES;
|
|
|
|
_dataTasks = [NSMutableDictionary dictionary];
|
|
_dataTasksAccessQueue = dispatch_queue_create("net.buzzert.attractor.dataTasksAccess", DISPATCH_QUEUE_SERIAL);
|
|
|
|
[webViewConfiguration setURLSchemeHandler:self forURLScheme:@"http"];
|
|
[webViewConfiguration setURLSchemeHandler:self forURLScheme:@"https"];
|
|
}
|
|
|
|
_webViewConfiguration = webViewConfiguration;
|
|
|
|
// User scripts
|
|
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
|
for (WKUserScript *script in [self _userScripts]) {
|
|
[userContentController addUserScript:script];
|
|
}
|
|
|
|
// Reload customized user scripts/stylesheets from settings
|
|
[self reloadCustomizedUserScriptsAndStylesheets];
|
|
|
|
// Instantiate web view
|
|
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webViewConfiguration];
|
|
if (@available(iOS 16.0, *)) {
|
|
webView.findInteractionEnabled = YES;
|
|
}
|
|
|
|
_webView = webView;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (WKUserScript *)_loadScriptForResource:(NSString *)resourceName withExtension:(NSString *)extension
|
|
{
|
|
NSURL *url = [[NSBundle mainBundle] URLForResource:resourceName withExtension:extension];
|
|
NSString *source = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
|
|
|
|
return [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
|
|
}
|
|
|
|
- (NSArray<WKUserScript *> *)_userScripts
|
|
{
|
|
if (!_userScripts) {
|
|
_userScripts = @[
|
|
[self _loadScriptForResource:@"Tagger" withExtension:@"js"],
|
|
];
|
|
}
|
|
|
|
return _userScripts;
|
|
}
|
|
|
|
- (void)reloadCustomizedUserScriptsAndStylesheets
|
|
{
|
|
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
|
[userContentController removeAllUserScripts];
|
|
|
|
NSString *scriptSource = [[NSUserDefaults standardUserDefaults] stringForKey:@"userScript"];
|
|
if ([scriptSource length]) {
|
|
_customizedUserScript = [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
|
|
[userContentController addUserScript:_customizedUserScript];
|
|
}
|
|
|
|
if (_customizedUserStylesheet) {
|
|
[userContentController __removeUserStyleSheet:_customizedUserStylesheet];
|
|
}
|
|
|
|
NSString *stylesheetSource = [[NSUserDefaults standardUserDefaults] stringForKey:@"userStylesheet"];
|
|
if ([stylesheetSource length]) {
|
|
_customizedUserStylesheet = [[DecodedClass(WKUserStyleSheetEncodedClassName) alloc] initWithSource:stylesheetSource forMainFrameOnly:YES];
|
|
[userContentController __addUserStyleSheet:_customizedUserStylesheet];
|
|
}
|
|
}
|
|
|
|
#pragma mark Former <SBRWebProcessDelegate> methods
|
|
|
|
- (void)webProcessDidAllowScriptWithOrigin:(NSString *)origin
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[[self delegate] webProcess:self didAllowScriptResourceFromOrigin:origin];
|
|
});
|
|
}
|
|
|
|
- (void)webProcessDidBlockScriptWithOrigin:(NSString *)origin
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[[self delegate] webProcess:self didBlockScriptResourceFromOrigin:origin];
|
|
});
|
|
}
|
|
|
|
#pragma mark <WKURLSchemeHandler>
|
|
|
|
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
|
|
{
|
|
NSString *hostOrigin = [[_webView URL] host];
|
|
NSURLRequest *request = [urlSchemeTask request];
|
|
|
|
LOG_DEBUG("Start URL scheme task: request: %@", request);
|
|
|
|
__weak __auto_type welf = self;
|
|
NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
|
|
{
|
|
if (!welf) return;
|
|
__strong __auto_type sself = welf;
|
|
|
|
if (error != nil) {
|
|
[urlSchemeTask didFailWithError:error];
|
|
} else if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
NSURL *requestURL = [request URL];
|
|
NSString *resourceOrigin = [requestURL host];
|
|
const __auto_type allowResource = ^{
|
|
os_log_debug(sself->_log, "Allowing resource: %@", requestURL.lastPathComponent);
|
|
[urlSchemeTask didReceiveResponse:response];
|
|
[urlSchemeTask didReceiveData:data];
|
|
[urlSchemeTask didFinish];
|
|
|
|
[self webProcessDidAllowScriptWithOrigin:resourceOrigin];
|
|
};
|
|
|
|
const __auto_type denyResource = ^{
|
|
os_log_debug(sself->_log, "Blocking resource: %@", requestURL.lastPathComponent);
|
|
NSHTTPURLResponse *altResponse = [[NSHTTPURLResponse alloc] initWithURL:requestURL
|
|
MIMEType:@"application/javascript"
|
|
expectedContentLength:0 textEncodingName:@"utf8"];
|
|
[urlSchemeTask didReceiveResponse:altResponse];
|
|
[urlSchemeTask didReceiveData:[NSData data]];
|
|
[urlSchemeTask didFinish];
|
|
|
|
[self webProcessDidBlockScriptWithOrigin:resourceOrigin];
|
|
};
|
|
|
|
// Check MIME type for JavaScript responses.
|
|
if ([response isJavascriptResponse] && ![sself allowAllScripts]) {
|
|
dispatch_async(sself->_dataTasksAccessQueue, ^{
|
|
NSDictionary<NSString *, NSNumber *> *policyTypes = [sself->_policyDataSource scriptPolicyTypeByOrigin];
|
|
NSNumber *policyType = [policyTypes objectForKey:hostOrigin];
|
|
|
|
SBRScriptPolicy *policy = [[SBRScriptPolicy alloc] initWithSecurityOrigin:hostOrigin policyType:[policyType integerValue]];
|
|
if ([policy allowsExternalJavaScriptResourceOrigin:resourceOrigin]) {
|
|
allowResource();
|
|
} else {
|
|
denyResource();
|
|
}
|
|
});
|
|
} else {
|
|
allowResource();
|
|
}
|
|
} else {
|
|
[urlSchemeTask didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:nil]];
|
|
}
|
|
|
|
[sself->_dataTasks removeObjectForKey:request];
|
|
}];
|
|
|
|
[_dataTasks setObject:dataTask forKey:request];
|
|
[dataTask resume];
|
|
}
|
|
|
|
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
|
|
{
|
|
NSURLRequest *request = [urlSchemeTask request];
|
|
NSURLSessionDataTask *dataTask = [_dataTasks objectForKey:request];
|
|
if (dataTask) {
|
|
if ([dataTask state] != NSURLSessionTaskStateCanceling) {
|
|
[dataTask cancel];
|
|
}
|
|
|
|
[_dataTasks removeObjectForKey:request];
|
|
}
|
|
}
|
|
|
|
#pragma mark Actions
|
|
|
|
- (void)policyDataSourceDidChange
|
|
{
|
|
// This was used when we had to signal the process bundle.
|
|
}
|
|
|
|
- (void)setAllowAllScripts:(BOOL)allowAllScripts
|
|
{
|
|
_allowAllScripts = allowAllScripts;
|
|
}
|
|
|
|
- (void)setDarkModeEnabled:(BOOL)darkModeEnabled
|
|
{
|
|
_darkModeEnabled = darkModeEnabled;
|
|
|
|
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
|
|
|
if (darkModeEnabled) {
|
|
if (!_darkModeStyleSheet) {
|
|
NSURL *styleSheetURL = [[NSBundle mainBundle] URLForResource:@"darkmode" withExtension:@"css"];
|
|
NSString *styleSheetSource = [NSString stringWithContentsOfURL:styleSheetURL encoding:NSUTF8StringEncoding error:nil];
|
|
_darkModeStyleSheet = [[DecodedClass(WKUserStyleSheetEncodedClassName) alloc] initWithSource:styleSheetSource forMainFrameOnly:NO];
|
|
}
|
|
|
|
[userContentController __addUserStyleSheet:_darkModeStyleSheet];
|
|
} else if (_darkModeStyleSheet) {
|
|
[userContentController __removeUserStyleSheet:_darkModeStyleSheet];
|
|
}
|
|
}
|
|
|
|
- (void)parseDocumentForReaderMode:(void (^)(NSString * _Nonnull))completionBlock
|
|
{
|
|
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
|
|
|
if (!_readabilityScript) {
|
|
_readabilityScript = [self _loadScriptForResource:@"Readability" withExtension:@"js"];
|
|
}
|
|
|
|
[userContentController __addUserScriptImmediately:_readabilityScript];
|
|
|
|
NSString *script = @""
|
|
"var documentClone = document.cloneNode(true);"
|
|
"var article = new Readability(documentClone).parse();"
|
|
"article.content";
|
|
|
|
os_log_t log = _log;
|
|
[_webView evaluateJavaScript:script completionHandler:^(NSString *result, NSError * _Nullable error) {
|
|
if (error != nil) {
|
|
os_log_error(log, "Bridge: Readability error: %@", error.localizedDescription);
|
|
} else {
|
|
completionBlock(result);
|
|
}
|
|
}];
|
|
}
|
|
|
|
@end
|