| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ios/chrome/browser/web/chrome_web_client.h" |
| |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_util.h" |
| #import "base/ios/ios_util.h" |
| #import "base/ios/ns_error_util.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/dom_distiller/core/url_constants.h" |
| #include "components/google/core/common/google_util.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/version_info/version_info.h" |
| #include "ios/chrome/browser/application_context.h" |
| #include "ios/chrome/browser/browser_about_rewriter.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/chrome_switches.h" |
| #include "ios/chrome/browser/chrome_url_constants.h" |
| #include "ios/chrome/browser/ios_chrome_main_parts.h" |
| #import "ios/chrome/browser/reading_list/offline_page_tab_helper.h" |
| #import "ios/chrome/browser/safe_browsing/safe_browsing_blocking_page.h" |
| #import "ios/chrome/browser/safe_browsing/safe_browsing_error.h" |
| #import "ios/chrome/browser/safe_browsing/safe_browsing_unsafe_resource_container.h" |
| #include "ios/chrome/browser/ssl/ios_ssl_error_handler.h" |
| #import "ios/chrome/browser/ui/elements/windowed_container_view.h" |
| #include "ios/chrome/browser/ui/ui_feature_flags.h" |
| #include "ios/chrome/browser/web/error_page_controller_bridge.h" |
| #import "ios/chrome/browser/web/error_page_util.h" |
| #include "ios/chrome/browser/web/features.h" |
| #import "ios/components/security_interstitials/ios_blocking_page_tab_helper.h" |
| #import "ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.h" |
| #import "ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h" |
| #import "ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h" |
| #import "ios/components/security_interstitials/lookalikes/lookalike_url_blocking_page.h" |
| #import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h" |
| #import "ios/components/security_interstitials/lookalikes/lookalike_url_controller_client.h" |
| #import "ios/components/security_interstitials/lookalikes/lookalike_url_error.h" |
| #include "ios/components/webui/web_ui_url_constants.h" |
| #import "ios/net/protocol_handler_util.h" |
| #include "ios/public/provider/chrome/browser/browser_url_rewriter_provider.h" |
| #import "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| #include "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| #include "ios/public/provider/chrome/browser/voice/audio_session_controller.h" |
| #include "ios/public/provider/chrome/browser/voice/voice_search_provider.h" |
| #include "ios/web/common/features.h" |
| #include "ios/web/common/user_agent.h" |
| #include "ios/web/public/navigation/browser_url_rewriter.h" |
| #include "ios/web/public/navigation/navigation_manager.h" |
| #include "net/base/net_errors.h" |
| #include "net/http/http_util.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| // The tag describing the product name with a placeholder for the version. |
| const char kProductTagWithPlaceholder[] = "CriOS/%s"; |
| |
| // Returns an autoreleased string containing the JavaScript loaded from a |
| // bundled resource file with the given name (excluding extension). |
| NSString* GetPageScript(NSString* script_file_name) { |
| DCHECK(script_file_name); |
| NSString* path = |
| [base::mac::FrameworkBundle() pathForResource:script_file_name |
| ofType:@"js"]; |
| DCHECK(path) << "Script file not found: " |
| << base::SysNSStringToUTF8(script_file_name) << ".js"; |
| NSError* error = nil; |
| NSString* content = [NSString stringWithContentsOfFile:path |
| encoding:NSUTF8StringEncoding |
| error:&error]; |
| DCHECK(!error) << "Error fetching script: " |
| << base::SysNSStringToUTF8(error.description); |
| DCHECK(content); |
| return content; |
| } |
| // Returns the safe browsing error page HTML. |
| NSString* GetSafeBrowsingErrorPageHTML(web::WebState* web_state, |
| int64_t navigation_id) { |
| // Fetch the unsafe resource causing this error page from the WebState's |
| // container. |
| SafeBrowsingUnsafeResourceContainer* container = |
| SafeBrowsingUnsafeResourceContainer::FromWebState(web_state); |
| const security_interstitials::UnsafeResource* resource = |
| container->GetMainFrameUnsafeResource() |
| ?: container->GetSubFrameUnsafeResource( |
| web_state->GetNavigationManager()->GetLastCommittedItem()); |
| |
| // Construct the blocking page and associate it with the WebState. |
| std::unique_ptr<security_interstitials::IOSSecurityInterstitialPage> page = |
| SafeBrowsingBlockingPage::Create(*resource); |
| std::string error_page_content = page->GetHtmlContents(); |
| security_interstitials::IOSBlockingPageTabHelper::FromWebState(web_state) |
| ->AssociateBlockingPage(navigation_id, std::move(page)); |
| |
| return base::SysUTF8ToNSString(error_page_content); |
| } |
| |
| // Returns the lookalike error page HTML. |
| NSString* GetLookalikeUrlErrorPageHtml(web::WebState* web_state, |
| int64_t navigation_id) { |
| // Fetch the lookalike URL info from the WebState's container. |
| LookalikeUrlContainer* container = |
| LookalikeUrlContainer::FromWebState(web_state); |
| std::unique_ptr<LookalikeUrlContainer::LookalikeUrlInfo> lookalike_info = |
| container->ReleaseLookalikeUrlInfo(); |
| |
| // Construct the blocking page and associate it with the WebState. |
| std::unique_ptr<security_interstitials::IOSSecurityInterstitialPage> page = |
| std::make_unique<LookalikeUrlBlockingPage>( |
| web_state, lookalike_info->safe_url, lookalike_info->request_url, |
| ukm::ConvertToSourceId(navigation_id, |
| ukm::SourceIdType::NAVIGATION_ID), |
| lookalike_info->match_type, |
| std::make_unique<LookalikeUrlControllerClient>( |
| web_state, lookalike_info->safe_url, lookalike_info->request_url, |
| GetApplicationContext()->GetApplicationLocale())); |
| std::string error_page_content = page->GetHtmlContents(); |
| security_interstitials::IOSBlockingPageTabHelper::FromWebState(web_state) |
| ->AssociateBlockingPage(navigation_id, std::move(page)); |
| |
| return base::SysUTF8ToNSString(error_page_content); |
| } |
| |
| // Returns the legacy TLS error page HTML. |
| NSString* GetLegacyTLSErrorPageHTML(web::WebState* web_state, |
| int64_t navigation_id) { |
| // Construct the blocking page and associate it with the WebState. |
| std::unique_ptr<security_interstitials::IOSSecurityInterstitialPage> page = |
| std::make_unique<LegacyTLSBlockingPage>( |
| web_state, web_state->GetVisibleURL() /*request_url*/, |
| std::make_unique<LegacyTLSControllerClient>( |
| web_state, web_state->GetVisibleURL(), |
| GetApplicationContext()->GetApplicationLocale())); |
| std::string error_page_content = page->GetHtmlContents(); |
| security_interstitials::IOSBlockingPageTabHelper::FromWebState(web_state) |
| ->AssociateBlockingPage(navigation_id, std::move(page)); |
| |
| return base::SysUTF8ToNSString(error_page_content); |
| } |
| |
| // Returns a string describing the product name and version, of the |
| // form "productname/version". Used as part of the user agent string. |
| std::string GetMobileProduct() { |
| return base::StringPrintf(kProductTagWithPlaceholder, |
| version_info::GetVersionNumber().c_str()); |
| } |
| |
| // Returns a string describing the product name and version, of the |
| // form "productname/version". Used as part of the user agent string. |
| // The Desktop UserAgent is only using the major version to reduce the surface |
| // for fingerprinting. The Mobile one is using the full version for legacy |
| // reasons. |
| std::string GetDesktopProduct() { |
| return base::StringPrintf(kProductTagWithPlaceholder, |
| version_info::GetMajorVersionNumber().c_str()); |
| } |
| |
| } // namespace |
| |
| ChromeWebClient::ChromeWebClient() {} |
| |
| ChromeWebClient::~ChromeWebClient() {} |
| |
| std::unique_ptr<web::WebMainParts> ChromeWebClient::CreateWebMainParts() { |
| return std::make_unique<IOSChromeMainParts>( |
| *base::CommandLine::ForCurrentProcess()); |
| } |
| |
| void ChromeWebClient::PreWebViewCreation() const { |
| // TODO(crbug.com/1082371): Confirm that this code is no longer needed and |
| // remove it entirely. Until then, prevent this from running on iOS 13.4+, as |
| // it occasionally triggers a permissions prompt. |
| if (!base::ios::IsRunningOnOrLater(13, 4, 0)) { |
| // Initialize the audio session to allow a web page's audio to continue |
| // playing after the app is backgrounded. |
| VoiceSearchProvider* voice_provider = |
| ios::GetChromeBrowserProvider()->GetVoiceSearchProvider(); |
| if (voice_provider) { |
| AudioSessionController* audio_controller = |
| voice_provider->GetAudioSessionController(); |
| if (audio_controller) { |
| audio_controller->InitializeSessionIfNecessary(); |
| } |
| } |
| } |
| } |
| |
| void ChromeWebClient::AddAdditionalSchemes(Schemes* schemes) const { |
| schemes->standard_schemes.push_back(kChromeUIScheme); |
| schemes->secure_schemes.push_back(kChromeUIScheme); |
| } |
| |
| std::string ChromeWebClient::GetApplicationLocale() const { |
| DCHECK(GetApplicationContext()); |
| return GetApplicationContext()->GetApplicationLocale(); |
| } |
| |
| bool ChromeWebClient::IsAppSpecificURL(const GURL& url) const { |
| return url.SchemeIs(kChromeUIScheme); |
| } |
| |
| bool ChromeWebClient::ShouldBlockUrlDuringRestore( |
| const GURL& url, |
| web::WebState* web_state) const { |
| return ios::GetChromeBrowserProvider()->ShouldBlockUrlDuringRestore( |
| url, web_state); |
| } |
| |
| void ChromeWebClient::AddSerializableData( |
| web::SerializableUserDataManager* user_data_manager, |
| web::WebState* web_state) { |
| return ios::GetChromeBrowserProvider()->AddSerializableData(user_data_manager, |
| web_state); |
| } |
| |
| base::string16 ChromeWebClient::GetPluginNotSupportedText() const { |
| return l10n_util::GetStringUTF16(IDS_PLUGIN_NOT_SUPPORTED); |
| } |
| |
| std::string ChromeWebClient::GetUserAgent(web::UserAgentType type) const { |
| // The user agent should not be requested for app-specific URLs. |
| DCHECK_NE(type, web::UserAgentType::NONE); |
| |
| // Using desktop user agent overrides a command-line user agent, so that |
| // request desktop site can still work when using an overridden UA. |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| if (type != web::UserAgentType::DESKTOP && |
| command_line->HasSwitch(switches::kUserAgent)) { |
| std::string user_agent = |
| command_line->GetSwitchValueASCII(switches::kUserAgent); |
| if (net::HttpUtil::IsValidHeaderValue(user_agent)) |
| return user_agent; |
| LOG(WARNING) << "Ignored invalid value for flag --" << switches::kUserAgent; |
| } |
| |
| if (type == web::UserAgentType::DESKTOP) |
| return web::BuildDesktopUserAgent(GetDesktopProduct()); |
| return web::BuildMobileUserAgent(GetMobileProduct()); |
| } |
| |
| base::string16 ChromeWebClient::GetLocalizedString(int message_id) const { |
| return l10n_util::GetStringUTF16(message_id); |
| } |
| |
| base::StringPiece ChromeWebClient::GetDataResource( |
| int resource_id, |
| ui::ScaleFactor scale_factor) const { |
| return ui::ResourceBundle::GetSharedInstance().GetRawDataResourceForScale( |
| resource_id, scale_factor); |
| } |
| |
| base::RefCountedMemory* ChromeWebClient::GetDataResourceBytes( |
| int resource_id) const { |
| return ui::ResourceBundle::GetSharedInstance().LoadDataResourceBytes( |
| resource_id); |
| } |
| |
| void ChromeWebClient::GetAdditionalWebUISchemes( |
| std::vector<std::string>* additional_schemes) { |
| additional_schemes->push_back(dom_distiller::kDomDistillerScheme); |
| } |
| |
| void ChromeWebClient::PostBrowserURLRewriterCreation( |
| web::BrowserURLRewriter* rewriter) { |
| rewriter->AddURLRewriter(&WillHandleWebBrowserAboutURL); |
| BrowserURLRewriterProvider* provider = |
| ios::GetChromeBrowserProvider()->GetBrowserURLRewriterProvider(); |
| if (provider) |
| provider->AddProviderRewriters(rewriter); |
| } |
| |
| NSString* ChromeWebClient::GetDocumentStartScriptForAllFrames( |
| web::BrowserState* browser_state) const { |
| return GetPageScript(@"chrome_bundle_all_frames"); |
| } |
| |
| NSString* ChromeWebClient::GetDocumentStartScriptForMainFrame( |
| web::BrowserState* browser_state) const { |
| NSMutableArray* scripts = [NSMutableArray array]; |
| [scripts addObject:GetPageScript(@"chrome_bundle_main_frame")]; |
| |
| return [scripts componentsJoinedByString:@";"]; |
| } |
| |
| void ChromeWebClient::AllowCertificateError( |
| web::WebState* web_state, |
| int cert_error, |
| const net::SSLInfo& info, |
| const GURL& request_url, |
| bool overridable, |
| int64_t navigation_id, |
| base::OnceCallback<void(bool)> callback) { |
| base::OnceCallback<void(NSString*)> null_callback; |
| // TODO(crbug.com/760873): IOSSSLErrorHandler will present an interstitial |
| // for the user to decide if it is safe to proceed. |
| // Handle the case of web_state not presenting UI to users like prerender tabs |
| // or web_state used to fetch offline content in Reading List. |
| IOSSSLErrorHandler::HandleSSLError( |
| web_state, cert_error, info, request_url, overridable, navigation_id, |
| std::move(callback), std::move(null_callback)); |
| } |
| |
| bool ChromeWebClient::IsLegacyTLSAllowedForHost(web::WebState* web_state, |
| const std::string& hostname) { |
| auto* allowlist = LegacyTLSTabAllowList::FromWebState(web_state); |
| if (!allowlist) |
| return false; |
| return allowlist->IsDomainAllowed(hostname); |
| } |
| |
| void ChromeWebClient::PrepareErrorPage( |
| web::WebState* web_state, |
| const GURL& url, |
| NSError* error, |
| bool is_post, |
| bool is_off_the_record, |
| const base::Optional<net::SSLInfo>& info, |
| int64_t navigation_id, |
| base::OnceCallback<void(NSString*)> callback) { |
| OfflinePageTabHelper* offline_page_tab_helper = |
| OfflinePageTabHelper::FromWebState(web_state); |
| // WebState that are not attached to a tab may not have an |
| // OfflinePageTabHelper. |
| if (offline_page_tab_helper && |
| offline_page_tab_helper->HasDistilledVersionForOnlineUrl(url)) { |
| // An offline version of the page will be displayed to replace this error |
| // page. Loading an error page here can cause a race between the |
| // navigation to load the error page and the navigation to display the |
| // offline version of the page. If the latter navigation interrupts the |
| // former and causes it to fail, this can incorrectly appear to be a |
| // navigation back to the previous committed URL. To avoid this race, |
| // return a nil error page here to avoid an error page load. See |
| // crbug.com/980912. |
| std::move(callback).Run(nil); |
| return; |
| } |
| DCHECK(error); |
| __block NSString* error_html = nil; |
| __block base::OnceCallback<void(NSString*)> error_html_callback = |
| std::move(callback); |
| NSError* final_underlying_error = |
| base::ios::GetFinalUnderlyingErrorFromError(error); |
| if ([final_underlying_error.domain isEqual:kSafeBrowsingErrorDomain]) { |
| // Only kUnsafeResourceErrorCode is supported. |
| DCHECK_EQ(kUnsafeResourceErrorCode, final_underlying_error.code); |
| std::move(error_html_callback) |
| .Run(GetSafeBrowsingErrorPageHTML(web_state, navigation_id)); |
| } else if ([final_underlying_error.domain isEqual:kLookalikeUrlErrorDomain]) { |
| // Only kLookalikeUrlErrorCode is supported. |
| DCHECK_EQ(kLookalikeUrlErrorCode, final_underlying_error.code); |
| std::move(error_html_callback) |
| .Run(GetLookalikeUrlErrorPageHtml(web_state, navigation_id)); |
| } else if ([final_underlying_error.domain isEqual:net::kNSErrorDomain] && |
| final_underlying_error.code == net::ERR_SSL_OBSOLETE_VERSION) { |
| std::move(error_html_callback) |
| .Run(GetLegacyTLSErrorPageHTML(web_state, navigation_id)); |
| } else if (info.has_value()) { |
| base::OnceCallback<void(bool)> proceed_callback; |
| base::OnceCallback<void(NSString*)> blocking_page_callback = |
| base::BindOnce(^(NSString* blocking_page_html) { |
| error_html = blocking_page_html; |
| std::move(error_html_callback).Run(error_html); |
| }); |
| IOSSSLErrorHandler::HandleSSLError( |
| web_state, net::MapCertStatusToNetError(info.value().cert_status), |
| info.value(), url, info.value().is_fatal_cert_error, navigation_id, |
| std::move(proceed_callback), std::move(blocking_page_callback)); |
| } else { |
| std::move(error_html_callback) |
| .Run(GetErrorPage(url, error, is_post, is_off_the_record)); |
| ErrorPageControllerBridge* error_page_controller = |
| ErrorPageControllerBridge::FromWebState(web_state); |
| if (error_page_controller) { |
| // ErrorPageControllerBridge may not be created for web_state not attached |
| // to a tab. |
| error_page_controller->StartHandlingJavascriptCommands(); |
| } |
| } |
| } |
| |
| UIView* ChromeWebClient::GetWindowedContainer() { |
| if (!windowed_container_) { |
| windowed_container_ = [[WindowedContainerView alloc] init]; |
| } |
| return windowed_container_; |
| } |
| |
| bool ChromeWebClient::EnableLongPressAndForceTouchHandling() const { |
| return !web::features::UseWebViewNativeContextMenuWeb(); |
| } |
| |
| bool ChromeWebClient::EnableLongPressUIContextMenu() const { |
| return web::features::UseWebViewNativeContextMenuSystem(); |
| } |
| |
| bool ChromeWebClient::ForceMobileVersionByDefault(const GURL& url) { |
| DCHECK(web::features::UseWebClientDefaultUserAgent()); |
| if (base::FeatureList::IsEnabled(web::kMobileGoogleSRP)) { |
| return google_util::IsGoogleSearchUrl(url); |
| } |
| return false; |
| } |
| |
| web::UserAgentType ChromeWebClient::GetDefaultUserAgent( |
| id<UITraitEnvironment> web_view, |
| const GURL& url) { |
| DCHECK(web::features::UseWebClientDefaultUserAgent()); |
| if (ForceMobileVersionByDefault(url)) |
| return web::UserAgentType::MOBILE; |
| BOOL isRegularRegular = web_view.traitCollection.horizontalSizeClass == |
| UIUserInterfaceSizeClassRegular && |
| web_view.traitCollection.verticalSizeClass == |
| UIUserInterfaceSizeClassRegular; |
| return isRegularRegular ? web::UserAgentType::DESKTOP |
| : web::UserAgentType::MOBILE; |
| } |
| |
| bool ChromeWebClient::IsEmbedderBlockRestoreUrlEnabled() { |
| return ios::GetChromeBrowserProvider()->MightBlockUrlDuringRestore(); |
| } |