//Copyright (c) 2014-2020 AgileBits Inc. // //Permission is hereby granted, free of charge, to any person obtaining a copy //of this software and associated documentation files (the "Software"), to deal //in the Software without restriction, including without limitation the rights //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell //copies of the Software, and to permit persons to whom the Software is //furnished to do so, subject to the following conditions: // //The above copyright notice and this permission notice shall be included in all //copies or substantial portions of the Software. // //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. #import "OnePasswordExtension.h" NSString *const AppExtensionURLStringKey = @"url_string"; NSString *const AppExtensionUsernameKey = @"username"; NSString *const AppExtensionPasswordKey = @"password"; NSString *const AppExtensionTOTPKey = @"totp"; NSString *const AppExtensionTitleKey = @"login_title"; NSString *const AppExtensionNotesKey = @"notes"; NSString *const AppExtensionSectionTitleKey = @"section_title"; NSString *const AppExtensionFieldsKey = @"fields"; NSString *const AppExtensionReturnedFieldsKey = @"returned_fields"; NSString *const AppExtensionOldPasswordKey = @"old_password"; NSString *const AppExtensionPasswordGeneratorOptionsKey = @"password_generator_options"; NSString *const AppExtensionGeneratedPasswordMinLengthKey = @"password_min_length"; NSString *const AppExtensionGeneratedPasswordMaxLengthKey = @"password_max_length"; NSString *const AppExtensionGeneratedPasswordRequireDigitsKey = @"password_require_digits"; NSString *const AppExtensionGeneratedPasswordRequireSymbolsKey = @"password_require_symbols"; NSString *const AppExtensionGeneratedPasswordForbiddenCharactersKey = @"password_forbidden_characters"; NSString *const AppExtensionErrorDomain = @"OnePasswordExtension"; // Version #define VERSION_NUMBER @(185) 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; } 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]; } #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; } 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]; } #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; } 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]; } #pragma mark - Web View filling Support - (void)fillItemIntoWebView:(nonnull WKWebView *)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:[WKWebView class]], @"webView must be an instance of WKWebView."); [self fillItemIntoWKWebView:webView forViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { if (completion) { completion(success, error); } }]; } #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 WKWebView *)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion { NSAssert(webView != nil, @"webView must not be nil"); NSAssert([webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView."); [webView 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:webView.URL.absoluteString webPageDetails:result completion:completion]; }]; } - (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull WKWebView *)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 { return [NSExtensionItem class] != nil; } - (void)findLoginIn1PasswordWithURLString:(nonnull NSString *)URLString collectedPageDetails:(nullable NSString *)collectedPageDetails forWebViewController:(nonnull UIViewController *)forViewController sender:(nullable id)sender withWebView:(nonnull WKWebView *)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]; } - (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); } }]; }]; } - (void)executeFillScript:(NSString * __nullable)fillScript inWebView:(nonnull WKWebView *)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]; [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); } }]; } - (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 { NSAssert(NO == (UIDevice.currentDevice.userInterfaceIdiom == 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; } - (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) {\ \ document.addEventListener('input',function(b){!1!==b.isTrusted&&'input'===b.target.tagName.toLowerCase()&&(b.target.dataset['com.agilebits.onepassword.userEdited']='yes')},!0);\ (function(b,a,c){a.FieldCollector=new function(){function f(d){return d?d.toString().toLowerCase():''}function e(d,b,a,e){e!==c&&e===a||null===a||a===c||(d[b]=a)}function k(d,b){var a=[];try{a=d.querySelectorAll(b)}catch(J){console.error('[COLLECT FIELDS] @ag_querySelectorAll Exception in selector \"'+b+'\"')}return a}function m(d){var a,c=[];if(d.labels&&d.labels.length&&0=a.cells.length)return null;d=r(a.cells[d.cellIndex]);return d=l(d)}function p(a){return a.options?(a=Array.prototype.slice.call(a.options).map(function(a){var d=a.text,d=d?f(d).replace(/\\s/mg,'').replace(/[~`!@$%^&*()\\-_+=:;'\"\\[\\]|\\\\,<.>\\/?]/mg,\ ''):null;return[d?d:null,a.value]}),{options:a}):null}function F(a){switch(f(a.type)){case 'checkbox':return a.checked?'✓':'';case 'hidden':a=a.value;if(!a||'number'!=typeof a.length)return'';254b.clientWidth||10>b.clientHeight)return!1;var n=b.getClientRects();if(0===n.length)return!1;for(var p=0;pf||0>m.right)return!1;if(0>k||k>f||0>a||a>e)return!1;for(c=b.ownerDocument.elementFromPoint(k+(c.right>window.innerWidth?(window.innerWidth-k)/2:c.width/2),a+(c.bottom>window.innerHeight?\ (window.innerHeight-a)/2:c.height/2));c&&c!==b&&c!==document;){if(c.tagName&&'string'===typeof c.tagName&&'label'===c.tagName.toLowerCase()&&b.labels&&0