Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(84)

Side by Side Diff: ios/web/web_state/ui/crw_wk_web_view_web_controller.mm

Issue 1357773002: WKWebView: Implemented recoverable SSL interstitials. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@lock_coloring
Patch Set: Merged with origin/master Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #import "ios/web/web_state/ui/crw_wk_web_view_web_controller.h" 5 #import "ios/web/web_state/ui/crw_wk_web_view_web_controller.h"
6 6
7 #import <WebKit/WebKit.h> 7 #import <WebKit/WebKit.h>
8 8
9 #include "base/ios/ios_util.h" 9 #include "base/ios/ios_util.h"
10 #include "base/ios/weak_nsobject.h" 10 #include "base/ios/weak_nsobject.h"
11 #include "base/json/json_reader.h" 11 #include "base/json/json_reader.h"
12 #import "base/mac/scoped_nsobject.h" 12 #import "base/mac/scoped_nsobject.h"
13 #include "base/macros.h" 13 #include "base/macros.h"
14 #include "base/metrics/histogram_macros.h" 14 #include "base/metrics/histogram_macros.h"
15 #include "base/strings/sys_string_conversions.h" 15 #include "base/strings/sys_string_conversions.h"
16 #include "base/values.h" 16 #include "base/values.h"
17 #import "ios/net/http_response_headers_util.h" 17 #import "ios/net/http_response_headers_util.h"
18 #import "ios/web/crw_network_activity_indicator_manager.h" 18 #import "ios/web/crw_network_activity_indicator_manager.h"
19 #import "ios/web/navigation/crw_session_controller.h" 19 #import "ios/web/navigation/crw_session_controller.h"
20 #import "ios/web/navigation/crw_session_entry.h" 20 #import "ios/web/navigation/crw_session_entry.h"
21 #include "ios/web/navigation/navigation_item_impl.h" 21 #include "ios/web/navigation/navigation_item_impl.h"
22 #include "ios/web/navigation/web_load_params.h" 22 #include "ios/web/navigation/web_load_params.h"
23 #include "ios/web/net/cert_verification_cache.h"
23 #import "ios/web/net/crw_cert_verification_controller.h" 24 #import "ios/web/net/crw_cert_verification_controller.h"
24 #include "ios/web/public/cert_store.h" 25 #include "ios/web/public/cert_store.h"
25 #include "ios/web/public/navigation_item.h" 26 #include "ios/web/public/navigation_item.h"
26 #include "ios/web/public/ssl_status.h" 27 #include "ios/web/public/ssl_status.h"
27 #include "ios/web/public/web_client.h" 28 #include "ios/web/public/web_client.h"
28 #import "ios/web/public/web_state/js/crw_js_injection_manager.h" 29 #import "ios/web/public/web_state/js/crw_js_injection_manager.h"
29 #import "ios/web/public/web_state/ui/crw_native_content_provider.h" 30 #import "ios/web/public/web_state/ui/crw_native_content_provider.h"
30 #import "ios/web/public/web_state/ui/crw_web_view_content_view.h" 31 #import "ios/web/public/web_state/ui/crw_web_view_content_view.h"
31 #import "ios/web/ui_web_view_util.h" 32 #import "ios/web/ui_web_view_util.h"
32 #include "ios/web/web_state/blocked_popup_info.h" 33 #include "ios/web/web_state/blocked_popup_info.h"
33 #import "ios/web/web_state/error_translation_util.h" 34 #import "ios/web/web_state/error_translation_util.h"
34 #include "ios/web/web_state/frame_info.h" 35 #include "ios/web/web_state/frame_info.h"
35 #import "ios/web/web_state/js/crw_js_window_id_manager.h" 36 #import "ios/web/web_state/js/crw_js_window_id_manager.h"
36 #import "ios/web/web_state/js/page_script_util.h" 37 #import "ios/web/web_state/js/page_script_util.h"
37 #import "ios/web/web_state/ui/crw_web_controller+protected.h" 38 #import "ios/web/web_state/ui/crw_web_controller+protected.h"
38 #import "ios/web/web_state/ui/crw_wk_web_view_crash_detector.h" 39 #import "ios/web/web_state/ui/crw_wk_web_view_crash_detector.h"
39 #import "ios/web/web_state/ui/web_view_js_utils.h" 40 #import "ios/web/web_state/ui/web_view_js_utils.h"
40 #import "ios/web/web_state/ui/wk_back_forward_list_item_holder.h" 41 #import "ios/web/web_state/ui/wk_back_forward_list_item_holder.h"
41 #import "ios/web/web_state/ui/wk_web_view_configuration_provider.h" 42 #import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
42 #import "ios/web/web_state/web_state_impl.h" 43 #import "ios/web/web_state/web_state_impl.h"
43 #import "ios/web/web_state/web_view_internal_creation_util.h" 44 #import "ios/web/web_state/web_view_internal_creation_util.h"
44 #import "ios/web/web_state/wk_web_view_security_util.h" 45 #import "ios/web/web_state/wk_web_view_security_util.h"
45 #import "ios/web/webui/crw_web_ui_manager.h" 46 #import "ios/web/webui/crw_web_ui_manager.h"
46 #include "net/cert/x509_certificate.h" 47 #include "net/cert/x509_certificate.h"
47 #import "net/base/mac/url_conversions.h" 48 #import "net/base/mac/url_conversions.h"
48 #include "net/ssl/ssl_info.h" 49 #include "net/ssl/ssl_info.h"
49 #include "url/url_constants.h" 50 #include "url/url_constants.h"
50 51
51 namespace { 52 namespace {
53
54 // Represents cert verification error, which happened inside
55 // |webView:didReceiveAuthenticationChallenge:completionHandler:| and should
56 // be checked inside |webView:didFailProvisionalNavigation:withError:|.
57 struct CertVerificationError {
58 BOOL is_recoverable;
59 net::CertStatus status;
60 };
61
52 // Extracts Referer value from WKNavigationAction request header. 62 // Extracts Referer value from WKNavigationAction request header.
53 NSString* GetRefererFromNavigationAction(WKNavigationAction* action) { 63 NSString* GetRefererFromNavigationAction(WKNavigationAction* action) {
54 return [action.request valueForHTTPHeaderField:@"Referer"]; 64 return [action.request valueForHTTPHeaderField:@"Referer"];
55 } 65 }
56 66
57 NSString* const kScriptMessageName = @"crwebinvoke"; 67 NSString* const kScriptMessageName = @"crwebinvoke";
58 NSString* const kScriptImmediateName = @"crwebinvokeimmediate"; 68 NSString* const kScriptImmediateName = @"crwebinvokeimmediate";
59 69
60 // Utility functions for storing the source of NSErrors received by WKWebViews: 70 // Utility functions for storing the source of NSErrors received by WKWebViews:
61 // - Errors received by |-webView:didFailProvisionalNavigation:withError:| are 71 // - Errors received by |-webView:didFailProvisionalNavigation:withError:| are
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
147 // bad SSL cert, presenting SSL interstitials and determining SSL status for 157 // bad SSL cert, presenting SSL interstitials and determining SSL status for
148 // Navigation Items. 158 // Navigation Items.
149 base::scoped_nsobject<CRWCertVerificationController> 159 base::scoped_nsobject<CRWCertVerificationController>
150 _certVerificationController; 160 _certVerificationController;
151 161
152 // Whether the pending navigation has been directly cancelled in 162 // Whether the pending navigation has been directly cancelled in
153 // |decidePolicyForNavigationAction| or |decidePolicyForNavigationResponse|. 163 // |decidePolicyForNavigationAction| or |decidePolicyForNavigationResponse|.
154 // Cancelled navigations should be simply discarded without handling any 164 // Cancelled navigations should be simply discarded without handling any
155 // specific error. 165 // specific error.
156 BOOL _pendingNavigationCancelled; 166 BOOL _pendingNavigationCancelled;
167
168 // CertVerification errors which happened inside
169 // |webView:didReceiveAuthenticationChallenge:completionHandler:|.
170 // Key is leaf-cert/host pair. This storage is used to carry calculated
171 // cert status from |didReceiveAuthenticationChallenge:| to
172 // |didFailProvisionalNavigation:| delegate method.
173 web::CertVerificationCache<CertVerificationError> _certVerificationErrors;
157 } 174 }
158 175
159 // Response's MIME type of the last known navigation. 176 // Response's MIME type of the last known navigation.
160 @property(nonatomic, copy) NSString* documentMIMEType; 177 @property(nonatomic, copy) NSString* documentMIMEType;
161 178
162 // Dictionary where keys are the names of WKWebView properties and values are 179 // Dictionary where keys are the names of WKWebView properties and values are
163 // selector names which should be called when a corresponding property has 180 // selector names which should be called when a corresponding property has
164 // changed. e.g. @{ @"URL" : @"webViewURLDidChange" } means that 181 // changed. e.g. @{ @"URL" : @"webViewURLDidChange" } means that
165 // -[self webViewURLDidChange] must be called every time when WKWebView.URL is 182 // -[self webViewURLDidChange] must be called every time when WKWebView.URL is
166 // changed. 183 // changed.
(...skipping 396 matching lines...) Expand 10 before | Expand all | Expand 10 after
563 } 580 }
564 return [super URLForHistoryNavigationFromItem:fromItem toItem:toItem]; 581 return [super URLForHistoryNavigationFromItem:fromItem toItem:toItem];
565 } 582 }
566 583
567 - (void)setPageChangeProbability:(web::PageChangeProbability)probability { 584 - (void)setPageChangeProbability:(web::PageChangeProbability)probability {
568 // Nothing to do; no polling timer. 585 // Nothing to do; no polling timer.
569 } 586 }
570 587
571 - (void)abortWebLoad { 588 - (void)abortWebLoad {
572 [_wkWebView stopLoading]; 589 [_wkWebView stopLoading];
590 _certVerificationErrors.reset();
573 } 591 }
574 592
575 - (void)resetLoadState { 593 - (void)resetLoadState {
576 // Nothing to do. 594 // Nothing to do.
577 } 595 }
578 596
579 - (void)setSuppressDialogsWithHelperScript:(NSString*)script { 597 - (void)setSuppressDialogsWithHelperScript:(NSString*)script {
580 [self evaluateJavaScript:script stringResultHandler:nil]; 598 [self evaluateJavaScript:script stringResultHandler:nil];
581 } 599 }
582 600
(...skipping 266 matching lines...) Expand 10 before | Expand all | Expand 10 after
849 sourceURL:sourceURL 867 sourceURL:sourceURL
850 referrerPolicy:base::SysNSStringToUTF8(policy)]; 868 referrerPolicy:base::SysNSStringToUTF8(policy)];
851 }]; 869 }];
852 }); 870 });
853 } 871 }
854 872
855 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) 873 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
856 - (void)handleSSLCertError:(NSError*)error { 874 - (void)handleSSLCertError:(NSError*)error {
857 DCHECK(web::IsWKWebViewSSLCertError(error)); 875 DCHECK(web::IsWKWebViewSSLCertError(error));
858 876
859 net::SSLInfo sslInfo; 877 net::SSLInfo SSLInfo;
860 web::GetSSLInfoFromWKWebViewSSLCertError(error, &sslInfo); 878 web::GetSSLInfoFromWKWebViewSSLCertError(error, &SSLInfo);
861 879
862 web::SSLStatus sslStatus; 880 web::SSLStatus SSLStatus;
863 sslStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN; 881 SSLStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN;
864 sslStatus.cert_status = sslInfo.cert_status; 882 SSLStatus.cert_status = SSLInfo.cert_status;
865 sslStatus.cert_id = web::CertStore::GetInstance()->StoreCert( 883 SSLStatus.cert_id = web::CertStore::GetInstance()->StoreCert(
866 sslInfo.cert.get(), self.certGroupID); 884 SSLInfo.cert.get(), self.certGroupID);
867 885
868 [self.delegate presentSSLError:sslInfo 886 NSArray* chain = error.userInfo[web::kNSErrorPeerCertificateChainKey];
869 forSSLStatus:sslStatus 887 NSString* host = [error.userInfo[web::kNSErrorFailingURLKey] host];
870 recoverable:NO 888 // Verification results are cached for leaf cert, because cert chain in
871 callback:nullptr]; 889 // |didReceiveAuthenticationChallenge:| maybe different from |chain|.
890 scoped_refptr<net::X509Certificate> leafCert;
891 BOOL recoverable = NO;
892 if (chain.count && host.length) {
893 // Complete cert chain may not be available inside this method, so leaf
894 // cert is used as a key to retrieve _certVerificationErrors as well as for
895 // storing cert decision.
896 leafCert = web::CreateCertFromChain(@[ chain.firstObject ]);
897 if (leafCert) {
898 CertVerificationError error;
899 if (_certVerificationErrors.Get(leafCert, base::SysNSStringToUTF8(host),
stuartmorgan 2015/10/08 16:52:39 Why |if| rather than a DCHECK? I thought the advan
Eugene But (OOO till 7-30) 2015/10/09 16:32:36 I don't want to assert that this leaf cert has bee
stuartmorgan 2015/10/09 21:30:00 Yes, UMA sounds good. If this is anything but incr
Eugene But (OOO till 7-30) 2015/10/12 18:19:40 Added TODO
900 &error)) {
901 SSLStatus.cert_status = error.status;
902 recoverable = error.is_recoverable;
903 }
904 }
905 }
906 [self.delegate presentSSLError:SSLInfo
907 forSSLStatus:SSLStatus
908 recoverable:recoverable
909 callback:^(BOOL proceed) {
910 if (proceed) {
911 // The interstitial will be removed during reload.
912 [_certVerificationController
913 allowCert:leafCert
914 forHost:host
915 status:SSLStatus.cert_status];
916 [self loadCurrentURL];
stuartmorgan 2015/10/08 16:52:39 There is (currently; it needs to move into NM) spe
Eugene But (OOO till 7-30) 2015/10/09 16:32:36 Are you sure? How is this case different form UIWe
917 }
918 }];
872 } 919 }
873 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) 920 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
874 921
875 - (void)addActivityIndicatorTask { 922 - (void)addActivityIndicatorTask {
876 [[CRWNetworkActivityIndicatorManager sharedInstance] 923 [[CRWNetworkActivityIndicatorManager sharedInstance]
877 startNetworkTaskForGroup:[self activityIndicatorGroupID]]; 924 startNetworkTaskForGroup:[self activityIndicatorGroupID]];
878 } 925 }
879 926
880 - (void)clearActivityIndicatorTasks { 927 - (void)clearActivityIndicatorTasks {
881 [[CRWNetworkActivityIndicatorManager sharedInstance] 928 [[CRWNetworkActivityIndicatorManager sharedInstance]
(...skipping 531 matching lines...) Expand 10 before | Expand all | Expand 10 after
1413 #endif // defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) 1460 #endif // defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
1414 1461
1415 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW) 1462 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
1416 if (web::IsWKWebViewSSLCertError(error)) 1463 if (web::IsWKWebViewSSLCertError(error))
1417 [self handleSSLCertError:error]; 1464 [self handleSSLCertError:error];
1418 else 1465 else
1419 #endif 1466 #endif
1420 [self handleLoadError:error inMainFrame:YES]; 1467 [self handleLoadError:error inMainFrame:YES];
1421 1468
1422 [self discardPendingNavigationTypeForMainFrame]; 1469 [self discardPendingNavigationTypeForMainFrame];
1470 _certVerificationErrors.reset();
1423 } 1471 }
1424 1472
1425 - (void)webView:(WKWebView *)webView 1473 - (void)webView:(WKWebView *)webView
1426 didCommitNavigation:(WKNavigation *)navigation { 1474 didCommitNavigation:(WKNavigation *)navigation {
1427 DCHECK_EQ(_wkWebView, webView); 1475 DCHECK_EQ(_wkWebView, webView);
1476 _certVerificationErrors.reset();
1428 // This point should closely approximate the document object change, so reset 1477 // This point should closely approximate the document object change, so reset
1429 // the list of injected scripts to those that are automatically injected. 1478 // the list of injected scripts to those that are automatically injected.
1430 _injectedScriptManagers.reset([[NSMutableSet alloc] init]); 1479 _injectedScriptManagers.reset([[NSMutableSet alloc] init]);
1431 [self injectWindowID]; 1480 [self injectWindowID];
1432 1481
1433 // The page has changed; commit the pending referrer. 1482 // The page has changed; commit the pending referrer.
1434 [self commitPendingReferrerString]; 1483 [self commitPendingReferrerString];
1435 1484
1436 // This is the point where the document's URL has actually changed. 1485 // This is the point where the document's URL has actually changed.
1437 _documentURL = net::GURLWithNSURL([_wkWebView URL]); 1486 _documentURL = net::GURLWithNSURL([_wkWebView URL]);
(...skipping 16 matching lines...) Expand all
1454 web::EvaluateJavaScript(webView, 1503 web::EvaluateJavaScript(webView,
1455 @"__gCrWeb.didFinishNavigation()", nil); 1504 @"__gCrWeb.didFinishNavigation()", nil);
1456 [self didFinishNavigation]; 1505 [self didFinishNavigation];
1457 } 1506 }
1458 1507
1459 - (void)webView:(WKWebView *)webView 1508 - (void)webView:(WKWebView *)webView
1460 didFailNavigation:(WKNavigation *)navigation 1509 didFailNavigation:(WKNavigation *)navigation
1461 withError:(NSError *)error { 1510 withError:(NSError *)error {
1462 [self handleLoadError:WKWebViewErrorWithSource(error, NAVIGATION) 1511 [self handleLoadError:WKWebViewErrorWithSource(error, NAVIGATION)
1463 inMainFrame:YES]; 1512 inMainFrame:YES];
1513 _certVerificationErrors.reset();
1464 } 1514 }
1465 1515
1466 - (void)webView:(WKWebView *)webView 1516 - (void)webView:(WKWebView *)webView
1467 didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge 1517 didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
1468 completionHandler: 1518 completionHandler:
1469 (void (^)(NSURLSessionAuthChallengeDisposition disposition, 1519 (void (^)(NSURLSessionAuthChallengeDisposition disposition,
1470 NSURLCredential *credential))completionHandler { 1520 NSURLCredential *credential))completionHandler {
1471 if (![challenge.protectionSpace.authenticationMethod 1521 if (![challenge.protectionSpace.authenticationMethod
1472 isEqual:NSURLAuthenticationMethodServerTrust]) { 1522 isEqual:NSURLAuthenticationMethodServerTrust]) {
1473 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); 1523 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
1474 return; 1524 return;
1475 } 1525 }
1476 1526
1477 SecTrustRef trust = challenge.protectionSpace.serverTrust; 1527 SecTrustRef trust = challenge.protectionSpace.serverTrust;
1478 scoped_refptr<net::X509Certificate> cert = web::CreateCertFromTrust(trust); 1528 base::WeakNSObject<CRWWKWebViewWebController> weakSelf(self);
1529 id handler = ^(web::CertAcceptPolicy policy, net::CertStatus status) {
1530 base::scoped_nsobject<CRWWKWebViewWebController> strongSelf(
1531 [weakSelf retain]);
1532 if (!strongSelf) {
1533 return;
1534 }
stuartmorgan 2015/10/08 16:52:39 Per my note in the other CL, I'd prefer everything
Eugene But (OOO till 7-30) 2015/10/09 16:32:36 Done.
1535
1536 if (policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) {
1537 // cert is invalid, but user agreed to proceed.
1538 completionHandler(NSURLSessionAuthChallengeUseCredential,
1539 [NSURLCredential credentialForTrust:trust]);
1540 return;
1541 }
1542
1543 if (policy != web::CERT_ACCEPT_POLICY_ALLOW) {
1544 // cert is invalid and user has not agreed to proceed.
1545
1546 // Cache cert verification result with _certVerificationErrors storage,
1547 // so it can be later reused inside |didFailProvisionalNavigation:|.
1548 // didFailProvisionalNavigation: does not receive full cert chain and it
1549 // will not be possible to resulculate cert status there.
1550 if (SecTrustGetCertificateCount(trust)) {
1551 // Leaf cert (w/o any intermidiates) is used for caching.
1552 // Chain inside |didFailProvisionalNavigation:| may be different and
1553 // using intermidiates will result in keys mismatch.
1554 scoped_refptr<net::X509Certificate> leafCert = web::CreateCertFromChain(
1555 @[ static_cast<id>(SecTrustGetCertificateAtIndex(trust, 0)) ]);
1556 if (leafCert) {
1557 BOOL is_recoverable =
1558 policy ==
1559 web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_NOT_ACCEPTED_BY_USER;
1560 strongSelf.get()->_certVerificationErrors.Set(
1561 leafCert, base::SysNSStringToUTF8(challenge.protectionSpace.host),
1562 {is_recoverable, status});
1563 }
1564 }
1565 }
1566 completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
1567 };
1568
1569 base::ScopedCFTypeRef<SecTrustRef> scopedTrust(trust,
1570 base::scoped_policy::RETAIN);
1479 [_certVerificationController 1571 [_certVerificationController
1480 decidePolicyForCert:cert 1572 decideLoadPolicyForTrust:scopedTrust
1481 host:challenge.protectionSpace.host 1573 host:challenge.protectionSpace.host
1482 completionHandler:^(web::CertAcceptPolicy policy, 1574 completionHandler:handler];
1483 net::CertStatus status) {
1484 completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace,
1485 nil);
1486 }];
1487 } 1575 }
1488 1576
1489 - (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView { 1577 - (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView {
1490 [self webViewWebProcessDidCrash]; 1578 [self webViewWebProcessDidCrash];
1491 } 1579 }
1492 1580
1493 #pragma mark WKUIDelegate Methods 1581 #pragma mark WKUIDelegate Methods
1494 1582
1495 - (WKWebView*)webView:(WKWebView*)webView 1583 - (WKWebView*)webView:(WKWebView*)webView
1496 createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration 1584 createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
1575 placeholderText:defaultText 1663 placeholderText:defaultText
1576 requestURL: 1664 requestURL:
1577 net::GURLWithNSURL(frame.request.URL) 1665 net::GURLWithNSURL(frame.request.URL)
1578 completionHandler:completionHandler]; 1666 completionHandler:completionHandler];
1579 } else if (completionHandler) { 1667 } else if (completionHandler) {
1580 completionHandler(nil); 1668 completionHandler(nil);
1581 } 1669 }
1582 } 1670 }
1583 1671
1584 @end 1672 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698