// // 1Password Extension // // Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov. // Copyright (c) 2014 AgileBits. All rights reserved. // #import "OnePasswordExtension.h" // Version #define VERSION_NUMBER @(184) static NSString *const AppExtensionVersionNumberKey = @"version_number"; // Available App Extension Actions static NSString *const kUTTypeAppExtensionFindLoginAction = @"org.appextension.find-login-action"; static NSString *const kUTTypeAppExtensionSaveLoginAction = @"org.appextension.save-login-action"; static NSString *const kUTTypeAppExtensionChangePasswordAction = @"org.appextension.change-password-action"; static NSString *const kUTTypeAppExtensionFillWebViewAction = @"org.appextension.fill-webview-action"; static NSString *const kUTTypeAppExtensionFillBrowserAction = @"org.appextension.fill-browser-action"; // WebView Dictionary keys static NSString *const AppExtensionWebViewPageFillScript = @"fillScript"; static NSString *const AppExtensionWebViewPageDetails = @"pageDetails"; @implementation OnePasswordExtension #pragma mark - Public Methods + (OnePasswordExtension *)sharedExtension { static dispatch_once_t onceToken; static OnePasswordExtension *__sharedExtension; dispatch_once(&onceToken, ^{ __sharedExtension = [OnePasswordExtension new]; }); return __sharedExtension; } - (BOOL)isAppExtensionAvailable { if ([self isSystemAppExtensionAPIAvailable]) { return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"org-appextension-feature-password-management://"]]; } return NO; } #pragma mark - Native app Login - (void)findLoginForURLString:(nonnull NSString *)URLString forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion { NSAssert(URLString != nil, @"URLString must not be nil"); NSAssert(viewController != nil, @"viewController must not be nil"); if (NO == [self isSystemAppExtensionAPIAvailable]) { NSLog(@"Failed to findLoginForURLString, system API is not available"); if (completion) { completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]); } return; } #ifdef __IPHONE_8_0 NSDictionary *item = @{ AppExtensionVersionNumberKey: VERSION_NUMBER, AppExtensionURLStringKey: URLString }; UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionFindLoginAction]; activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { if (returnedItems.count == 0) { NSError *error = nil; if (activityError) { NSLog(@"Failed to findLoginForURLString: %@", activityError); error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError]; } else { error = [OnePasswordExtension extensionCancelledByUserError]; } if (completion) { completion(nil, error); } return; } [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) { if (completion) { completion(itemDictionary, error); } }]; }; [viewController presentViewController:activityViewController animated:YES completion:nil]; #endif } #pragma mark - New User Registration - (void)storeLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion { NSAssert(URLString != nil, @"URLString must not be nil"); NSAssert(viewController != nil, @"viewController must not be nil"); if (NO == [self isSystemAppExtensionAPIAvailable]) { NSLog(@"Failed to storeLoginForURLString, system API is not available"); if (completion) { completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]); } return; } #ifdef __IPHONE_8_0 NSMutableDictionary *newLoginAttributesDict = [NSMutableDictionary new]; newLoginAttributesDict[AppExtensionVersionNumberKey] = VERSION_NUMBER; newLoginAttributesDict[AppExtensionURLStringKey] = URLString; [newLoginAttributesDict addEntriesFromDictionary:loginDetailsDictionary]; if (passwordGenerationOptions.count > 0) { newLoginAttributesDict[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions; } UIActivityViewController *activityViewController = [self activityViewControllerForItem:newLoginAttributesDict viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionSaveLoginAction]; activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { if (returnedItems.count == 0) { NSError *error = nil; if (activityError) { NSLog(@"Failed to storeLoginForURLString: %@", activityError); error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError]; } else { error = [OnePasswordExtension extensionCancelledByUserError]; } if (completion) { completion(nil, error); } return; } [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) { if (completion) { completion(itemDictionary, error); } }]; }; [viewController presentViewController:activityViewController animated:YES completion:nil]; #endif } #pragma mark - Change Password - (void)changePasswordForLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion { NSAssert(URLString != nil, @"URLString must not be nil"); NSAssert(viewController != nil, @"viewController must not be nil"); if (NO == [self isSystemAppExtensionAPIAvailable]) { NSLog(@"Failed to changePasswordForLoginWithUsername, system API is not available"); if (completion) { completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]); } return; } #ifdef __IPHONE_8_0 NSMutableDictionary *item = [NSMutableDictionary new]; item[AppExtensionVersionNumberKey] = VERSION_NUMBER; item[AppExtensionURLStringKey] = URLString; [item addEntriesFromDictionary:loginDetailsDictionary]; if (passwordGenerationOptions.count > 0) { item[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions; } UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionChangePasswordAction]; activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { if (returnedItems.count == 0) { NSError *error = nil; if (activityError) { NSLog(@"Failed to changePasswordForLoginWithUsername: %@", activityError); error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError]; } else { error = [OnePasswordExtension extensionCancelledByUserError]; } if (completion) { completion(nil, error); } return; } [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) { if (completion) { completion(itemDictionary, error); } }]; }; [viewController presentViewController:activityViewController animated:YES completion:nil]; #endif } #pragma mark - Web View filling Support - (void)fillItemIntoWebView:(nonnull id)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion { NSAssert(webView != nil, @"webView must not be nil"); NSAssert(viewController != nil, @"viewController must not be nil"); NSAssert([webView isKindOfClass:[UIWebView class]] || [webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView or UIWebView."); #ifdef __IPHONE_8_0 if ([webView isKindOfClass:[UIWebView class]]) { [self fillItemIntoUIWebView:webView webViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { if (completion) { completion(success, error); } }]; } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW else if ([webView isKindOfClass:[WKWebView class]]) { [self fillItemIntoWKWebView:webView forViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { if (completion) { completion(success, error); } }]; } #endif #endif } #pragma mark - Support for custom UIActivityViewControllers - (BOOL)isOnePasswordExtensionActivityType:(nullable NSString *)activityType { return [@"com.agilebits.onepassword-ios.extension" isEqualToString:activityType] || [@"com.agilebits.beta.onepassword-ios.extension" isEqualToString:activityType]; } - (void)createExtensionItemForWebView:(nonnull id)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion { NSAssert(webView != nil, @"webView must not be nil"); NSAssert([webView isKindOfClass:[UIWebView class]] || [webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView or UIWebView."); #ifdef __IPHONE_8_0 if ([webView isKindOfClass:[UIWebView class]]) { UIWebView *uiWebView = (UIWebView *)webView; NSString *collectedPageDetails = [uiWebView stringByEvaluatingJavaScriptFromString:OPWebViewCollectFieldsScript]; [self createExtensionItemForURLString:uiWebView.request.URL.absoluteString webPageDetails:collectedPageDetails completion:completion]; } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW else if ([webView isKindOfClass:[WKWebView class]]) { WKWebView *wkWebView = (WKWebView *)webView; [wkWebView evaluateJavaScript:OPWebViewCollectFieldsScript completionHandler:^(NSString *result, NSError *evaluateError) { if (result == nil) { NSLog(@"1Password Extension failed to collect web page fields: %@", evaluateError); NSError *failedToCollectFieldsError = [OnePasswordExtension failedToCollectFieldsErrorWithUnderlyingError:evaluateError]; if (completion) { if ([NSThread isMainThread]) { completion(nil, failedToCollectFieldsError); } else { dispatch_async(dispatch_get_main_queue(), ^{ completion(nil, failedToCollectFieldsError); }); } } return; } [self createExtensionItemForURLString:wkWebView.URL.absoluteString webPageDetails:result completion:completion]; }]; } #endif #endif } - (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull id)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion { NSAssert(webView != nil, @"webView must not be nil"); if (returnedItems.count == 0) { NSError *error = [OnePasswordExtension extensionCancelledByUserError]; if (completion) { completion(NO, error); } return; } [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) { if (itemDictionary.count == 0) { if (completion) { completion(NO, error); } return; } NSString *fillScript = itemDictionary[AppExtensionWebViewPageFillScript]; [self executeFillScript:fillScript inWebView:webView completion:^(BOOL success, NSError *executeFillScriptError) { if (completion) { completion(success, executeFillScriptError); } }]; }]; } #pragma mark - Private methods - (BOOL)isSystemAppExtensionAPIAvailable { #ifdef __IPHONE_8_0 return [NSExtensionItem class] != nil; #else return NO; #endif } - (void)findLoginIn1PasswordWithURLString:(nonnull NSString *)URLString collectedPageDetails:(nullable NSString *)collectedPageDetails forWebViewController:(nonnull UIViewController *)forViewController sender:(nullable id)sender withWebView:(nonnull id)webView showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion { if ([URLString length] == 0) { NSError *URLStringError = [OnePasswordExtension failedToObtainURLStringFromWebViewError]; NSLog(@"Failed to findLoginIn1PasswordWithURLString: %@", URLStringError); if (completion) { completion(NO, URLStringError); } return; } NSError *jsonError = nil; NSData *data = [collectedPageDetails dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *collectedPageDetailsDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError]; if (collectedPageDetailsDictionary.count == 0) { NSLog(@"Failed to parse JSON collected page details: %@", jsonError); if (completion) { completion(NO, jsonError); } return; } NSDictionary *item = @{ AppExtensionVersionNumberKey : VERSION_NUMBER, AppExtensionURLStringKey : URLString, AppExtensionWebViewPageDetails : collectedPageDetailsDictionary }; NSString *typeIdentifier = yesOrNo ? kUTTypeAppExtensionFillWebViewAction : kUTTypeAppExtensionFillBrowserAction; UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:forViewController sender:sender typeIdentifier:typeIdentifier]; activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { if (returnedItems.count == 0) { NSError *error = nil; if (activityError) { NSLog(@"Failed to findLoginIn1PasswordWithURLString: %@", activityError); error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError]; } else { error = [OnePasswordExtension extensionCancelledByUserError]; } if (completion) { completion(NO, error); } return; } [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *processExtensionItemError) { if (itemDictionary.count == 0) { if (completion) { completion(NO, processExtensionItemError); } return; } NSString *fillScript = itemDictionary[AppExtensionWebViewPageFillScript]; [self executeFillScript:fillScript inWebView:webView completion:^(BOOL success, NSError *executeFillScriptError) { if (completion) { completion(success, executeFillScriptError); } }]; }]; }; [forViewController presentViewController:activityViewController animated:YES completion:nil]; } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW - (void)fillItemIntoWKWebView:(nonnull WKWebView *)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion { [webView evaluateJavaScript:OPWebViewCollectFieldsScript completionHandler:^(NSString *result, NSError *error) { if (result == nil) { NSLog(@"1Password Extension failed to collect web page fields: %@", error); if (completion) { completion(NO,[OnePasswordExtension failedToCollectFieldsErrorWithUnderlyingError:error]); } return; } [self findLoginIn1PasswordWithURLString:webView.URL.absoluteString collectedPageDetails:result forWebViewController:viewController sender:sender withWebView:webView showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *findLoginError) { if (completion) { completion(success, findLoginError); } }]; }]; } #endif - (void)fillItemIntoUIWebView:(nonnull UIWebView *)webView webViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion { NSString *collectedPageDetails = [webView stringByEvaluatingJavaScriptFromString:OPWebViewCollectFieldsScript]; [self findLoginIn1PasswordWithURLString:webView.request.URL.absoluteString collectedPageDetails:collectedPageDetails forWebViewController:viewController sender:sender withWebView:webView showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { if (completion) { completion(success, error); } }]; } - (void)executeFillScript:(NSString * __nullable)fillScript inWebView:(nonnull id)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion { if (fillScript == nil) { NSLog(@"Failed to executeFillScript, fillScript is missing"); if (completion) { completion(NO, [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script is missing", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:nil]); } return; } NSMutableString *scriptSource = [OPWebViewFillScript mutableCopy]; [scriptSource appendFormat:@"(document, %@, undefined);", fillScript]; #ifdef __IPHONE_8_0 if ([webView isKindOfClass:[UIWebView class]]) { NSString *result = [((UIWebView *)webView) stringByEvaluatingJavaScriptFromString:scriptSource]; BOOL success = (result != nil); NSError *error = nil; if (!success) { NSLog(@"Cannot executeFillScript, stringByEvaluatingJavaScriptFromString failed"); error = [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script could not be evaluated", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:nil]; } if (completion) { completion(success, error); } } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW else if ([webView isKindOfClass:[WKWebView class]]) { [((WKWebView *)webView) evaluateJavaScript:scriptSource completionHandler:^(NSString *result, NSError *evaluationError) { BOOL success = (result != nil); NSError *error = nil; if (!success) { NSLog(@"Cannot executeFillScript, evaluateJavaScript failed: %@", evaluationError); error = [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script could not be evaluated", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:error]; } if (completion) { completion(success, error); } }]; } #endif #endif } #ifdef __IPHONE_8_0 - (void)processExtensionItem:(nullable NSExtensionItem *)extensionItem completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion { if (extensionItem.attachments.count == 0) { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unexpected data returned by App Extension: extension item had no attachments." }; NSError *error = [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeUnexpectedData userInfo:userInfo]; if (completion) { completion(nil, error); } return; } NSItemProvider *itemProvider = extensionItem.attachments.firstObject; if (NO == [itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypePropertyList]) { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unexpected data returned by App Extension: extension item attachment does not conform to kUTTypePropertyList type identifier" }; NSError *error = [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeUnexpectedData userInfo:userInfo]; if (completion) { completion(nil, error); } return; } [itemProvider loadItemForTypeIdentifier:(__bridge NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *itemDictionary, NSError *itemProviderError) { NSError *error = nil; if (itemDictionary.count == 0) { NSLog(@"Failed to loadItemForTypeIdentifier: %@", itemProviderError); error = [OnePasswordExtension failedToLoadItemProviderDataErrorWithUnderlyingError:itemProviderError]; } if (completion) { if ([NSThread isMainThread]) { completion(itemDictionary, error); } else { dispatch_async(dispatch_get_main_queue(), ^{ completion(itemDictionary, error); }); } } }]; } - (UIActivityViewController *)activityViewControllerForItem:(nonnull NSDictionary *)item viewController:(nonnull UIViewController*)viewController sender:(nullable id)sender typeIdentifier:(nonnull NSString *)typeIdentifier { #ifdef __IPHONE_8_0 NSAssert(NO == (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && sender == nil), @"sender must not be nil on iPad."); NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:typeIdentifier]; NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init]; extensionItem.attachments = @[ itemProvider ]; UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[ extensionItem ] applicationActivities:nil]; if ([sender isKindOfClass:[UIBarButtonItem class]]) { controller.popoverPresentationController.barButtonItem = sender; } else if ([sender isKindOfClass:[UIView class]]) { controller.popoverPresentationController.sourceView = [sender superview]; controller.popoverPresentationController.sourceRect = [sender frame]; } else { NSLog(@"sender can be nil on iPhone"); } return controller; #else return nil; #endif } #endif - (void)createExtensionItemForURLString:(nonnull NSString *)URLString webPageDetails:(nullable NSString *)webPageDetails completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion { NSError *jsonError = nil; NSData *data = [webPageDetails dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *webPageDetailsDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError]; if (webPageDetailsDictionary.count == 0) { NSLog(@"Failed to parse JSON collected page details: %@", jsonError); if (completion) { completion(nil, jsonError); } return; } NSDictionary *item = @{ AppExtensionVersionNumberKey : VERSION_NUMBER, AppExtensionURLStringKey : URLString, AppExtensionWebViewPageDetails : webPageDetailsDictionary }; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:kUTTypeAppExtensionFillBrowserAction]; NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init]; extensionItem.attachments = @[ itemProvider ]; if (completion) { if ([NSThread isMainThread]) { completion(extensionItem, nil); } else { dispatch_async(dispatch_get_main_queue(), ^{ completion(extensionItem, nil); }); } } } #pragma mark - Errors + (NSError *)systemAppExtensionAPINotAvailableError { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"App Extension API is not available in this version of iOS", @"OnePasswordExtension", @"1Password Extension Error Message") }; return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeAPINotAvailable userInfo:userInfo]; } + (NSError *)extensionCancelledByUserError { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"1Password Extension was cancelled by the user", @"OnePasswordExtension", @"1Password Extension Error Message") }; return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCancelledByUser userInfo:userInfo]; } + (NSError *)failedToContactExtensionErrorWithActivityError:(nullable NSError *)activityError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to contact the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message"); if (activityError) { userInfo[NSUnderlyingErrorKey] = activityError; } return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToContactExtension userInfo:userInfo]; } + (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(nullable NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to execute script that collects web page information", @"OnePasswordExtension", @"1Password Extension Error Message"); if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCollectFieldsScriptFailed userInfo:userInfo]; } + (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(nullable NSString *)errorMessage underlyingError:(nullable NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; if (errorMessage) { userInfo[NSLocalizedDescriptionKey] = errorMessage; } if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFillFieldsScriptFailed userInfo:userInfo]; } + (NSError *)failedToLoadItemProviderDataErrorWithUnderlyingError:(nullable NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to parse information returned by 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message"); if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } return [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToLoadItemProviderData userInfo:userInfo]; } + (NSError *)failedToObtainURLStringFromWebViewError { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"Failed to obtain URL String from web view. The web view must be loaded completely when calling the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message") }; return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToObtainURLStringFromWebView userInfo:userInfo]; } #pragma mark - WebView field collection and filling scripts static NSString *const OPWebViewCollectFieldsScript = @";(function(document, undefined) {\ var isFirefox = false, isChrome = false, isSafari = true;\ \ document.elementsByOPID={};document.addEventListener('input',function(b){!1!==b.a&&'input'===b.target.tagName.toLowerCase()&&(b.target.dataset['com.agilebits.onepassword.userEdited']='yes')},!0);\ function q(b,d){function f(a,e){var c=a[e];if('string'==typeof c)return c;c=a.getAttribute(e);return'string'==typeof c?c:null}function h(a,e){if(-1===['text','password'].indexOf(e.type.toLowerCase())||!(m.test(a.value)||m.test(a.htmlID)||m.test(a.htmlName)||m.test(a.placeholder)||m.test(a['label-tag'])||m.test(a['label-data'])||m.test(a['label-aria'])))return!1;if(!a.visible)return!0;if('password'==e.type.toLowerCase())return!1;var c=e.type;v(e,!0);return c!==e.type}function n(a){switch(p(a.type)){case 'checkbox':return a.checked?\ '✓':'';case 'hidden':a=a.value;if(!a||'number'!=typeof a.length)return'';254\\/?]/mg,''):null;return[c?c:null,a.value]}),{options:a}):null}function r(a){var e;for(a=a.parentElement||a.parentNode;a&&'td'!=p(a.tagName);)a=a.parentElement||a.parentNode;if(!a||\ void 0===a)return null;e=a.parentElement||a.parentNode;if('tr'!=e.tagName.toLowerCase())return null;e=e.previousElementSibling;if(!e||'tr'!=(e.tagName+'').toLowerCase()||e.cells&&a.cellIndex>=e.cells.length)return null;a=e.cells[a.cellIndex];a=a.textContent||a.innerText;return a=x(a)}function s(a){var e,c=[];if(a.labels&&a.labels.length&&0b.clientWidth||10>b.clientHeight)return!1;var s=b.getClientRects();if(0===s.length)return!1;for(var g=0;gh||0>r.right)return!1;if(0>l||l>h||0>d||d>n)return!1;for(f=b.ownerDocument.elementFromPoint(l+(f.right>window.innerWidth?(window.innerWidth-l)/2:f.width/2),d+(f.bottom>window.innerHeight?\ (window.innerHeight-d)/2:f.height/2));f&&f!==b&&f!==document;){if(f.tagName&&'string'===typeof f.tagName&&'label'===f.tagName.toLowerCase()&&b.labels&&0