// // 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 @(120) 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)isSystemAppExtensionAPIAvailable { #ifdef __IPHONE_8_0 return NSClassFromString(@"NSExtensionItem") != nil; #else return NO; #endif } - (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:(NSString *)URLString forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))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:(NSString *)URLString loginDetails:(NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion { NSAssert(URLString != nil, @"URLString must not be nil"); NSAssert(loginDetailsDictionary != nil, @"loginDetailsDict 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:(NSString *)URLString loginDetails:(NSDictionary *)loginDetailsDict passwordGenerationOptions:(NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))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:loginDetailsDict]; 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:(id)webView forViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))completion { NSAssert(webView != nil, @"webView must not be nil"); NSAssert(viewController != nil, @"viewController must not be nil"); #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 else { [NSException raise:@"Invalid argument: web view must be an instance of WKWebView or UIWebView." format:@""]; } #endif } #pragma mark - Support for custom UIActivityViewControllers - (BOOL)isOnePasswordExtensionActivityType:(NSString *)activityType { return [@"com.agilebits.onepassword-ios.extension" isEqualToString:activityType] || [@"com.agilebits.beta.onepassword-ios.extension" isEqualToString:activityType]; } - (void)createExtensionItemForWebView:(id)webView completion:(void (^)(NSExtensionItem *extensionItem, NSError *error))completion { NSAssert(webView != nil, @"webView must not be nil"); #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 else { [NSException raise:@"Invalid argument: web view must be an instance of WKWebView or UIWebView." format:@""]; } #endif } - (void)fillReturnedItems:(NSArray *)returnedItems intoWebView:(id)webView completion:(void (^)(BOOL success, NSError *error))completion { 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 - (void)findLoginIn1PasswordWithURLString:(NSString *)URLString collectedPageDetails:(NSString *)collectedPageDetails forWebViewController:(UIViewController *)forViewController sender:(id)sender withWebView:(id)webView showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))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:(WKWebView *)webView forViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))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:(UIWebView *)webView webViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))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 *)fillScript inWebView:(id)webView completion:(void (^)(BOOL success, NSError *error))completion { if (fillScript == nil) { NSLog(@"Failed to executeFillScript, fillScript is missing"); if (completion) { completion(NO, [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedString(@"Failed to fill web page because script is missing", @"1Password Extension Error Message") underlyingError:nil]); } return; } NSMutableString *scriptSource = [OPWebViewFillScript mutableCopy]; [scriptSource appendFormat:@"(document, %@);", fillScript]; 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:NSLocalizedString(@"Failed to fill web page because script could not be evaluated", @"1Password Extension Error Message") underlyingError:nil]; } if (completion) { completion(success, error); } return; } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW 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:NSLocalizedString(@"Failed to fill web page because script could not be evaluated", @"1Password Extension Error Message") underlyingError:error]; } if (completion) { completion(success, error); } }]; return; } #endif [NSException raise:@"Invalid argument: web view must be an instance of WKWebView or UIWebView." format:@""]; } #ifdef __IPHONE_8_0 - (void)processExtensionItem:(NSExtensionItem *)extensionItem completion:(void (^)(NSDictionary *itemDictionary, NSError *error))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:(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:(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:(NSDictionary *)item viewController:(UIViewController*)viewController sender:(id)sender typeIdentifier:(NSString *)typeIdentifier { #ifdef __IPHONE_8_0 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:(NSString *)URLString webPageDetails:(NSString *)webPageDetails completion:(void (^)(NSExtensionItem *extensionItem, NSError *error))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 : NSLocalizedString(@"App Extension API is not available is this version of iOS", @"1Password Extension Error Message") }; return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeAPINotAvailable userInfo:userInfo]; } + (NSError *)extensionCancelledByUserError { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedString(@"1Password Extension was cancelled by the user", @"1Password Extension Error Message") }; return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCancelledByUser userInfo:userInfo]; } + (NSError *)failedToContactExtensionErrorWithActivityError:(NSError *)activityError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Failed to contact the 1Password Extension", @"1Password Extension Error Message"); if (activityError) { userInfo[NSUnderlyingErrorKey] = activityError; } return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToContactExtension userInfo:userInfo]; } + (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Failed to execute script that collects web page information", @"1Password Extension Error Message"); if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCollectFieldsScriptFailed userInfo:userInfo]; } + (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(NSString *)errorMessage underlyingError:(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:(NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Failed to parse information returned by 1Password Extension", @"1Password Extension Error Message"); if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } return [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToLoadItemProviderData userInfo:userInfo]; } + (NSError *)failedToObtainURLStringFromWebViewError { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedString(@"Failed to obtain URL String from web view. The web view must be loaded completely when calling the 1Password Extension", @"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.com_agilebits_onepassword_collect=n;document.elementsByOPID={};\ function n(d,b){function e(a,f){var c=a[f];if('string'==typeof c)return c;c=a.getAttribute(f);return'string'==typeof c?c:null}function h(a){switch(l(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 s(a){var f;for(a=a.parentElement||a.parentNode;a&&'td'!=l(a.tagName);)a=a.parentElement||a.parentNode;if(!a||void 0===a)return null;f=a.parentElement||a.parentNode;if('tr'!=f.tagName.toLowerCase())return null;f=f.previousElementSibling;if(!f||'tr'!=(f.tagName+'').toLowerCase()||f.cells&&a.cellIndex>=f.cells.length)return null;a=p(f.cells[a.cellIndex]);return a=r(a)}function x(a){var f=d.documentElement,c=a.getBoundingClientRect(),b=f.getBoundingClientRect(),\ e=c.left-f.clientLeft,f=c.top-f.clientTop;return a.offsetParent?0>e||e>b.width||0>f||f>b.height?t(a):(b=a.ownerDocument.elementFromPoint(e+3,f+3))?'label'===l(b.tagName)?b===y(a):b.tagName===a.tagName:!1:!1}function t(a){for(var f;a!==d&&a;a=a.parentNode)if(f=u.getComputedStyle?u.getComputedStyle(a,null):a.style,'none'===f.display||'hidden'==f.visibility)return!1;return a===d}function y(a){var f;if(a.id&&(f=z(d,'label[for='+JSON.stringify(a.id)+']'))||a.name&&(f=z(d,'label[for='+JSON.stringify(a.name)+\ ']')))return p(f);for(;a&&a!=d;a=a.parentNode)if('label'===l(a.tagName))return p(a);return null}function g(a,f,c,d){void 0!==d&&d===c||null===c||void 0===c||(a[f]=c)}function l(a){return'string'===typeof a?a.toLowerCase():(''+a).toLowerCase()}function z(a,d){var c=null;try{c=a.querySelector(d)}catch(b){}return c}function A(a,d){var c=[];try{c=a.querySelectorAll(d)}catch(b){}return c}var u=d.defaultView?d.defaultView:window,m=RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|passe|contraseña|senha|密码|adgangskode|hasło|wachtwoord)',\ 'i'),F=Array.prototype.slice.call(A(d,'form')).map(function(a,d){var c={},b='__form__'+d;a.opid=b;c.opid=b;g(c,'htmlName',e(a,'name'));g(c,'htmlID',e(a,'id'));g(c,'htmlAction',v(e(a,'action')));g(c,'htmlMethod',e(a,'method'));return c}),C=Array.prototype.slice.call(A(d,'input, select')).map(function(a,f){var c={},b='__'+f,k=-1==a.maxLength?999:a.maxLength;d.elementsByOPID[b]=a;a.opid=b;c.opid=b;c.elementNumber=f;g(c,'maxLength',Math.min(k,999),999);c.visible=t(a);c.viewable=x(a);g(c,'htmlID',e(a,\ 'id'));g(c,'htmlName',e(a,'name'));g(c,'htmlClass',e(a,'class'));if('hidden'!=l(a.type)){g(c,'label-tag',y(a));g(c,'label-data',e(a,'data-label'));g(c,'label-aria',e(a,'aria-label'));g(c,'label-top',s(a));b=[];for(k=a;k&&k.nextSibling;){k=k.nextSibling;if(w(k))break;B(b,k)}g(c,'label-right',b.join(''));b=[];D(a,b);b=b.reverse().join('');g(c,'label-left',b);g(c,'placeholder',e(a,'placeholder'))}g(c,'rel',e(a,'rel'));g(c,'type',l(e(a,'type')));g(c,'value',h(a));g(c,'checked',a.checked,!1);g(c,'autoCompleteType',\ a.getAttribute('x-autocompletetype')||a.getAttribute('autocompletetype')||a.getAttribute('autocomplete'),'off');g(c,'selectInfo',q(a));g(c,'aria-hidden','true'==a.getAttribute('aria-hidden'),!1);g(c,'aria-disabled','true'==a.getAttribute('aria-disabled'),!1);g(c,'aria-haspopup','true'==a.getAttribute('aria-haspopup'),!1);g(c,'data-stripe',e(a,'data-stripe'));a.form&&(c.form=e(a.form,'opid'));b=(m.test(c.value)||m.test(c.htmlID)||m.test(c.htmlName)||m.test(c.placeholder)||m.test(c['label-tag'])||m.test(c['label-data'])||\ m.test(c['label-aria']))&&('text'==c.type||'password'==c.type&&!c.visible);g(c,'fakeTested',b,!1);return c});C.filter(function(a){return a.fakeTested}).forEach(function(a){var b=d.elementsByOPID[a.opid];b.getBoundingClientRect();!b||b&&'function'!==typeof b.click||b.click();b.focus();E(b,'keydown');E(b,'keyup');E(b,'keypress');b.click&&b.click();a.postFakeTestVisible=t(b);a.postFakeTestViewable=x(b);a=b.ownerDocument.createEvent('HTMLEvents');var c=b.ownerDocument.createEvent('HTMLEvents');E(b,'keydown');\ E(b,'keyup');E(b,'keypress');c.initEvent('input',!0,!0);b.dispatchEvent(c);a.initEvent('change',!0,!0);b.dispatchEvent(a);b.blur()});return{documentUUID:b,title:d.title,url:u.location.href,forms:function(a){var b={};a.forEach(function(a){b[a.opid]=a});return b}(F),fields:C,collectedTimestamp:(new Date).getTime()}};document.elementForOPID=G;function E(d,b){var e;e=d.ownerDocument.createEvent('KeyboardEvent');e.initKeyboardEvent?e.initKeyboardEvent(b,!0,!0):e.initKeyEvent&&e.initKeyEvent(b,!0,!0,null,!1,!1,!1,!1,0,0);d.dispatchEvent(e)}function p(d){return d.textContent||d.innerText}function r(d){var b=null;d&&(b=d.replace(/^\\s+|\\s+$|\\r?\\n.*$/mg,''),b=0