diff --git a/chromium_src/ios/CPPLINT.cfg b/chromium_src/ios/CPPLINT.cfg new file mode 100644 index 000000000000..5c959f3835e5 --- /dev/null +++ b/chromium_src/ios/CPPLINT.cfg @@ -0,0 +1,2 @@ +# Common linting false posititives from Obj-C syntax in headers +filter=-readability/casting,-whitespace/parens,-whitespace/operators diff --git a/chromium_src/ios/chrome/browser/https_upgrades/model/DEPS b/chromium_src/ios/chrome/browser/https_upgrades/model/DEPS new file mode 100644 index 000000000000..2a2cb68fa627 --- /dev/null +++ b/chromium_src/ios/chrome/browser/https_upgrades/model/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+brave/components/https_upgrade_exceptions/browser", + "+brave/ios/browser/application_context", +] diff --git a/chromium_src/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm b/chromium_src/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm new file mode 100644 index 000000000000..b6f1d5cfc042 --- /dev/null +++ b/chromium_src/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm @@ -0,0 +1,33 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/https_upgrade_exceptions/browser/https_upgrade_exceptions_service.h" +#include "brave/ios/browser/application_context/brave_application_context_impl.h" +#import "ios/chrome/browser/shared/model/prefs/pref_names.h" +#import "ios/components/security_interstitials/https_only_mode/feature.h" +#import "ios/components/security_interstitials/https_only_mode/https_upgrade_service.h" +#include "net/base/features.h" +#include "net/base/url_util.h" + +namespace { +bool CanUpgradeToHTTPS(const GURL& url) { + // FIXME: Move this impl out of the chromium_src override + BraveApplicationContextImpl* braveContext = + static_cast(GetApplicationContext()); + return braveContext->https_upgrade_exceptions_service()->CanUpgradeToHTTPS( + url); +} +} // namespace + +// Add checks for Brave-by-default feature flag, standard HTTPS upgrades pref +// and the brave https upgrade exception list when determining if the navigation +// should be upgraded +#define kHttpsUpgrades kHttpsUpgrades) && \ + !base::FeatureList::IsEnabled(net::features::kBraveHttpsByDefault) && \ + !(prefs_ && prefs_->GetBoolean(prefs::kHttpsUpgradesEnabled) +#define IsLocalhost IsLocalhost(url) || !CanUpgradeToHTTPS +#include "src/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm" +#undef IsLocalhost +#undef kHttpsUpgrades diff --git a/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm b/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm index aeb867f72224..08b9861f328b 100644 --- a/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm +++ b/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm @@ -6,13 +6,16 @@ #include "ios/chrome/browser/profile/model/keyed_service_factories.h" #include "brave/ios/browser/profile/model/brave_keyed_service_factories.h" +#include "ios/chrome/browser/affiliations/model/ios_chrome_affiliation_service_factory.h" #include "ios/chrome/browser/autocomplete/model/autocomplete_classifier_factory.h" #include "ios/chrome/browser/autocomplete/model/zero_suggest_cache_service_factory.h" +#include "ios/chrome/browser/autofill/model/autofill_log_router_factory.h" #include "ios/chrome/browser/autofill/model/personal_data_manager_factory.h" #include "ios/chrome/browser/bookmarks/model/account_bookmark_sync_service_factory.h" #include "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h" #include "ios/chrome/browser/bookmarks/model/bookmark_undo_service_factory.h" #include "ios/chrome/browser/bookmarks/model/local_or_syncable_bookmark_sync_service_factory.h" +#include "ios/chrome/browser/browsing_data/model/browsing_data_remover_factory.h" #include "ios/chrome/browser/consent_auditor/model/consent_auditor_factory.h" #include "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h" #include "ios/chrome/browser/credential_provider/model/credential_provider_buildflags.h" @@ -22,25 +25,33 @@ #include "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h" #include "ios/chrome/browser/favicon/model/ios_chrome_large_icon_cache_factory.h" #include "ios/chrome/browser/favicon/model/ios_chrome_large_icon_service_factory.h" +#include "ios/chrome/browser/gcm/model/ios_chrome_gcm_profile_service_factory.h" #include "ios/chrome/browser/history/model/history_service_factory.h" #include "ios/chrome/browser/history/model/top_sites_factory.h" #include "ios/chrome/browser/history/model/web_history_service_factory.h" #include "ios/chrome/browser/https_upgrades/model/https_upgrade_service_factory.h" #include "ios/chrome/browser/invalidation/model/ios_chrome_profile_invalidation_provider_factory.h" +#include "ios/chrome/browser/language/model/accept_languages_service_factory.h" +#include "ios/chrome/browser/language/model/language_model_manager_factory.h" +#include "ios/chrome/browser/language/model/url_language_histogram_factory.h" #include "ios/chrome/browser/metrics/model/google_groups_manager_factory.h" #include "ios/chrome/browser/optimization_guide/model/optimization_guide_service_factory.h" #include "ios/chrome/browser/page_info/about_this_site_service_factory.h" #include "ios/chrome/browser/passwords/model/ios_chrome_account_password_store_factory.h" +#include "ios/chrome/browser/passwords/model/ios_chrome_bulk_leak_check_service_factory.h" #include "ios/chrome/browser/passwords/model/ios_chrome_password_receiver_service_factory.h" #include "ios/chrome/browser/passwords/model/ios_chrome_password_reuse_manager_factory.h" #include "ios/chrome/browser/passwords/model/ios_chrome_password_sender_service_factory.h" #include "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h" #include "ios/chrome/browser/passwords/model/ios_password_manager_settings_service_factory.h" +#include "ios/chrome/browser/passwords/model/ios_password_requirements_service_factory.h" +#include "ios/chrome/browser/passwords/model/password_manager_log_router_factory.h" #include "ios/chrome/browser/plus_addresses/model/plus_address_service_factory.h" #include "ios/chrome/browser/plus_addresses/model/plus_address_setting_service_factory.h" #include "ios/chrome/browser/power_bookmarks/model/power_bookmark_service_factory.h" #include "ios/chrome/browser/push_notification/model/push_notification_profile_service_factory.h" #include "ios/chrome/browser/reading_list/model/reading_list_model_factory.h" +#include "ios/chrome/browser/safe_browsing/model/safe_browsing_client_factory.h" #include "ios/chrome/browser/safe_browsing/model/safe_browsing_metrics_collector_factory.h" #include "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h" #include "ios/chrome/browser/search_engines/model/template_url_service_factory.h" @@ -50,6 +61,7 @@ #include "ios/chrome/browser/signin/model/account_consistency_service_factory.h" #include "ios/chrome/browser/signin/model/account_reconcilor_factory.h" #include "ios/chrome/browser/signin/model/identity_manager_factory.h" +#include "ios/chrome/browser/signin/model/signin_client_factory.h" #include "ios/chrome/browser/supervised_user/model/child_account_service_factory.h" #include "ios/chrome/browser/supervised_user/model/list_family_members_service_factory.h" #include "ios/chrome/browser/supervised_user/model/supervised_user_service_factory.h" @@ -70,20 +82,22 @@ #endif void EnsureProfileKeyedServiceFactoriesBuilt() { + autofill::AutofillLogRouterFactory::GetInstance(); autofill::PersonalDataManagerFactory::GetInstance(); data_sharing::DataSharingServiceFactory::GetInstance(); ios::AccountBookmarkSyncServiceFactory::GetInstance(); ios::AccountConsistencyServiceFactory::GetInstance(); ios::AccountReconcilorFactory::GetInstance(); + ios::AutocompleteClassifierFactory::GetInstance(); ios::BookmarkModelFactory::GetInstance(); ios::BookmarkUndoServiceFactory::GetInstance(); ios::FaviconServiceFactory::GetInstance(); ios::HistoryServiceFactory::GetInstance(); - ios::LocalOrSyncableBookmarkSyncServiceFactory::GetInstance(); - ios::TopSitesFactory::GetInstance(); - ios::AutocompleteClassifierFactory::GetInstance(); ios::HostContentSettingsMapFactory::GetInstance(); + ios::LocalOrSyncableBookmarkSyncServiceFactory::GetInstance(); + ios::PasswordManagerLogRouterFactory::GetInstance(); ios::TemplateURLServiceFactory::GetInstance(); + ios::TopSitesFactory::GetInstance(); ios::WebDataServiceFactory::GetInstance(); ios::WebHistoryServiceFactory::GetInstance(); ios::ZeroSuggestCacheServiceFactory::GetInstance(); @@ -91,26 +105,33 @@ void EnsureProfileKeyedServiceFactoriesBuilt() { tab_groups::TabGroupSyncServiceFactory::GetInstance(); translate::TranslateRankerFactory::GetInstance(); AboutThisSiteServiceFactory::GetInstance(); + AcceptLanguagesServiceFactory::GetInstance(); BackgroundDownloadServiceFactory::GetInstance(); BrowserListFactory::GetInstance(); + BrowsingDataRemoverFactory::GetInstance(); ChildAccountServiceFactory::GetInstance(); ConsentAuditorFactory::GetInstance(); DeviceInfoSyncServiceFactory::GetInstance(); GoogleGroupsManagerFactory::GetInstance(); + HttpsUpgradeServiceFactory::GetInstance(); IdentityManagerFactory::GetInstance(); IOSChromeAccountPasswordStoreFactory::GetInstance(); + IOSChromeAffiliationServiceFactory::GetInstance(); + IOSChromeBulkLeakCheckServiceFactory::GetInstance(); IOSChromeFaviconLoaderFactory::GetInstance(); + IOSChromeGCMProfileServiceFactory::GetInstance(); IOSChromeLargeIconCacheFactory::GetInstance(); IOSChromeLargeIconServiceFactory::GetInstance(); IOSChromePasswordReceiverServiceFactory::GetInstance(); IOSChromePasswordReuseManagerFactory::GetInstance(); IOSChromePasswordSenderServiceFactory::GetInstance(); - IOSChromeProfilePasswordStoreFactory::GetInstance(); IOSChromeProfileInvalidationProviderFactory::GetInstance(); + IOSChromeProfilePasswordStoreFactory::GetInstance(); IOSPasskeyModelFactory::GetInstance(); IOSPasswordManagerSettingsServiceFactory::GetInstance(); + IOSPasswordRequirementsServiceFactory::GetInstance(); IOSUserEventServiceFactory::GetInstance(); - HttpsUpgradeServiceFactory::GetInstance(); + LanguageModelManagerFactory::GetInstance(); ListFamilyMembersServiceFactory::GetInstance(); OptimizationGuideServiceFactory::GetInstance(); DataTypeStoreServiceFactory::GetInstance(); @@ -122,11 +143,17 @@ void EnsureProfileKeyedServiceFactoriesBuilt() { SyncServiceFactory::GetInstance(); UnifiedConsentServiceFactory::GetInstance(); ReadingListModelFactory::GetInstance(); + SafeBrowsingClientFactory::GetInstance(); SafeBrowsingMetricsCollectorFactory::GetInstance(); SendTabToSelfSyncServiceFactory::GetInstance(); SessionRestorationServiceFactory::GetInstance(); SessionSyncServiceFactory::GetInstance(); + SigninClientFactory::GetInstance(); + SupervisedUserServiceFactory::GetInstance(); SupervisedUserSettingsServiceFactory::GetInstance(); + SyncServiceFactory::GetInstance(); + UnifiedConsentServiceFactory::GetInstance(); + UrlLanguageHistogramFactory::GetInstance(); #if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED) CredentialProviderServiceFactory::GetInstance(); diff --git a/chromium_src/ios/chrome/browser/shared/model/prefs/browser_prefs.mm b/chromium_src/ios/chrome/browser/shared/model/prefs/browser_prefs.mm index 9d676faf496e..a226a95de2c4 100644 --- a/chromium_src/ios/chrome/browser/shared/model/prefs/browser_prefs.mm +++ b/chromium_src/ios/chrome/browser/shared/model/prefs/browser_prefs.mm @@ -26,6 +26,7 @@ #include "brave/components/skus/browser/skus_utils.h" #include "brave/ios/browser/brave_stats/brave_stats_prefs.h" #include "components/pref_registry/pref_registry_syncable.h" +#include "ios/chrome/browser/shared/model/prefs/pref_names.h" void BraveRegisterBrowserStatePrefs( user_prefs::PrefRegistrySyncable* registry) { @@ -42,6 +43,8 @@ void BraveRegisterBrowserStatePrefs( ai_chat::ModelService::RegisterProfilePrefs(registry); omnibox::RegisterBraveProfilePrefs(registry); brave_news::prefs::RegisterProfilePrefs(registry); + + registry->RegisterBooleanPref(prefs::kHttpsUpgradesEnabled, true); } void BraveRegisterLocalStatePrefs(PrefRegistrySimple* registry) { diff --git a/chromium_src/ios/chrome/browser/shared/model/prefs/pref_names.h b/chromium_src/ios/chrome/browser/shared/model/prefs/pref_names.h new file mode 100644 index 000000000000..6db861557d96 --- /dev/null +++ b/chromium_src/ios/chrome/browser/shared/model/prefs/pref_names.h @@ -0,0 +1,16 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_CHROME_BROWSER_SHARED_MODEL_PREFS_PREF_NAMES_H_ +#define BRAVE_CHROMIUM_SRC_IOS_CHROME_BROWSER_SHARED_MODEL_PREFS_PREF_NAMES_H_ + +#include "src/ios/chrome/browser/shared/model/prefs/pref_names.h" // IWYU pragma: export + +namespace prefs { +// A boolean specifying whether HTTPS Upgrades are enabled. +inline constexpr char kHttpsUpgradesEnabled[] = "ios.https_upgrades_enabled"; +} // namespace prefs + +#endif // BRAVE_CHROMIUM_SRC_IOS_CHROME_BROWSER_SHARED_MODEL_PREFS_PREF_NAMES_H_ diff --git a/chromium_src/ios/chrome/browser/shared/model/profile/profile_ios.h b/chromium_src/ios/chrome/browser/shared/model/profile/profile_ios.h new file mode 100644 index 000000000000..878c78b298a6 --- /dev/null +++ b/chromium_src/ios/chrome/browser/shared/model/profile/profile_ios.h @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_CHROME_BROWSER_SHARED_MODEL_PROFILE_PROFILE_IOS_H_ +#define BRAVE_CHROMIUM_SRC_IOS_CHROME_BROWSER_SHARED_MODEL_PROFILE_PROFILE_IOS_H_ + +#include "ios/web/public/browser_state.h" + +#define GetPrefs() \ + GetPrefs_Unused(); \ + PrefService* GetPrefs() override +#include "src/ios/chrome/browser/shared/model/profile/profile_ios.h" // IWYU pragma: export +#undef GetPrefs + +#endif // BRAVE_CHROMIUM_SRC_IOS_CHROME_BROWSER_SHARED_MODEL_PROFILE_PROFILE_IOS_H_ diff --git a/chromium_src/ios/chrome/browser/shared/model/profile/profile_ios.mm b/chromium_src/ios/chrome/browser/shared/model/profile/profile_ios.mm new file mode 100644 index 000000000000..ccaf21971a70 --- /dev/null +++ b/chromium_src/ios/chrome/browser/shared/model/profile/profile_ios.mm @@ -0,0 +1,10 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "src/ios/chrome/browser/shared/model/profile/profile_ios.mm" + +PrefService* ChromeBrowserState::GetPrefs_Unused() { + return nullptr; +} diff --git a/chromium_src/ios/chrome/browser/tabs/model/tab_helper_util.mm b/chromium_src/ios/chrome/browser/tabs/model/tab_helper_util.mm index b7c5680b541e..a6e323e91b9e 100644 --- a/chromium_src/ios/chrome/browser/tabs/model/tab_helper_util.mm +++ b/chromium_src/ios/chrome/browser/tabs/model/tab_helper_util.mm @@ -5,14 +5,46 @@ #import "ios/chrome/browser/tabs/model/tab_helper_util.h" +#import "components/omnibox/common/omnibox_features.h" #include "ios/chrome/browser/complex_tasks/model/ios_task_tab_helper.h" +#import "ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h" +#import "ios/chrome/browser/https_upgrades/model/https_upgrade_service_factory.h" +#import "ios/chrome/browser/https_upgrades/model/typed_navigation_upgrade_tab_helper.h" +#import "ios/chrome/browser/prerender/model/prerender_service_factory.h" #include "ios/chrome/browser/sessions/model/ios_chrome_session_tab_helper.h" -#import "ios/chrome/browser/sessions/model/web_session_state_tab_helper.h" +#include "ios/chrome/browser/sessions/model/web_session_state_tab_helper.h" +#import "ios/chrome/browser/shared/model/profile/profile_ios.h" #include "ios/chrome/browser/tabs/model/ios_chrome_synced_tab_delegate.h" +#import "ios/components/security_interstitials/https_only_mode/feature.h" +#import "ios/components/security_interstitials/https_only_mode/https_only_mode_container.h" +#import "ios/components/security_interstitials/ios_blocking_page_tab_helper.h" void AttachTabHelpers(web::WebState* web_state, TabHelperFilter filter_flags) { + ChromeBrowserState* const browser_state = + ChromeBrowserState::FromBrowserState(web_state->GetBrowserState()); + IOSChromeSessionTabHelper::CreateForWebState(web_state); IOSChromeSyncedTabDelegate::CreateForWebState(web_state); WebSessionStateTabHelper::CreateForWebState(web_state); IOSTaskTabHelper::CreateForWebState(web_state); + + security_interstitials::IOSBlockingPageTabHelper::CreateForWebState( + web_state); + + if (base::FeatureList::IsEnabled( + security_interstitials::features::kHttpsOnlyMode) || + base::FeatureList::IsEnabled( + security_interstitials::features::kHttpsUpgrades)) { + HttpsOnlyModeUpgradeTabHelper::CreateForWebState( + web_state, browser_state->GetPrefs(), + PrerenderServiceFactory::GetForBrowserState(browser_state), + HttpsUpgradeServiceFactory::GetForBrowserState(browser_state)); + HttpsOnlyModeContainer::CreateForWebState(web_state); + } + + if (base::FeatureList::IsEnabled(omnibox::kDefaultTypedNavigationsToHttps)) { + TypedNavigationUpgradeTabHelper::CreateForWebState( + web_state, PrerenderServiceFactory::GetForBrowserState(browser_state), + HttpsUpgradeServiceFactory::GetForBrowserState(browser_state)); + } } diff --git a/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm b/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm new file mode 100644 index 000000000000..7712a6b3effb --- /dev/null +++ b/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm @@ -0,0 +1,140 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web/navigation/crw_wk_navigation_handler.h" + +#import +#include + +#include "ios/web/public/web_state.h" +#include "ios/web/security/crw_cert_verification_controller.h" +#include "net/cert/cert_status_flags.h" + +@interface CRWWKNavigationHandler (Brave) + +/// Returns YES if the cert policy & status matched a certificate pinning +/// violation and the resulting auth challenge was cancelled +- (BOOL)handleCertificatePinningForChallenge: + (NSURLAuthenticationChallenge*)challenge + certAcceptPolicy:(web::CertAcceptPolicy)policy + certStatus:(net::CertStatus)certStatus + completionHandler: + (void (^)(NSURLSessionAuthChallengeDisposition, + NSURLCredential*))completionHandler; + +@end + +#define BRAVE_SHOULD_BLOCK_JAVASCRIPT \ + brave::ShouldBlockJavaScript(static_cast(self.webStateImpl), \ + action.request, preferences); +#define BRAVE_SHOULD_BLOCK_UNIVERSAL_LINKS \ + brave::ShouldBlockUniversalLinks( \ + static_cast(self.webStateImpl), action.request, \ + &forceBlockUniversalLinks); +#define BRAVE_PROCESS_AUTH_CHALLENGE \ + if ([self handleCertificatePinningForChallenge:challenge \ + certAcceptPolicy:policy \ + certStatus:certStatus \ + completionHandler:completionHandler]) { \ + return; \ + } +#define BRAVE_HANDLE_LOAD_ERROR \ + if (policyDecisionCancellationError && \ + policyDecisionCancellationError.code == \ + net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN) { \ + policyDecisionCancellationError = nil; \ + } +#define BRAVE_DID_FAIL_PROVISIONAL_NAVIGATION \ + if (self.pendingNavigationInfo.cancellationError && \ + self.pendingNavigationInfo.cancellationError.code == \ + net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN) { \ + NSMutableDictionary* userInfo = \ + [self.pendingNavigationInfo.cancellationError.userInfo mutableCopy]; \ + userInfo[NSURLErrorFailingURLStringErrorKey] = \ + base::SysUTF8ToNSString(navigationContext->GetUrl().spec()); \ + userInfo[web::kNSErrorFailingURLKey] = \ + [NSURL URLWithString:base::SysUTF8ToNSString( \ + navigationContext->GetUrl().spec())]; \ + error = [NSError errorWithDomain:NSURLErrorDomain \ + code:NSURLErrorServerCertificateUntrusted \ + userInfo:userInfo]; \ + self.pendingNavigationInfo.cancellationError = nil; \ + self.pendingNavigationInfo.cancelled = NO; \ + } +#include "src/ios/web/navigation/crw_wk_navigation_handler.mm" +#undef BRAVE_DID_FAIL_PROVISIONAL_NAVIGATION +#undef BRAVE_HANDLE_LOAD_ERROR +#undef BRAVE_PROCESS_AUTH_CHALLENGE +#undef BRAVE_SHOULD_BLOCK_UNIVERSAL_LINKS +#undef BRAVE_SHOULD_BLOCK_JAVASCRIPT + +@implementation CRWWKNavigationHandler (Brave) + +- (NSURL*)URLForProtectionSpace:(NSURLProtectionSpace*)protectionSpace { + return [NSURL + URLWithString:[NSString stringWithFormat:@"%@://%@:%ld", + protectionSpace.protocol, + protectionSpace.host, + static_cast( + protectionSpace.port)]]; +} + +- (NSArray*)certificateChainForTrust:(SecTrustRef)trust { + DCHECK(trust); + return (__bridge NSArray*)SecTrustCopyCertificateChain(trust); +} + +- (BOOL)handleCertificatePinningForChallenge: + (NSURLAuthenticationChallenge*)challenge + certAcceptPolicy:(web::CertAcceptPolicy)policy + certStatus:(net::CertStatus)certStatus + completionHandler: + (void (^)(NSURLSessionAuthChallengeDisposition, + NSURLCredential*))completionHandler { + if (policy == web::CERT_ACCEPT_POLICY_NON_RECOVERABLE_ERROR && + (certStatus & net::CERT_STATUS_PINNED_KEY_MISSING) != 0) { + if (self.pendingNavigationInfo) { + self.pendingNavigationInfo.cancelled = YES; + NSMutableDictionary* userInfo = [[NSMutableDictionary alloc] init]; + userInfo[web::kNSErrorPeerCertificateChainKey] = + [self certificateChainForTrust:challenge.protectionSpace.serverTrust]; + self.pendingNavigationInfo.cancellationError = + [NSError errorWithDomain:net::kNSErrorDomain + code:net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN + userInfo:userInfo]; + } + + if (SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust)) { + scoped_refptr leafCert = nil; + base::apple::ScopedCFTypeRef certificateChain( + SecTrustCopyCertificateChain(challenge.protectionSpace.serverTrust)); + SecCertificateRef secCertificate = + base::apple::CFCastStrict( + CFArrayGetValueAtIndex(certificateChain.get(), 0)); + leafCert = net::x509_util::CreateX509CertificateFromSecCertificate( + base::apple::ScopedCFTypeRef( + secCertificate, base::scoped_policy::RETAIN), + {}); + + if (leafCert) { + bool is_recoverable = + policy == + web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_UNDECIDED_BY_USER; + std::string host = + base::SysNSStringToUTF8(challenge.protectionSpace.host); + _certVerificationErrors->Put( + web::CertHostPair(leafCert, host), + web::CertVerificationError(is_recoverable, certStatus)); + } + } + + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, + nil); + return YES; + } + return NO; +} + +@end diff --git a/chromium_src/ios/web/navigation/wk_navigation_action_policy_util.h b/chromium_src/ios/web/navigation/wk_navigation_action_policy_util.h new file mode 100644 index 000000000000..f91770c76d5a --- /dev/null +++ b/chromium_src/ios/web/navigation/wk_navigation_action_policy_util.h @@ -0,0 +1,27 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_NAVIGATION_WK_NAVIGATION_ACTION_POLICY_UTIL_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_NAVIGATION_WK_NAVIGATION_ACTION_POLICY_UTIL_H_ + +#import +#import + +#include "src/ios/web/navigation/wk_navigation_action_policy_util.h" // IWYU pragma: export + +namespace web { +class WebState; +} + +namespace brave { +void ShouldBlockUniversalLinks(web::WebState* webState, + NSURLRequest* request, + bool* forceBlockUniversalLinks); +void ShouldBlockJavaScript(web::WebState* webState, + NSURLRequest* request, + WKWebpagePreferences* preferences); +} // namespace brave + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_NAVIGATION_WK_NAVIGATION_ACTION_POLICY_UTIL_H_ diff --git a/chromium_src/ios/web/public/browser_state.h b/chromium_src/ios/web/public/browser_state.h new file mode 100644 index 000000000000..c205a945ff3d --- /dev/null +++ b/chromium_src/ios/web/public/browser_state.h @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_PUBLIC_BROWSER_STATE_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_PUBLIC_BROWSER_STATE_H_ + +class PrefService; + +#define GetRequestContext() \ + GetRequestContext() = 0; \ + virtual PrefService* GetPrefs() +#include "src/ios/web/public/browser_state.h" // IWYU pragma: export +#undef GetRequestContext + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_PUBLIC_BROWSER_STATE_H_ diff --git a/chromium_src/ios/web/security/DEPS b/chromium_src/ios/web/security/DEPS new file mode 100644 index 000000000000..21adc36a10d5 --- /dev/null +++ b/chromium_src/ios/web/security/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+brave/ios/browser/api/net", +] diff --git a/chromium_src/ios/web/security/crw_cert_verification_controller.h b/chromium_src/ios/web/security/crw_cert_verification_controller.h new file mode 100644 index 000000000000..38adaa9d5515 --- /dev/null +++ b/chromium_src/ios/web/security/crw_cert_verification_controller.h @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_SECURITY_CRW_CERT_VERIFICATION_CONTROLLER_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_SECURITY_CRW_CERT_VERIFICATION_CONTROLLER_H_ + +#define decideLoadPolicyForTrust decideLoadPolicyForTrust_ChromiumImpl +#define querySSLStatusForTrust querySSLStatusForTrust_ChromiumImpl +#include "src/ios/web/security/crw_cert_verification_controller.h" // IWYU pragma: export +#undef querySSLStatusForTrust +#undef decideLoadPolicyForTrust + +@interface CRWCertVerificationController (Override) +- (void)decideLoadPolicyForTrust: + (base::apple::ScopedCFTypeRef)trust + host:(NSString*)host + completionHandler:(web::PolicyDecisionHandler)completionHandler; +- (void)querySSLStatusForTrust:(base::apple::ScopedCFTypeRef)trust + host:(NSString*)host + completionHandler:(web::StatusQueryHandler)completionHandler; +@end + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_SECURITY_CRW_CERT_VERIFICATION_CONTROLLER_H_ diff --git a/chromium_src/ios/web/security/crw_cert_verification_controller.mm b/chromium_src/ios/web/security/crw_cert_verification_controller.mm new file mode 100644 index 000000000000..8cc4e332b640 --- /dev/null +++ b/chromium_src/ios/web/security/crw_cert_verification_controller.mm @@ -0,0 +1,106 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#import "ios/web/security/crw_cert_verification_controller.h" + +#define decideLoadPolicyForTrust decideLoadPolicyForTrust_ChromiumImpl +#define querySSLStatusForTrust querySSLStatusForTrust_ChromiumImpl +#include "src/ios/web/security/crw_cert_verification_controller.mm" +#undef querySSLStatusForTrust +#undef decideLoadPolicyForTrust + +#include "brave/ios/browser/api/net/certificate_utility.h" + +@implementation CRWCertVerificationController (Override) + +- (void)verifyTrustWithPinning:(base::apple::ScopedCFTypeRef)trust + host:(NSString*)host + completionHandler:(void (^)(bool trusted))completionHandler { + base::ThreadPool::PostTask( + FROM_HERE, {TaskShutdownBehavior::BLOCK_SHUTDOWN, base::MayBlock()}, + base::BindOnce(^{ + auto result = [BraveCertificateUtility verifyTrust:trust.get() + host:host + port:443]; + bool trusted = + result == 0 || result == std::numeric_limits::min(); + // TODO(crbug.com/40588591): This should use PostTask to post to + // WebThread::UI with BLOCK_SHUTDOWN once shutdown behaviors are + // supported on the UI thread. BLOCK_SHUTDOWN is necessary because + // WKWebView throws an exception if the completion handler doesn't run. + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(trusted); + }); + })); +} + +- (void)decideLoadPolicyForTrust: + (base::apple::ScopedCFTypeRef)trust + host:(NSString*)host + completionHandler:(web::PolicyDecisionHandler)completionHandler { + DCHECK_CURRENTLY_ON(WebThread::UI); + DCHECK(completionHandler); + [self + decideLoadPolicyForTrust_ChromiumImpl:trust + host:host + completionHandler:^(web::CertAcceptPolicy policy, + net::CertStatus status) { + if (policy == web::CERT_ACCEPT_POLICY_ALLOW || + policy == + web:: + CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) { + // SSL pinning check + [self + verifyTrustWithPinning:trust + host:host + completionHandler:^(bool trusted) { + completionHandler( + trusted + ? policy + : web:: + CERT_ACCEPT_POLICY_NON_RECOVERABLE_ERROR, + trusted + ? status + : net:: + CERT_STATUS_PINNED_KEY_MISSING); + }]; + return; + } + completionHandler(policy, status); + }]; +} + +- (void)querySSLStatusForTrust:(base::apple::ScopedCFTypeRef)trust + host:(NSString*)host + completionHandler:(web::StatusQueryHandler)completionHandler { + [self + querySSLStatusForTrust_ChromiumImpl:trust + host:host + completionHandler:^(web::SecurityStyle style, + net::CertStatus status) { + if (status != + web::SECURITY_STYLE_AUTHENTICATION_BROKEN) { + // SSL pinning check + [self + verifyTrustWithPinning:trust + host:host + completionHandler:^(bool trusted) { + completionHandler( + trusted + ? style + : web:: + SECURITY_STYLE_AUTHENTICATION_BROKEN, + trusted + ? status + : net:: + CERT_STATUS_PINNED_KEY_MISSING); + }]; + return; + } + completionHandler(style, status); + }]; +} + +@end diff --git a/chromium_src/ios/web/web_state/ui/crw_web_controller.h b/chromium_src/ios/web/web_state/ui/crw_web_controller.h index 2fcf14e37de4..df958a84f144 100644 --- a/chromium_src/ios/web/web_state/ui/crw_web_controller.h +++ b/chromium_src/ios/web/web_state/ui/crw_web_controller.h @@ -9,7 +9,8 @@ #define webViewNavigationProxy \ webViewNavigationProxy_ChromiumImpl; \ @property(weak, nonatomic, readonly) id \ - webViewNavigationProxy + webViewNavigationProxy; \ + @property(weak, nonatomic, readonly) WKWebView* webView #include "src/ios/web/web_state/ui/crw_web_controller.h" // IWYU pragma: export diff --git a/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm b/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm new file mode 100644 index 000000000000..67bc1e2bd535 --- /dev/null +++ b/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm @@ -0,0 +1,40 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#import +#import + +#include "base/notreached.h" + +#define BRAVE_RESET_WITH_WEB_VIEW_CONFIGURATION \ + brave::ResetWithWebViewConfiguration(configuration_); + +namespace brave { + +// FIXME: Replace patch + override with WKWebViewConfigurationProviderObserver +void ResetWithWebViewConfiguration(WKWebViewConfiguration* configuration) { + // Adjusts the underlying WKWebViewConfiguration for settings we don't want + // to inherit from Chromium + + // Restore WKWebView long press + @try { + [configuration setValue:@YES forKey:@"longPressActionsEnabled"]; + } @catch (NSException* exception) { + NOTREACHED_IN_MIGRATION() + << "Error setting value for longPressActionsEnabled"; + } + + // Restore Apple's safe browsing implementation + [[configuration preferences] setFraudulentWebsiteWarningEnabled:YES]; + + // Reset fullscreen to default as it wasn't set in Brave + [[configuration preferences] setElementFullscreenEnabled:NO]; +} + +} // namespace brave + +#include "src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm" + +#undef BRAVE_RESET_WITH_WEB_VIEW_CONFIGURATION diff --git a/chromium_src/ios/web_view/internal/autofill/web_view_autofill_client_ios.h b/chromium_src/ios/web_view/internal/autofill/web_view_autofill_client_ios.h new file mode 100644 index 000000000000..fefc7a11b030 --- /dev/null +++ b/chromium_src/ios/web_view/internal/autofill/web_view_autofill_client_ios.h @@ -0,0 +1,30 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_AUTOFILL_WEB_VIEW_AUTOFILL_CLIENT_IOS_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_AUTOFILL_WEB_VIEW_AUTOFILL_CLIENT_IOS_H_ + +#include + +#include "components/autofill/core/browser/autofill_client.h" +#include "components/autofill/core/browser/personal_data_manager.h" +#include "ios/web/public/browser_state.h" +#include "ios/web/public/web_state.h" +#import "ios/web_view/internal/autofill/cwv_autofill_client_ios_bridge.h" + +namespace autofill { + +class WebViewAutofillClientIOS : public AutofillClient { + public: + static std::unique_ptr Create( + web::WebState* web_state, + web::BrowserState* browser_state); + + void set_bridge(id bridge) {} +}; + +} // namespace autofill + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_AUTOFILL_WEB_VIEW_AUTOFILL_CLIENT_IOS_H_ diff --git a/chromium_src/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm b/chromium_src/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm new file mode 100644 index 000000000000..5243fa075346 --- /dev/null +++ b/chromium_src/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web_view/internal/autofill/web_view_autofill_client_ios.h" + +namespace autofill { + +// static +std::unique_ptr WebViewAutofillClientIOS::Create( + web::WebState* web_state, + web::BrowserState* browser_state) { + return nullptr; +} + +} // namespace autofill diff --git a/chromium_src/ios/web_view/internal/browser_state_keyed_service_factories.mm b/chromium_src/ios/web_view/internal/browser_state_keyed_service_factories.mm new file mode 100644 index 000000000000..85ef910f3b93 --- /dev/null +++ b/chromium_src/ios/web_view/internal/browser_state_keyed_service_factories.mm @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace ios_web_view { + +void EnsureBrowserStateKeyedServiceFactoriesBuilt() { + // Intentionally left empty, please add any factories that need to be built + // to + // chromium_src/ios/chrome/browser/browser_state/model/browser_state_keyed_service_factories.mm +} + +} // namespace ios_web_view diff --git a/chromium_src/ios/web_view/internal/cwv_ssl_status.mm b/chromium_src/ios/web_view/internal/cwv_ssl_status.mm new file mode 100644 index 000000000000..58049a9ed147 --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_ssl_status.mm @@ -0,0 +1,12 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "src/ios/web_view/internal/cwv_ssl_status.mm" + +@implementation CWVSSLStatus (Internal) +- (web::SSLStatus)internalStatus { + return _internalStatus; +} +@end diff --git a/chromium_src/ios/web_view/internal/cwv_ssl_status_internal.h b/chromium_src/ios/web_view/internal/cwv_ssl_status_internal.h new file mode 100644 index 000000000000..c48546e53a6d --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_ssl_status_internal.h @@ -0,0 +1,15 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_SSL_STATUS_INTERNAL_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_SSL_STATUS_INTERNAL_H_ + +#include "src/ios/web_view/internal/cwv_ssl_status_internal.h" // IWYU pragma: export + +@interface CWVSSLStatus (Internal) +@property(readonly) web::SSLStatus internalStatus; +@end + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_SSL_STATUS_INTERNAL_H_ diff --git a/chromium_src/ios/web_view/internal/cwv_web_view.mm b/chromium_src/ios/web_view/internal/cwv_web_view.mm new file mode 100644 index 000000000000..677bd75211b1 --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_web_view.mm @@ -0,0 +1,45 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "base/notimplemented.h" +#include "ios/web/public/web_state.h" + +namespace ios_web_view { +void AttachTabHelpers(web::WebState* web_state); +} + +#define BRAVE_NOP_SERVICE \ + NOTIMPLEMENTED(); \ + return nil; +#define BRAVE_ATTACH_TAB_HELPERS \ + ios_web_view::AttachTabHelpers(_webState.get()); +#include "src/ios/web_view/internal/cwv_web_view.mm" +#undef BRAVE_ATTACH_TAB_HELPERS +#undef BRAVE_NOP_SERVICE + +@implementation CWVWebView (Internal) + +- (web::WebState*)webState { + return _webState.get(); +} + ++ (BOOL)_isRestoreDataValid:(NSData*)data { + NSError* error = nil; + NSKeyedUnarchiver* coder = + [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error]; + if (error) { + return NO; + } + coder.requiresSecureCoding = NO; + CWVWebViewProtobufStorage* cachedProtobufStorage = + base::apple::ObjCCastStrict( + [coder decodeObjectForKey:kProtobufStorageKey]); + CRWSessionStorage* cachedSessionStorage = + base::apple::ObjCCastStrict( + [coder decodeObjectForKey:kSessionStorageKey]); + return (cachedProtobufStorage != nil || cachedSessionStorage != nil); +} + +@end diff --git a/chromium_src/ios/web_view/internal/cwv_web_view_configuration.mm b/chromium_src/ios/web_view/internal/cwv_web_view_configuration.mm new file mode 100644 index 000000000000..7b291e3d756b --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_web_view_configuration.mm @@ -0,0 +1,118 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#import + +#import "base/memory/raw_ptr.h" +#import "base/notimplemented.h" +#import "base/threading/thread_restrictions.h" +#import "components/keyed_service/core/service_access_type.h" +#import "components/keyed_service/ios/browser_state_dependency_manager.h" +#import "components/password_manager/core/browser/password_store/password_store_interface.h" +#import "ios/web/public/browser_state.h" +#import "ios/web_view/internal/app/application_context.h" +#import "ios/web_view/internal/autofill/cwv_autofill_data_manager_internal.h" +#import "ios/web_view/internal/autofill/web_view_personal_data_manager_factory.h" +#import "ios/web_view/internal/browser_state_keyed_service_factories.h" +#import "ios/web_view/internal/cwv_preferences_internal.h" +#import "ios/web_view/internal/cwv_user_content_controller_internal.h" +#import "ios/web_view/internal/cwv_web_view_configuration_internal.h" +#import "ios/web_view/internal/cwv_web_view_internal.h" +#import "ios/web_view/internal/passwords/web_view_account_password_store_factory.h" +#import "ios/web_view/internal/web_view_global_state_util.h" + +@interface CWVWebViewConfiguration () { + // The BrowserState for this configuration. + raw_ptr _browserState; + + // Holds all CWVWebViews created with this class. Weak references. + NSHashTable* _webViews; +} +@end + +@implementation CWVWebViewConfiguration + ++ (void)shutDown { +} + ++ (instancetype)defaultConfiguration { + NOTIMPLEMENTED(); + return nil; +} + ++ (instancetype)incognitoConfiguration { + NOTIMPLEMENTED(); + return nil; +} + ++ (CWVWebViewConfiguration*)nonPersistentConfiguration { + NOTIMPLEMENTED(); + return nil; +} + +- (instancetype)initWithBrowserState:(web::BrowserState*)browserState { + self = [super init]; + if (self) { + _browserState = browserState; + _webViews = [NSHashTable weakObjectsHashTable]; + } + return self; +} + +#pragma mark - Unused Services + +- (CWVPreferences*)preferences { + // This property is not nullable, so will crash anyways on the Swift side if + // accessed. + NOTIMPLEMENTED(); + return nil; +} + +- (CWVUserContentController*)userContentController { + // This property is not nullable, so will crash anyways on the Swift side if + // accessed. + NOTIMPLEMENTED(); + return nil; +} + +- (CWVAutofillDataManager*)autofillDataManager { + return nil; +} + +- (CWVSyncController*)syncController { + return nil; +} + +- (CWVLeakCheckService*)leakCheckService { + return nil; +} + +- (CWVReuseCheckService*)reuseCheckService { + return nil; +} + +#pragma mark - Public Methods + +- (BOOL)isPersistent { + return !_browserState->IsOffTheRecord(); +} + +#pragma mark - Private Methods + +- (web::BrowserState*)browserState { + return _browserState; +} + +- (void)registerWebView:(CWVWebView*)webView { + [_webViews addObject:webView]; +} + +- (void)shutDown { + for (CWVWebView* webView in _webViews) { + [webView shutDown]; + } +} + +@end diff --git a/chromium_src/ios/web_view/internal/cwv_web_view_configuration_internal.h b/chromium_src/ios/web_view/internal/cwv_web_view_configuration_internal.h new file mode 100644 index 000000000000..b2d435d09512 --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_web_view_configuration_internal.h @@ -0,0 +1,45 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_WEB_VIEW_CONFIGURATION_INTERNAL_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_WEB_VIEW_CONFIGURATION_INTERNAL_H_ + +#import "ios/web_view/public/cwv_web_view_configuration.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace web { +class BrowserState; +} // namespace web + +@class CWVWebView; + +@interface CWVWebViewConfiguration () + +// The browser state associated with this configuration. +@property(nonatomic, readonly) web::BrowserState* browserState; + +// Calls |shutDown| on the singletons returned by |defaultConfiguration| and +// |incognitoConfiguration|. ++ (void)shutDown; + +// Initializes with |browserState| that this instance is to be associated with. +- (instancetype)initWithBrowserState:(web::BrowserState*)browserState + NS_DESIGNATED_INITIALIZER; + +// Registers a |webView| so that this class can call |shutDown| on it later on. +// Only weak references are held, so no need for de-register method. +- (void)registerWebView:(CWVWebView*)webView; + +// If this instance is going to outlive the globals created in +// ios_web_view::InitializeGlobalState, this method must be called to ensure the +// correct tear down order. +- (void)shutDown; + +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_WEB_VIEW_CONFIGURATION_INTERNAL_H_ diff --git a/chromium_src/ios/web_view/internal/cwv_web_view_internal.h b/chromium_src/ios/web_view/internal/cwv_web_view_internal.h new file mode 100644 index 000000000000..2914020c05dc --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_web_view_internal.h @@ -0,0 +1,20 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_WEB_VIEW_INTERNAL_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_WEB_VIEW_INTERNAL_H_ + +#include "src/ios/web_view/internal/cwv_web_view_internal.h" // IWYU pragma: export + +NS_ASSUME_NONNULL_BEGIN + +@interface CWVWebView (Internal) +@property(readonly) web::WebState* webState; ++ (BOOL)_isRestoreDataValid:(NSData*)data; +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_WEB_VIEW_INTERNAL_H_ diff --git a/chromium_src/ios/web_view/internal/cwv_x509_certificate.mm b/chromium_src/ios/web_view/internal/cwv_x509_certificate.mm new file mode 100644 index 000000000000..d9024eab4df9 --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_x509_certificate.mm @@ -0,0 +1,12 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "src/ios/web_view/internal/cwv_x509_certificate.mm" + +@implementation CWVX509Certificate (Internal) +- (scoped_refptr)internalCertificate { + return _internalCertificate; +} +@end diff --git a/chromium_src/ios/web_view/internal/cwv_x509_certificate_internal.h b/chromium_src/ios/web_view/internal/cwv_x509_certificate_internal.h new file mode 100644 index 000000000000..09d0528cb3ad --- /dev/null +++ b/chromium_src/ios/web_view/internal/cwv_x509_certificate_internal.h @@ -0,0 +1,15 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_X509_CERTIFICATE_INTERNAL_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_X509_CERTIFICATE_INTERNAL_H_ + +#include "src/ios/web_view/internal/cwv_x509_certificate_internal.h" // IWYU pragma: export + +@interface CWVX509Certificate (Internal) +@property(readonly) scoped_refptr internalCertificate; +@end + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_CWV_X509_CERTIFICATE_INTERNAL_H_ diff --git a/chromium_src/ios/web_view/internal/passwords/web_view_password_manager_client.h b/chromium_src/ios/web_view/internal/passwords/web_view_password_manager_client.h new file mode 100644 index 000000000000..976e4aea6ebe --- /dev/null +++ b/chromium_src/ios/web_view/internal/passwords/web_view_password_manager_client.h @@ -0,0 +1,30 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_PASSWORDS_WEB_VIEW_PASSWORD_MANAGER_CLIENT_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_PASSWORDS_WEB_VIEW_PASSWORD_MANAGER_CLIENT_H_ + +#include + +#include "components/password_manager/core/browser/password_manager_client.h" +#include "components/password_manager/ios/password_manager_client_bridge.h" +#include "ios/web/public/browser_state.h" +#include "ios/web/public/web_state.h" + +namespace ios_web_view { + +class WebViewPasswordManagerClient + : public password_manager::PasswordManagerClient { + public: + static std::unique_ptr Create( + web::WebState* web_state, + web::BrowserState* browser_state); + + void set_bridge(id bridge) {} +}; + +} // namespace ios_web_view + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_PASSWORDS_WEB_VIEW_PASSWORD_MANAGER_CLIENT_H_ diff --git a/chromium_src/ios/web_view/internal/passwords/web_view_password_manager_client.mm b/chromium_src/ios/web_view/internal/passwords/web_view_password_manager_client.mm new file mode 100644 index 000000000000..8bb3c59c4bc4 --- /dev/null +++ b/chromium_src/ios/web_view/internal/passwords/web_view_password_manager_client.mm @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web_view/internal/passwords/web_view_password_manager_client.h" + +namespace ios_web_view { + +// static +std::unique_ptr +WebViewPasswordManagerClient::Create(web::WebState* web_state, + web::BrowserState* browser_state) { + return nullptr; +} + +} // namespace ios_web_view diff --git a/chromium_src/ios/web_view/internal/translate/web_view_translate_client.h b/chromium_src/ios/web_view/internal/translate/web_view_translate_client.h new file mode 100644 index 000000000000..25d5a4e8a7f9 --- /dev/null +++ b/chromium_src/ios/web_view/internal/translate/web_view_translate_client.h @@ -0,0 +1,34 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_TRANSLATE_WEB_VIEW_TRANSLATE_CLIENT_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_TRANSLATE_WEB_VIEW_TRANSLATE_CLIENT_H_ + +#include + +#include "components/translate/core/browser/translate_client.h" +#include "ios/web/public/browser_state.h" +#include "ios/web/public/web_state.h" +#import "ios/web_view/internal/translate/cwv_translation_controller_internal.h" + +namespace ios_web_view { + +class WebViewTranslateClient : public translate::TranslateClient { + public: + static std::unique_ptr Create( + web::BrowserState* browser_state, + web::WebState* web_state); + + void set_translation_controller(CWVTranslationController* controller) {} + void TranslatePage(const std::string& source_lang, + const std::string& target_lang, + bool triggered_from_menu) {} + void RevertTranslation() {} + bool RequestTranslationOffer() { return false; } +}; + +} // namespace ios_web_view + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_TRANSLATE_WEB_VIEW_TRANSLATE_CLIENT_H_ diff --git a/chromium_src/ios/web_view/internal/translate/web_view_translate_client.mm b/chromium_src/ios/web_view/internal/translate/web_view_translate_client.mm new file mode 100644 index 000000000000..7f30bceb6ed8 --- /dev/null +++ b/chromium_src/ios/web_view/internal/translate/web_view_translate_client.mm @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web_view/internal/translate/web_view_translate_client.h" + +namespace ios_web_view { + +// static +std::unique_ptr WebViewTranslateClient::Create( + web::BrowserState* browser_state, + web::WebState* web_state) { + return nullptr; +} + +} // namespace ios_web_view diff --git a/chromium_src/ios/web_view/internal/web_view_browser_state.h b/chromium_src/ios/web_view/internal/web_view_browser_state.h new file mode 100644 index 000000000000..3b7367f63b7b --- /dev/null +++ b/chromium_src/ios/web_view/internal/web_view_browser_state.h @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_WEB_VIEW_BROWSER_STATE_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_WEB_VIEW_BROWSER_STATE_H_ + +#include "ios/web/public/browser_state.h" + +#define GetPrefs() \ + GetPrefs_Unused(); \ + PrefService* GetPrefs() override +#include "src/ios/web_view/internal/web_view_browser_state.h" // IWYU pragma: export +#undef GetPrefs + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_VIEW_INTERNAL_WEB_VIEW_BROWSER_STATE_H_ diff --git a/chromium_src/ios/web_view/internal/web_view_browser_state.mm b/chromium_src/ios/web_view/internal/web_view_browser_state.mm new file mode 100644 index 000000000000..e22e62367f08 --- /dev/null +++ b/chromium_src/ios/web_view/internal/web_view_browser_state.mm @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "src/ios/web_view/internal/web_view_browser_state.mm" + +namespace ios_web_view { + +PrefService* WebViewBrowserState::GetPrefs_Unused() { + return nullptr; +} + +} // namespace ios_web_view diff --git a/ios/BUILD.gn b/ios/BUILD.gn index d6a389e50358..90df03aaa1ae 100644 --- a/ios/BUILD.gn +++ b/ios/BUILD.gn @@ -16,19 +16,20 @@ import("//brave/ios/browser/api/brave_shields/headers.gni") import("//brave/ios/browser/api/brave_stats/headers.gni") import("//brave/ios/browser/api/brave_wallet/headers.gni") import("//brave/ios/browser/api/certificate/headers.gni") +import("//brave/ios/browser/api/content_settings/headers.gni") import("//brave/ios/browser/api/credential_provider/headers.gni") import("//brave/ios/browser/api/de_amp/headers.gni") import("//brave/ios/browser/api/debounce/headers.gni") import("//brave/ios/browser/api/developer_options_code/headers.gni") import("//brave/ios/browser/api/favicon/headers.gni") import("//brave/ios/browser/api/features/headers.gni") -import("//brave/ios/browser/api/https_upgrade_exceptions/headers.gni") import("//brave/ios/browser/api/https_upgrades/headers.gni") import("//brave/ios/browser/api/ipfs/headers.gni") import("//brave/ios/browser/api/ntp_background_images/headers.gni") import("//brave/ios/browser/api/omnibox/headers.gni") import("//brave/ios/browser/api/opentabs/headers.gni") import("//brave/ios/browser/api/p3a/headers.gni") +import("//brave/ios/browser/api/prefs/headers.gni") import("//brave/ios/browser/api/qr_code/headers.gni") import("//brave/ios/browser/api/query_filter/headers.gni") import("//brave/ios/browser/api/session_restore/headers.gni") @@ -40,6 +41,7 @@ import("//brave/ios/browser/api/web/ui/headers.gni") import("//brave/ios/browser/api/web/web_state/headers.gni") import("//brave/ios/browser/debounce/headers.gni") import("//brave/ios/browser/url_sanitizer/headers.gni") +import("//brave/ios/web_view/headers.gni") import("//build/apple/tweak_info_plist.gni") import("//build/config/compiler/compiler.gni") import("//build/config/ios/rules.gni") @@ -98,8 +100,6 @@ brave_core_public_headers += browser_api_url_sanitizer_public_headers brave_core_public_headers += browser_url_sanitizer_public_headers brave_core_public_headers += browser_api_de_amp_public_headers brave_core_public_headers += browser_api_debounce_public_headers -brave_core_public_headers += - browser_api_https_upgrade_exceptions_service_public_headers brave_core_public_headers += browser_api_https_upgrades_public_headers brave_core_public_headers += browser_debounce_public_headers brave_core_public_headers += brave_stats_public_headers @@ -124,6 +124,8 @@ brave_core_public_headers += browser_api_features_public_headers brave_core_public_headers += credential_provider_public_headers brave_core_public_headers += developer_options_code_public_headers brave_core_public_headers += browser_api_storekit_receipt_public_headers +brave_core_public_headers += content_settings_public_headers +brave_core_public_headers += prefs_public_headers action("brave_core_umbrella_header") { script = "//build/config/ios/generate_umbrella_header.py" @@ -137,6 +139,7 @@ action("brave_core_umbrella_header") { ] args += rebase_path(brave_core_public_headers, root_build_dir) + args += rebase_path(ios_web_view_public_headers, root_build_dir) } tweak_info_plist("info_plist") { @@ -167,6 +170,7 @@ ios_framework_bundle("brave_core_ios_framework") { ":brave_core_plist", ":brave_core_umbrella_header", "//brave/ios/app", + "//brave/ios/web_view", ] deps += ads_public_deps @@ -177,9 +181,11 @@ ios_framework_bundle("brave_core_ios_framework") { deps += credential_provider_public_deps sources = brave_core_public_headers + sources += ios_web_view_public_headers public_headers = get_target_outputs(":brave_core_umbrella_header") public_headers += brave_core_public_headers + public_headers += ios_web_view_public_headers } ios_create_xcframework("brave_core_xcframework") { diff --git a/ios/app/BUILD.gn b/ios/app/BUILD.gn index 464f9f7683a1..3b2744758cff 100644 --- a/ios/app/BUILD.gn +++ b/ios/app/BUILD.gn @@ -47,14 +47,15 @@ source_set("app") { "//brave/ios/browser/api/brave_stats", "//brave/ios/browser/api/brave_wallet", "//brave/ios/browser/api/brave_wallet:wallet_mojom_wrappers", + "//brave/ios/browser/api/content_settings", "//brave/ios/browser/api/de_amp", "//brave/ios/browser/api/history", - "//brave/ios/browser/api/https_upgrade_exceptions", "//brave/ios/browser/api/ipfs", "//brave/ios/browser/api/ntp_background_images", "//brave/ios/browser/api/opentabs", "//brave/ios/browser/api/p3a", "//brave/ios/browser/api/password", + "//brave/ios/browser/api/prefs", "//brave/ios/browser/api/qr_code", "//brave/ios/browser/api/query_filter", "//brave/ios/browser/api/skus:skus_mojom_wrappers", @@ -62,8 +63,10 @@ source_set("app") { "//brave/ios/browser/api/web_image", "//brave/ios/browser/skus", "//brave/ios/browser/ui/webui", + "//brave/ios/browser/web", "//components/browser_sync", "//components/component_updater", + "//components/content_settings/core/browser", "//components/history/core/browser", "//components/keyed_service/core", "//components/password_manager/core/browser", @@ -77,6 +80,8 @@ source_set("app") { "//ios/chrome/app/startup:startup", "//ios/chrome/app/startup:startup_basic", "//ios/chrome/browser/bookmarks/model", + "//ios/chrome/browser/browsing_data/model", + "//ios/chrome/browser/content_settings/model", "//ios/chrome/browser/credential_provider/model:buildflags", "//ios/chrome/browser/flags:system_flags", "//ios/chrome/browser/history/model", @@ -97,6 +102,7 @@ source_set("app") { "//ios/public/provider/chrome/browser/ui_utils:ui_utils_api", "//ios/web/public/init", "//ios/web/public/webui", + "//ios/web_view:web_view_sources", # Use the provider API controlled by args.gn. ios_provider_target, diff --git a/ios/app/DEPS b/ios/app/DEPS index 84d5bc5cbf6b..75cd4b4cc8ed 100644 --- a/ios/app/DEPS +++ b/ios/app/DEPS @@ -2,6 +2,8 @@ include_rules = [ "+ios/chrome/app", "+ios/chrome/browser", "+ios/chrome/common", + "+ios/web/public", + "+ios/web_view", "+brave/ios/app", "+brave/ios/browser", "+components/browser_sync", @@ -9,6 +11,7 @@ include_rules = [ "+components/component_updater/component_updater_service.h", "+components/component_updater/component_updater_switches.h", "+components/component_updater/installer_policies", + "+components/content_settings/core/browser", "+components/history/core/browser", "+components/keyed_service/core", "+components/prefs", diff --git a/ios/app/brave_core_main.h b/ios/app/brave_core_main.h index 13b11e3e81a6..be187dd5fac8 100644 --- a/ios/app/brave_core_main.h +++ b/ios/app/brave_core_main.h @@ -26,7 +26,9 @@ @class NTPBackgroundImagesService; @class DeAmpPrefs; @class AIChat; -@class HTTPSUpgradeExceptionsService; +@class DefaultHostContentSettings; +@class BrowserPrefs; +@class CWVWebViewConfiguration; @protocol AIChatDelegate; @protocol IpfsAPI; @@ -66,9 +68,6 @@ OBJC_EXPORT @property(nonatomic, readonly) WebImageDownloader* webImageDownloader; -@property(nonatomic, readonly) - HTTPSUpgradeExceptionsService* httpsUpgradeExceptionsService; - /// Sets the global log handler for Chromium & BraveCore logs. /// /// When a custom log handler is set, it is the responsibility of the client @@ -101,8 +100,13 @@ OBJC_EXPORT @property(readonly) DeAmpPrefs* deAmpPrefs; +@property(readonly) BrowserPrefs* browserPrefs; + @property(readonly) NTPBackgroundImagesService* backgroundImagesService; +/// The default content settings for regular browsing windows +@property(readonly) DefaultHostContentSettings* defaultHostContentSettings; + - (AIChat*)aiChatAPIWithDelegate:(id)delegate; /// Sets up bundle path overrides and initializes ICU from the BraveCore bundle @@ -111,6 +115,10 @@ OBJC_EXPORT /// Should only be called in unit tests + (bool)initializeICUForTesting; +@property(readonly) CWVWebViewConfiguration* defaultWebViewConfiguration; +@property(readonly) CWVWebViewConfiguration* nonPersistentWebViewConfiguration; +- (void)notifyLastPrivateTabClosed; + @end NS_ASSUME_NONNULL_END diff --git a/ios/app/brave_core_main.mm b/ios/app/brave_core_main.mm index 2533291675f1..e7e69d68adf2 100644 --- a/ios/app/brave_core_main.mm +++ b/ios/app/brave_core_main.mm @@ -8,6 +8,8 @@ #import #import +#include + #include "base/apple/bundle_locations.h" #include "base/apple/foundation_util.h" #include "base/compiler_specific.h" @@ -30,9 +32,10 @@ #include "brave/ios/browser/api/brave_shields/adblock_service+private.h" #include "brave/ios/browser/api/brave_stats/brave_stats+private.h" #include "brave/ios/browser/api/brave_wallet/brave_wallet_api+private.h" +#include "brave/ios/browser/api/content_settings/default_host_content_settings.h" +#include "brave/ios/browser/api/content_settings/default_host_content_settings_internal.h" #include "brave/ios/browser/api/de_amp/de_amp_prefs+private.h" #include "brave/ios/browser/api/history/brave_history_api+private.h" -#include "brave/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service+private.h" #include "brave/ios/browser/api/ipfs/ipfs_api+private.h" #include "brave/ios/browser/api/ntp_background_images/ntp_background_images_service_ios+private.h" #include "brave/ios/browser/api/opentabs/brave_opentabs_api+private.h" @@ -40,12 +43,14 @@ #include "brave/ios/browser/api/opentabs/brave_tabgenerator_api+private.h" #include "brave/ios/browser/api/p3a/brave_p3a_utils+private.h" #include "brave/ios/browser/api/password/brave_password_api+private.h" +#include "brave/ios/browser/api/prefs/browser_prefs+private.h" #include "brave/ios/browser/api/sync/brave_sync_api+private.h" #include "brave/ios/browser/api/sync/driver/brave_sync_profile_service+private.h" #include "brave/ios/browser/api/web_image/web_image+private.h" -#include "brave/ios/browser/brave_web_client.h" #include "brave/ios/browser/ui/webui/brave_web_ui_controller_factory.h" +#include "brave/ios/browser/web/brave_web_client.h" #include "components/component_updater/component_updater_paths.h" +#include "components/content_settings/core/browser/content_settings_utils.h" #include "components/history/core/browser/history_service.h" #include "components/keyed_service/core/service_access_type.h" #include "components/password_manager/core/browser/password_store/password_store.h" @@ -56,6 +61,9 @@ #include "ios/chrome/app/startup/provider_registration.h" #include "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h" #include "ios/chrome/browser/bookmarks/model/bookmark_undo_service_factory.h" +#include "ios/chrome/browser/browsing_data/model/browsing_data_remover.h" +#include "ios/chrome/browser/browsing_data/model/browsing_data_remover_factory.h" +#include "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h" #include "ios/chrome/browser/credential_provider/model/credential_provider_buildflags.h" #include "ios/chrome/browser/history/model/history_service_factory.h" #include "ios/chrome/browser/history/model/web_history_service_factory.h" @@ -76,6 +84,11 @@ #include "ios/public/provider/chrome/browser/overrides/overrides_api.h" #include "ios/public/provider/chrome/browser/ui_utils/ui_utils_api.h" #include "ios/web/public/init/web_main.h" +#include "ios/web/public/thread/web_task_traits.h" +#include "ios/web/public/thread/web_thread.h" +#include "ios/web_view/internal/cwv_web_view_configuration_internal.h" +#include "ios/web_view/internal/web_view_browser_state.h" +#include "ios/web_view/internal/web_view_download_manager.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED) @@ -104,6 +117,8 @@ @interface BraveCoreMain () { std::unique_ptr _webMain; std::unique_ptr _browser; std::unique_ptr _otr_browser; + std::unique_ptr _downloadManager; + std::unique_ptr _otrDownloadManager; raw_ptr _browserList; raw_ptr _otr_browserList; raw_ptr _mainBrowserState; @@ -124,8 +139,9 @@ @interface BraveCoreMain () { @property(nonatomic) BraveP3AUtils* p3aUtils; @property(nonatomic) DeAmpPrefs* deAmpPrefs; @property(nonatomic) NTPBackgroundImagesService* backgroundImagesService; -@property(nonatomic) - HTTPSUpgradeExceptionsService* httpsUpgradeExceptionsService; +@property(nonatomic) DefaultHostContentSettings* defaultHostContentSettings; +@property(nonatomic) CWVWebViewConfiguration* defaultWebViewConfiguration; +@property(nonatomic) CWVWebViewConfiguration* nonPersistentWebViewConfiguration; @end @implementation BraveCoreMain @@ -172,7 +188,6 @@ - (instancetype)initWithUserAgent:(NSString*)userAgent // Setup WebClient ([ClientRegistration registerClients]) _webClient.reset(new BraveWebClient()); - _webClient->SetUserAgent(base::SysNSStringToUTF8(userAgent)); web::SetWebClient(_webClient.get()); _delegate.reset(new BraveMainDelegate()); @@ -232,6 +247,13 @@ - (instancetype)initWithUserAgent:(NSString*)userAgent _otr_browser = Browser::Create(otrChromeBrowserState, {}); _otr_browserList->AddBrowser(_otr_browser.get()); + // Setup download managers for CWVWebView + _downloadManager = std::make_unique( + _mainBrowserState); + _otrDownloadManager = + std::make_unique( + otrChromeBrowserState); + // Initialize the provider UI global state. ios::provider::InitializeUI(); @@ -274,6 +296,15 @@ - (void)dealloc { _tabGeneratorAPI = nil; _webImageDownloader = nil; + [_nonPersistentWebViewConfiguration shutDown]; + [_defaultWebViewConfiguration shutDown]; + + _nonPersistentWebViewConfiguration = nil; + _defaultWebViewConfiguration = nil; + + _downloadManager.reset(); + _otrDownloadManager.reset(); + _otr_browserList = BrowserListFactory::GetForBrowserState(_otr_browser->GetBrowserState()); [_otr_browser->GetCommandDispatcher() prepareForShutdown]; @@ -459,14 +490,6 @@ - (BraveWalletAPI*)braveWalletAPI { return _braveWalletAPI; } -- (HTTPSUpgradeExceptionsService*)httpsUpgradeExceptionsService { - if (!_httpsUpgradeExceptionsService) { - _httpsUpgradeExceptionsService = - [[HTTPSUpgradeExceptionsService alloc] init]; - } - return _httpsUpgradeExceptionsService; -} - - (BraveStats*)braveStats { return [[BraveStats alloc] initWithBrowserState:_mainBrowserState]; } @@ -508,6 +531,11 @@ - (DeAmpPrefs*)deAmpPrefs { return _deAmpPrefs; } +- (BrowserPrefs*)browserPrefs { + return + [[BrowserPrefs alloc] initWithPrefService:_mainBrowserState->GetPrefs()]; +} + - (AIChat*)aiChatAPIWithDelegate:(id)delegate { return [[AIChat alloc] initWithChromeBrowserState:_mainBrowserState delegate:delegate]; @@ -533,4 +561,115 @@ - (void)performFaviconsCleanup { } #endif +- (DefaultHostContentSettings*)defaultHostContentSettings { + if (!_defaultHostContentSettings) { + HostContentSettingsMap* map = + ios::HostContentSettingsMapFactory::GetForBrowserState( + _mainBrowserState); + _defaultHostContentSettings = + [[DefaultHostContentSettings alloc] initWithSettingsMap:map]; + } + return _defaultHostContentSettings; +} + +- (CWVWebViewConfiguration*)defaultWebViewConfiguration { + if (!_defaultWebViewConfiguration) { + _defaultWebViewConfiguration = [[CWVWebViewConfiguration alloc] + initWithBrowserState:ios_web_view::WebViewBrowserState:: + FromBrowserState(_mainBrowserState)]; + } + return _defaultWebViewConfiguration; +} + +- (CWVWebViewConfiguration*)nonPersistentWebViewConfiguration { + if (!_nonPersistentWebViewConfiguration) { + _nonPersistentWebViewConfiguration = [[CWVWebViewConfiguration alloc] + initWithBrowserState: + ios_web_view::WebViewBrowserState::FromBrowserState( + _mainBrowserState->GetOffTheRecordChromeBrowserState())]; + } + return _nonPersistentWebViewConfiguration; +} + +#pragma mark - Handling of destroying the incognito BrowserState + +// The incognito BrowserState should be closed when the last incognito tab is +// closed (i.e. if there are other incognito tabs open in another Scene, the +// BrowserState must not be destroyed). +- (BOOL)shouldDestroyAndRebuildIncognitoBrowserState { + return _mainBrowserState->HasOffTheRecordChromeBrowserState(); +} + +// Matches lastIncognitoTabClosed from Chrome's SceneController +- (void)notifyLastPrivateTabClosed { + // If no other window has incognito tab, then destroy and rebuild the + // BrowserState. Otherwise, just do the state transition animation. + if ([self shouldDestroyAndRebuildIncognitoBrowserState]) { + // Incognito browser state cannot be deleted before all the requests are + // deleted. Queue empty task on IO thread and destroy the BrowserState + // when the task has executed, again verifying that no incognito tabs are + // present. When an incognito tab is moved between browsers, there is + // a point where the tab isn't attached to any web state list. However, when + // this queued cleanup step executes, the moved tab will be attached, so + // the cleanup shouldn't proceed. + + auto cleanup = ^{ + if ([self shouldDestroyAndRebuildIncognitoBrowserState]) { + [self destroyAndRebuildIncognitoBrowserState]; + } + }; + + web::GetIOThreadTaskRunner({})->PostTaskAndReply( + FROM_HERE, base::DoNothing(), base::BindRepeating(cleanup)); + } +} + +// Matches cleanupBrowser from Chrome's BrowserViewWrangler +- (void)cleanupBrowser:(Browser*)browser { + DCHECK(browser); + + // Remove the Browser from the browser list. The browser itself is still + // alive during this call, so any observer can act on it. + ProfileIOS* profile = browser->GetProfile(); + BrowserList* browserList = BrowserListFactory::GetForProfile(profile); + browserList->RemoveBrowser(browser); + + WebStateList* webStateList = browser->GetWebStateList(); + // Close all webstates in `webStateList`. Do this in an @autoreleasepool as + // WebStateList observers will be notified (they are unregistered later). As + // some of them may be implemented in Objective-C and unregister themselves + // in their -dealloc method, ensure the -autorelease introduced by ARC are + // processed before the WebStateList destructor is called. + @autoreleasepool { + CloseAllWebStates(*webStateList, WebStateList::CLOSE_NO_FLAGS); + } +} + +- (void)destroyAndRebuildIncognitoBrowserState { + DCHECK(_mainBrowserState->HasOffTheRecordChromeBrowserState()); + _nonPersistentWebViewConfiguration = nil; + + ChromeBrowserState* otrBrowserState = + _mainBrowserState->GetOffTheRecordChromeBrowserState(); + + BrowsingDataRemover* browsingDataRemover = + BrowsingDataRemoverFactory::GetForBrowserState(otrBrowserState); + browsingDataRemover->Remove(browsing_data::TimePeriod::ALL_TIME, + BrowsingDataRemoveMask::REMOVE_ALL, + base::DoNothing()); + + [self cleanupBrowser:_otr_browser.get()]; + _otr_browser.reset(); + + // Destroy and recreate the off-the-record BrowserState. + _mainBrowserState->DestroyOffTheRecordChromeBrowserState(); + + otrBrowserState = _mainBrowserState->GetOffTheRecordChromeBrowserState(); + _otr_browser = Browser::Create(otrBrowserState, {}); + + BrowserList* browserList = + BrowserListFactory::GetForBrowserState(otrBrowserState); + browserList->AddBrowser(_otr_browser.get()); +} + @end diff --git a/ios/brave-ios/App/Client.xcodeproj/Brave.xctestplan b/ios/brave-ios/App/Client.xcodeproj/Brave.xctestplan index 1091c6653638..66ab8f7775c8 100644 --- a/ios/brave-ios/App/Client.xcodeproj/Brave.xctestplan +++ b/ios/brave-ios/App/Client.xcodeproj/Brave.xctestplan @@ -130,13 +130,6 @@ "name" : "BraveVPNTests" } }, - { - "target" : { - "containerPath" : "container:..", - "identifier" : "UserAgentTests", - "name" : "UserAgentTests" - } - }, { "target" : { "containerPath" : "container:..", diff --git a/ios/brave-ios/App/iOS/Delegates/AppState.swift b/ios/brave-ios/App/iOS/Delegates/AppState.swift index 0a26f77d4f7e..2e427df1dba9 100644 --- a/ios/brave-ios/App/iOS/Delegates/AppState.swift +++ b/ios/brave-ios/App/iOS/Delegates/AppState.swift @@ -205,11 +205,9 @@ public class AppState { (AboutHomeHandler.path, AboutHomeHandler()), (AboutLicenseHandler.path, AboutLicenseHandler()), (SessionRestoreHandler.path, SessionRestoreHandler()), - (ErrorPageHandler.path, ErrorPageHandler()), - (ReaderModeHandler.path, ReaderModeHandler(profile: profile)), + (ReaderModeHandler.path, ReaderModeHandler(profile: profile, braveCore: braveCore)), (Web3DomainHandler.path, Web3DomainHandler()), (BlockedDomainHandler.path, BlockedDomainHandler()), - (HTTPBlockedHandler.path, HTTPBlockedHandler()), ] responders.forEach { (path, responder) in diff --git a/ios/brave-ios/App/iOS/Delegates/SceneDelegate.swift b/ios/brave-ios/App/iOS/Delegates/SceneDelegate.swift index 43317b64507a..85691be32563 100644 --- a/ios/brave-ios/App/iOS/Delegates/SceneDelegate.swift +++ b/ios/brave-ios/App/iOS/Delegates/SceneDelegate.swift @@ -40,6 +40,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private var cancellables: Set = [] + /// Potentially destroys and rebuilds OTR browser states if the there are no private browsing + /// sessions active. + private func maybeDestroyPrivateModeData() { + let privateBrowsingManagers = UIApplication.shared.connectedScenes + .compactMap({ ($0 as? UIWindowScene)?.browserViewController?.privateBrowsingManager }) + if privateBrowsingManagers.allSatisfy({ !$0.isPrivateBrowsing }) { + AppState.shared.braveCore.notifyLastPrivateTabClosed() + } + } + func scene( _ scene: UIScene, willConnectTo session: UISceneSession, @@ -93,11 +103,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { browserViewController.privateBrowsingManager.$isPrivateBrowsing .removeDuplicates() .receive(on: RunLoop.main) - .sink { [weak self, weak scene] _ in + .sink { [weak self, weak scene] isPrivateBrowsing in guard let self = self, let scene = scene as? UIWindowScene else { return } self.updateTheme(for: scene) + if !isPrivateBrowsing { + maybeDestroyPrivateModeData() + } } .store(in: &cancellables) diff --git a/ios/brave-ios/Package.swift b/ios/brave-ios/Package.swift index 4cf57036b72f..118c8f9787ec 100644 --- a/ios/brave-ios/Package.swift +++ b/ios/brave-ios/Package.swift @@ -346,7 +346,6 @@ var package = Package( name: "CredentialProviderUI", dependencies: ["BraveCore", "DesignSystem", "BraveShared", "Strings", "BraveUI"] ), - .testTarget(name: "UserAgentTests", dependencies: ["UserAgent", "Brave"]), .testTarget(name: "SharedTests", dependencies: ["Shared"]), .testTarget( name: "BraveSharedTests", @@ -417,6 +416,7 @@ var package = Package( dependencies: ["PlaylistUI", "Playlist", "Preferences", "Data", "TestHelpers"], resources: [.copy("Resources/Big_Buck_Bunny_360_10s_1MB.mp4")] ), + .target(name: "IsTesting"), .plugin(name: "IntentBuilderPlugin", capability: .buildTool()), .plugin(name: "LoggerPlugin", capability: .buildTool()), .plugin( @@ -464,6 +464,7 @@ var braveTarget: PackageDescription.Target = .target( .product(name: "Lottie", package: "lottie-spm"), .product(name: "Collections", package: "swift-collections"), "PlaylistUI", + "IsTesting", ], exclude: [ "Frontend/UserContent/UserScripts/AllFrames", @@ -496,10 +497,6 @@ var braveTarget: PackageDescription.Target = .target( .copy("Assets/Fonts/NewYorkMedium-Regular.otf"), .copy("Assets/Fonts/NewYorkMedium-RegularItalic.otf"), .copy("Assets/Interstitial Pages/Pages/BlockedDomain.html"), - .copy("Assets/Interstitial Pages/Pages/HTTPBlocked.html"), - .copy("Assets/Interstitial Pages/Pages/CertificateError.html"), - .copy("Assets/Interstitial Pages/Pages/GenericError.html"), - .copy("Assets/Interstitial Pages/Pages/NetworkError.html"), .copy("Assets/Interstitial Pages/Pages/Web3Domain.html"), .copy("Assets/Interstitial Pages/Images/Carret.png"), .copy("Assets/Interstitial Pages/Images/Clock.svg"), @@ -511,9 +508,7 @@ var braveTarget: PackageDescription.Target = .target( .copy("Assets/Interstitial Pages/Images/Warning.svg"), .copy("Assets/Interstitial Pages/Images/warning-triangle-outline.svg"), .copy("Assets/Interstitial Pages/Styles/BlockedDomain.css"), - .copy("Assets/Interstitial Pages/Styles/CertificateError.css"), .copy("Assets/Interstitial Pages/Styles/InterstitialStyles.css"), - .copy("Assets/Interstitial Pages/Styles/NetworkError.css"), .copy("Assets/Interstitial Pages/Styles/Web3Domain.css"), .copy("Assets/Lottie/shred.json"), .copy("Assets/SearchPlugins"), @@ -590,7 +585,6 @@ var braveTarget: PackageDescription.Target = .target( .copy("WebFilters/ContentBlocker/Lists/block-ads.json"), .copy("WebFilters/ContentBlocker/Lists/block-cookies.json"), .copy("WebFilters/ContentBlocker/Lists/block-trackers.json"), - .copy("WebFilters/ContentBlocker/Lists/mixed-content-upgrade.json"), .copy("WebFilters/ShieldStats/Adblock/Resources/ABPFilterParserData.dat"), ], plugins: ["LoggerPlugin"] diff --git a/ios/brave-ios/Sources/AIChat/ModelView/AIChatJavaScript.swift b/ios/brave-ios/Sources/AIChat/ModelView/AIChatJavaScript.swift index 8f7adbb45982..983326ca33ef 100644 --- a/ios/brave-ios/Sources/AIChat/ModelView/AIChatJavaScript.swift +++ b/ios/brave-ios/Sources/AIChat/ModelView/AIChatJavaScript.swift @@ -3,21 +3,21 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import Foundation -import WebKit public protocol AIChatJavascript { @MainActor - static func getPageContentType(webView: WKWebView) async -> String? + static func getPageContentType(webView: CWVWebView) async -> String? @MainActor - static func getMainArticle(webView: WKWebView) async -> String? + static func getMainArticle(webView: CWVWebView) async -> String? @MainActor - static func getPDFDocument(webView: WKWebView) async -> String? + static func getPDFDocument(webView: CWVWebView) async -> String? @MainActor - static func getPrintViewPDF(webView: WKWebView) async -> Data + static func getPrintViewPDF(webView: CWVWebView) async -> Data } public protocol AIChatBraveTalkJavascript { diff --git a/ios/brave-ios/Sources/AIChat/ModelView/AIChatViewModel.swift b/ios/brave-ios/Sources/AIChat/ModelView/AIChatViewModel.swift index daebcf28e655..3a8581267091 100644 --- a/ios/brave-ios/Sources/AIChat/ModelView/AIChatViewModel.swift +++ b/ios/brave-ios/Sources/AIChat/ModelView/AIChatViewModel.swift @@ -22,7 +22,7 @@ public class AIChatViewModel: NSObject, ObservableObject { // be passed directly to this object instead of proxied through the // AIChat class. private var api: AIChat! - private weak var webView: WKWebView? + private weak var webView: CWVWebView? private let script: any AIChatJavascript.Type private let braveTalkScript: AIChatBraveTalkJavascript? var querySubmited: String? @@ -52,7 +52,7 @@ public class AIChatViewModel: NSObject, ObservableObject { } public var isContentAssociationPossible: Bool { - return webView?.url?.isWebPage(includeDataURIs: true) == true + return webView?.lastCommittedURL?.isWebPage(includeDataURIs: true) == true } public var shouldSendPageContents: Bool { @@ -117,7 +117,7 @@ public class AIChatViewModel: NSObject, ObservableObject { public init( braveCore: BraveCoreMain, - webView: WKWebView?, + webView: CWVWebView?, script: any AIChatJavascript.Type, braveTalkScript: AIChatBraveTalkJavascript?, querySubmited: String? = nil @@ -257,7 +257,7 @@ extension AIChatViewModel: AIChatDelegate { } public func getLastCommittedURL() -> URL? { - if let url = webView?.url { + if let url = webView?.lastCommittedURL { return InternalURL.isValid(url: url) ? nil : url } return nil diff --git a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/CertificateError.html b/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/CertificateError.html deleted file mode 100644 index b13105b96b8b..000000000000 --- a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/CertificateError.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - %page_title% - - - - - - - -
- Icon - -

- %error_title% -

- - - - - - - - - - -
- diff --git a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/GenericError.html b/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/GenericError.html deleted file mode 100644 index 740a7476a8ba..000000000000 --- a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/GenericError.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - %page_title% - - - - -
- Icon - -

- %error_title% -

- -

- %error_description% -


- %error_domain% -

-

-
- diff --git a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/NetworkError.html b/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/NetworkError.html deleted file mode 100644 index ba662cd59c96..000000000000 --- a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Pages/NetworkError.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - %page_title% - - - - - -
- Icon - -

- %error_title% -

- -

- %error_try_list%: -

    -
  • %error_list_1%
  • -
  • %error_list_2%
  • -
-
- %error_domain% -

-

-
- diff --git a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Styles/CertificateError.css b/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Styles/CertificateError.css deleted file mode 100644 index 6e44cf6714db..000000000000 --- a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Styles/CertificateError.css +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright (c) 2021 The Brave Authors. All rights reserved. - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this file, - You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -* { - -webkit-tap-highlight-color: transparent; -} - -body { - background-color: rgb(255, 252, 240) !important; -} - -.navigationButtons { - display: flex; -} - -.leftNavButton { - background: transparent; - background-repeat: no-repeat; - border: none; - height: 44px; - margin-left: -20px; - padding-left: 20px; - padding-right: 20px; -} - -@media (max-width: 320px) { - .leftNavButton { - background: transparent; - background-repeat: no-repeat; - border: none; - height: 44px; - margin-left: -20px; - padding-left: 20px; - padding-right: 3px; - } -} - -.leftNavButtonTitle { - color: rgb(76, 84, 210); - font-size: 16px; - font-family: SFProText-Semibold, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-weight: 600; - line-height: 16px; - - cursor: pointer; - overflow: hidden; - outline: none; -} - -.rightNavButton { - background: rgb(76, 84, 210); - border-radius: 100px; - background-repeat: no-repeat; - border: none; - height: 44px; - margin-right: 0px; - padding-left: 20px; - padding-right: 20px; - margin-left: auto; -} - -.rightNavButtonTitle { - color: rgb(255, 255, 255); - font-size: 16px; - font-family: SFProText-Semibold, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-weight: 600; - text-align: center; - line-height: 16px; - - cursor: pointer; - overflow: hidden; - outline: none; -} - -.moreDetails { - color: rgb(82, 82, 82); - font-family: SFProText-Medium, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-size: 14px; - font-weight: 500; - height: 133px; - letter-spacing: 0px; - line-height: 19px; -} - -.hidden { - display: none; -} - -.arrow { - display: inline-block; - float: right; - width: 18px; - height: 10px; - margin-left: 6px; - margin-top: 4px; -} - -.up { - transform: rotate(-180deg); - transition: 0.3s; - -webkit-transform: rotate(-180deg); -} - -.down { - transform: rotate(0deg); - transition: 0.3s; - -webkit-transform: rotate(0deg); -} diff --git a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Styles/NetworkError.css b/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Styles/NetworkError.css deleted file mode 100644 index aa2e6bde09f3..000000000000 --- a/ios/brave-ios/Sources/Brave/Assets/Interstitial Pages/Styles/NetworkError.css +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright (c) 2021 The Brave Authors. All rights reserved. - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this file, - You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -ul { - list-style: none; - position: relative; -} - -li { - margin-left: -25px; -} - -ul li::before { - content: '•'; - position: absolute; - left: 0pt; - - font-size: 14px; - font-family: SFProText-Regular, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-weight: normal; - letter-spacing: 0px; - line-height: 19px; -} diff --git a/ios/brave-ios/Sources/Brave/BraveSkus/BraveSkusAccountLink.swift b/ios/brave-ios/Sources/Brave/BraveSkus/BraveSkusAccountLink.swift index dfd781677095..3e042c172af6 100644 --- a/ios/brave-ios/Sources/Brave/BraveSkus/BraveSkusAccountLink.swift +++ b/ios/brave-ios/Sources/Brave/BraveSkus/BraveSkusAccountLink.swift @@ -44,7 +44,7 @@ class BraveSkusAccountLink { } @MainActor - static func injectLocalStorage(webView: WKWebView) async { + static func injectLocalStorage(webView: BraveWebView) async { if let vpnSubscriptionProductId = Preferences.VPN.subscriptionProductId.value, let product = BraveStoreProduct(rawValue: vpnSubscriptionProductId) { @@ -63,11 +63,11 @@ class BraveSkusAccountLink { /// - Parameter product: The product whose receipt information to inject @MainActor @discardableResult private static func injectLocalStorage( - webView: WKWebView, + webView: BraveWebView, product: BraveStoreProduct ) async -> Bool { // The WebView has no URL so do nothing - guard let url = webView.url else { + guard let url = webView.lastCommittedURL else { return false } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Authenticator.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Authenticator.swift index c34e9742cba6..693327a3a0d3 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Authenticator.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Authenticator.swift @@ -26,30 +26,19 @@ class Authenticator { enum LoginDataError: Error { case usernameOrPasswordFieldLeftBlank case userCancelledAuthentication - case tooManyAttemptsFailed case missingProtectionSpaceHostName } - private static let maxAuthenticationAttempts = 3 - static func handleAuthRequest( _ viewController: UIViewController, credential: URLCredential?, - protectionSpace: URLProtectionSpace, - previousFailureCount: Int + protectionSpace: URLProtectionSpace ) async throws -> LoginData { var credential = credential - // If there have already been too many login attempts, we'll just fail. - if previousFailureCount >= Authenticator.maxAuthenticationAttempts { - throw LoginDataError.tooManyAttemptsFailed - } - // If we were passed an initial set of credentials from iOS, try and use them. if let proposed = credential { if !(proposed.user?.isEmpty ?? true) { - if previousFailureCount == 0 { - return LoginData(credentials: proposed, protectionSpace: protectionSpace) - } + return LoginData(credentials: proposed, protectionSpace: protectionSpace) } else { credential = nil } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift index 8f855122dfc2..e6f038c54c75 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared import Shared import SnapKit @@ -49,8 +50,8 @@ class BackForwardListViewController: UIViewController, UIGestureRecognizerDelega var tabManager: TabManager? weak var bvc: BrowserViewController? - private var currentItem: WKBackForwardListItem? - private var backForwardListData = [WKBackForwardListItem]() + private var currentItem: CWVBackForwardListItem? + private var backForwardListData = [CWVBackForwardListItem]() var tableHeight: CGFloat { assert( @@ -75,7 +76,7 @@ class BackForwardListViewController: UIViewController, UIGestureRecognizerDelega return tabManager?.privateBrowsingManager.isPrivateBrowsing ?? false } - init(profile: Profile, backForwardList: WKBackForwardList) { + init(profile: Profile, backForwardList: CWVBackForwardList) { self.profile = profile super.init(nibName: nil, bundle: nil) @@ -104,7 +105,7 @@ class BackForwardListViewController: UIViewController, UIGestureRecognizerDelega setupDismissTap() } - func homeAndNormalPagesOnly(_ bfList: WKBackForwardList) { + func homeAndNormalPagesOnly(_ bfList: CWVBackForwardList) { let items = bfList.forwardList.reversed() + [bfList.currentItem].compactMap({ $0 }) + bfList.backList.reversed() @@ -115,14 +116,11 @@ class BackForwardListViewController: UIViewController, UIGestureRecognizerDelega if internalUrl.isAboutHomeURL { return true } - if let url = internalUrl.originalURLFromErrorPage, InternalURL.isValid(url: url) { - return false - } return true } } - func loadSites(_ bfList: WKBackForwardList) { + func loadSites(_ bfList: CWVBackForwardList) { currentItem = bfList.currentItem homeAndNormalPagesOnly(bfList) } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveGetUA.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveGetUA.swift index a9b068677aa3..5ba0a0a46ae0 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveGetUA.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveGetUA.swift @@ -8,12 +8,6 @@ import Shared import WebKit class BraveGetUA: TabContentScript { - fileprivate weak var tab: Tab? - - required init(tab: Tab) { - self.tab = tab - } - static let scriptName = "BraveGetUA" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -34,9 +28,9 @@ class BraveGetUA: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { // 🙀 😭 🏃‍♀️💨 diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveWebView.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveWebView.swift index 4a5bce6d7945..65a329d251d7 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveWebView.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BraveWebView.swift @@ -2,13 +2,14 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared import Foundation import Shared import UserAgent import WebKit -class BraveWebView: WKWebView { +class BraveWebView: CWVWebView { /// Stores last position when the webview was touched on. private(set) var lastHitPoint = CGPoint(x: 0, y: 0) @@ -25,23 +26,25 @@ class BraveWebView: WKWebView { init( frame: CGRect, - configuration: WKWebViewConfiguration = WKWebViewConfiguration(), + wkConfiguration: WKWebViewConfiguration?, + configuration: CWVWebViewConfiguration, isPrivate: Bool = true ) { if isPrivate { - configuration.websiteDataStore = BraveWebView.sharedNonPersistentStore() + wkConfiguration?.websiteDataStore = BraveWebView.sharedNonPersistentStore() } else { - configuration.websiteDataStore = WKWebsiteDataStore.default() + wkConfiguration?.websiteDataStore = WKWebsiteDataStore.default() } + CWVWebView.webInspectorEnabled = true + CWVWebView.chromeContextMenuEnabled = false + CWVWebView.skipAccountStorageCheckEnabled = true - super.init(frame: frame, configuration: configuration) - - isFindInteractionEnabled = true - - customUserAgent = UserAgent.userAgentForDesktopMode - if #available(iOS 16.4, *) { - isInspectable = true - } + super.init( + frame: frame, + configuration: configuration, + wkConfiguration: wkConfiguration, + createdWKWebView: nil + ) } static func removeNonPersistentStore() { @@ -59,13 +62,11 @@ class BraveWebView: WKWebView { } } -extension WKWebView { +extension CWVWebView { public var sessionData: Data? { - get { - interactionState as? Data - } - set { - interactionState = newValue - } + if lastCommittedURL == nil { return nil } + let coder = NSKeyedArchiver(requiringSecureCoding: false) + encodeRestorableState(with: coder) + return coder.encodedData } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserPrompts.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserPrompts.swift index bf24ffcd756f..05acd8ba2b15 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserPrompts.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserPrompts.swift @@ -90,13 +90,13 @@ protocol JSAlertInfo { struct MessageAlert: JSAlertInfo { let message: String - let frame: WKFrameInfo + let pageURL: URL let completionHandler: () -> Void var suppressHandler: SuppressHandler? func alertController() -> JSPromptAlertController { let alertController = JSPromptAlertController( - title: titleForJavaScriptPanelInitiatedByFrame(frame), + title: pageURL.origin.serialized ?? pageURL.absoluteString, message: message, info: self, showCancel: false @@ -117,14 +117,14 @@ struct MessageAlert: JSAlertInfo { struct ConfirmPanelAlert: JSAlertInfo { let message: String - let frame: WKFrameInfo + let pageURL: URL let completionHandler: (Bool) -> Void var suppressHandler: SuppressHandler? func alertController() -> JSPromptAlertController { // Show JavaScript confirm dialogs. let alertController = JSPromptAlertController( - title: titleForJavaScriptPanelInitiatedByFrame(frame), + title: pageURL.origin.serialized ?? pageURL.absoluteString, message: message, info: self ) @@ -144,14 +144,14 @@ struct ConfirmPanelAlert: JSAlertInfo { struct TextInputAlert: JSAlertInfo { let message: String - let frame: WKFrameInfo + let pageURL: URL let completionHandler: (String?) -> Void let defaultText: String? var suppressHandler: SuppressHandler? func alertController() -> JSPromptAlertController { let alertController = JSPromptAlertController( - title: titleForJavaScriptPanelInitiatedByFrame(frame), + title: pageURL.origin.serialized ?? pageURL.absoluteString, message: message, info: self ) diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+DownloadQueueDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+DownloadQueueDelegate.swift deleted file mode 100644 index dae018a02016..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+DownloadQueueDelegate.swift +++ /dev/null @@ -1,110 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Foundation -import Shared -import UIKit - -extension BrowserViewController: DownloadQueueDelegate { - func downloadQueue(_ downloadQueue: DownloadQueue, didStartDownload download: Download) { - // If no other download toast is shown, create a new download toast and show it. - guard let downloadToast = self.downloadToast else { - let downloadToast = DownloadToast( - download: download, - completion: { buttonPressed in - // When this toast is dismissed, be sure to clear this so that any - // subsequent downloads cause a new toast to be created. - self.downloadToast = nil - - // Handle download cancellation - if buttonPressed, !downloadQueue.isEmpty { - downloadQueue.cancelAll() - - let downloadCancelledToast = ButtonToast( - labelText: Strings.downloadCancelledToastLabelText, - backgroundColor: UIColor.braveLabel, - textAlignment: .center - ) - - self.show(toast: downloadCancelledToast) - } - } - ) - - show(toast: downloadToast, duration: nil) - return - } - - // Otherwise, just add this download to the existing download toast. - downloadToast.addDownload(download) - } - - func downloadQueue( - _ downloadQueue: DownloadQueue, - didDownloadCombinedBytes combinedBytesDownloaded: Int64, - combinedTotalBytesExpected: Int64? - ) { - downloadToast?.combinedBytesDownloaded = combinedBytesDownloaded - downloadToast?.combinedTotalBytesExpected = combinedTotalBytesExpected - } - - func downloadQueue( - _ downloadQueue: DownloadQueue, - download: Download, - didFinishDownloadingTo location: URL - ) { - print("didFinishDownloadingTo(): \(location)") - } - - func downloadQueue(_ downloadQueue: DownloadQueue, didCompleteWithError error: Error?) { - guard let downloadToast = self.downloadToast, let download = downloadToast.downloads.first - else { - return - } - - DispatchQueue.main.async { - downloadToast.dismiss(false) - - if error == nil { - let downloadCompleteToast = ButtonToast( - labelText: download.filename, - image: UIImage(named: "check", in: .module, compatibleWith: nil)?.template, - buttonText: Strings.downloadsButtonTitle, - completion: { buttonPressed in - guard buttonPressed else { return } - - UIApplication.shared.openBraveDownloadsFolder { [weak self] success in - if !success { - self?.displayOpenDownloadsError() - } - } - } - ) - - self.show(toast: downloadCompleteToast, duration: DispatchTimeInterval.seconds(8)) - } else { - let downloadFailedToast = ButtonToast( - labelText: Strings.downloadFailedToastLabelText, - backgroundColor: UIColor.braveLabel, - textAlignment: .center - ) - - self.show(toast: downloadFailedToast, duration: nil) - } - } - } - - func displayOpenDownloadsError() { - let alert = UIAlertController( - title: Strings.genericErrorTitle, - message: Strings.openDownloadsFolderErrorDescription, - preferredStyle: .alert - ) - alert.addAction( - UIAlertAction(title: Strings.PlayList.okayButtonTitle, style: .default, handler: nil) - ) - - present(alert, animated: true, completion: nil) - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+OpenSearch.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+OpenSearch.swift index 6cc502ce0979..e68068d8aab9 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+OpenSearch.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+OpenSearch.swift @@ -29,10 +29,10 @@ extension BrowserViewController { /// Adding Toolbar button over the keyboard for adding Open Search Engine /// - Parameter webView: webview triggered open seach engine @discardableResult - func evaluateWebsiteSupportOpenSearchEngine(_ webView: WKWebView) -> Bool { + func evaluateWebsiteSupportOpenSearchEngine(_ webView: BraveWebView) -> Bool { if let tab = tabManager[webView], let openSearchMetaData = tab.pageMetadata?.search, - let url = webView.url, + let url = webView.lastCommittedURL, url.isSecureWebPage() { return updateAddOpenSearchEngine( @@ -49,7 +49,7 @@ extension BrowserViewController { @discardableResult private func updateAddOpenSearchEngine( - _ webView: WKWebView, + _ webView: BraveWebView, referenceObject: OpenSearchReference ) -> Bool { var supportsAutoAdd = true @@ -142,8 +142,8 @@ extension BrowserViewController { return } - guard let scheme = tabManager.selectedTab?.webView?.url?.scheme, - let host = tabManager.selectedTab?.webView?.url?.host + guard let scheme = tabManager.selectedTab?.webView?.lastCommittedURL?.scheme, + let host = tabManager.selectedTab?.webView?.lastCommittedURL?.host else { Logger.module.error("Selected Tab doesn't have URL") return diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ReaderMode.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ReaderMode.swift index 117b7a7c0c12..f2b5e6c7c156 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ReaderMode.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ReaderMode.swift @@ -54,7 +54,7 @@ extension BrowserViewController: ReaderModeStyleViewControllerDelegate { as? ReaderModeScriptHandler { if readerMode.state == ReaderModeState.active { - readerMode.style = style + readerMode.setStyle(style, in: tab) } } } @@ -157,13 +157,13 @@ extension BrowserViewController { recordTimeBasedNumberReaderModeUsedP3A(activated: true) - if backList.count > 1 && backList.last?.url == readerModeURL { + if backList.count > 1 && backList[backList.count - 1].url == readerModeURL { let playlistItem = tab.playlistItem - webView.go(to: backList.last!) + webView.go(to: backList[backList.count - 1]) PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) - } else if !forwardList.isEmpty && forwardList.first?.url == readerModeURL { + } else if forwardList.count > 0 && forwardList[0].url == readerModeURL { let playlistItem = tab.playlistItem - webView.go(to: forwardList.first!) + webView.go(to: forwardList[0]) PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) } else { // Store the readability result in the cache and load it. This will later move to the ReadabilityHelper. @@ -175,9 +175,8 @@ extension BrowserViewController { let playlistItem = tab.playlistItem Task { try? await self.readerModeCache.put(currentURL, readabilityResult) - if webView.load(PrivilegedRequest(url: readerModeURL) as URLRequest) != nil { - PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) - } + webView.load(PrivilegedRequest(url: readerModeURL) as URLRequest) + PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) } } } @@ -198,19 +197,18 @@ extension BrowserViewController { if let currentURL = webView.backForwardList.currentItem?.url { if let originalURL = currentURL.decodeEmbeddedInternalURL(for: .readermode) { - if backList.count > 1 && backList.last?.url == originalURL { + if backList.count > 1 && backList[backList.count - 1].url == originalURL { let playlistItem = tab.playlistItem - webView.go(to: backList.last!) + webView.go(to: backList[backList.count - 1]) PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) - } else if !forwardList.isEmpty && forwardList.first?.url == originalURL { + } else if forwardList.count > 0 && forwardList[0].url == originalURL { let playlistItem = tab.playlistItem - webView.go(to: forwardList.first!) + webView.go(to: forwardList[0]) PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) } else { let playlistItem = tab.playlistItem - if webView.load(URLRequest(url: originalURL)) != nil { - PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) - } + webView.load(URLRequest(url: originalURL)) + PlaylistScriptHandler.updatePlaylistTab(tab: tab, item: playlistItem) } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift index 1d6dabd26d7e..3a00f833bac7 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift @@ -59,7 +59,7 @@ extension BrowserViewController { guard let tab = tabManager.selectedTab else { return } // System components sit on top so we want to dismiss it - tab.webView?.findInteraction?.dismissFindNavigator() + tab.webView?.findInPageController.stopFindInPage() let braveRewardsPanel = BraveRewardsViewController( tab: tab, diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ShareActivity.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ShareActivity.swift index 225e50e87356..538730c503b4 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ShareActivity.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ShareActivity.swift @@ -103,10 +103,7 @@ extension BrowserViewController { callback: { [weak self] in guard let self = self else { return } - if let findInteraction = self.tabManager.selectedTab?.webView?.findInteraction { - findInteraction.searchText = "" - findInteraction.presentFindNavigator(showingReplace: false) - } + self.tabManager.selectedTab?.webView?.findInPageController.findString(inPage: "") } ) ) @@ -190,13 +187,12 @@ extension BrowserViewController { BasicMenuActivity( activityType: .createPDF, callback: { - webView.createPDF { [weak self] result in + webView.createPDF { [weak self] data in dispatchPrecondition(condition: .onQueue(.main)) guard let self = self else { return } - switch result { - case .success(let pdfData): + if let pdfData = data { Task { // Create a valid filename let validFilenameSet = CharacterSet(charactersIn: ":/") @@ -229,10 +225,9 @@ extension BrowserViewController { ) } } - - case .failure(let error): + } else { Logger.module.error( - "Failed to create PDF with error: \(error.localizedDescription)" + "Failed to create PDF" ) } } @@ -288,9 +283,8 @@ extension BrowserViewController { } // Display Certificate Activity - if let tabURL = tabManager.selectedTab?.webView?.url, - tabManager.selectedTab?.webView?.serverTrust != nil - || ErrorPageHelper.hasCertificates(for: tabURL) + if let tabURL = tabManager.selectedTab?.webView?.lastCommittedURL, + tabManager.selectedTab?.webView?.visibleSSLStatus?.certificate != nil { activities.append( BasicMenuActivity( diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift index 321e26317708..a571545b37de 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift @@ -109,7 +109,7 @@ extension BrowserViewController: TabManagerDelegate { webView.accessibilityIdentifier = "contentView" webView.accessibilityElementsHidden = false - if webView.url == nil { + if webView.lastCommittedURL == nil { // The web view can go gray if it was zombified due to memory pressure. // When this happens, the URL is nil, so try restoring the page upon selection. tab.reload() diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift index 897bcce5f779..e55f1362f1dd 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift @@ -267,7 +267,10 @@ extension BrowserViewController: TopToolbarDelegate { if let url = URL(string: text), url.scheme == "brave" || url.scheme == "chrome" { topToolbar.leaveOverlayMode() - return handleChromiumWebUIURL(url) + + finishEditingAndSubmit(url, isUserDefinedURLNavigation: isUserDefinedURLNavigation) + return true + // return handleChromiumWebUIURL(url) } guard let fixupURL = URIFixup.getURL(text) else { @@ -662,7 +665,7 @@ extension BrowserViewController: TopToolbarDelegate { return } // System components sit on top so we want to dismiss it - selectedTab.webView?.findInteraction?.dismissFindNavigator() + selectedTab.webView?.findInPageController.stopFindInPage() presentWalletPanel(from: selectedTab.getOrigin(), with: selectedTab.tabDappStore) } @@ -927,9 +930,6 @@ extension BrowserViewController: ToolbarDelegate { guard let url = tabManager.selectedTab?.url else { return nil } if let internalURL = InternalURL(url) { - if internalURL.isErrorPage { - return internalURL.originalURLFromErrorPage - } if internalURL.isReaderModePage { return internalURL.extractedUrlParam } @@ -1010,8 +1010,7 @@ extension BrowserViewController: ToolbarDelegate { guard let tab = tabManager.selectedTab, let url = tab.url, let secureContentStateButton = urlBar.locationView.secureContentStateButton else { return } - let hasCertificate = - (tab.webView?.serverTrust ?? (try? ErrorPageHelper.serverTrust(from: url))) != nil + let hasCertificate = tab.webView?.visibleSSLStatus?.certificate != nil let pageSecurityView = PageSecurityView( displayURL: urlBar.locationView.urlDisplayLabel.text ?? url.absoluteDisplayString, secureState: tab.lastKnownSecureContentState, diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKDownloadDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKDownloadDelegate.swift index e72641b519f0..aee5f6cf90ce 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKDownloadDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKDownloadDelegate.swift @@ -3,205 +3,127 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared import Foundation +import OSLog import PassKit import Shared import WebKit -extension BrowserViewController: WKDownloadDelegate { +extension BrowserViewController: CWVDownloadTaskDelegate { + fileprivate func uniqueDownloadPathForFilename(_ filename: String) async throws -> URL { + let downloadsPath = try await AsyncFileManager.default.downloadsPath() + let basePath = downloadsPath.appending(path: filename) + let fileExtension = basePath.pathExtension + let filenameWithoutExtension = + !fileExtension.isEmpty ? String(filename.dropLast(fileExtension.count + 1)) : filename - public func download( - _ download: WKDownload, - decideDestinationUsing response: URLResponse, - suggestedFilename: String - ) async -> URL? { + var proposedPath = basePath + var count = 0 - if let httpResponse = response as? HTTPURLResponse { - if httpResponse.mimeType != MIMEType.passbook { - let shouldDownload = await downloadAlert( - download, - response: response, - suggestedFileName: suggestedFilename - ) - if !shouldDownload { - return nil - } - } - } + while await AsyncFileManager.default.fileExists(atPath: proposedPath.path) { + count += 1 - let temporaryDir = NSTemporaryDirectory() - let fileName = temporaryDir + "/" + suggestedFilename - let url = URL(fileURLWithPath: fileName) - - // WKDownload will fail with a -3000 error code if the file already exists at the given path - if await AsyncFileManager.default.fileExists(atPath: url.path(percentEncoded: false)) { - try? await AsyncFileManager.default.removeItem(at: url) + let proposedFilenameWithoutExtension = "\(filenameWithoutExtension) (\(count))" + proposedPath = downloadsPath.appending(path: proposedFilenameWithoutExtension) + .appending(path: fileExtension) } - let pendingDownload = WebKitDownload( - fileURL: url, - response: response, - suggestedFileName: suggestedFilename, - download: download, - downloadQueue: downloadQueue - ) - - downloadQueue.enqueue(pendingDownload) - - return url - } - - public func download( - _ download: WKDownload, - decidedPolicyForHTTPRedirection response: HTTPURLResponse, - newRequest request: URLRequest - ) async -> WKDownload.RedirectPolicy { - return .allow + return proposedPath } - public func download( - _ download: WKDownload, - respondTo challenge: URLAuthenticationChallenge - ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - return (.performDefaultHandling, nil) - } - - @MainActor - public func downloadDidFinish(_ download: WKDownload) { - guard - let downloadInfo = downloadQueue.downloads.compactMap({ $0 as? WebKitDownload }).first( - where: { $0.download == download }) - else { + public func downloadTask( + _ downloadTask: CWVDownloadTask, + didFinishWithError error: (any Error)? + ) { + defer { + downloadToast?.dismiss(false) + } + if let error { + // display an error + let alertController = UIAlertController( + // FIXME: This error needs to generalized to any download + title: Strings.unableToAddPassErrorTitle, + message: Strings.unableToAddPassErrorMessage, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: Strings.unableToAddPassErrorDismiss, style: .cancel) { (action) in + // Do nothing. + } + ) + present(alertController, animated: true, completion: nil) return } - let response = URLResponse( - url: downloadInfo.fileURL, - mimeType: downloadInfo.response.mimeType, - expectedContentLength: Int(downloadInfo.response.expectedContentLength), - textEncodingName: downloadInfo.response.textEncodingName + let fileURL = FileManager.default.temporaryDirectory.appending( + path: downloadTask.suggestedFileName ) - if downloadInfo.response.mimeType == MIMEType.passbook { - downloadQueue.download(downloadInfo, didFinishDownloadingTo: downloadInfo.fileURL) - if let passbookHelper = OpenPassBookHelper( - request: nil, - response: response, - canShowInWebView: false, - forceDownload: false, - browserViewController: self - ) { - Task { - await passbookHelper.open() - try await AsyncFileManager.default.removeItem(at: downloadInfo.fileURL) - } + if let passbookHelper = OpenPassBookHelper( + mimeType: downloadTask.mimeType, + url: fileURL, + browserViewController: self + ) { + Task { + await passbookHelper.open() + try await AsyncFileManager.default.removeItem(at: fileURL) } - return - } - - // Handle non-passbook downloads the same as HTTPDownload - let filename = downloadInfo.filename - let location = downloadInfo.fileURL - let temporaryLocation = FileManager.default.temporaryDirectory - .appending(component: "\(filename)-\(location.lastPathComponent)") - try? FileManager.default.moveItem(at: location, to: temporaryLocation) - Task { - do { - let destination = try await downloadInfo.uniqueDownloadPathForFilename(filename) - try await AsyncFileManager.default.moveItem(at: temporaryLocation, to: destination) - downloadQueue.download(downloadInfo, didFinishDownloadingTo: destination) - } catch { - downloadQueue.download(downloadInfo, didCompleteWithError: error) + } else { + Task { + if error == nil { + do { + let destination = try await self.uniqueDownloadPathForFilename( + downloadTask.suggestedFileName + ) + try await AsyncFileManager.default.moveItem(at: fileURL, to: destination) + } catch { + Logger.module.error("Failed to move downloaded file to downloads") + } + let downloadCompleteToast = ButtonToast( + labelText: downloadTask.suggestedFileName, + image: UIImage(named: "check", in: .module, compatibleWith: nil)?.template, + buttonText: Strings.downloadsButtonTitle, + completion: { buttonPressed in + guard buttonPressed else { return } + + UIApplication.shared.openBraveDownloadsFolder { [weak self] success in + if !success { + self?.displayOpenDownloadsError() + } + } + } + ) + + self.show(toast: downloadCompleteToast, duration: DispatchTimeInterval.seconds(8)) + } else { + let downloadFailedToast = ButtonToast( + labelText: Strings.downloadFailedToastLabelText, + backgroundColor: UIColor.braveLabel, + textAlignment: .center + ) + + self.show(toast: downloadFailedToast, duration: nil) + } } } } - @MainActor - public func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) { - guard - let downloadInfo = downloadQueue.downloads.compactMap({ $0 as? WebKitDownload }).first( - where: { $0.download == download }) - else { - return - } - - downloadQueue.download(downloadInfo, didCompleteWithError: error) + public func downloadTaskProgressDidChange(_ downloadTask: CWVDownloadTask) { + downloadToast?.updatePercent() + } - // display an error - let alertController = UIAlertController( - title: Strings.unableToAddPassErrorTitle, - message: Strings.unableToAddPassErrorMessage, + func displayOpenDownloadsError() { + let alert = UIAlertController( + title: Strings.genericErrorTitle, + message: Strings.openDownloadsFolderErrorDescription, preferredStyle: .alert ) - alertController.addAction( - UIAlertAction(title: Strings.unableToAddPassErrorDismiss, style: .cancel) { (action) in - // Do nothing. - } + alert.addAction( + UIAlertAction(title: Strings.PlayList.okayButtonTitle, style: .default, handler: nil) ) - present(alertController, animated: true, completion: nil) - } - - @MainActor - private func downloadAlert( - _ download: WKDownload, - response: URLResponse, - suggestedFileName: String - ) async -> Bool { - // Only download if there is a valid host - guard let host = download.originalRequest?.url?.host() else { - return false - } - - // Never present the download alert on a tab that isn't visible - guard let webView = download.webView, let tab = tabManager.tabForWebView(webView), - tab === tabManager.selectedTab - else { - return false - } - - let filename = HTTPDownload.stripUnicode(fromFilename: suggestedFileName) - let totalBytesExpected = - response.expectedContentLength > 0 ? response.expectedContentLength : nil - - let expectedSize = - totalBytesExpected != nil - ? ByteCountFormatter.string(fromByteCount: totalBytesExpected!, countStyle: .file) - : nil - - let title = "\(filename) - \(host)" - - var downloadActionText = Strings.download - if let expectedSize = expectedSize { - downloadActionText += " (\(expectedSize))" - } - return await withCheckedContinuation { continuation in - let downloadAlert = UIAlertController( - title: title, - message: nil, - preferredStyle: .actionSheet - ) - - downloadAlert.addAction( - UIAlertAction(title: downloadActionText, style: .default) { _ in - continuation.resume(returning: true) - } - ) - - downloadAlert.addAction( - UIAlertAction(title: Strings.cancelButtonTitle, style: .cancel) { _ in - continuation.resume(returning: false) - } - ) - - downloadAlert.popoverPresentationController?.do { - $0.sourceView = view - $0.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 16, width: 0, height: 0) - $0.permittedArrowDirections = [] - } - - present(downloadAlert, animated: true, completion: nil) - } + present(alert, animated: true, completion: nil) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift index 30f3cdaa9272..500310cc0117 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift @@ -21,11 +21,44 @@ import UniformTypeIdentifiers import WebKit import os.log -extension WKNavigationAction { +/// Observes a single change from the private API `_sampledPageTopColor` +/// +/// This should only be installed at the start of a navigation as that will guarantee that the +/// underlying `WKWebView` has already been created by `CRWWebController` +class SampledTopPageColorNotifier: NSObject { + private let keyPath = "_sampl\("edPageTopC")olor" + private weak var webView: CWVWebView? + private let handler: (UIColor?) -> Void + private var notified: Bool = false + init(webView: CWVWebView, handler: @escaping (UIColor?) -> Void) { + self.webView = webView + self.handler = handler + super.init() + webView.underlyingWebView?.addObserver(self, forKeyPath: keyPath, context: nil) + } + deinit { + if !notified { + webView?.underlyingWebView?.removeObserver(self, forKeyPath: keyPath) + } + } + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer? + ) { + defer { notified = true } + guard let webView, keyPath == self.keyPath else { return } + handler(webView.underlyingWebView?.sampledPageTopColor) + webView.underlyingWebView?.removeObserver(self, forKeyPath: self.keyPath) + } +} + +extension URLRequest { /// Allow local requests only if the request is privileged. /// If the request is internal or unprivileged, we should deny it. var isInternalUnprivileged: Bool { - guard let url = request.url else { + guard let url = url else { return true } @@ -57,20 +90,67 @@ extension UTType { static let mobileConfiguration = UTType(mimeType: "application/x-apple-aspen-config")! } -// MARK: WKNavigationDelegate -extension BrowserViewController: WKNavigationDelegate { - private static let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "navigation") +extension BrowserViewController: CWVNavigationDelegate { + public func webViewDidCommitNavigation(_ webView: CWVWebView) { + guard let tab = tab(for: webView) else { return } + + // Set the committed url which will also set tab.url + tab.committedURL = webView.lastCommittedURL + + // Need to evaluate Night mode script injection after url is set inside the Tab + tab.nightMode = Preferences.General.nightModeEnabled.value + tab.clearSolanaConnectedAccounts() + + // Providers need re-initialized when changing origin to align with desktop in + // `BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame` + // https://github.com/brave/brave-core/blob/1.52.x/browser/brave_content_browser_client.cc#L608 + if let provider = braveCore.braveWalletAPI.ethereumProvider( + with: tab, + isPrivateBrowsing: tab.isPrivate + ) { + // The Ethereum provider will fetch allowed accounts from it's delegate (the tab) + // on initialization. Fetching allowed accounts requires the origin; so we need to + // initialize after `commitedURL` / `url` are updated above + tab.walletEthProvider = provider + tab.walletEthProvider?.initialize(eventsListener: tab) + } + if let provider = braveCore.braveWalletAPI.solanaProvider( + with: tab, + isPrivateBrowsing: tab.isPrivate + ) { + tab.walletSolProvider = provider + tab.walletSolProvider?.initialize(eventsListener: tab) + } + + rewards.reportTabNavigation(tabId: tab.rewardsId) + + // The toolbar and url bar changes can not be + // on different tab than selected. Or the webview + // previews and etc will effect the status + guard tabManager.selectedTab === tab else { + return + } + + updateUIForReaderHomeStateForTab(tab) + updateBackForwardActionStatus(for: tab.webView) + } - public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) - { + public func webViewDidStartProvisionalNavigation(_ webView: CWVWebView) { if tabManager.selectedTab?.webView !== webView { return } toolbarVisibilityViewModel.toolbarState = .expanded + if let tab = tabManager[webView] { + tab.sampledTopPageColorNotifier = SampledTopPageColorNotifier(webView: webView) { + [weak self] color in + self?.updateStatusBarOverlayColor() + } + } + // check if web view is loading a different origin than the one currently loaded if let selectedTab = tabManager.selectedTab, - selectedTab.url?.origin != webView.url?.origin + selectedTab.url?.origin != webView.visibleURL?.origin { // new site has a different origin, hide wallet icon. @@ -90,11 +170,9 @@ extension BrowserViewController: WKNavigationDelegate { // If we are going to navigate to a new page, hide the reader mode button. Unless we // are going to a about:reader page. Then we keep it on screen: it will change status // (orange color) as soon as the page has loaded. - if let url = webView.url { - if !url.isInternalURL(for: .readermode) { - topToolbar.updateReaderModeState(ReaderModeState.unavailable) - hideReaderModeBar(animated: false) - } + if let url = webView.visibleURL, !url.isInternalURL(for: .readermode) { + topToolbar.updateReaderModeState(ReaderModeState.unavailable) + hideReaderModeBar(animated: false) } resetRedirectChain(webView) @@ -103,90 +181,58 @@ extension BrowserViewController: WKNavigationDelegate { appendUrlToRedirectChain(webView) } - fileprivate func resetRedirectChain(_ webView: WKWebView) { + fileprivate func resetRedirectChain(_ webView: CWVWebView) { if let tab = tab(for: webView) { tab.redirectChain = [] } } - fileprivate func appendUrlToRedirectChain(_ webView: WKWebView) { + fileprivate func appendUrlToRedirectChain(_ webView: CWVWebView) { // The redirect chain MUST be sorted by the order of redirects with the // first URL being the source URL. - if let tab = tab(for: webView), let url = webView.url { + if let tab = tab(for: webView), let url = webView.visibleURL { tab.redirectChain.append(url) } } - // Recognize an Apple Maps URL. This will trigger the native app. But only if a search query is present. - // Otherwise it could just be a visit to a regular page on maps.apple.com. - // Exchaging https/https scheme with maps in order to open URLS properly on Apple Maps - fileprivate func isAppleMapsURL(_ url: URL) -> (enabled: Bool, url: URL)? { - if url.scheme == "http" || url.scheme == "https" { - if url.host == "maps.apple.com" && url.query != nil { - guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return nil - } - urlComponents.scheme = "maps" - - if let url = urlComponents.url { - return (true, url) - } - return nil - } - } - return (false, url) - } - - // Recognize a iTunes Store URL. These all trigger the native apps. Note that appstore.com and phobos.apple.com - // used to be in this list. I have removed them because they now redirect to itunes.apple.com. If we special case - // them then iOS will actually first open Safari, which then redirects to the app store. This works but it will - // leave a 'Back to Safari' button in the status bar, which we do not want. - fileprivate func isStoreURL(_ url: URL) -> Bool { - let isStoreScheme = ["itms-apps", "itms-appss", "itmss"].contains(url.scheme) - if isStoreScheme { - return true + public func webView( + _ webView: CWVWebView, + decidePolicyFor navigationAction: CWVNavigationAction, + decisionHandler: @escaping (CWVNavigationActionPolicy) -> Void + ) { + Task { @MainActor in + let policy = await self.webView(webView, decidePolicyFor: navigationAction) + decisionHandler(policy) } - - let isHttpScheme = ["http", "https"].contains(url.scheme) - let isAppStoreHost = ["itunes.apple.com", "apps.apple.com", "appsto.re"].contains(url.host) - return isHttpScheme && isAppStoreHost - } - - // This is the place where we decide what to do with a new navigation action. There are a number of special schemes - // and http(s) urls that need to be handled in a different way. All the logic for that is inside this delegate - // method. - - fileprivate func isUpholdOAuthAuthorization(_ url: URL) -> Bool { - return url.scheme == "rewards" && url.host == "uphold" } - @MainActor - public func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - preferences: WKWebpagePreferences - ) async -> (WKNavigationActionPolicy, WKWebpagePreferences) { + @MainActor public func webView( + _ webView: CWVWebView, + decidePolicyFor navigationAction: CWVNavigationAction + ) async -> CWVNavigationActionPolicy { guard var requestURL = navigationAction.request.url else { - return (.cancel, preferences) + return .cancel } if InternalURL.isValid(url: requestURL) { - if navigationAction.navigationType != .backForward, navigationAction.isInternalUnprivileged, - navigationAction.sourceFrame != nil || navigationAction.targetFrame?.isMainFrame == false + if navigationAction.navigationType.contains(.forwardBack), + navigationAction.request.isInternalUnprivileged, + !navigationAction.navigationType.isMainFrame // FIXME: Test + // navigationAction.targetFrame?.isMainFrame == false || navigationAction.request.cachePolicy == .useProtocolCachePolicy { Logger.module.warning("Denying unprivileged request: \(navigationAction.request)") - return (.cancel, preferences) + return .cancel } - return (.allow, preferences) + return .allow } if requestURL.scheme == "about" { - return (.allow, preferences) + return .allow } if requestURL.isBookmarklet { - return (.cancel, preferences) + return .cancel } // Universal links do not work if the request originates from the app, manual handling is required. @@ -196,7 +242,7 @@ extension BrowserViewController: WKNavigationDelegate { switch universalLink { case .buyVPN: presentCorrespondingVPNViewController() - return (.cancel, preferences) + return .cancel } } @@ -210,7 +256,7 @@ extension BrowserViewController: WKNavigationDelegate { tab: tab, navigationAction: navigationAction ) - return (shouldOpen ? .allow : .cancel, preferences) + return shouldOpen ? .allow : .cancel } // Second special case are a set of URLs that look like regular http links, but should be handed over to iOS @@ -223,7 +269,7 @@ extension BrowserViewController: WKNavigationDelegate { tab: tab, navigationAction: navigationAction ) - return (shouldOpen ? .allow : .cancel, preferences) + return shouldOpen ? .allow : .cancel } if #available(iOS 17.4, *), !ProcessInfo.processInfo.isiOSAppOnVisionOS { @@ -234,12 +280,12 @@ extension BrowserViewController: WKNavigationDelegate { let adpURL = queryItems.first(where: { $0.name.caseInsensitiveCompare("alternativeDistributionPackage") == .orderedSame })?.value?.asURL, - navigationAction.sourceFrame.isMainFrame, - adpURL.baseDomain == navigationAction.sourceFrame.request.url?.baseDomain + navigationAction.navigationType.isMainFrame, + adpURL.baseDomain == webView.lastCommittedURL?.baseDomain { - return (.allow, preferences) + return .allow } - return (.cancel, preferences) + return .cancel } } @@ -249,7 +295,7 @@ extension BrowserViewController: WKNavigationDelegate { tab: tab, navigationAction: navigationAction ) - return (shouldOpen ? .allow : .cancel, preferences) + return shouldOpen ? .allow : .cancel } // Handles custom mailto URL schemes. @@ -259,12 +305,12 @@ extension BrowserViewController: WKNavigationDelegate { tab: tab, navigationAction: navigationAction ) - return (shouldOpen ? .allow : .cancel, preferences) + return shouldOpen ? .allow : .cancel } // handles Decentralized DNS if let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: requestURL), - navigationAction.targetFrame?.isMainFrame == true + navigationAction.navigationType.isMainFrame { topToolbar.locationView.loading = true let result = await decentralizedDNSHelper.lookup( @@ -272,12 +318,12 @@ extension BrowserViewController: WKNavigationDelegate { ) topToolbar.locationView.loading = tabManager.selectedTab?.loading ?? false guard !Task.isCancelled else { // user pressed stop, or typed new url - return (.cancel, preferences) + return .cancel } switch result { case .loadInterstitial(let service): showWeb3ServiceInterstitialPage(service: service, originalURL: requestURL) - return (.cancel, preferences) + return .cancel case .load(let resolvedURL): if resolvedURL.isIPFSScheme, let resolvedIPFSURL = braveCore.ipfsAPI.resolveGatewayUrl(for: resolvedURL) @@ -292,19 +338,17 @@ extension BrowserViewController: WKNavigationDelegate { } let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing - tab?.rewardsReportingState.isNewNavigation = - navigationAction.navigationType != WKNavigationType.backForward - && navigationAction.navigationType != WKNavigationType.reload + // FIXME: Test + tab?.rewardsReportingState.isNewNavigation = navigationAction.navigationType.isNewNavigation tab?.currentRequestURL = requestURL // Website redirection logic if requestURL.isWebPage(includeDataURIs: false), - navigationAction.targetFrame?.isMainFrame == true, + navigationAction.navigationType.isMainFrame, // FIXME: Test? let redirectURL = WebsiteRedirects.redirect(for: requestURL) { - tab?.loadRequest(URLRequest(url: redirectURL)) - return (.cancel, preferences) + return .cancel } let signpostID = ContentBlockerManager.signpost.makeSignpostID() @@ -324,7 +368,9 @@ extension BrowserViewController: WKNavigationDelegate { } // Handle the "forget me" feature on navigation - if let tab = tab, navigationAction.targetFrame?.isMainFrame == true { + if let tab = tab, //navigationAction.targetFrame?.isMainFrame == true { + navigationAction.navigationType.isMainFrame + { // FIXME: Test // Cancel any forget data requests tabManager.cancelForgetData(for: mainDocumentURL, in: tab) @@ -352,8 +398,6 @@ extension BrowserViewController: WKNavigationDelegate { ) { tab.isInternalRedirect = true - tab.loadRequest(modifiedRequest) - if let url = modifiedRequest.url { Self.log.debug( "Redirected to `\(url.absoluteString, privacy: .private)`" @@ -365,13 +409,17 @@ extension BrowserViewController: WKNavigationDelegate { state, "Redirected navigation" ) - return (.cancel, preferences) + DispatchQueue.main.async { + tab.loadRequest(modifiedRequest) + } + return .cancel } else { tab?.isInternalRedirect = false } // Set some additional user scripts - if navigationAction.targetFrame?.isMainFrame == true { + // if navigationAction.targetFrame?.isMainFrame == true { + if navigationAction.navigationType.isMainFrame { // FIXME: Test tab?.setScripts(scripts: [ // Add de-amp script // The user script manager will take care to not reload scripts if this value doesn't change @@ -395,29 +443,28 @@ extension BrowserViewController: WKNavigationDelegate { } // Check if custom user scripts must be added to or removed from the web view. - if let targetFrame = navigationAction.targetFrame { - tab?.currentPageData?.addSubframeURL( - forRequestURL: requestURL, - isForMainFrame: targetFrame.isMainFrame - ) - let scriptTypes = - await tab?.currentPageData?.makeUserScriptTypes( - domain: domainForMainFrame, - isDeAmpEnabled: braveCore.deAmpPrefs.isDeAmpEnabled - ) ?? [] - tab?.setCustomUserScript(scripts: scriptTypes) - } + tab?.currentPageData?.addSubframeURL( + forRequestURL: requestURL, + isForMainFrame: navigationAction.navigationType.isMainFrame + ) + let scriptTypes = + await tab?.currentPageData?.makeUserScriptTypes( + domain: domainForMainFrame, + isDeAmpEnabled: braveCore.deAmpPrefs.isDeAmpEnabled + ) ?? [] + tab?.setCustomUserScript(scripts: scriptTypes) } // Brave Search logic. - if navigationAction.targetFrame?.isMainFrame == true, - BraveSearchManager.isValidURL(requestURL) + if navigationAction.navigationType.isMainFrame, + BraveSearchManager.isValidURL(requestURL), + let webView = tabManager[webView]?.webView { if let braveSearchResultAdManager = tab?.braveSearchResultAdManager, braveSearchResultAdManager.isSearchResultAdClickedURL(requestURL), - navigationAction.navigationType == .linkActivated + navigationAction.navigationType == .link { braveSearchResultAdManager.maybeTriggerSearchResultAdClickedEvent(requestURL) tab?.braveSearchResultAdManager = nil @@ -440,7 +487,7 @@ extension BrowserViewController: WKNavigationDelegate { state, "Redirected to search" ) - return (.cancel, preferences) + return .cancel } let domain = Domain.getOrCreate(forUrl: requestURL, persistent: !isPrivateBrowsing) @@ -456,7 +503,10 @@ extension BrowserViewController: WKNavigationDelegate { // We fetch cookies to determine if backup search was enabled on the website. let profile = self.profile - let cookies = await webView.configuration.websiteDataStore.httpCookieStore.allCookies() + // FIXME: Find a way to get this out of CWVWebView + let cookies = + await webView.wkConfiguration.websiteDataStore.httpCookieStore.allCookies() + ?? [] tab?.braveSearchManager = BraveSearchManager( profile: profile, url: requestURL, @@ -488,10 +538,9 @@ extension BrowserViewController: WKNavigationDelegate { // This is the normal case, opening a http or https url, which we handle by loading them in this WKWebView. We // always allow this. Additionally, data URIs are also handled just like normal web pages. - if ["http", "https", "data", "blob", "file"].contains(requestURL.scheme) { - if navigationAction.targetFrame?.isMainFrame == true { - tab?.updateUserAgent(webView, newURL: requestURL) - + if ["http", "https", "data", "blob", "file", "brave", "chrome"].contains(requestURL.scheme) { + // FIXME: Test + if navigationAction.navigationType.isMainFrame { if let etldP1 = requestURL.baseDomain, tab?.proceedAnywaysDomainList.contains(etldP1) == false { @@ -512,7 +561,7 @@ extension BrowserViewController: WKNavigationDelegate { state, "Blocked navigation" ) - return (.cancel, preferences) + return .cancel } } } @@ -531,8 +580,9 @@ extension BrowserViewController: WKNavigationDelegate { if let mainDocumentURL = navigationAction.request.mainDocumentURL, mainDocumentURL.schemelessAbsoluteString == requestURL.schemelessAbsoluteString, !(InternalURL(requestURL)?.isSessionRestore ?? false), - navigationAction.sourceFrame.isMainFrame - || navigationAction.targetFrame?.isMainFrame == true + navigationAction.navigationType.isMainFrame // FIXME: Test + // navigationAction.sourceFrame.isMainFrame + // || navigationAction.targetFrame?.isMainFrame == true { // Identify specific block lists that need to be applied to the requesting domain let domainForShields = Domain.getOrCreate( @@ -546,53 +596,32 @@ extension BrowserViewController: WKNavigationDelegate { } let documentTargetURL: URL? = - navigationAction.request.mainDocumentURL ?? navigationAction.targetFrame?.request - .mainDocumentURL ?? requestURL // Should be the same as the sourceFrame URL - if let documentTargetURL = documentTargetURL { - let domainForShields = Domain.getOrCreate( - forUrl: documentTargetURL, - persistent: !isPrivateBrowsing - ) - let isScriptsEnabled = !domainForShields.isShieldExpected( - .noScript, - considerAllShieldsOption: true - ) - - // Due to a bug in iOS WKWebpagePreferences.allowsContentJavaScript does NOT work! - // https://github.com/brave/brave-ios/issues/8585 - // - // However, the deprecated API WKWebViewConfiguration.preferences.javaScriptEnabled DOES work! - // Even though `configuration` is @NSCopying, somehow this actually updates the preferences LIVE!! - // This follows the same behaviour as Safari - // - // - Brandon T. - // - preferences.allowsContentJavaScript = isScriptsEnabled - webView.configuration.preferences.javaScriptEnabled = isScriptsEnabled - } - + // FIXME: Test + navigationAction.request.mainDocumentURL + ?? requestURL // Should be the same as the sourceFrame URL // Cookie Blocking code below if let tab = tab { tab.setScript(script: .cookieBlocking, enabled: Preferences.Privacy.blockAllCookies.value) } // Reset the block alert bool on new host. - if let newHost: String = requestURL.host, let oldHost: String = webView.url?.host, + if let newHost: String = requestURL.host, let oldHost: String = webView.visibleURL?.host, newHost != oldHost { self.tabManager.selectedTab?.alertShownCount = 0 self.tabManager.selectedTab?.blockAllAlerts = false } - self.shouldDownloadNavigationResponse = navigationAction.shouldPerformDownload ContentBlockerManager.signpost.endInterval("decidePolicyFor", state) - return (.allow, preferences) + return .allow } // Standard schemes are handled in previous if-case. // This check handles custom app schemes to open external apps. // Our own 'brave' scheme does not require the switch-app prompt. - if requestURL.scheme?.contains("brave") == false { + if requestURL.scheme?.contains("brave") == false + && requestURL.scheme?.contains("chrome") == false + { // Do not allow opening external URLs from child tabs let shouldOpen = await handleExternalURL( requestURL, @@ -605,7 +634,8 @@ extension BrowserViewController: WKNavigationDelegate { // Do not show error message for JS navigated links or redirect // as it's not the result of a user action. - if !shouldOpen, navigationAction.navigationType == .linkActivated && !isSyntheticClick { + // FIXME: Test + if !shouldOpen, navigationAction.navigationType == .link && !isSyntheticClick { if self.presentedViewController == nil && self.presentingViewController == nil && tab?.isExternalAppAlertPresented == false && tab?.isExternalAppAlertSuppressed == false { @@ -620,41 +650,33 @@ extension BrowserViewController: WKNavigationDelegate { ) alert.addAction(UIAlertAction(title: Strings.OKString, style: .default, handler: nil)) self.present(alert, animated: true) { - continuation.resume(returning: (shouldOpen ? .allow : .cancel, preferences)) + continuation.resume(returning: shouldOpen ? .allow : .cancel) } } } } - return (shouldOpen ? .allow : .cancel, preferences) + return shouldOpen ? .allow : .cancel } - return (.cancel, preferences) + return .cancel } - /// Handles a link by opening it in an SFSafariViewController and presenting it on the BVC. - /// - /// This is unfortunately neccessary to handle certain downloads natively such as ics/calendar invites and - /// mobileconfiguration files. - /// - /// The user unfortunately has to dismiss it manually after they have handled the file. - /// Chrome iOS does the same - private func handleLinkWithSafariViewController(_ url: URL, tab: Tab) { - let vc = SFSafariViewController(url: url, configuration: .init()) - vc.modalPresentationStyle = .formSheet - self.present(vc, animated: true) - - // If the website opened this URL in a separate tab, remove the empty tab - if tab.url == nil || tab.url?.absoluteString == "about:blank" { - tabManager.removeTab(tab) + public func webView( + _ webView: CWVWebView, + decidePolicyFor navigationResponse: CWVNavigationResponse, + decisionHandler: @escaping (CWVNavigationResponsePolicy) -> Void + ) { + Task { @MainActor in + let policy = await self.webView(webView, decidePolicyFor: navigationResponse) + decisionHandler(policy) } } - @MainActor - public func webView( - _ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse - ) async -> WKNavigationResponsePolicy { + @MainActor public func webView( + _ webView: CWVWebView, + decidePolicyFor navigationResponse: CWVNavigationResponse + ) async -> CWVNavigationResponsePolicy { let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing let response = navigationResponse.response let responseURL = response.url @@ -698,11 +720,6 @@ extension BrowserViewController: WKNavigationDelegate { request = pendingRequests.removeValue(forKey: url.absoluteString) } - // We can only show this content in the web view if this web view is not pending - // download via the context menu. - let canShowInWebView = navigationResponse.canShowMIMEType && (webView != pendingDownloadWebView) - let forceDownload = webView == pendingDownloadWebView - let mimeTypesThatRequireSFSafariViewControllerHandling: [UTType] = [ .textCalendar, .mobileConfiguration, @@ -719,27 +736,6 @@ extension BrowserViewController: WKNavigationDelegate { return .cancel } - // If the response has the attachment content-disposition, download it - let mimeType = response.mimeType ?? MIMEType.octetStream - let isAttachment = - (response as? HTTPURLResponse)?.value( - forHTTPHeaderField: "Content-Disposition" - )?.starts(with: "attachment") ?? (mimeType == MIMEType.octetStream) - - if isAttachment { - return .download - } - - if response.mimeType == MIMEType.passbook { - return .download - } - - // Check if this response should be handed off to Passbook. - if shouldDownloadNavigationResponse { - shouldDownloadNavigationResponse = false - return .download - } - // If the content type is not HTML, create a temporary document so it can be downloaded and // shared to external applications later. Otherwise, clear the old temporary document. if let tab = tab, navigationResponse.isForMainFrame { @@ -756,149 +752,276 @@ extension BrowserViewController: WKNavigationDelegate { tab.mimeType = response.mimeType } - if canShowInWebView { - return .allow + // If none of our helpers are responsible for handling this response, + // just let the webview handle it as normal. + return .allow + } + + public func webViewDidFinishNavigation(_ webView: CWVWebView) { + guard let tab = tabManager[webView], let webView = tab.webView else { return } + // Inject app's IAP receipt for Brave SKUs if necessary + if !tab.isPrivate { + Task { @MainActor in + await BraveSkusAccountLink.injectLocalStorage(webView: webView) + } } - // Check if this response should be downloaded. - let cookieStore = webView.configuration.websiteDataStore.httpCookieStore - if let downloadHelper = DownloadHelper( - request: request, - response: response, - cookieStore: cookieStore, - canShowInWebView: canShowInWebView, - forceDownload: forceDownload - ) { - // Clear the pending download web view so that subsequent navigations from the same - // web view don't invoke another download. - pendingDownloadWebView = nil + tab.sampledTopPageColorNotifier = nil - let downloadAlertAction: (HTTPDownload) -> Void = { [weak self] download in - self?.downloadQueue.enqueue(download) + // Second attempt to inject results to the BraveSearch. + // This will be called if we got fallback results faster than + // the page navigation. + if let braveSearchManager = tab.braveSearchManager { + // Fallback results are ready before navigation finished, + // they must be injected here. + if !braveSearchManager.fallbackQueryResultsPending { + tab.injectResults() } + } else { + // If not applicable, null results must be injected regardless. + // The website waits on us until this is called with either results or null. + tab.injectResults() + } - // Open our helper and cancel this response from the webview. - if tab === tabManager.selectedTab, - let downloadAlert = downloadHelper.downloadAlert(from: view, okAction: downloadAlertAction) - { - present(downloadAlert, animated: true, completion: nil) - } + navigateInTab(tab: tab) + rewards.reportTabUpdated( + tab: tab, + isSelected: tabManager.selectedTab == tab, + isPrivate: privateBrowsingManager.isPrivateBrowsing + ) + tab.reportPageLoad(to: rewards, redirectChain: tab.redirectChain) + // Reset `rewardsReportingState` tab property so that listeners + // can be notified of tab changes when a new navigation happens. + tab.rewardsReportingState = RewardsTabChangeReportingState() - return .cancel + Task { + await tab.updateEthereumProperties() + await tab.updateSolanaProperties() } - // If none of our helpers are responsible for handling this response, - // just let the webview handle it as normal. - return .download - } + if webView.visibleURL?.isLocal == false { + // Set rewards inter site url as new page load url. + tab.rewardsXHRLoadURL = webView.visibleURL + } - public func webView( - _ webView: WKWebView, - navigationAction: WKNavigationAction, - didBecome download: WKDownload - ) { - Logger.module.error( - "ERROR: Should Never download NavigationAction since we never return .download from decidePolicyForAction." - ) - } + if tab.walletEthProvider != nil { + tab.emitEthereumEvent(.connect) + } - public func webView( - _ webView: WKWebView, - navigationResponse: WKNavigationResponse, - didBecome download: WKDownload - ) { - download.delegate = self + if let lastCommittedURL = webView.lastCommittedURL { + maybeRecordBraveSearchDailyUsage(url: lastCommittedURL) + } + + // Added this method to determine long press menu actions better + // Since these actions are depending on tabmanager opened WebsiteCount + updateToolbarUsingTabManager(tabManager) + + recordFinishedPageLoadP3A() } - @MainActor - public func webView( - _ webView: WKWebView, - respondTo challenge: URLAuthenticationChallenge - ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - - // If this is a certificate challenge, see if the certificate has previously been - // accepted by the user. - let host = challenge.protectionSpace.host - let origin = "\(host):\(challenge.protectionSpace.port)" - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust - { + public func webView(_ webView: CWVWebView, didFailNavigationWithError error: any Error) { + guard let tab = tab(for: webView), let webView = tab.webView else { return } + + // Ignore the "Frame load interrupted" error that is triggered when we cancel a request + // to open an external application and hand it over to UIApplication.openURL(). The result + // will be that we switch to the external app, for example the app store, while keeping the + // original web page in the tab instead of replacing it with an error page. + var error = error as NSError + if error.domain == "WebKitErrorDomain" && error.code == 102 { + return + } - let cert = await Task.detached { - return (SecTrustCopyCertificateChain(trust) as? [SecCertificate])?.first - }.value + if checkIfWebContentProcessHasCrashed(webView, error: error) { + return + } - if let cert = cert, profile.certStore.containsCertificate(cert, forOrigin: origin) { - return (.useCredential, URLCredential(trust: trust)) + if error.code == Int(CFNetworkErrors.cfurlErrorCancelled.rawValue) { + if tab === tabManager.selectedTab { + updateToolbarCurrentURL(tab.url?.displayURL) + updateWebViewPageZoom(tab: tab) } + return } + } - // Certificate Pinning - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - if let serverTrust = challenge.protectionSpace.serverTrust { - let host = challenge.protectionSpace.host - let port = challenge.protectionSpace.port - - let result = await BraveCertificateUtils.verifyTrust(serverTrust, host: host, port: port) - let certificateChain = await Task<[SecCertificate], Never>.detached { - return SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] ?? [] - }.value + public func webView(_ webView: CWVWebView, didRequestDownloadWith task: CWVDownloadTask) { + guard let tab = tabManager[webView] else { return } - // Cert is valid and should be pinned - if result == 0 { - return (.useCredential, URLCredential(trust: serverTrust)) - } + let mimeTypesThatRequireSFSafariViewControllerHandling: [UTType] = [ + .textCalendar, + .mobileConfiguration, + ] - // Cert is valid and should not be pinned - // Let the system handle it and we'll show an error if the system cannot validate it - if result == Int32.min { - // Cert is POTENTIALLY invalid and cannot be pinned + if case let url = task.originalURL, url.isWebPage(includeDataURIs: false), + tab === tabManager.selectedTab, + let mimeType = UTType(mimeType: task.mimeType), + mimeTypesThatRequireSFSafariViewControllerHandling.contains(mimeType) + { + handleLinkWithSafariViewController(task.originalURL, tab: tab) + task.cancel() + return + } - // Let WebKit handle the request and validate the cert - // This is the same as calling `BraveCertificateUtils.evaluateTrust` but with more error info provided by WebKit - return (.performDefaultHandling, nil) - } + task.delegate = self - // Cert is invalid and cannot be pinned - Logger.module.error("CERTIFICATE_INVALID") - let errorCode = CFNetworkErrors.braveCertificatePinningFailed.rawValue + let downloadHelper = DownloadHelper(task: task) - let underlyingError = NSError( - domain: kCFErrorDomainCFNetwork as String, - code: Int(errorCode), - userInfo: ["_kCFStreamErrorCodeKey": Int(errorCode)] + let downloadAlertAction: () -> Void = { [weak self] in + guard let self else { return } + let downloadPath = FileManager.default.temporaryDirectory.appending( + path: task.suggestedFileName + ) + task.startDownloadToLocalFile(atPath: downloadPath.path()) + + // If no other download toast is shown, create a new download toast and show it. + // FIXME: We need new UI here to handle multiple downloads at once + guard let downloadToast = self.downloadToast else { + let downloadToast = DownloadToast( + download: task, + completion: { buttonPressed in + // When this toast is dismissed, be sure to clear this so that any + // subsequent downloads cause a new toast to be created. + self.downloadToast = nil + + // Handle download cancellation + if buttonPressed { + task.cancel() + + let downloadCancelledToast = ButtonToast( + labelText: Strings.downloadCancelledToastLabelText, + backgroundColor: UIColor.braveLabel, + textAlignment: .center + ) + + self.show(toast: downloadCancelledToast) + } + } ) - let error = NSError( - domain: kCFErrorDomainCFNetwork as String, - code: Int(errorCode), - userInfo: [ - NSURLErrorFailingURLErrorKey: webView.url as Any, - "NSErrorPeerCertificateChainKey": certificateChain, - NSUnderlyingErrorKey: underlyingError, - ] - ) + self.show(toast: downloadToast, duration: nil) + return + } + + // Otherwise, just add this download to the existing download toast. + downloadToast.addDownload(task) + } - // Handle the error later in `didFailProvisionalNavigation` - self.tab(for: webView)?.sslPinningError = error + // Open our helper and cancel this response from the webview. + if tab === tabManager.selectedTab { + // There's already an existing download happening + // Prompt the user to cancel the existing download in order to start a new one + if let downloadToast = downloadToast, !downloadToast.downloads.isEmpty { + let cancelExistingDownloadAction = { [weak self] in + guard let self = self, + let downloadAlert = downloadHelper.downloadAlert( + from: self.view, + okAction: downloadAlertAction + ) + else { return } + + // Cancel the download + downloadToast.downloads.forEach({ $0.cancel() }) + + // Dismiss the existing download toast, and display the new download alert + downloadToast.dismiss(false) { [weak self] in + self?.present(downloadAlert, animated: true, completion: nil) + } + } + + if let alert = downloadHelper.cancelDownloadAlert( + from: view, + okAction: cancelExistingDownloadAction + ) { + present(alert, animated: true, completion: nil) + } + } else { + if let downloadAlert = downloadHelper.downloadAlert( + from: view, + okAction: downloadAlertAction + ) { + present(downloadAlert, animated: true, completion: nil) + } + } + } + } - return (.cancelAuthenticationChallenge, nil) + public func webView( + _ webView: CWVWebView, + shouldBlockUniversalLinksFor request: URLRequest + ) -> Bool { + let isPrivateBrowsing = tabManager.privateBrowsingManager.isPrivateBrowsing == true + func isYouTubeLoad() -> Bool { + guard let domain = request.mainDocumentURL?.baseDomain else { + return false } + let domainsWithUniversalLinks: Set = ["youtube.com", "youtu.be"] + return domainsWithUniversalLinks.contains(domain) + } + if isPrivateBrowsing || !Preferences.General.followUniversalLinks.value + || (Preferences.General.keepYouTubeInBrave.value && isYouTubeLoad()) + { + // Stop Brave from opening universal links by using the private enum value + // `_WKNavigationActionPolicyAllowWithoutTryingAppLink` which is defined here: + // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/API/Cocoa/WKNavigationDelegatePrivate.h#L62 + return true } + return false + } - // URLAuthenticationChallenge isn't Sendable atm - let protectionSpace = challenge.protectionSpace - let credential = challenge.proposedCredential - let previousFailureCount = challenge.previousFailureCount + public func webView(_ webView: CWVWebView, shouldBlockJavaScriptFor request: URLRequest) -> Bool { + guard let url = request.mainDocumentURL else { return false } + let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing + let domainForShields = Domain.getOrCreate( + forUrl: url, + persistent: !isPrivateBrowsing + ) + let isScriptsEnabled = !domainForShields.isShieldExpected( + .noScript, + considerAllShieldsOption: true + ) + // Due to a bug in iOS WKWebpagePreferences.allowsContentJavaScript does NOT work! + // https://github.com/brave/brave-ios/issues/8585 + // + // However, the deprecated API WKWebViewConfiguration.preferences.javaScriptEnabled DOES work! + // Even though `configuration` is @NSCopying, somehow this actually updates the preferences LIVE!! + // This follows the same behaviour as Safari + // + // - Brandon T. + // FIXME: Test if this is still needed. + webView.wkConfiguration.preferences.javaScriptEnabled = isScriptsEnabled + return !isScriptsEnabled + } - guard - protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic - || protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest - || protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM, - let tab = tab(for: webView) - else { - return (.performDefaultHandling, nil) + public func webView( + _ webView: CWVWebView, + didRequestHTTPAuthFor protectionSpace: URLProtectionSpace, + proposedCredential: URLCredential, + completionHandler handler: @escaping (String?, String?) -> Void + ) { + Task { @MainActor in + do { + guard let tab = tabManager[webView] else { return } + let credential = try await credentialForHTTPAuthRequest( + webView: webView, + tab: tab, + protectionSpace: protectionSpace, + proposedCredential: proposedCredential + ) + handler(credential.user, credential.password) + } catch { + handler(nil, nil) + } } + } + + @MainActor private func credentialForHTTPAuthRequest( + webView: CWVWebView, + tab: Tab, + protectionSpace: URLProtectionSpace, + proposedCredential: URLCredential + ) async throws -> URLCredential { + let host = protectionSpace.host + let origin = "\(host):\(protectionSpace.port)" // The challenge may come from a background tab, so ensure it's the one visible. tabManager.selectTab(tab) @@ -913,221 +1036,92 @@ extension BrowserViewController: WKNavigationDelegate { webView.isHidden = true observeValue( - forKeyPath: KVOConstants.url.keyPath, + forKeyPath: KVOConstants.visibleURL.keyPath, of: webView, - change: [.newKey: webView.url as Any, .kindKey: 1], + change: [.newKey: webView.visibleURL as Any, .kindKey: 1], context: nil ) } - do { - let credentials = try await Authenticator.handleAuthRequest( - self, - credential: credential, - protectionSpace: protectionSpace, - previousFailureCount: previousFailureCount - ) - - if BasicAuthCredentialsManager.validDomains.contains(host) { - BasicAuthCredentialsManager.setCredential( - origin: origin, - credential: credentials.credentials - ) - } - - return (.useCredential, credentials.credentials) - } catch { - return (.rejectProtectionSpace, nil) - } - } - - public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { - guard let tab = tab(for: webView) else { return } - - // Reset the stored http request now that load has committed. - tab.upgradedHTTPSRequest = nil - - // Set the committed url which will also set tab.url - tab.committedURL = webView.url - - // Server Trust and URL is also updated in didCommit - // However, WebKit does NOT trigger the `serverTrust` observer when the URL changes, but the trust has not. - // WebKit also does NOT trigger the `serverTrust` observer when the page is actually insecure (non-https). - // So manually trigger it with the current trust. - - observeValue( - forKeyPath: KVOConstants.serverTrust.keyPath, - of: webView, - change: [.newKey: webView.serverTrust as Any, .kindKey: 1], - context: nil + let credentials = try await Authenticator.handleAuthRequest( + self, + credential: proposedCredential, + protectionSpace: protectionSpace ) - // Need to evaluate Night mode script injection after url is set inside the Tab - tab.nightMode = Preferences.General.nightModeEnabled.value - tab.clearSolanaConnectedAccounts() - - // Providers need re-initialized when changing origin to align with desktop in - // `BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame` - // https://github.com/brave/brave-core/blob/1.52.x/browser/brave_content_browser_client.cc#L608 - if let provider = braveCore.braveWalletAPI.ethereumProvider( - with: tab, - isPrivateBrowsing: tab.isPrivate - ) { - // The Ethereum provider will fetch allowed accounts from it's delegate (the tab) - // on initialization. Fetching allowed accounts requires the origin; so we need to - // initialize after `commitedURL` / `url` are updated above - tab.walletEthProvider = provider - tab.walletEthProvider?.initialize(eventsListener: tab) - } - if let provider = braveCore.braveWalletAPI.solanaProvider( - with: tab, - isPrivateBrowsing: tab.isPrivate - ) { - tab.walletSolProvider = provider - tab.walletSolProvider?.initialize(eventsListener: tab) - } - - rewards.reportTabNavigation(tabId: tab.rewardsId) - - // The toolbar and url bar changes can not be - // on different tab than selected. Or the webview - // previews and etc will effect the status - guard tabManager.selectedTab === tab else { - return + if BasicAuthCredentialsManager.validDomains.contains(host) { + BasicAuthCredentialsManager.setCredential( + origin: origin, + credential: credentials.credentials + ) } - updateUIForReaderHomeStateForTab(tab) - updateBackForwardActionStatus(for: webView) + return credentials.credentials } +} - public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if let tab = tabManager[webView] { - // Inject app's IAP receipt for Brave SKUs if necessary - if !tab.isPrivate { - Task { @MainActor in - await BraveSkusAccountLink.injectLocalStorage(webView: webView) - } - } +// MARK: WKNavigationDelegate +extension BrowserViewController { + private static let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "navigation") - // Second attempt to inject results to the BraveSearch. - // This will be called if we got fallback results faster than - // the page navigation. - if let braveSearchManager = tab.braveSearchManager { - // Fallback results are ready before navigation finished, - // they must be injected here. - if !braveSearchManager.fallbackQueryResultsPending { - tab.injectResults() + // Recognize an Apple Maps URL. This will trigger the native app. But only if a search query is present. + // Otherwise it could just be a visit to a regular page on maps.apple.com. + // Exchaging https/https scheme with maps in order to open URLS properly on Apple Maps + fileprivate func isAppleMapsURL(_ url: URL) -> (enabled: Bool, url: URL)? { + if url.scheme == "http" || url.scheme == "https" { + if url.host == "maps.apple.com" && url.query != nil { + guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return nil } - } else { - // If not applicable, null results must be injected regardless. - // The website waits on us until this is called with either results or null. - tab.injectResults() - } - - navigateInTab(tab: tab, to: navigation) - rewards.reportTabUpdated( - tab: tab, - isSelected: tabManager.selectedTab == tab, - isPrivate: privateBrowsingManager.isPrivateBrowsing - ) - tab.reportPageLoad(to: rewards, redirectChain: tab.redirectChain) - // Reset `rewardsReportingState` tab property so that listeners - // can be notified of tab changes when a new navigation happens. - tab.rewardsReportingState = RewardsTabChangeReportingState() - - Task { - await tab.updateEthereumProperties() - await tab.updateSolanaProperties() - } - - if webView.url?.isLocal == false { - // Set rewards inter site url as new page load url. - tab.rewardsXHRLoadURL = webView.url - } - - if tab.walletEthProvider != nil { - tab.emitEthereumEvent(.connect) - } + urlComponents.scheme = "maps" - if let lastCommittedURL = tab.committedURL { - maybeRecordBraveSearchDailyUsage(url: lastCommittedURL) + if let url = urlComponents.url { + return (true, url) + } + return nil } } - - // Added this method to determine long press menu actions better - // Since these actions are depending on tabmanager opened WebsiteCount - updateToolbarUsingTabManager(tabManager) - - recordFinishedPageLoadP3A() - } - - public func webView( - _ webView: WKWebView, - didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation! - ) { - appendUrlToRedirectChain(webView) + return (false, url) } - /// Invoked when an error occurs while starting to load data for the main frame. - public func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation!, - withError error: Error - ) { - guard let tab = tab(for: webView) else { return } - - // Handle invalid upgrade to https - if let responseURL = webView.url, - let response = handleInvalidHTTPSUpgrade( - tab: tab, - responseURL: responseURL - ) - { - tab.loadRequest(response) - return - } - - // Ignore the "Frame load interrupted" error that is triggered when we cancel a request - // to open an external application and hand it over to UIApplication.openURL(). The result - // will be that we switch to the external app, for example the app store, while keeping the - // original web page in the tab instead of replacing it with an error page. - var error = error as NSError - if error.domain == "WebKitErrorDomain" && error.code == 102 { - return - } - - if checkIfWebContentProcessHasCrashed(webView, error: error) { - return + // Recognize a iTunes Store URL. These all trigger the native apps. Note that appstore.com and phobos.apple.com + // used to be in this list. I have removed them because they now redirect to itunes.apple.com. If we special case + // them then iOS will actually first open Safari, which then redirects to the app store. This works but it will + // leave a 'Back to Safari' button in the status bar, which we do not want. + fileprivate func isStoreURL(_ url: URL) -> Bool { + let isStoreScheme = ["itms-apps", "itms-appss", "itmss"].contains(url.scheme) + if isStoreScheme { + return true } - if let sslPinningError = tab.sslPinningError { - error = sslPinningError as NSError - } + let isHttpScheme = ["http", "https"].contains(url.scheme) + let isAppStoreHost = ["itunes.apple.com", "apps.apple.com", "appsto.re"].contains(url.host) + return isHttpScheme && isAppStoreHost + } - if error.code == Int(CFNetworkErrors.cfurlErrorCancelled.rawValue) { - if tab === tabManager.selectedTab { - updateToolbarCurrentURL(tab.url?.displayURL) - updateWebViewPageZoom(tab: tab) - } - return - } + // This is the place where we decide what to do with a new navigation action. There are a number of special schemes + // and http(s) urls that need to be handled in a different way. All the logic for that is inside this delegate + // method. - if let url = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL { - ErrorPageHelper(certStore: profile.certStore).loadPage(error, forUrl: url, inWebView: webView) + fileprivate func isUpholdOAuthAuthorization(_ url: URL) -> Bool { + return url.scheme == "rewards" && url.host == "uphold" + } - // Submitting same erroneous URL using toolbar will cause progress bar get stuck - // Reseting the progress bar in case there is an error is necessary - if tab == self.tabManager.selectedTab { - self.topToolbar.hideProgressBar() - } + /// Handles a link by opening it in an SFSafariViewController and presenting it on the BVC. + /// + /// This is unfortunately neccessary to handle certain downloads natively such as ics/calendar invites and + /// mobileconfiguration files. + /// + /// The user unfortunately has to dismiss it manually after they have handled the file. + /// Chrome iOS does the same + private func handleLinkWithSafariViewController(_ url: URL, tab: Tab) { + let vc = SFSafariViewController(url: url, configuration: .init()) + vc.modalPresentationStyle = .formSheet + self.present(vc, animated: true) - // If the local web server isn't working for some reason (Brave cellular data is - // disabled in settings, for example), we'll fail to load the session restore URL. - // We rely on loading that page to get the restore callback to reset the restoring - // flag, so if we fail to load that page, reset it here. - if InternalURL(url)?.aboutComponent == "sessionrestore" { - tab.restoring = false - } + // If the website opened this URL in a separate tab, remove the empty tab + if tab.url == nil || tab.url?.absoluteString == "about:blank" { + tabManager.removeTab(tab) } } } @@ -1153,18 +1147,23 @@ extension BrowserViewController { ) } - private func tab(for webView: WKWebView) -> Tab? { - tabManager[webView] ?? (webView as? TabWebView)?.tab + private func tab(for webView: BraveWebView) -> Tab? { + tabManager[webView] + } + + private func tab(for webView: CWVWebView) -> Tab? { + tabManager[webView] } private func handleExternalURL( _ url: URL, tab: Tab?, - navigationAction: WKNavigationAction + navigationAction: CWVNavigationAction ) async -> Bool { // Do not open external links for child tabs automatically // The user must tap on the link to open it. - if tab?.parent != nil && navigationAction.navigationType != .linkActivated { + // FIXME: Test + if tab?.parent != nil && navigationAction.navigationType != .link { return false } @@ -1294,20 +1293,18 @@ extension BrowserViewController { // MARK: WKUIDelegate -extension BrowserViewController: WKUIDelegate { +extension BrowserViewController: CWVUIDelegate { public func webView( - _ webView: WKWebView, - createWebViewWith configuration: WKWebViewConfiguration, - for navigationAction: WKNavigationAction, - windowFeatures: WKWindowFeatures - ) -> WKWebView? { + _ webView: CWVWebView, + createWebViewWith configuration: CWVWebViewConfiguration, + for action: CWVNavigationAction + ) -> CWVWebView? { guard let parentTab = tabManager[webView] else { return nil } - - guard !navigationAction.isInternalUnprivileged, - let navigationURL = navigationAction.request.url, + guard !action.request.isInternalUnprivileged, + let navigationURL = action.request.url, navigationURL.shouldRequestBeOpenedAsPopup() else { - print("Denying popup from request: \(navigationAction.request)") + print("Denying popup from request: \(action.request)") return nil } @@ -1319,7 +1316,6 @@ extension BrowserViewController: WKUIDelegate { // IMPORTANT!!: WebKit will perform the `URLRequest` automatically!! Attempting to do // the request here manually leads to incorrect results!! let newTab = tabManager.addPopupForParentTab(parentTab, configuration: configuration) - newTab.url = URL(string: "about:blank") toolbarVisibilityViewModel.toolbarState = .expanded @@ -1328,7 +1324,7 @@ extension BrowserViewController: WKUIDelegate { // restore it as if it was a dead tab. var observation: NSKeyValueObservation? observation = newTab.webView?.observe( - \.url, + \.visibleURL, changeHandler: { [weak self] webView, _ in _ = observation // Silence write but not read warning observation = nil @@ -1340,18 +1336,16 @@ extension BrowserViewController: WKUIDelegate { return newTab.webView } - public func webViewDidClose(_ webView: WKWebView) { + public func webViewDidClose(_ webView: CWVWebView) { guard let tab = tabManager[webView] else { return } tabManager.addTabToRecentlyClosed(tab) tabManager.removeTab(tab) } public func webView( - _ webView: WKWebView, - requestMediaCapturePermissionFor origin: WKSecurityOrigin, - initiatedByFrame frame: WKFrameInfo, - type: WKMediaCaptureType, - decisionHandler: @escaping (WKPermissionDecision) -> Void + _ webView: CWVWebView, + requestMediaCapturePermissionFor type: CWVMediaCaptureType, + decisionHandler: @escaping (CWVPermissionDecision) -> Void ) { let presentAlert = { [weak self] in guard let self = self else { return } @@ -1368,7 +1362,9 @@ extension BrowserViewController: WKUIDelegate { return Strings.requestCaptureDevicePermissionPrompt } }() - let title = String.localizedStringWithFormat(titleFormat, origin.host) + // FIXME: Test + guard let host = webView.visibleURL?.host else { return } + let title = String.localizedStringWithFormat(titleFormat, host) let alertController = BrowserAlertController( title: title, message: nil, @@ -1395,13 +1391,6 @@ extension BrowserViewController: WKUIDelegate { alertController.dismissedWithoutAction = { decisionHandler(.prompt) } - if webView.fullscreenState == .inFullscreen || webView.fullscreenState == .enteringFullscreen - { - webView.closeAllMediaPresentations { - self.present(alertController, animated: true) - } - return - } self.present(alertController, animated: true) } @@ -1414,166 +1403,18 @@ extension BrowserViewController: WKUIDelegate { } } - fileprivate func shouldDisplayJSAlertForWebView(_ webView: WKWebView) -> Bool { - // Only display a JS Alert if we are selected and there isn't anything being shown - return ((tabManager.selectedTab == nil ? false : tabManager.selectedTab!.webView == webView)) - && (self.presentedViewController == nil) - } - - public func webView( - _ webView: WKWebView, - runJavaScriptAlertPanelWithMessage message: String, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping () -> Void - ) { - var messageAlert = MessageAlert( - message: message, - frame: frame, - completionHandler: completionHandler, - suppressHandler: nil - ) - handleAlert(webView: webView, alert: &messageAlert) { - completionHandler() - } - } - public func webView( - _ webView: WKWebView, - runJavaScriptConfirmPanelWithMessage message: String, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping (Bool) -> Void - ) { - var confirmAlert = ConfirmPanelAlert( - message: message, - frame: frame, - completionHandler: completionHandler, - suppressHandler: nil - ) - handleAlert(webView: webView, alert: &confirmAlert) { - completionHandler(false) - } - } - - public func webView( - _ webView: WKWebView, - runJavaScriptTextInputPanelWithPrompt prompt: String, - defaultText: String?, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping (String?) -> Void - ) { - var textInputAlert = TextInputAlert( - message: prompt, - frame: frame, - completionHandler: completionHandler, - defaultText: defaultText, - suppressHandler: nil - ) - handleAlert(webView: webView, alert: &textInputAlert) { - completionHandler(nil) - } - } - - func suppressJSAlerts(webView: WKWebView) { - let script = """ - window.alert=window.confirm=window.prompt=function(n){}, - [].slice.apply(document.querySelectorAll('iframe')).forEach(function(n){if(n.contentWindow != window){n.contentWindow.alert=n.contentWindow.confirm=n.contentWindow.prompt=function(n){}}}) - """ - webView.evaluateSafeJavaScript( - functionName: script, - contentWorld: .defaultClient, - asFunction: false - ) - } - - func handleAlert( - webView: WKWebView, - alert: inout T, - completionHandler: @escaping () -> Void + _ webView: CWVWebView, + contextMenuConfigurationFor element: CWVHTMLElement, + completionHandler: @escaping (UIContextMenuConfiguration?) -> Void ) { - guard let promptingTab = tabManager[webView], !promptingTab.blockAllAlerts else { - suppressJSAlerts(webView: webView) - tabManager[webView]?.cancelQueuedAlerts() - completionHandler() - return - } - promptingTab.alertShownCount += 1 - let suppressBlock: JSAlertInfo.SuppressHandler = { [unowned self] suppress in - if suppress { - func suppressDialogues(_: UIAlertAction) { - self.suppressJSAlerts(webView: webView) - promptingTab.blockAllAlerts = true - self.tabManager[webView]?.cancelQueuedAlerts() - completionHandler() - } - // Show confirm alert here. - let suppressSheet = UIAlertController( - title: nil, - message: Strings.suppressAlertsActionMessage, - preferredStyle: .actionSheet - ) - suppressSheet.addAction( - UIAlertAction( - title: Strings.suppressAlertsActionTitle, - style: .destructive, - handler: suppressDialogues - ) - ) - suppressSheet.addAction( - UIAlertAction( - title: Strings.cancelButtonTitle, - style: .cancel, - handler: { _ in - completionHandler() - } - ) - ) - if UIDevice.current.userInterfaceIdiom == .pad, - let popoverController = suppressSheet.popoverPresentationController - { - popoverController.sourceView = self.view - popoverController.sourceRect = CGRect( - x: self.view.bounds.midX, - y: self.view.bounds.midY, - width: 0, - height: 0 - ) - popoverController.permittedArrowDirections = [] - } - self.present(suppressSheet, animated: true) - } else { - completionHandler() - } - } - alert.suppressHandler = promptingTab.alertShownCount > 1 ? suppressBlock : nil - if shouldDisplayJSAlertForWebView(webView) { - let controller = alert.alertController() - controller.delegate = self - present(controller, animated: true) - } else { - promptingTab.queueJavascriptAlertPrompt(alert) - } - } - - func checkIfWebContentProcessHasCrashed(_ webView: WKWebView, error: NSError) -> Bool { - if error.code == WKError.webContentProcessTerminated.rawValue - && error.domain == "WebKitErrorDomain" - { - print("WebContent process has crashed. Trying to reload to restart it.") - webView.reload() - return true - } - - return false - } - - @MainActor public func webView( - _ webView: WKWebView, - contextMenuConfigurationFor elementInfo: WKContextMenuElementInfo - ) async -> UIContextMenuConfiguration? { // Only show context menu for valid links such as `http`, `https`, `data`. Safari does not show it for anything else. // This is because you cannot open `javascript:something` URLs in a new page, or share it, or anything else. - guard let url = elementInfo.linkURL, url.isWebPage() else { - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: nil) + guard let url = element.hyperlink, url.isWebPage() else { + completionHandler( + UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: nil) + ) + return } let actionProvider: UIContextMenuActionProvider = { _ -> UIMenu? in @@ -1716,27 +1557,182 @@ extension BrowserViewController: WKUIDelegate { } let linkPreview: UIContextMenuContentPreviewProvider? = { [unowned self] in - if let tab = tabManager.tabForWebView(webView) { + if let tab = tabManager[webView] { return LinkPreviewViewController(url: url, for: tab, browserController: self) } return nil } let linkPreviewProvider = Preferences.General.enableLinkPreview.value ? linkPreview : nil - return UIContextMenuConfiguration( - identifier: nil, - previewProvider: linkPreviewProvider, - actionProvider: actionProvider + completionHandler( + UIContextMenuConfiguration( + identifier: nil, + previewProvider: linkPreviewProvider, + actionProvider: actionProvider + ) + ) + } + + public func webView(_ webView: CWVWebView, didLoad favIcons: [CWVFavicon]) { + print("loaded favicons: \(favIcons)") + // FIXME: Gotta run this through to download them and update the favicons on disk? + } + + public func webView( + _ webView: CWVWebView, + runJavaScriptAlertPanelWithMessage message: String, + pageURL url: URL, + completionHandler: @escaping () -> Void + ) { + guard let tab = tabManager[webView], let webView = tab.webView else { return } + var messageAlert = MessageAlert( + message: message, + pageURL: url, + completionHandler: completionHandler, + suppressHandler: nil + ) + handleAlert(webView: webView, alert: &messageAlert) { + completionHandler() + } + } + + public func webView( + _ webView: CWVWebView, + runJavaScriptConfirmPanelWithMessage message: String, + pageURL url: URL, + completionHandler: @escaping (Bool) -> Void + ) { + guard let tab = tabManager[webView], let webView = tab.webView else { return } + var confirmAlert = ConfirmPanelAlert( + message: message, + pageURL: url, + completionHandler: completionHandler, + suppressHandler: nil ) + handleAlert(webView: webView, alert: &confirmAlert) { + completionHandler(false) + } } public func webView( - _ webView: WKWebView, - contextMenuForElement elementInfo: WKContextMenuElementInfo, - willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating + _ webView: CWVWebView, + runJavaScriptTextInputPanelWithPrompt prompt: String, + defaultText: String, + pageURL url: URL, + completionHandler: @escaping (String?) -> Void ) { - guard let url = elementInfo.linkURL else { return } - webView.load(URLRequest(url: url)) + guard let tab = tabManager[webView], let webView = tab.webView else { return } + var textInputAlert = TextInputAlert( + message: prompt, + pageURL: url, + completionHandler: completionHandler, + defaultText: defaultText, + suppressHandler: nil + ) + handleAlert(webView: webView, alert: &textInputAlert) { + completionHandler(nil) + } + } +} + +extension BrowserViewController { + fileprivate func shouldDisplayJSAlertForWebView(_ webView: BraveWebView) -> Bool { + // Only display a JS Alert if we are selected and there isn't anything being shown + return ((tabManager.selectedTab == nil ? false : tabManager.selectedTab!.webView == webView)) + && (self.presentedViewController == nil) + } + + func suppressJSAlerts(webView: BraveWebView) { + let script = """ + window.alert=window.confirm=window.prompt=function(n){}, + [].slice.apply(document.querySelectorAll('iframe')).forEach(function(n){if(n.contentWindow != window){n.contentWindow.alert=n.contentWindow.confirm=n.contentWindow.prompt=function(n){}}}) + """ + webView.evaluateSafeJavaScript( + functionName: script, + contentWorld: .defaultClient, + asFunction: false + ) + } + + func handleAlert( + webView: BraveWebView, + alert: inout T, + completionHandler: @escaping () -> Void + ) { + guard let promptingTab = tabManager[webView], !promptingTab.blockAllAlerts else { + suppressJSAlerts(webView: webView) + tabManager[webView]?.cancelQueuedAlerts() + completionHandler() + return + } + promptingTab.alertShownCount += 1 + let suppressBlock: JSAlertInfo.SuppressHandler = { [unowned self] suppress in + if suppress { + func suppressDialogues(_: UIAlertAction) { + self.suppressJSAlerts(webView: webView) + promptingTab.blockAllAlerts = true + self.tabManager[webView]?.cancelQueuedAlerts() + completionHandler() + } + // Show confirm alert here. + let suppressSheet = UIAlertController( + title: nil, + message: Strings.suppressAlertsActionMessage, + preferredStyle: .actionSheet + ) + suppressSheet.addAction( + UIAlertAction( + title: Strings.suppressAlertsActionTitle, + style: .destructive, + handler: suppressDialogues + ) + ) + suppressSheet.addAction( + UIAlertAction( + title: Strings.cancelButtonTitle, + style: .cancel, + handler: { _ in + completionHandler() + } + ) + ) + if UIDevice.current.userInterfaceIdiom == .pad, + let popoverController = suppressSheet.popoverPresentationController + { + popoverController.sourceView = self.view + popoverController.sourceRect = CGRect( + x: self.view.bounds.midX, + y: self.view.bounds.midY, + width: 0, + height: 0 + ) + popoverController.permittedArrowDirections = [] + } + self.present(suppressSheet, animated: true) + } else { + completionHandler() + } + } + alert.suppressHandler = promptingTab.alertShownCount > 1 ? suppressBlock : nil + if shouldDisplayJSAlertForWebView(webView) { + let controller = alert.alertController() + controller.delegate = self + present(controller, animated: true) + } else { + promptingTab.queueJavascriptAlertPrompt(alert) + } + } + + func checkIfWebContentProcessHasCrashed(_ webView: CWVWebView, error: NSError) -> Bool { + if error.code == WKError.webContentProcessTerminated.rawValue + && error.domain == "WebKitErrorDomain" + { + print("WebContent process has crashed. Trying to reload to restart it.") + webView.reload() + return true + } + + return false } fileprivate func addTab(url: URL, inPrivateMode: Bool, currentTab: Tab) { @@ -1765,7 +1761,7 @@ extension BrowserViewController: WKUIDelegate { /// Get a possible redirect request from debouncing or query param stripping private func getInternalRedirect( - from navigationAction: WKNavigationAction, + from navigationAction: CWVNavigationAction, in tab: Tab, domainForMainFrame: Domain ) -> URLRequest? { @@ -1774,7 +1770,7 @@ extension BrowserViewController: WKUIDelegate { // For main frame only and if shields are enabled guard requestURL.isWebPage(includeDataURIs: false), domainForMainFrame.globalBlockAdsAndTrackingLevel.isEnabled, - navigationAction.targetFrame?.isMainFrame == true + navigationAction.navigationType.isMainFrame else { return nil } // Handle Debounce @@ -1785,7 +1781,7 @@ extension BrowserViewController: WKUIDelegate { // (i.e. appropriate settings are enabled for that redirect rule) if let debounceService = DebounceServiceFactory.get(privateMode: tab.isPrivate), debounceService.isEnabled, - let currentURL = tab.webView?.url, + let currentURL = tab.webView?.lastCommittedURL, currentURL.baseDomain != requestURL.baseDomain { if let redirectURL = debounceService.debounce(requestURL) { @@ -1825,86 +1821,8 @@ extension BrowserViewController: WKUIDelegate { return request } - // HTTPS by Default - if shouldUpgradeToHttps(url: requestURL, isPrivate: tab.isPrivate), - var urlComponents = URLComponents(url: requestURL, resolvingAgainstBaseURL: true) - { - // Attempt to upgrade to HTTPS - urlComponents.scheme = "https" - if let upgradedURL = urlComponents.url { - Self.log.debug( - "Upgrading `\(requestURL.absoluteString)` to HTTPS" - ) - tab.upgradedHTTPSRequest = navigationAction.request - var request = navigationAction.request - request.url = upgradedURL - return request - } - } - return nil } - - /// Determines if the given url should be upgraded from http to https. - private func shouldUpgradeToHttps(url: URL, isPrivate: Bool) -> Bool { - guard FeatureList.kBraveHttpsByDefault.enabled, - let httpUpgradeService = HttpsUpgradeServiceFactory.get(privateMode: isPrivate), - url.scheme == "http", let host = url.host - else { - return false - } - let isInUserAllowList = httpUpgradeService.isHttpAllowed(forHost: host) - let shouldUpgrade: Bool - switch ShieldPreferences.httpsUpgradeLevel { - case .strict: - // Always upgrade for Strict HTTPS upgrade unless previously allowed by user. - shouldUpgrade = !isInUserAllowList - case .standard: - // Upgrade for Standard HTTPS upgrade if host is not on the exceptions list and not previously allowed by user. - shouldUpgrade = - braveCore.httpsUpgradeExceptionsService.canUpgradeToHTTPS(for: url) - && !isInUserAllowList - case .disabled: - shouldUpgrade = false - } - return shouldUpgrade - } - - /// Upon an invalid response, check that we need to roll back any HTTPS upgrade - /// or show the interstitial page - private func handleInvalidHTTPSUpgrade(tab: Tab, responseURL: URL) -> URLRequest? { - // Handle invalid upgrade to https - guard responseURL.scheme == "https", - let originalRequest = tab.upgradedHTTPSRequest, - let originalURL = originalRequest.url, - responseURL.baseDomain == originalURL.baseDomain - else { - return nil - } - - if FeatureList.kHttpsOnlyMode.enabled, ShieldPreferences.httpsUpgradeLevel.isStrict, - let url = originalURL.encodeEmbeddedInternalURL(for: .httpBlocked) - { - Self.log.debug( - "Show http blocked interstitial for `\(originalURL.absoluteString)`" - ) - - let request = PrivilegedRequest(url: url) as URLRequest - return request - } else { - Self.log.debug( - "Revert HTTPS upgrade for `\(originalURL.absoluteString)`" - ) - - tab.upgradedHTTPSRequest = nil - if let httpsUpgradeService = HttpsUpgradeServiceFactory.get(privateMode: tab.isPrivate), - let host = originalURL.host - { - httpsUpgradeService.allowHttp(forHost: host) - } - return originalRequest - } - } } extension P3ATimedStorage where Value == Int { diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index 70d0ecd4e1cc..2a4bbd81aa0d 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -45,11 +45,9 @@ private let KVOs: [KVOConstants] = [ .loading, .canGoBack, .canGoForward, - .url, .title, - .hasOnlySecureContent, - .serverTrust, - ._sampledPageTopColor, + .visibleURL, + .visibleSSLStatus ] public class BrowserViewController: UIViewController { @@ -225,12 +223,6 @@ public class BrowserViewController: UIViewController { // allow us to re-trigger the `URLRequest` if the user requests a file to be downloaded. var pendingRequests = [String: URLRequest]() - // This is set when the user taps "Download Link" from the context menu. We then force a - // download of the next request through the `WKNavigationDelegate` that matches this web view. - weak var pendingDownloadWebView: WKWebView? - - let downloadQueue = DownloadQueue() - private var cancellables: Set = [] let rewards: BraveRewards @@ -316,8 +308,7 @@ public class BrowserViewController: UIViewController { windowId: windowId, prefs: profile.prefs, rewards: rewards, - tabGeneratorAPI: braveCore.tabGeneratorAPI, - historyAPI: braveCore.historyAPI, + braveCore: braveCore, privateBrowsingManager: privateBrowsingManager ) @@ -467,12 +458,10 @@ public class BrowserViewController: UIViewController { tabManager.addDelegate(self) tabManager.addNavigationDelegate(self) UserScriptManager.shared.fetchWalletScripts(from: braveCore.braveWalletAPI) - downloadQueue.delegate = self // Observe some user preferences Preferences.Privacy.privateBrowsingOnly.observe(from: self) Preferences.General.tabBarVisibility.observe(from: self) - Preferences.UserAgent.alwaysRequestDesktopSite.observe(from: self) Preferences.General.enablePullToRefresh.observe(from: self) Preferences.General.mediaAutoBackgrounding.observe(from: self) Preferences.General.youtubeHighQuality.observe(from: self) @@ -494,7 +483,7 @@ public class BrowserViewController: UIViewController { queue: .main ) { [weak self] _ in self?.tabManager.allTabs.forEach({ - guard let url = $0.webView?.url else { return } + guard let url = $0.webView?.visibleURL else { return } let zoomLevel = self?.privateBrowsingManager.isPrivateBrowsing == true ? 1.0 @@ -1868,7 +1857,7 @@ public class BrowserViewController: UIViewController { context: UnsafeMutableRawPointer? ) { - guard let webView = object as? WKWebView else { + guard let webView = object as? BraveWebView else { Logger.module.error( "An object of type: \(String(describing: object), privacy: .public) is being observed instead of a WKWebView" ) @@ -1896,7 +1885,7 @@ public class BrowserViewController: UIViewController { // `WKWebView.estimatedProgress` is a `Double` type so it must be casted as such let progress = change?[.newKey] as? Double else { break } - if let url = webView.url, !InternalURL.isValid(url: url) { + if let url = webView.lastCommittedURL, !InternalURL.isValid(url: url) { topToolbar.updateProgressBar(Float(progress)) } else { topToolbar.hideProgressBar() @@ -1909,19 +1898,19 @@ public class BrowserViewController: UIViewController { topToolbar.updateProgressBar(1) } } - case .url: + case .visibleURL: guard let tab = tabManager[webView] else { break } - // Special case for "about:blank" popups, if the webView.url is nil, keep the tab url as "about:blank" - if tab.url?.absoluteString == "about:blank" && webView.url == nil { + // Special case for "about:blank" popups, if the webView.lastCommittedURL is nil, keep the tab url as "about:blank" + if tab.url?.absoluteString == "about:blank" && webView.lastCommittedURL == nil { break } // To prevent spoofing, only change the URL immediately if the new URL is on // the same origin as the current URL. Otherwise, do nothing and wait for // didCommitNavigation to confirm the page load. - if tab.url?.origin == webView.url?.origin { - tab.url = webView.url + if tab.url?.origin == webView.visibleURL?.origin { + tab.url = webView.visibleURL if tab === tabManager.selectedTab && !tab.restoring { updateUIForReaderHomeStateForTab(tab) @@ -1936,7 +1925,7 @@ public class BrowserViewController: UIViewController { // didCommit is called and it will cause url bar be empty in that period // To fix this when tab display url is empty, webview url is used if tab === tabManager.selectedTab, tab.url?.displayURL == nil { - if let url = webView.url, !url.isLocal, !InternalURL.isValid(url: url) { + if let url = webView.lastCommittedURL, !url.isLocal, !InternalURL.isValid(url: url) { updateToolbarCurrentURL(url.displayURL) } } else if tab === tabManager.selectedTab, tab.isDisplayingBasicAuthPrompt { @@ -1954,7 +1943,7 @@ public class BrowserViewController: UIViewController { url.host == rewardsURL.host { tab.reportPageNavigation(to: rewards) - if let url = webView.url { + if let url = webView.lastCommittedURL { tab.reportPageLoad(to: rewards, redirectChain: [url]) } } @@ -1962,24 +1951,18 @@ public class BrowserViewController: UIViewController { // Update the estimated progress when the URL changes. Estimated progress may update to 0.1 when the url // is still an internal URL even though a request may be pending for a web page. - if tab === tabManager.selectedTab, let url = webView.url, + if tab === tabManager.selectedTab, let url = webView.lastCommittedURL, !InternalURL.isValid(url: url), webView.estimatedProgress > 0 { topToolbar.updateProgressBar(Float(webView.estimatedProgress)) } - - Task { - await tab.updateSecureContentState() - self.logSecureContentState(tab: tab, path: .url, change: change) - if self.tabManager.selectedTab === tab { - self.updateToolbarSecureContentState(tab.lastKnownSecureContentState) - } - } case .title: // Ensure that the tab title *actually* changed to prevent repeated calls // to navigateInTab(tab:). guard - let title = (webView.title?.isEmpty == true ? webView.url?.absoluteString : webView.title) + let title = + (webView.title?.isEmpty == true + ? webView.lastCommittedURL?.absoluteString : webView.title) else { break } if !title.isEmpty && title != tab.lastTitle { navigateInTab(tab: tab) @@ -1991,66 +1974,22 @@ public class BrowserViewController: UIViewController { } updateBackForwardActionStatus(for: webView) - case .hasOnlySecureContent: + case .visibleSSLStatus: Task { await tab.updateSecureContentState() - self.logSecureContentState(tab: tab, path: .hasOnlySecureContent, change: change) if tabManager.selectedTab === tab { self.updateToolbarSecureContentState(tab.lastKnownSecureContentState) } } - case .serverTrust: - Task { - await tab.updateSecureContentState() - self.logSecureContentState(tab: tab, path: .serverTrust, change: change) - if self.tabManager.selectedTab === tab { - self.updateToolbarSecureContentState(tab.lastKnownSecureContentState) - } - } - case ._sampledPageTopColor: - updateStatusBarOverlayColor() default: assertionFailure("Unhandled KVO key: \(kp)") } } - func logSecureContentState( - tab: Tab, - path: KVOConstants? = nil, - change: [NSKeyValueChangeKey: Any]? = nil - ) { - var text = """ - Tab URL: \(tab.url?.absoluteString ?? "Empty Tab URL") - Secure State: \(tab.lastKnownSecureContentState.rawValue) - """ - - if let keyPath = path?.keyPath { - text.append("\n Value Observed: \(keyPath)\n") - } - - if let webView = tab.webView { - text.append( - """ - WebView url: \(webView.url?.absoluteString ?? "nil") - WebView hasOnlySecureContent: \(webView.hasOnlySecureContent ? "true" : "false") - WebView serverTrust: \(webView.serverTrust != nil ? "present" : "nil") - """ - ) - } - - if let change, path == .serverTrust, let newServerTrust = change[.newKey] { - text.append("\n Change: \(newServerTrust != nil ? "present" : "nil")") - } else if let change, let value = change[.newKey] { - text.append("\n Change: \(String(describing: value))") - } - - DebugLogger.log(for: .secureState, text: text) - } - - func updateBackForwardActionStatus(for webView: WKWebView?) { + func updateBackForwardActionStatus(for webView: BraveWebView?) { guard let webView = webView else { return } - if let forwardListItem = webView.backForwardList.forwardList.first, + if let forwardListItem = webView.backForwardList.forwardList.first(where: { _ in true }), forwardListItem.url.isInternalURL(for: .readermode) { navigationToolbar.updateForwardStatus(false) @@ -2324,14 +2263,14 @@ public class BrowserViewController: UIViewController { let zoomLevel = privateBrowsingManager.isPrivateBrowsing ? 1.0 : domain?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value - tab.webView?.setValue(zoomLevel, forKey: PageZoomHandler.propertyName) + tab.webView?.underlyingWebView?.setValue(zoomLevel, forKey: PageZoomHandler.propertyName) } } public override var preferredStatusBarStyle: UIStatusBarStyle { if isUsingBottomBar, let tab = tabManager.selectedTab, tab.url.map(InternalURL.isValid) == false, - let color = tab.webView?.sampledPageTopColor + let color = tab.webView?.underlyingWebView?.sampledPageTopColor { return color.isLight ? .darkContent : .lightContent } @@ -2342,7 +2281,7 @@ public class BrowserViewController: UIViewController { defer { setNeedsStatusBarAppearanceUpdate() } guard isUsingBottomBar, let tab = tabManager.selectedTab, tab.url.map(InternalURL.isValid) == false, - let color = tab.webView?.sampledPageTopColor + let color = tab.webView?.underlyingWebView?.sampledPageTopColor else { statusBarOverlay.backgroundColor = privateBrowsingManager.browserColors.chromeBackground return @@ -2350,7 +2289,7 @@ public class BrowserViewController: UIViewController { statusBarOverlay.backgroundColor = color } - func navigateInTab(tab: Tab, to navigation: WKNavigation? = nil) { + func navigateInTab(tab: Tab) { tabManager.expireSnackbars() guard let webView = tab.webView else { @@ -2358,7 +2297,7 @@ public class BrowserViewController: UIViewController { return } - if let url = webView.url { + if let url = webView.lastCommittedURL { // Whether to show search icon or + icon toolbar?.setSearchButtonState(url: url) @@ -2628,41 +2567,47 @@ extension BrowserViewController: TabsBarViewControllerDelegate { } extension BrowserViewController: TabDelegate { - func tab(_ tab: Tab, didCreateWebView webView: WKWebView) { + func tab(_ tab: Tab, didCreateWebView webView: BraveWebView) { webView.frame = webViewContainer.frame + if webView.frame.size == .zero { + // Set a non empty CGRect to avoid DCHECKs that occur when a load happens + // after state restoration, and before the view hierarchy is laid out for the + // first time. + // https://source.chromium.org/chromium/chromium/src/+/main:ios/web/web_state/ui/crw_web_request_controller.mm;l=518;drc=df887034106ef438611326745a7cd276eedd4953 + webView.frame.size = .init(width: 1, height: 1) + } // Observers that live as long as the tab. Make sure these are all cleared in willDeleteWebView below! KVOs.forEach { webView.addObserver(self, forKeyPath: $0.keyPath, options: .new, context: nil) } webView.uiDelegate = self var injectedScripts: [TabContentScript] = [ - ReaderModeScriptHandler(tab: tab), - ErrorPageHelper(certStore: profile.certStore), - SessionRestoreScriptHandler(tab: tab), - BlockedDomainScriptHandler(tab: tab), - HTTPBlockedScriptHandler(tab: tab, tabManager: tabManager), - PrintScriptHandler(browserController: self, tab: tab), - CustomSearchScriptHandler(tab: tab), - DarkReaderScriptHandler(tab: tab), - FocusScriptHandler(tab: tab), - BraveGetUA(tab: tab), - BraveSearchScriptHandler(tab: tab, profile: profile, rewards: rewards), - ResourceDownloadScriptHandler(tab: tab), - DownloadContentScriptHandler(browserController: self, tab: tab), - WindowRenderScriptHandler(tab: tab), + ReaderModeScriptHandler(), + SessionRestoreScriptHandler(), + BlockedDomainScriptHandler(), + PrintScriptHandler(browserController: self), + CustomSearchScriptHandler(), + DarkReaderScriptHandler(), + FocusScriptHandler(), + BraveGetUA(), + BraveSearchScriptHandler(profile: profile, rewards: rewards), + ResourceDownloadScriptHandler(), + DownloadContentScriptHandler(browserController: self), + WindowRenderScriptHandler(), PlaylistScriptHandler(tab: tab), - PlaylistFolderSharingScriptHandler(tab: tab), - RewardsReportingScriptHandler(rewards: rewards, tab: tab), - AdsMediaReportingScriptHandler(rewards: rewards, tab: tab), - ReadyStateScriptHandler(tab: tab), - DeAmpScriptHandler(tab: tab), - SiteStateListenerScriptHandler(tab: tab), - CosmeticFiltersScriptHandler(tab: tab), - URLPartinessScriptHandler(tab: tab), - FaviconScriptHandler(tab: tab), - Web3NameServiceScriptHandler(tab: tab), + PlaylistFolderSharingScriptHandler(), + RewardsReportingScriptHandler(rewards: rewards), + AdsMediaReportingScriptHandler(rewards: rewards), + ReadyStateScriptHandler(), + DeAmpScriptHandler(), + SiteStateListenerScriptHandler(), + CosmeticFiltersScriptHandler(), + URLPartinessScriptHandler(), + FaviconScriptHandler(), + Web3NameServiceScriptHandler(), YoutubeQualityScriptHandler(tab: tab), - BraveLeoScriptHandler(tab: tab), + BraveLeoScriptHandler(), + BraveSkusScriptHandler(), tab.contentBlocker, tab.requestBlockingContentHelper, @@ -2671,7 +2616,6 @@ extension BrowserViewController: TabDelegate { #if canImport(BraveTalk) injectedScripts.append( BraveTalkScriptHandler( - tab: tab, rewards: rewards, launchNativeBraveTalk: { [weak self] tab, room, token in self?.launchNativeBraveTalk(tab: tab, room: room, token: token) @@ -2680,17 +2624,13 @@ extension BrowserViewController: TabDelegate { ) #endif - if let braveSkusHandler = BraveSkusScriptHandler(tab: tab) { - injectedScripts.append(braveSkusHandler) - } - // Only add the logins handler and wallet provider if the tab is NOT a private browsing tab if !tab.isPrivate { injectedScripts += [ - LoginsScriptHandler(tab: tab, profile: profile, passwordAPI: braveCore.passwordAPI), - EthereumProviderScriptHandler(tab: tab), - SolanaProviderScriptHandler(tab: tab), - BraveSearchResultAdScriptHandler(tab: tab), + LoginsScriptHandler(profile: profile, passwordAPI: braveCore.passwordAPI), + EthereumProviderScriptHandler(), + SolanaProviderScriptHandler(), + BraveSearchResultAdScriptHandler(), ] } @@ -2718,7 +2658,7 @@ extension BrowserViewController: TabDelegate { as? Web3NameServiceScriptHandler)?.delegate = self } - func tab(_ tab: Tab, willDeleteWebView webView: WKWebView) { + func tab(_ tab: Tab, willDeleteWebView webView: BraveWebView) { tab.cancelQueuedAlerts() KVOs.forEach { webView.removeObserver(self, forKeyPath: $0.keyPath) } toolbarVisibilityViewModel.endScrollViewObservation(webView.scrollView) @@ -2760,9 +2700,8 @@ extension BrowserViewController: TabDelegate { /// Triggered when "Find in Page" is selected on selected text func tab(_ tab: Tab, didSelectFindInPageFor selectedText: String) { - if let findInteraction = tab.webView?.findInteraction { - findInteraction.searchText = selectedText - findInteraction.presentFindNavigator(showingReplace: false) + if let findInteraction = tab.webView?.findInPageController { + findInteraction.findString(inPage: selectedText) } } @@ -2978,14 +2917,11 @@ extension BrowserViewController: SearchViewControllerDelegate { shouldFindInPage query: String ) { topToolbar.leaveOverlayMode() - if let findInteraction = tabManager.selectedTab?.webView?.findInteraction { - findInteraction.searchText = query - findInteraction.presentFindNavigator(showingReplace: false) - } + tabManager.selectedTab?.webView?.findInPageController.findString(inPage: query) } func searchViewControllerAllowFindInPage() -> Bool { - if let url = tabManager.selectedTab?.webView?.url, + if let url = tabManager.selectedTab?.webView?.lastCommittedURL, let internalURL = InternalURL(url), internalURL.isAboutHomeURL { @@ -3256,9 +3192,6 @@ extension BrowserViewController: PreferencesObserver { setupTabs() updateTabsBarVisibility() updateApplicationShortcuts() - case Preferences.UserAgent.alwaysRequestDesktopSite.key: - tabManager.reset() - tabManager.reloadSelectedTab() case Preferences.General.enablePullToRefresh.key: tabManager.selectedTab?.updatePullToRefreshVisibility() case Preferences.Shields.blockScripts.key, @@ -3273,7 +3206,7 @@ extension BrowserViewController: PreferencesObserver { recordGlobalFingerprintingShieldsP3A() case Preferences.General.defaultPageZoomLevel.key: tabManager.allTabs.forEach({ - guard let url = $0.webView?.url else { return } + guard let url = $0.webView?.lastCommittedURL else { return } let zoomLevel = $0.isPrivate ? 1.0 @@ -3289,7 +3222,7 @@ extension BrowserViewController: PreferencesObserver { self.tabManager.reloadSelectedTab() for tab in self.tabManager.allTabs where tab != self.tabManager.selectedTab { tab.createWebview() - if let url = tab.webView?.url { + if let url = tab.webView?.lastCommittedURL { tab.loadRequest(PrivilegedRequest(url: url) as URLRequest) } } @@ -3505,7 +3438,7 @@ extension BrowserViewController: UIScreenshotServiceDelegate { guard screenshotService.windowScene != nil, presentedViewController == nil, let webView = tabManager.selectedTab?.webView, - let url = webView.url, + let url = webView.lastCommittedURL, url.isWebPage() else { continuation.resume(returning: (nil, 0, .zero)) @@ -3517,12 +3450,10 @@ extension BrowserViewController: UIScreenshotServiceDelegate { rect.origin.y = webView.scrollView.contentSize.height - rect.height - webView.scrollView.contentOffset.y - webView.createPDF { result in - - switch result { - case .success(let data): + webView.createPDF { data in + if let data { continuation.resume(returning: (data, 0, rect)) - case .failure: + } else { continuation.resume(returning: (nil, 0, .zero)) } } @@ -3586,56 +3517,27 @@ extension BrowserViewController { return } - let getServerTrustForErrorPage = { () -> SecTrust? in - do { - if let url = webView.url { - return try ErrorPageHelper.serverTrust(from: url) - } - } catch { - Logger.module.error("\(error.localizedDescription)") - } - - return nil - } - - guard let trust = webView.serverTrust ?? getServerTrustForErrorPage() else { - return - } - - let host = webView.url?.host - Task.detached { - let serverCertificates: [SecCertificate] = - SecTrustCopyCertificateChain(trust) as? [SecCertificate] ?? [] + guard let visibleSSLStatus = await webView.visibleSSLStatus else { return } // TODO: Instead of showing only the first cert in the chain, // have a UI that allows users to select any certificate in the chain (similar to Desktop browsers) - if let serverCertificate = serverCertificates.first, - let certificate = BraveCertificateModel(certificate: serverCertificate) + if let serverCertificate = visibleSSLStatus.certificate?.certificateRef, + let certificate = BraveCertificateModel(certificate: serverCertificate), + let lastCommittedURL = await webView.lastCommittedURL { - var errorDescription: String? - - do { - try await BraveCertificateUtils.evaluateTrust(trust, for: host) - } catch { - Logger.module.error("\(error.localizedDescription)") - - // Remove the common-name from the first part of the error message - // This is because the certificate viewer already displays it. - // If it doesn't match, it won't be removed, so this is fine. - errorDescription = error.localizedDescription - if let range = errorDescription?.range(of: "“\(certificate.subjectName.commonName)” ") - ?? errorDescription?.range(of: "\"\(certificate.subjectName.commonName)\" ") - { - errorDescription = - errorDescription?.replacingCharacters(in: range, with: "").capitalizeFirstLetter - } + if visibleSSLStatus.securityStyle == .authenticationBroken, + visibleSSLStatus.isCertStatusError + { + errorDescription = visibleSSLStatus.certStatusErrors(for: lastCommittedURL) + .map(\.shortDescription) + .joined(separator: ", ") } await MainActor.run { [errorDescription] in // System components sit on top so we want to dismiss it - webView.findInteraction?.dismissFindNavigator() + webView.findInPageController.stopFindInPage() let certificateViewController = CertificateViewController( certificate: certificate, evaluationError: errorDescription diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadQueue.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadQueue.swift deleted file mode 100644 index ef72b1fc8c2c..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadQueue.swift +++ /dev/null @@ -1,378 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveShared -import Foundation -import WebKit - -private let downloadOperationQueue = OperationQueue() - -protocol DownloadDelegate { - func download(_ download: Download, didCompleteWithError error: Error?) - func download(_ download: Download, didDownloadBytes bytesDownloaded: Int64) - func download(_ download: Download, didFinishDownloadingTo location: URL) -} - -class Download: NSObject { - var delegate: DownloadDelegate? - - fileprivate(set) var filename: String - fileprivate(set) var mimeType: String - - fileprivate(set) var isComplete = false - - fileprivate(set) var totalBytesExpected: Int64? - fileprivate(set) var bytesDownloaded: Int64 - - override init() { - self.filename = "unknown" - self.mimeType = "application/octet-stream" - - self.bytesDownloaded = 0 - - super.init() - } - - func cancel() {} - func pause() {} - func resume() {} - - func uniqueDownloadPathForFilename(_ filename: String) async throws -> URL { - let downloadsPath = try await AsyncFileManager.default.downloadsPath() - let basePath = downloadsPath.appending(path: filename) - let fileExtension = basePath.pathExtension - let filenameWithoutExtension = - !fileExtension.isEmpty ? String(filename.dropLast(fileExtension.count + 1)) : filename - - var proposedPath = basePath - var count = 0 - - while await AsyncFileManager.default.fileExists(atPath: proposedPath.path) { - count += 1 - - let proposedFilenameWithoutExtension = "\(filenameWithoutExtension) (\(count))" - proposedPath = downloadsPath.appending(path: proposedFilenameWithoutExtension) - .appending(path: fileExtension) - } - - return proposedPath - } -} - -class HTTPDownload: Download { - let preflightResponse: URLResponse - let request: URLRequest - - var state: URLSessionTask.State { - return task?.state ?? .suspended - } - - fileprivate(set) var session: URLSession? - fileprivate(set) var task: URLSessionDownloadTask? - fileprivate(set) var cookieStore: WKHTTPCookieStore - - private var resumeData: Data? - - // Used to avoid name spoofing using Unicode RTL char to change file extension - public static func stripUnicode(fromFilename string: String) -> String { - let validFilenameSet = CharacterSet(charactersIn: ":/") - .union(.newlines) - .union(.controlCharacters) - .union(.illegalCharacters) - return string.components(separatedBy: validFilenameSet).joined() - } - - init(cookieStore: WKHTTPCookieStore, preflightResponse: URLResponse, request: URLRequest) { - self.cookieStore = cookieStore - self.preflightResponse = preflightResponse - self.request = request - - super.init() - - if let filename = preflightResponse.suggestedFilename { - self.filename = HTTPDownload.stripUnicode(fromFilename: filename) - } - - if let mimeType = preflightResponse.mimeType { - self.mimeType = mimeType - } - - self.totalBytesExpected = - preflightResponse.expectedContentLength > 0 ? preflightResponse.expectedContentLength : nil - - self.session = URLSession( - configuration: .ephemeral, - delegate: self, - delegateQueue: downloadOperationQueue - ) - self.task = session?.downloadTask(with: request) - } - - override func cancel() { - task?.cancel() - } - - override func pause() { - task?.cancel(byProducingResumeData: { resumeData in - self.resumeData = resumeData - }) - } - - override func resume() { - cookieStore.getAllCookies { [self] cookies in - cookies.forEach { cookie in - session?.configuration.httpCookieStorage?.setCookie(cookie) - } - - guard let resumeData = self.resumeData else { - self.task?.resume() - return - } - self.task = session?.downloadTask(withResumeData: resumeData) - self.task?.resume() - } - } -} - -extension HTTPDownload: URLSessionTaskDelegate, URLSessionDownloadDelegate { - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - // Don't bubble up cancellation as an error if the - // error is `.cancelled` and we have resume data. - if let urlError = error as? URLError, - urlError.code == .cancelled, - resumeData != nil - { - return - } - - delegate?.download(self, didCompleteWithError: error) - } - - func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didWriteData bytesWritten: Int64, - totalBytesWritten: Int64, - totalBytesExpectedToWrite: Int64 - ) { - bytesDownloaded = totalBytesWritten - totalBytesExpected = totalBytesExpectedToWrite - - delegate?.download(self, didDownloadBytes: bytesWritten) - } - - func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didFinishDownloadingTo location: URL - ) { - // This method will delete the downloaded file immediately after this delegate method returns - // so we must synchonously move the file to a temporary directory first before processing it - // on a different thread since the Task will execute after this method returns - let temporaryLocation = FileManager.default.temporaryDirectory - .appending(component: "\(filename)-\(location.lastPathComponent)") - try? FileManager.default.moveItem(at: location, to: temporaryLocation) - Task { - do { - let destination = try await uniqueDownloadPathForFilename(filename) - try await AsyncFileManager.default.moveItem(at: temporaryLocation, to: destination) - isComplete = true - delegate?.download(self, didFinishDownloadingTo: destination) - } catch { - delegate?.download(self, didCompleteWithError: error) - } - } - } -} - -protocol DownloadQueueDelegate { - func downloadQueue(_ downloadQueue: DownloadQueue, didStartDownload download: Download) - func downloadQueue( - _ downloadQueue: DownloadQueue, - didDownloadCombinedBytes combinedBytesDownloaded: Int64, - combinedTotalBytesExpected: Int64? - ) - func downloadQueue( - _ downloadQueue: DownloadQueue, - download: Download, - didFinishDownloadingTo location: URL - ) - func downloadQueue(_ downloadQueue: DownloadQueue, didCompleteWithError error: Error?) -} - -class DownloadQueue { - var downloads: [Download] - - var delegate: DownloadQueueDelegate? - - var isEmpty: Bool { - return downloads.isEmpty - } - - fileprivate var combinedBytesDownloaded: Int64 = 0 - fileprivate var combinedTotalBytesExpected: Int64? - fileprivate var lastDownloadError: Error? - - init() { - self.downloads = [] - } - - func enqueue(_ download: Download) { - // Clear the download stats if the queue was empty at the start. - if downloads.isEmpty { - combinedBytesDownloaded = 0 - combinedTotalBytesExpected = 0 - lastDownloadError = nil - } - - downloads.append(download) - download.delegate = self - - if let totalBytesExpected = download.totalBytesExpected, combinedTotalBytesExpected != nil { - combinedTotalBytesExpected! += totalBytesExpected - } else { - combinedTotalBytesExpected = nil - } - - download.resume() - delegate?.downloadQueue(self, didStartDownload: download) - } - - func cancelAll() { - for download in downloads where !download.isComplete { - download.cancel() - } - } - - func pauseAll() { - for download in downloads where !download.isComplete { - download.pause() - } - } - - func resumeAll() { - for download in downloads where !download.isComplete { - download.resume() - } - } -} - -extension DownloadQueue: DownloadDelegate { - func download(_ download: Download, didCompleteWithError error: Error?) { - guard let error = error, let index = downloads.firstIndex(of: download) else { - return - } - - lastDownloadError = error - downloads.remove(at: index) - - if downloads.isEmpty { - delegate?.downloadQueue(self, didCompleteWithError: lastDownloadError) - } - } - - func download(_ download: Download, didDownloadBytes bytesDownloaded: Int64) { - combinedBytesDownloaded += bytesDownloaded - delegate?.downloadQueue( - self, - didDownloadCombinedBytes: combinedBytesDownloaded, - combinedTotalBytesExpected: combinedTotalBytesExpected - ) - } - - func download(_ download: Download, didFinishDownloadingTo location: URL) { - guard let index = downloads.firstIndex(of: download) else { - return - } - - downloads.remove(at: index) - delegate?.downloadQueue(self, download: download, didFinishDownloadingTo: location) - - NotificationCenter.default.post(name: .fileDidDownload, object: location) - - if downloads.isEmpty { - delegate?.downloadQueue(self, didCompleteWithError: lastDownloadError) - } - } -} - -class WebKitDownload: Download { - let fileURL: URL - let response: URLResponse - let suggestedFileName: String - weak var download: WKDownload? - - private weak var webView: WKWebView? - private var downloadResumeData: Data? - private weak var downloadQueue: DownloadQueue? - private var completedUnitCountObserver: NSKeyValueObservation? - - init( - fileURL: URL, - response: URLResponse, - suggestedFileName: String, - download: WKDownload, - downloadQueue: DownloadQueue - ) { - self.fileURL = fileURL - self.response = response - self.suggestedFileName = suggestedFileName - self.download = download - self.webView = download.webView - self.downloadQueue = downloadQueue - super.init() - - self.filename = suggestedFileName - self.bytesDownloaded = download.progress.completedUnitCount - self.totalBytesExpected = download.progress.totalUnitCount - - completedUnitCountObserver = download.progress.observe( - \.completedUnitCount, - changeHandler: { [weak self] progress, value in - guard let self = self else { return } - - self.bytesDownloaded = progress.completedUnitCount - self.totalBytesExpected = progress.totalUnitCount - - guard let downloadQueue = self.downloadQueue else { return } - - downloadQueue.delegate?.downloadQueue( - downloadQueue, - didDownloadCombinedBytes: self.bytesDownloaded, - combinedTotalBytesExpected: self.totalBytesExpected - ) - } - ) - } - - override func cancel() { - download?.cancel({ [weak self] data in - guard let self = self else { return } - if let data = data { - self.downloadResumeData = data - } - - self.delegate?.download(self, didCompleteWithError: nil) - }) - } - - override func pause() { - - } - - override func resume() { - if let downloadResumeData = downloadResumeData { - webView?.resumeDownload( - fromResumeData: downloadResumeData, - completionHandler: { [weak self] download in - guard let self = self else { return } - self.download = download - self.bytesDownloaded = download.progress.completedUnitCount - self.totalBytesExpected = download.progress.totalUnitCount - self.downloadQueue?.enqueue(self) - } - ) - } - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadToast.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadToast.swift index 28600d7a152d..a6574a8e9a50 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadToast.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/DownloadToast.swift @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import Foundation import Shared import SnapKit @@ -29,10 +30,8 @@ class DownloadToast: Toast { } } - var combinedBytesDownloaded: Int64 = 0 { - didSet { - updatePercent() - } + var combinedBytesDownloaded: Int64 { + downloads.reduce(0, { $0 + $1.receivedBytes }) } var combinedTotalBytesExpected: Int64? { @@ -71,22 +70,23 @@ class DownloadToast: Toast { ) } - var downloads: [Download] = [] + var downloads: [CWVDownloadTask] = [] let descriptionLabel = UILabel() var progressWidthConstraint: Constraint? - init(download: Download, completion: @escaping (_ buttonPressed: Bool) -> Void) { + init(download: CWVDownloadTask, completion: @escaping (_ buttonPressed: Bool) -> Void) { super.init(frame: .zero) self.completionHandler = completion self.clipsToBounds = true - self.combinedTotalBytesExpected = download.totalBytesExpected + self.combinedTotalBytesExpected = + download.totalBytes != CWVDownloadSizeUnknown ? download.totalBytes : nil self.downloads.append(download) - self.addSubview(createView(download.filename, descriptionText: self.descriptionText)) + self.addSubview(createView(download.suggestedFileName, descriptionText: self.descriptionText)) self.toastView.snp.makeConstraints { make in make.left.right.height.equalTo(self) @@ -102,14 +102,14 @@ class DownloadToast: Toast { fatalError("init(coder:) has not been implemented") } - func addDownload(_ download: Download) { + func addDownload(_ download: CWVDownloadTask) { downloads.append(download) if let combinedTotalBytesExpected = self.combinedTotalBytesExpected { - if let totalBytesExpected = download.totalBytesExpected { + if let totalBytesExpected = download.totalBytes != CWVDownloadSizeUnknown + ? download.totalBytes : nil + { self.combinedTotalBytesExpected = combinedTotalBytesExpected + totalBytesExpected - } else { - self.combinedTotalBytesExpected = nil } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/FingerprintingProtection.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/FingerprintingProtection.swift index bf07db9d86a3..669d07899c0b 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/FingerprintingProtection.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/FingerprintingProtection.swift @@ -8,12 +8,6 @@ import Foundation import WebKit class FingerprintingProtection: TabContentScript { - fileprivate weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "FingerprintingProtection" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -34,15 +28,14 @@ class FingerprintingProtection: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } - if let stats = self.tab?.contentBlocker.stats { - self.tab?.contentBlocker.stats = stats.adding(fingerprintingCount: 1) - BraveGlobalShieldStats.shared.fpProtection += 1 - } + let stats = tab.contentBlocker.stats + tab.contentBlocker.stats = stats.adding(fingerprintingCount: 1) + BraveGlobalShieldStats.shared.fpProtection += 1 } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/HTTPBlockedHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/HTTPBlockedHandler.swift deleted file mode 100644 index ee2aafc31f6d..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/HTTPBlockedHandler.swift +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2024 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveShared -import BraveShields -import Foundation -import Shared -import WebKit - -public class HTTPBlockedHandler: InternalSchemeResponse { - public static let path = InternalURL.Path.httpBlocked.rawValue - - public init() {} - - public func response(forRequest request: URLRequest) async -> (URLResponse, Data)? { - guard let url = request.url, let internalURL = InternalURL(url), - let originalURL = internalURL.extractedUrlParam - else { return nil } - let response = InternalSchemeHandler.response(forUrl: internalURL.url) - - guard let asset = Bundle.module.url(forResource: "HTTPBlocked", withExtension: "html") else { - assert(false) - return nil - } - - guard var html = await AsyncFileManager.default.utf8Contents(at: asset) else { - assert(false) - return nil - } - - html = - html - .replacingOccurrences( - of: "%page_title%", - with: Strings.Shields.siteIsNotSecure - ) - .replacingOccurrences( - of: "%blocked_title%", - with: String.localizedStringWithFormat( - Strings.Shields.theConnectionIsNotSecure, - "\(originalURL.domainURL.absoluteDisplayString)" - ) - ) - .replacingOccurrences( - of: "%blocked_description%", - with: Strings.Shields.httpBlockedDescription - ) - .replacingOccurrences( - of: "%learn_more%", - with: Strings.learnMore - ) - .replacingOccurrences( - of: "%proceed_action%", - with: Strings.Shields.domainBlockedProceedAction - ) - .replacingOccurrences(of: "%go_back_action%", with: Strings.Shields.domainBlockedGoBackAction) - .replacingOccurrences( - of: "%message_handler%", - with: HTTPBlockedScriptHandler.messageHandlerName - ) - .replacingOccurrences(of: "%security_token%", with: UserScriptManager.securityToken) - - html = html.replacingOccurrences( - of: "", - with: "" - ) - - let data = Data(html.utf8) - return (response, data) - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/ReaderModeHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/ReaderModeHandler.swift index 697792c16842..c0b4516851ce 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/ReaderModeHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/ReaderModeHandler.swift @@ -3,6 +3,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared import Foundation import Shared @@ -10,12 +11,14 @@ import WebKit public class ReaderModeHandler: InternalSchemeResponse { private let profile: Profile + private let braveCore: BraveCoreMain public static let path = InternalURL.Path.readermode.rawValue internal static var readerModeCache: ReaderModeCache = DiskReaderModeCache.sharedInstance private static let readerModeStyleHash = "sha256-L2W8+0446ay9/L1oMrgucknQXag570zwgQrHwE68qbQ=" - public init(profile: Profile) { + public init(profile: Profile, braveCore: BraveCoreMain) { self.profile = profile + self.braveCore = braveCore } public func response(forRequest request: URLRequest) async -> (URLResponse, Data)? { @@ -171,7 +174,8 @@ public class ReaderModeHandler: InternalSchemeResponse { // become available. ReadabilityService.sharedInstance.process( readerModeUrl, - cache: ReaderModeHandler.readerModeCache + cache: ReaderModeHandler.readerModeCache, + braveCore: braveCore ) if let asset = Bundle.module.url(forResource: "ReaderViewLoading", withExtension: "html"), var contents = await AsyncFileManager.default.utf8Contents(at: asset) diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/SessionRestoreHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/SessionRestoreHandler.swift index 1ebe19fe8348..956e1bf58746 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/SessionRestoreHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Handlers/SessionRestoreHandler.swift @@ -7,23 +7,6 @@ import Foundation import Shared import WebKit -private let apostropheEncoded = "%27" - -extension WKWebView { - // Use JS to redirect the page without adding a history entry - func replaceLocation(with url: URL) { - let safeUrl = url.absoluteString.replacingOccurrences(of: "'", with: apostropheEncoded) - evaluateSafeJavaScript( - functionName: "location.replace", - args: ["'\(safeUrl)'"], - contentWorld: .defaultClient, - escapeArgs: false, - asFunction: true, - completion: nil - ) - } -} - extension InternalSchemeResponse { func generateInvalidSchemeResponse(url: String, for originURL: URL) -> (URLResponse, Data)? { // Same validation as in WKNavigationDelegate -> decidePolicyFor @@ -61,6 +44,7 @@ extension InternalSchemeResponse { return invalidSchemeResponse } + let apostropheEncoded = "%27" urlString = urlString.replacingOccurrences(of: "'", with: apostropheEncoded) let html = """ diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ErrorPageHelper.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ErrorPageHelper.swift deleted file mode 100644 index a7b4f7ef32bf..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ErrorPageHelper.swift +++ /dev/null @@ -1,218 +0,0 @@ -import BraveShared -import CertificateUtilities -import Foundation -import Shared -import Storage -import WebKit - -struct ErrorPageModel { - let requestURL: URL - let originalURL: URL - let components: URLComponents - let errorCode: Int - let description: String - let originalHost: String - let domain: String - let error: NSError - - init?(request: URLRequest) { - guard let requestUrl = request.url, - let originalUrl = InternalURL(requestUrl)?.originalURLFromErrorPage - else { - return nil - } - - guard let components = URLComponents(url: requestUrl, resolvingAgainstBaseURL: false), - let code = components.valueForQuery("code"), - let errCode = Int(code), - let errDescription = components.valueForQuery("description"), - let errURLDomain = originalUrl.host, - let errDomain = components.valueForQuery("domain") - else { - return nil - } - - self.requestURL = requestUrl - self.originalURL = originalUrl - self.components = components - self.errorCode = errCode - self.description = errDescription - self.originalHost = errURLDomain - self.domain = errDomain - - var userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: errDescription - ] - - if let encodedCertificate = components.valueForQuery("certerror") { - userInfo["NSErrorPeerCertificateChainKey"] = encodedCertificate - } - - self.error = NSError(domain: errDomain, code: errCode, userInfo: userInfo) - } -} - -public class ErrorPageHandler: InternalSchemeResponse { - public static let path = InternalURL.Path.errorpage.rawValue - - private let errorHandlers: [InterstitialPageHandler] = [ - CertificateErrorPageHandler(), - NetworkErrorPageHandler(), - GenericErrorPageHandler(), - ] - - public func response(forRequest request: URLRequest) async -> (URLResponse, Data)? { - guard let model = ErrorPageModel(request: request) else { - return nil - } - return await errorHandlers.filter({ $0.canHandle(error: model.error) }).first?.response( - for: model - ) - } - - public init() {} -} - -class ErrorPageHelper { - - fileprivate weak var certStore: CertStore? - - init(certStore: CertStore?) { - self.certStore = certStore - } - - func loadPage(_ error: NSError, forUrl url: URL, inWebView webView: WKWebView) { - guard var components = URLComponents(string: "\(InternalURL.baseUrl)/\(ErrorPageHandler.path)") - else { - return - } - - // In rare cases the web view's url might be nil, like when opening a non existing website via share menu. - // In this case we fall back to the failing url. - let webViewUrl = webView.url ?? url - - // Page has failed to load again, just return and keep showing the existing error page. - if let internalUrl = InternalURL(webViewUrl), internalUrl.originalURLFromErrorPage == url { - return - } - - var queryItems = [ - URLQueryItem(name: InternalURL.Param.url.rawValue, value: url.absoluteString), - URLQueryItem(name: "code", value: String(error.code)), - URLQueryItem(name: "domain", value: error.domain), - URLQueryItem(name: "description", value: error.localizedDescription), - // 'timestamp' is used for the js reload logic - URLQueryItem(name: "timestamp", value: "\(Int(Date().timeIntervalSince1970 * 1000))"), - ] - - // If this is an invalid certificate, show a certificate error allowing the - // user to go back or continue. The certificate itself is encoded and added as - // a query parameter to the error page URL; we then read the certificate from - // the URL if the user wants to continue. - if CertificateErrorPageHandler.isValidCertificateError(error: error), - let certChain = error.userInfo["NSErrorPeerCertificateChainKey"] as? [SecCertificate], - let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError, - let certErrorCode = underlyingError.userInfo["_kCFStreamErrorCodeKey"] as? Int - { - let encodedCerts = ErrorPageHelper.encodeCertChain(certChain) - queryItems.append(URLQueryItem(name: "badcerts", value: encodedCerts)) - - let certError = CertificateErrorPageHandler.CertErrorCodes[OSStatus(certErrorCode)] ?? "" - queryItems.append(URLQueryItem(name: "certerror", value: String(certError))) - } - - components.queryItems = queryItems - if let urlWithQuery = components.url { - if let internalUrl = InternalURL(webViewUrl), internalUrl.isSessionRestore, - let page = InternalURL.authorize(url: urlWithQuery) - { - // A session restore page is already on the history stack, so don't load another page on the history stack. - webView.replaceLocation(with: page) - } else { - // A new page needs to be added to the history stack (i.e. the simple case of trying to navigate to an url for the first time and it fails, without pushing a page on the history stack, the webview will just show the current page). - webView.load(PrivilegedRequest(url: urlWithQuery) as URLRequest) - } - } - } -} - -extension ErrorPageHelper { - static func errorCode(for url: URL) -> Int { - // ErrorCode is zero if there's no error. - // Non-Zero (negative or positive) when there is an error - - if InternalURL.isValid(url: url), - let internalUrl = InternalURL(url), - internalUrl.isErrorPage - { - - let query = url.getQuery() - guard let code = query["code"], - let errCode = Int(code) - else { - return 0 - } - - return errCode - } - return 0 - } - - static func certificateError(for url: URL) -> Int { - let errCode = errorCode(for: url) - - // ErrorCode is zero if there's no error. - // Non-Zero (negative or positive) when there is an error - if errCode != 0 { - if let code = CFNetworkErrors(rawValue: Int32(errCode)), - CertificateErrorPageHandler.CFNetworkErrorsCertErrors.contains(code) - { - return errCode - } - - if CertificateErrorPageHandler.NSURLCertErrors.contains(errCode) { - return errCode - } - - if CertificateErrorPageHandler.CertErrorCodes[OSStatus(errCode)] != nil { - return errCode - } - return 0 - } - return 0 - } - - static func hasCertificates(for url: URL) -> Bool { - return (url as NSURL).valueForQueryParameter(key: "badcerts") != nil - } - - static func serverTrust(from errorURL: URL) throws -> SecTrust? { - guard let internalUrl = InternalURL(errorURL), - internalUrl.isErrorPage, - let originalURL = internalUrl.originalURLFromErrorPage - else { - return nil - } - - guard let certs = CertificateErrorPageHandler.certsFromErrorURL(errorURL), - !certs.isEmpty, - let host = originalURL.host - else { - return nil - } - - return try BraveCertificateUtils.createServerTrust(certs, for: host) - } - - private static func encodeCertChain(_ certificates: [SecCertificate]) -> String { - let certs = certificates.map({ - (SecCertificateCopyData($0) as Data).base64EncodedString - }) - - return certs.joined(separator: ",") - } - - func addCertificate(_ cert: SecCertificate, forOrigin origin: String) { - certStore?.addCertificate(cert, forOrigin: origin) - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/MetadataParserHelper.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/MetadataParserHelper.swift index eb4cd21ea1ea..d928d95d6cc1 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/MetadataParserHelper.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/MetadataParserHelper.swift @@ -26,7 +26,8 @@ class MetadataParserHelper: TabEventHandler { // Get the metadata out of the page-metadata-parser, and into a type safe struct as soon // as possible. guard let webView = tab.webView, - let url = webView.url, url.isWebPage(includeDataURIs: false), !InternalURL.isValid(url: url) + let url = webView.lastCommittedURL, url.isWebPage(includeDataURIs: false), + !InternalURL.isValid(url: url) else { // TabEvent.post(.pageMetadataNotAvailable, for: tab) tab.pageMetadata = nil diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/OpenInHelper.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/OpenInHelper.swift index e9029ffce9cb..492601e4f8bb 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/OpenInHelper.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/OpenInHelper.swift @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared import Foundation import MobileCoreServices @@ -49,57 +50,26 @@ extension String { } class DownloadHelper: NSObject { - fileprivate let request: URLRequest - fileprivate let preflightResponse: URLResponse - fileprivate let cookieStore: WKHTTPCookieStore + fileprivate let task: CWVDownloadTask - required init?( - request: URLRequest?, - response: URLResponse, - cookieStore: WKHTTPCookieStore, - canShowInWebView: Bool, - forceDownload: Bool - ) { - guard let request = request else { - return nil - } - - let contentDisposition = (response as? HTTPURLResponse)?.value( - forHTTPHeaderField: "Content-Disposition" - ) - let mimeType = response.mimeType ?? MIMEType.octetStream - let isAttachment = - contentDisposition?.starts(with: "attachment") ?? (mimeType == MIMEType.octetStream) - - guard isAttachment || !canShowInWebView || forceDownload else { - return nil - } - - self.cookieStore = cookieStore - self.request = request - self.preflightResponse = response + required init(task: CWVDownloadTask) { + self.task = task } func downloadAlert( from view: UIView, - okAction: @escaping (HTTPDownload) -> Void + okAction: @escaping () -> Void ) -> UIAlertController? { - guard let host = request.url?.host, let filename = request.url?.lastPathComponent else { + guard let host = task.originalURL.host else { return nil } - let download = HTTPDownload( - cookieStore: cookieStore, - preflightResponse: preflightResponse, - request: request - ) - let expectedSize = - download.totalBytesExpected != nil - ? ByteCountFormatter.string(fromByteCount: download.totalBytesExpected!, countStyle: .file) + task.totalBytes != CWVDownloadSizeUnknown + ? ByteCountFormatter.string(fromByteCount: task.totalBytes, countStyle: .file) : nil - let title = "\(filename) - \(host)" + let title = "\(task.suggestedFileName) - \(host)" let downloadAlert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) @@ -110,7 +80,7 @@ class DownloadHelper: NSObject { } let okAction = UIAlertAction(title: downloadActionText, style: .default) { _ in - okAction(download) + okAction() } let cancelAction = UIAlertAction(title: Strings.cancelButtonTitle, style: .cancel) @@ -126,6 +96,35 @@ class DownloadHelper: NSObject { return downloadAlert } + + func cancelDownloadAlert(from view: UIView, okAction: @escaping () -> Void) -> UIAlertController? + { + let alert = UIAlertController( + title: Strings.downloadAlreadyInProgressToastLabelTitle, + message: Strings.downloadAlreadyInProgressToastLabelText, + preferredStyle: .alert + ) + + alert.addAction( + UIAlertAction( + title: Strings.OKString, + style: .default, + handler: { _ in + okAction() + } + ) + ) + + alert.addAction(UIAlertAction(title: Strings.cancelButtonTitle, style: .cancel)) + + alert.popoverPresentationController?.do { + $0.sourceView = view + $0.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 16, width: 0, height: 0) + $0.permittedArrowDirections = [] + } + + return alert + } } class OpenPassBookHelper: NSObject { @@ -134,17 +133,14 @@ class OpenPassBookHelper: NSObject { fileprivate let browserViewController: BrowserViewController required init?( - request: URLRequest?, - response: URLResponse, - canShowInWebView: Bool, - forceDownload: Bool, + mimeType: String, + url: URL, browserViewController: BrowserViewController ) { - guard let mimeType = response.mimeType, mimeType == MIMEType.passbook, - PKAddPassesViewController.canAddPasses(), - let responseURL = response.url, !forceDownload + guard mimeType == MIMEType.passbook, + PKAddPassesViewController.canAddPasses() else { return nil } - self.url = responseURL + self.url = url self.browserViewController = browserViewController super.init() } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ScreenshotHelper.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ScreenshotHelper.swift index 312fb5c7d90f..5a1985f8d980 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ScreenshotHelper.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/ScreenshotHelper.swift @@ -33,16 +33,9 @@ class ScreenshotHelper { tab.setScreenshot(nil) } } else { - let configuration = WKSnapshotConfiguration() - // This is for a bug in certain iOS 13 versions, snapshots cannot be taken correctly without this boolean being set - configuration.afterScreenUpdates = false - - webView.takeSnapshot(with: configuration) { [weak tab] image, error in + webView.takeSnapshot(with: webView.bounds) { [weak tab] image in if let image = image { tab?.setScreenshot(image) - } else if let error = error { - Logger.module.error("\(error.localizedDescription)") - tab?.setScreenshot(nil) } else { Logger.module.error("Cannot snapshot Tab Screenshot - No error description") tab?.setScreenshot(nil) diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/CertificateErrorPageHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/CertificateErrorPageHandler.swift deleted file mode 100644 index 326bb5cf3a0c..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/CertificateErrorPageHandler.swift +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2021 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveShared -import Foundation -import Shared - -extension CFNetworkErrors { - static let braveCertificatePinningFailed = CFNetworkErrors(rawValue: Int32.min)! -} - -class CertificateErrorPageHandler: InterstitialPageHandler { - func canHandle(error: NSError) -> Bool { - return CertificateErrorPageHandler.isValidCertificateError(error: error) - } - - func response(for model: ErrorPageModel) async -> (URLResponse, Data)? { - let hasCertificate = model.components.valueForQuery("certerror") != nil - - guard let asset = Bundle.module.url(forResource: "CertificateError", withExtension: "html") - else { - assert(false) - return nil - } - - guard var html = await AsyncFileManager.default.utf8Contents(at: asset) else { - assert(false) - return nil - } - - var domain = model.domain - - // Update the error code domain - if domain == kCFErrorDomainCFNetwork as String, - let code = CFNetworkErrors(rawValue: Int32(model.errorCode)) - { - domain = GenericErrorPageHandler.CFErrorToName(code) - } else if domain == NSURLErrorDomain { - domain = GenericErrorPageHandler.NSURLErrorToName(model.errorCode) - } - - let host = model.originalURL.normalizedHost(stripWWWSubdomainOnly: true) ?? model.originalHost - let isBadRoot = model.errorCode == CFNetworkErrors.braveCertificatePinningFailed.rawValue - - var variables = [String: String]() - if hasCertificate { - variables = [ - "page_title": host, - "allow_bypass": "\(!isBadRoot)", - "error_code": "\(model.errorCode)", - "error_title": Strings.errorPagesCertWarningTitle, - "error_description": String(format: Strings.errorPagesAdvancedWarningTitle, host), - "error_more_details_description": isBadRoot - ? String(format: Strings.errorPagesAdvancedErrorPinningDetails, host, host, host, host) - : String(format: Strings.errorPagesAdvancedWarningDetails, host), - "error_domain": domain, - "learn_more": Strings.errorPagesLearnMoreButton, - "more_details": Strings.errorPagesMoreDetailsButton, - "hide_details": Strings.errorPagesHideDetailsButton, - "back_to_safety_or_reload": isBadRoot - ? Strings.errorPageReloadButtonTitle : Strings.errorPagesBackToSafetyButton, - "visit_unsafe": String(format: Strings.errorPagesProceedAnywayButton, host), - "has_certificate": "\(hasCertificate)", - "message_handler": ErrorPageHelper.messageHandlerName, - "security_token": ErrorPageHelper.scriptId, - "actions": - "", - ] - } else { - variables = [ - "page_title": host, - "allow_bypass": "\(!isBadRoot)", - "error_code": "\(model.errorCode)", - "error_title": Strings.errorPagesCertErrorTitle, - "error_description": model.description, - "error_more_details_description": "", - "error_domain": domain, - "learn_more": "", - "more_details": "", - "hide_details": "", - "back_to_safety_or_reload": "", - "visit_unsafe": "", - "has_certificate": "\(hasCertificate)", - "message_handler": ErrorPageHelper.messageHandlerName, - "security_token": ErrorPageHelper.scriptId, - "actions": "", - ] - } - - variables.forEach { (arg, value) in - html = html.replacingOccurrences(of: "%\(arg)%", with: value) - } - - let data = Data(html.utf8) - let response = InternalSchemeHandler.response(forUrl: model.originalURL) - return (response, data) - } - - static func certsFromErrorURL(_ url: URL) -> [SecCertificate]? { - func getCerts(_ url: URL) -> [SecCertificate]? { - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if let encodedCerts = components?.queryItems?.filter({ $0.name == "badcerts" }).first?.value? - .split(separator: ",") - { - - return encodedCerts.compactMap({ - guard let certData = Data(base64Encoded: String($0), options: []) else { - return nil - } - - return SecCertificateCreateWithData(nil, certData as CFData) - }) - } - - return nil - } - - let result = getCerts(url) - if result != nil { - return result - } - - // Fallback case when the error url is nested, this happens when restoring an error url, it will be inside a 'sessionrestore' url. - // TODO: Investigate if we can restore directly as an error url and avoid the 'sessionrestore?url=' wrapping. - if let internalUrl = InternalURL(url), let url = internalUrl.extractedUrlParam { - return getCerts(url) - } - return nil - } - - static func isValidCertificateError(error: NSError) -> Bool { - // Handle CFNetwork Error - if error.domain == kCFErrorDomainCFNetwork as String, - let code = CFNetworkErrors(rawValue: Int32(error.code)) - { - return CertificateErrorPageHandler.CFNetworkErrorsCertErrors.contains(code) - } - - // Handle NSURLError - if error.domain == NSURLErrorDomain as String { - return CertificateErrorPageHandler.NSURLCertErrors.contains(error.code) - } - return false - } - - // Regardless of cause, cfurlErrorServerCertificateUntrusted is currently returned in all cases. - // Check the other cases in case this gets fixed in the future. - // NOTE: In rare cases like Bad Cipher algorithm, it can show cfurlErrorSecureConnectionFailed - // swift-format-ignore - static let CFNetworkErrorsCertErrors: [CFNetworkErrors] = [ - .cfurlErrorSecureConnectionFailed, - .cfurlErrorServerCertificateHasBadDate, - .cfurlErrorServerCertificateUntrusted, - .cfurlErrorServerCertificateHasUnknownRoot, - .cfurlErrorServerCertificateNotYetValid, - .cfurlErrorClientCertificateRejected, - .cfurlErrorClientCertificateRequired, - .braveCertificatePinningFailed, - ] - - // Regardless of cause, NSURLErrorServerCertificateUntrusted is currently returned in all cases. - // Check the other cases in case this gets fixed in the future. - // NOTE: In rare cases like Bad Cipher algorithm, it can show NSURLErrorSecureConnectionFailed - // swift-format-ignore - static let NSURLCertErrors = [ - NSURLErrorSecureConnectionFailed, - NSURLErrorServerCertificateHasBadDate, - NSURLErrorServerCertificateUntrusted, - NSURLErrorServerCertificateHasUnknownRoot, - NSURLErrorServerCertificateNotYetValid, - NSURLErrorClientCertificateRejected, - NSURLErrorClientCertificateRequired, - Int(CFNetworkErrors.braveCertificatePinningFailed.rawValue), - - // Apple lists `NSURLErrorCannotLoadFromNetwork` as an `SSL Error`, but I believe the documentation is incorrect as it is for any cache miss. - ] - - // From: Security.SecBase / - // kCFStreamErrorDomainSSL - // swift-format-ignore - static let CertErrorCodes = [ - errSSLProtocol: "SSL_PROTOCOL_ERROR", - errSSLNegotiation: "SSL_NEGOTIATION_ERROR", - errSSLFatalAlert: "SSL_FATAL_ERROR", - errSSLWouldBlock: "SSL_WOULD_BLOCK_ERROR", - errSSLSessionNotFound: "SSL_SESSION_NOT_FOUND_ERROR", - errSSLClosedGraceful: "SSL_CLOSED_GRACEFUL_ERROR", - errSSLClosedAbort: "SSL_CLOSED_ABORT_ERROR", - errSSLXCertChainInvalid: "SSL_INVALID_CERTIFICATE_CHAIN_ERROR", - errSSLBadCert: "SSL_BAD_CERTIFICATE_ERROR", - errSSLCrypto: "SSL_UNDERLYING_CRYPTO_ERROR", - errSSLInternal: "SSL_INTERNAL_ERROR", - errSSLModuleAttach: "SSL_MODULE_ATTACH_ERROR", - errSSLUnknownRootCert: "SSL_VALID_CERT_CHAIN_UNTRUSTED_ROOT_ERROR", - errSSLNoRootCert: "SSL_NO_ROOT_CERT_ERROR", // SEC_ERROR_UNKNOWN_ISSUER - errSSLCertExpired: "SSL_CERTIFICATE_EXPIRED_ERROR", // SEC_ERROR_EXPIRED_CERTIFICATE - errSSLCertNotYetValid: "SSL_CERT_IN_CHAIN_NOT_YET_VALID_ERROR", - errSSLClosedNoNotify: "SSL_SERVER_CLOSED_NO_NOTIFY_ERROR", - errSSLBufferOverflow: "SSL_BUFFER_OVERFLOW_ERROR", - errSSLBadCipherSuite: "SSL_BAD_CIPHER_SUITE_ERROR", - errSSLPeerUnexpectedMsg: "SSL_UNEXPECTED_PEER_MESSAGE_ERROR", - errSSLPeerBadRecordMac: "SSL_BAD_PEER_RECORD_MAC_ERROR", - errSSLPeerDecryptionFail: "SSL_PEER_DECRYPTION_FAIL_ERROR", - errSSLPeerRecordOverflow: "SSL_PEER_RECORD_OVERFLOW_ERROR", - errSSLPeerDecompressFail: "SSL_PEER_DECOMPRESS_FAIL_ERROR", - errSSLPeerHandshakeFail: "SSL_PEER_HANDSHAKE_FAIL_ERROR", - errSSLPeerBadCert: "SSL_PEER_BAD_CERTIFICATE_ERROR", - errSSLPeerUnsupportedCert: "SSL_PEER_UNSUPPORTED_CERTIFICATE_FORMAT_ERROR", - errSSLPeerCertRevoked: "SSL_PEER_CERTIFICATE_REVOKED_ERROR", - errSSLPeerCertExpired: "SSL_PEER_CERTIFICATE_EXPIRED_ERROR", - errSSLPeerCertUnknown: "SSL_PEER_CERTIFICATE_UNKNOWN_ERROR", - errSSLIllegalParam: "SSL_ILLEGAL_PARAMETER_ERROR", - errSSLPeerUnknownCA: "SSL_PEER_UNKNOWN_CERTIFICATE_AUTHORITY_ERROR", - errSSLPeerAccessDenied: "SSL_PEER_ACCESS_DENIED_ERROR", - errSSLPeerDecodeError: "SSL_PEER_DECODE_ERROR", - errSSLPeerDecryptError: "SSL_PEER_DECRYPT_ERROR", - errSSLPeerExportRestriction: "SSL_PEER_EXPORT_RESTRICTION_ERROR", - errSSLPeerProtocolVersion: "SSL_PEER_BAD_PROTOCOL_VERSION_ERROR", - errSSLPeerInsufficientSecurity: "SSL_PEER_INSUFFICIENT_SECURITY_ERROR", - errSSLPeerInternalError: "SSL_PEER_INTERNAL_ERROR", - errSSLPeerUserCancelled: "SSL_PEER_USER_CANCELLED_ERROR", - errSSLPeerNoRenegotiation: "SSL_PEER_NO_RENEGOTIATION_ALLOWED_ERROR", - errSSLPeerAuthCompleted: "SSL_PEER_AUTH_COMPLETED_ERROR", - errSSLClientCertRequested: "SSL_CLIENT_CERT_REQUESTED_ERROR", - errSSLHostNameMismatch: "SSL_HOST_NAME_MISMATCH_ERROR", // SSL_ERROR_BAD_CERT_DOMAIN - errSSLConnectionRefused: "SSL_CONNECTION_REFUSED_ERROR", - errSSLDecryptionFail: "SSL_DECRYPTION_FAIL_ERROR", - errSSLBadRecordMac: "SSL_BAD_RECORD_MAC_ERROR", - errSSLRecordOverflow: "SSL_RECORD_OVERFLOW_ERROR", - errSSLBadConfiguration: "SSL_BAD_CONFIGURATION_ERROR", - errSSLUnexpectedRecord: "SSL_UNEXPECTED_RECORD_ERROR", - errSSLWeakPeerEphemeralDHKey: "SSL_WEAK_PEER_EPHEMERAL_DH_KEY_ERROR", - errSSLClientHelloReceived: "SSL_CLIENT_HELLO_RECEIVED_ERROR", - errSSLTransportReset: "SSL_TRANSPORT_RESET_ERROR", - errSSLNetworkTimeout: "SSL_NETWORK_TIMEOUT_ERROR", - errSSLConfigurationFailed: "SSL_CONFIGURATION_FAILED_ERROR", - errSSLUnsupportedExtension: "SSL_UNSUPPORTED_EXTENSION_ERROR", - errSSLUnexpectedMessage: "SSL_UNEXPECTED_MESSAGE_ERROR", - errSSLDecompressFail: "SSL_DECOMPRESS_FAILED_ERROR", - errSSLHandshakeFail: "SSL_HANDSHAKE_FAIL_ERROR", - errSSLDecodeError: "SSL_DECODE_ERROR", - errSSLInappropriateFallback: "SSL_INAPPROPRIATE_FALLBACK_ERROR", - errSSLMissingExtension: "SSL_MISSING_EXTENSION_ERROR", - errSSLBadCertificateStatusResponse: "SSL_BAD_CERTIFICATE_STATUS_RESPONSE_ERROR", - errSSLCertificateRequired: "SSL_CERTIFICATE_REQUIRED_ERROR", - errSSLUnknownPSKIdentity: "SSL_UNKNOWN_PSK_IDENTITY_ERROR", - errSSLUnrecognizedName: "SSL_UNRECOGNIZED_NAME_ERROR", - errSSLATSViolation: "SSL_ATS_VIOLATION_ERROR", - errSSLATSMinimumVersionViolation: "SSL_ATS_MINIMUM_VERSION_VIOLATION_ERROR", - errSSLATSCiphersuiteViolation: "SSL_ATS_CIPHER_SUITE_VIOLATION_ERROR", - errSSLATSMinimumKeySizeViolation: "SSL_ATS_MINIMUM_KEY_SIZE_VIOLATION_ERROR", - errSSLATSLeafCertificateHashAlgorithmViolation: "SSL_LEAF_CERT_HASH_ALG_VIOLATION_ERROR", - errSSLATSCertificateHashAlgorithmViolation: "SSL_ATS_CERT_HASH_ALG_VIOLATION_ERROR", - errSSLATSCertificateTrustViolation: "SSL_ATS_CERT_TRUST_VIOLATION_ERROR", - errSSLEarlyDataRejected: "SSL_EARLY_DATA_REJECTION_ERROR", - ] -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/GenericErrorPageHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/GenericErrorPageHandler.swift deleted file mode 100644 index d473f87372c6..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/GenericErrorPageHandler.swift +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2021 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveShared -import Foundation -import Shared - -class GenericErrorPageHandler: InterstitialPageHandler { - func canHandle(error: NSError) -> Bool { - // Handle CFNetwork Error - if error.domain == kCFErrorDomainCFNetwork as String, - let code = CFNetworkErrors(rawValue: Int32(error.code)) - { - - let unhandledCodes: [CFNetworkErrors] = [ - // Handled by NetworkErrorPageHandler - .cfurlErrorNotConnectedToInternet, - - // Handled by CertificateErrorPageHandler - .cfurlErrorSecureConnectionFailed, - .cfurlErrorServerCertificateHasBadDate, - .cfurlErrorServerCertificateUntrusted, - .cfurlErrorServerCertificateHasUnknownRoot, - .cfurlErrorServerCertificateNotYetValid, - .cfurlErrorClientCertificateRejected, - .cfurlErrorClientCertificateRequired, - .braveCertificatePinningFailed, - ] - - return !unhandledCodes.contains(code) - } - - // Handle NSURLError - if error.domain == NSURLErrorDomain { - let unhandledCodes: [Int] = [ - // Handled by NetworkErrorPageHandler - NSURLErrorNotConnectedToInternet, - - // Handled by CertificateErrorPageHandler - NSURLErrorSecureConnectionFailed, - NSURLErrorServerCertificateHasBadDate, - NSURLErrorServerCertificateUntrusted, - NSURLErrorServerCertificateHasUnknownRoot, - NSURLErrorServerCertificateNotYetValid, - NSURLErrorClientCertificateRejected, - NSURLErrorClientCertificateRequired, - ] - return !unhandledCodes.contains(error.code) - } - - return true - } - - func response(for model: ErrorPageModel) async -> (URLResponse, Data)? { - guard let asset = Bundle.module.url(forResource: "GenericError", withExtension: "html") else { - assert(false) - return nil - } - - guard var html = await AsyncFileManager.default.utf8Contents(at: asset) else { - assert(false) - return nil - } - - var action = "" - var domain = model.domain - if domain == kCFErrorDomainCFNetwork as String, - let code = CFNetworkErrors(rawValue: Int32(model.errorCode)) - { - - // Update the error code domain - domain = GenericErrorPageHandler.CFErrorToName(code) - - // If there are too many redirects, show a `reload` action button - if code == .cfurlErrorHTTPTooManyRedirects { - action = """ - - - """ - } - } else if domain == NSURLErrorDomain { - // Update the error code domain - domain = GenericErrorPageHandler.NSURLErrorToName(model.errorCode) - } - - let variables = [ - "page_title": model.originalURL.normalizedHost(stripWWWSubdomainOnly: true) - ?? model.originalHost, - "error_code": "\(model.errorCode)", - "error_title": "This site can't be reached", - "error_description": model.description + "

\(Strings.errorPageCantBeReachedTry)", - "error_domain": domain, - "actions": action, - ] - - variables.forEach { (arg, value) in - html = html.replacingOccurrences(of: "%\(arg)%", with: value) - } - - let data = Data(html.utf8) - let response = InternalSchemeHandler.response(forUrl: model.originalURL) - return (response, data) - } - - // swift-format-ignore - /// Converts `CFNetworkErrors` to a `String`. - static func CFErrorToName(_ err: CFNetworkErrors) -> String { - switch err { - case .cfHostErrorHostNotFound: return "CFHostErrorHostNotFound" - case .cfHostErrorUnknown: return "CFHostErrorUnknown" - case .cfsocksErrorUnknownClientVersion: return "CFSOCKSErrorUnknownClientVersion" - case .cfsocksErrorUnsupportedServerVersion: return "CFSOCKSErrorUnsupportedServerVersion" - case .cfsocks4ErrorRequestFailed: return "CFSOCKS4ErrorRequestFailed" - case .cfsocks4ErrorIdentdFailed: return "CFSOCKS4ErrorIdentdFailed" - case .cfsocks4ErrorIdConflict: return "CFSOCKS4ErrorIdConflict" - case .cfsocks4ErrorUnknownStatusCode: return "CFSOCKS4ErrorUnknownStatusCode" - case .cfsocks5ErrorBadState: return "CFSOCKS5ErrorBadState" - case .cfsocks5ErrorBadResponseAddr: return "CFSOCKS5ErrorBadResponseAddr" - case .cfsocks5ErrorBadCredentials: return "CFSOCKS5ErrorBadCredentials" - case .cfsocks5ErrorUnsupportedNegotiationMethod: - return "CFSOCKS5ErrorUnsupportedNegotiationMethod" - case .cfsocks5ErrorNoAcceptableMethod: return "CFSOCKS5ErrorNoAcceptableMethod" - case .cfftpErrorUnexpectedStatusCode: return "CFFTPErrorUnexpectedStatusCode" - case .cfErrorHTTPAuthenticationTypeUnsupported: - return "CFErrorHTTPAuthenticationTypeUnsupported" - case .cfErrorHTTPBadCredentials: return "CFErrorHTTPBadCredentials" - case .cfErrorHTTPConnectionLost: return "CFErrorHTTPConnectionLost" - case .cfErrorHTTPParseFailure: return "CFErrorHTTPParseFailure" - case .cfErrorHTTPRedirectionLoopDetected: return "CFErrorHTTPRedirectionLoopDetected" - case .cfErrorHTTPBadURL: return "CFErrorHTTPBadURL" - case .cfErrorHTTPProxyConnectionFailure: return "CFErrorHTTPProxyConnectionFailure" - case .cfErrorHTTPBadProxyCredentials: return "CFErrorHTTPBadProxyCredentials" - case .cfErrorPACFileError: return "CFErrorPACFileError" - case .cfErrorPACFileAuth: return "CFErrorPACFileAuth" - case .cfErrorHTTPSProxyConnectionFailure: return "CFErrorHTTPSProxyConnectionFailure" - case .cfStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod: - return "CFStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod" - - case .cfurlErrorBackgroundSessionInUseByAnotherProcess: - return "CFURLErrorBackgroundSessionInUseByAnotherProcess" - case .cfurlErrorBackgroundSessionWasDisconnected: - return "CFURLErrorBackgroundSessionWasDisconnected" - case .cfurlErrorUnknown: return "CFURLErrorUnknown" - case .cfurlErrorCancelled: return "CFURLErrorCancelled" - case .cfurlErrorBadURL: return "CFURLErrorBadURL" - case .cfurlErrorTimedOut: return "CFURLErrorTimedOut" - case .cfurlErrorUnsupportedURL: return "CFURLErrorUnsupportedURL" - case .cfurlErrorCannotFindHost: return "CFURLErrorCannotFindHost" - case .cfurlErrorCannotConnectToHost: return "CFURLErrorCannotConnectToHost" - case .cfurlErrorNetworkConnectionLost: return "CFURLErrorNetworkConnectionLost" - case .cfurlErrorDNSLookupFailed: return "CFURLErrorDNSLookupFailed" - case .cfurlErrorHTTPTooManyRedirects: return "CFURLErrorHTTPTooManyRedirects" - case .cfurlErrorResourceUnavailable: return "CFURLErrorResourceUnavailable" - case .cfurlErrorNotConnectedToInternet: return "CFURLErrorNotConnectedToInternet" - case .cfurlErrorRedirectToNonExistentLocation: return "CFURLErrorRedirectToNonExistentLocation" - case .cfurlErrorBadServerResponse: return "CFURLErrorBadServerResponse" - case .cfurlErrorUserCancelledAuthentication: return "CFURLErrorUserCancelledAuthentication" - case .cfurlErrorUserAuthenticationRequired: return "CFURLErrorUserAuthenticationRequired" - case .cfurlErrorZeroByteResource: return "CFURLErrorZeroByteResource" - case .cfurlErrorCannotDecodeRawData: return "CFURLErrorCannotDecodeRawData" - case .cfurlErrorCannotDecodeContentData: return "CFURLErrorCannotDecodeContentData" - case .cfurlErrorCannotParseResponse: return "CFURLErrorCannotParseResponse" - case .cfurlErrorInternationalRoamingOff: return "CFURLErrorInternationalRoamingOff" - case .cfurlErrorCallIsActive: return "CFURLErrorCallIsActive" - case .cfurlErrorDataNotAllowed: return "CFURLErrorDataNotAllowed" - case .cfurlErrorRequestBodyStreamExhausted: return "CFURLErrorRequestBodyStreamExhausted" - case .cfurlErrorFileDoesNotExist: return "CFURLErrorFileDoesNotExist" - case .cfurlErrorFileIsDirectory: return "CFURLErrorFileIsDirectory" - case .cfurlErrorNoPermissionsToReadFile: return "CFURLErrorNoPermissionsToReadFile" - case .cfurlErrorDataLengthExceedsMaximum: return "CFURLErrorDataLengthExceedsMaximum" - case .cfurlErrorSecureConnectionFailed: return "CFURLErrorSecureConnectionFailed" - case .cfurlErrorServerCertificateHasBadDate: return "CFURLErrorServerCertificateHasBadDate" - case .cfurlErrorServerCertificateUntrusted: return "CFURLErrorServerCertificateUntrusted" - case .cfurlErrorServerCertificateHasUnknownRoot: - return "CFURLErrorServerCertificateHasUnknownRoot" - case .cfurlErrorServerCertificateNotYetValid: return "CFURLErrorServerCertificateNotYetValid" - case .cfurlErrorClientCertificateRejected: return "CFURLErrorClientCertificateRejected" - case .cfurlErrorClientCertificateRequired: return "CFURLErrorClientCertificateRequired" - case .cfurlErrorCannotLoadFromNetwork: return "CFURLErrorCannotLoadFromNetwork" - case .cfurlErrorCannotCreateFile: return "CFURLErrorCannotCreateFile" - case .cfurlErrorCannotOpenFile: return "CFURLErrorCannotOpenFile" - case .cfurlErrorCannotCloseFile: return "CFURLErrorCannotCloseFile" - case .cfurlErrorCannotWriteToFile: return "CFURLErrorCannotWriteToFile" - case .cfurlErrorCannotRemoveFile: return "CFURLErrorCannotRemoveFile" - case .cfurlErrorCannotMoveFile: return "CFURLErrorCannotMoveFile" - case .cfurlErrorDownloadDecodingFailedMidStream: - return "CFURLErrorDownloadDecodingFailedMidStream" - case .cfurlErrorDownloadDecodingFailedToComplete: - return "CFURLErrorDownloadDecodingFailedToComplete" - - case .cfhttpCookieCannotParseCookieFile: return "CFHTTPCookieCannotParseCookieFile" - case .cfNetServiceErrorUnknown: return "CFNetServiceErrorUnknown" - case .cfNetServiceErrorCollision: return "CFNetServiceErrorCollision" - case .cfNetServiceErrorNotFound: return "CFNetServiceErrorNotFound" - case .cfNetServiceErrorInProgress: return "CFNetServiceErrorInProgress" - case .cfNetServiceErrorBadArgument: return "CFNetServiceErrorBadArgument" - case .cfNetServiceErrorCancel: return "CFNetServiceErrorCancel" - case .cfNetServiceErrorInvalid: return "CFNetServiceErrorInvalid" - case .cfNetServiceErrorTimeout: return "CFNetServiceErrorTimeout" - case .cfNetServiceErrorDNSServiceFailure: return "CFNetServiceErrorDNSServiceFailure" - - case .braveCertificatePinningFailed: return "ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN" - default: return "Unknown: \(err.rawValue)" - } - } - - // swift-format-ignore - /// Converts `NSURLError` to a `String`. - static func NSURLErrorToName(_ err: Int) -> String { - switch err { - case NSURLErrorUnknown: return "NSURLErrorUnknown" - case NSURLErrorCancelled: return "NSURLErrorCancelled" - case NSURLErrorBadURL: return "NSURLErrorBadURL" - case NSURLErrorTimedOut: return "NSURLErrorTimedOut" - case NSURLErrorUnsupportedURL: return "NSURLErrorUnsupportedURL" - case NSURLErrorCannotFindHost: return "NSURLErrorCannotFindHost" - case NSURLErrorCannotConnectToHost: return "NSURLErrorCannotConnectToHost" - case NSURLErrorNetworkConnectionLost: return "NSURLErrorNetworkConnectionLost" - case NSURLErrorDNSLookupFailed: return "NSURLErrorDNSLookupFailed" - case NSURLErrorHTTPTooManyRedirects: return "NSURLErrorHTTPTooManyRedirects" - case NSURLErrorResourceUnavailable: return "NSURLErrorResourceUnavailable" - case NSURLErrorNotConnectedToInternet: return "NSURLErrorNotConnectedToInternet" - case NSURLErrorRedirectToNonExistentLocation: return "NSURLErrorRedirectToNonExistentLocation" - case NSURLErrorBadServerResponse: return "NSURLErrorBadServerResponse" - case NSURLErrorUserCancelledAuthentication: return "NSURLErrorUserCancelledAuthentication" - case NSURLErrorUserAuthenticationRequired: return "NSURLErrorUserAuthenticationRequired" - case NSURLErrorZeroByteResource: return "NSURLErrorZeroByteResource" - case NSURLErrorCannotDecodeRawData: return "NSURLErrorCannotDecodeRawData" - case NSURLErrorCannotDecodeContentData: return "NSURLErrorCannotDecodeContentData" - case NSURLErrorCannotParseResponse: return "NSURLErrorCannotParseResponse" - case NSURLErrorAppTransportSecurityRequiresSecureConnection: - return "NSURLErrorAppTransportSecurityRequiresSecureConnection" - case NSURLErrorFileDoesNotExist: return "NSURLErrorFileDoesNotExist" - case NSURLErrorFileIsDirectory: return "NSURLErrorFileIsDirectory" - case NSURLErrorNoPermissionsToReadFile: return "NSURLErrorNoPermissionsToReadFile" - case NSURLErrorDataLengthExceedsMaximum: return "NSURLErrorDataLengthExceedsMaximum" - case NSURLErrorFileOutsideSafeArea: return "NSURLErrorFileOutsideSafeArea" - - // SSL errors - case NSURLErrorSecureConnectionFailed: return "NSURLErrorSecureConnectionFailed" - case NSURLErrorServerCertificateHasBadDate: return "NSURLErrorServerCertificateHasBadDate" - case NSURLErrorServerCertificateUntrusted: return "NSURLErrorServerCertificateUntrusted" - case NSURLErrorServerCertificateHasUnknownRoot: - return "NSURLErrorServerCertificateHasUnknownRoot" - case NSURLErrorServerCertificateNotYetValid: return "NSURLErrorServerCertificateNotYetValid" - case NSURLErrorClientCertificateRejected: return "NSURLErrorClientCertificateRejected" - case NSURLErrorClientCertificateRequired: return "NSURLErrorClientCertificateRequired" - case NSURLErrorCannotLoadFromNetwork: return "NSURLErrorCannotLoadFromNetwork" - - // Download and file I/O errors - case NSURLErrorCannotCreateFile: return "NSURLErrorCannotCreateFile" - case NSURLErrorCannotOpenFile: return "NSURLErrorCannotOpenFile" - case NSURLErrorCannotCloseFile: return "NSURLErrorCannotCloseFile" - case NSURLErrorCannotWriteToFile: return "NSURLErrorCannotWriteToFile" - case NSURLErrorCannotRemoveFile: return "NSURLErrorCannotRemoveFile" - case NSURLErrorCannotMoveFile: return "NSURLErrorCannotMoveFile" - case NSURLErrorDownloadDecodingFailedMidStream: - return "NSURLErrorDownloadDecodingFailedMidStream" - case NSURLErrorDownloadDecodingFailedToComplete: - return "NSURLErrorDownloadDecodingFailedToComplete" - case NSURLErrorInternationalRoamingOff: return "NSURLErrorInternationalRoamingOff" - case NSURLErrorCallIsActive: return "NSURLErrorCallIsActive" - case NSURLErrorDataNotAllowed: return "NSURLErrorDataNotAllowed" - case NSURLErrorRequestBodyStreamExhausted: return "NSURLErrorRequestBodyStreamExhausted" - case NSURLErrorBackgroundSessionRequiresSharedContainer: - return "NSURLErrorBackgroundSessionRequiresSharedContainer" - case NSURLErrorBackgroundSessionInUseByAnotherProcess: - return "NSURLErrorBackgroundSessionInUseByAnotherProcess" - case NSURLErrorBackgroundSessionWasDisconnected: - return "NSURLErrorBackgroundSessionWasDisconnected" - - case Int(CFNetworkErrors.braveCertificatePinningFailed.rawValue): - return "ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN" - default: return "Unknown: \(err)" - } - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InternalSchemeHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InternalSchemeHandler.swift index a82847880a94..e9dec1db238a 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InternalSchemeHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InternalSchemeHandler.swift @@ -59,8 +59,6 @@ public class InternalSchemeHandler: NSObject, WKURLSchemeHandler { // interstitial "/interstitial-style/InterstitialStyles.css": "text/css", "/interstitial-style/BlockedDomain.css": "text/css", - "/interstitial-style/NetworkError.css": "text/css", - "/interstitial-style/CertificateError.css": "text/css", "/interstitial-style/Web3Domain.css": "text/css", "/interstitial-style/IPFSPreference.css": "text/css", "/interstitial-icon/Generic.svg": "image/svg+xml", diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InterstitialPageHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InterstitialPageHandler.swift deleted file mode 100644 index f6afafff8318..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/InterstitialPageHandler.swift +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2021 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Foundation - -protocol InterstitialPageHandler { - func canHandle(error: NSError) -> Bool - func response(for model: ErrorPageModel) async -> (URLResponse, Data)? -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/NetworkErrorPageHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/NetworkErrorPageHandler.swift deleted file mode 100644 index cee7a7ce8ccc..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Interstitial Pages/NetworkErrorPageHandler.swift +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2021 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveShared -import Foundation -import Shared - -class NetworkErrorPageHandler: InterstitialPageHandler { - static func isNetworkError(errorCode: Int) -> Bool { - if let code = CFNetworkErrors(rawValue: Int32(errorCode)) { - return code == .cfurlErrorNotConnectedToInternet - } - - return errorCode == NSURLErrorNotConnectedToInternet - } - - func canHandle(error: NSError) -> Bool { - // Handle CFNetwork Error - if error.domain == kCFErrorDomainCFNetwork as String, - let code = CFNetworkErrors(rawValue: Int32(error.code)) - { - - let handledCodes: [CFNetworkErrors] = [ - .cfurlErrorNotConnectedToInternet - ] - - return handledCodes.contains(code) - } - - // Handle NSURLError - if error.domain == NSURLErrorDomain as String { - let handledCodes: [Int] = [ - NSURLErrorNotConnectedToInternet - ] - - return handledCodes.contains(error.code) - } - return false - } - - func response(for model: ErrorPageModel) async -> (URLResponse, Data)? { - guard let asset = Bundle.module.url(forResource: "NetworkError", withExtension: "html") else { - assert(false) - return nil - } - - guard var html = await AsyncFileManager.default.utf8Contents(at: asset) else { - assert(false) - return nil - } - var domain = model.domain - - // Update the error code domain - if domain == kCFErrorDomainCFNetwork as String, - let code = CFNetworkErrors(rawValue: Int32(model.errorCode)) - { - domain = GenericErrorPageHandler.CFErrorToName(code) - } else if domain == NSURLErrorDomain { - domain = GenericErrorPageHandler.NSURLErrorToName(model.errorCode) - } - - let host = model.originalURL.normalizedHost(stripWWWSubdomainOnly: true) ?? model.originalHost - - let variables = [ - "page_title": host, - "error_code": "\(model.errorCode)", - "error_title": Strings.errorPagesNoInternetTitle, - "error_domain": domain, - "error_try_list": Strings.errorPagesNoInternetTry, - "error_list_1": Strings.errorPagesNoInternetTryItem1, - "error_list_2": Strings.errorPagesNoInternetTryItem2, - ] - - variables.forEach { (arg, value) in - html = html.replacingOccurrences(of: "%\(arg)%", with: value) - } - - let data = Data(html.utf8) - let response = InternalSchemeHandler.response(forUrl: model.originalURL) - return (response, data) - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift index 32b4083e8c9d..163684a4b38c 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift @@ -32,12 +32,16 @@ class LinkPreviewViewController: UIViewController { } currentTab = Tab( + wkConfiguration: .init(), configuration: tabWebView.configuration, type: parentTab.isPrivate ? .private : .regular, - tabGeneratorAPI: nil + contentScriptManager: .init(tabForWebView: { [weak self] _ in self?.currentTab }), + tabGeneratorAPI: nil, + browserPrefs: browserController?.braveCore.browserPrefs ).then { $0.tabDelegate = browserController $0.navigationDelegate = browserController + // FIXME: This should probably be treated as a pre-render or bottom-sheet web view which typically don't have all tab helpers attached $0.createWebview() $0.webView?.scrollView.layer.masksToBounds = true } @@ -54,7 +58,7 @@ class LinkPreviewViewController: UIViewController { Task(priority: .userInitiated) { let ruleLists = await AdBlockGroupsManager.shared.ruleLists(for: domain) for ruleList in ruleLists { - webView.configuration.userContentController.add(ruleList) + webView.wkConfiguration.userContentController.add(ruleList) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/PageZoom/PageZoomHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/PageZoom/PageZoomHandler.swift index cfc3fae5ce22..528deb9a998d 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/PageZoom/PageZoomHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/PageZoom/PageZoomHandler.swift @@ -38,7 +38,7 @@ class PageZoomHandler: ObservableObject { if let webView = webView { // Fetch the current value for zoom - if let url = webView.url, let domain = Domain.getPersistedDomain(for: url) { + if let url = webView.lastCommittedURL, let domain = Domain.getPersistedDomain(for: url) { currentValue = domain.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value } else { @@ -75,7 +75,7 @@ class PageZoomHandler: ObservableObject { private func storeChanges() { guard let webView = webView, - let url = webView.url + let url = webView.lastCommittedURL else { return } webView.setValue(currentValue, forKey: PageZoomHandler.propertyName) diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift index ccac21c3e7dc..39b209198ecd 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift @@ -5,6 +5,7 @@ import AVFoundation import AVKit +import BraveCore import BraveShared import Combine import CoreData @@ -47,6 +48,7 @@ class PlaylistListViewController: UIViewController { public var initialItemPlaybackOffset = 0.0 weak var delegate: PlaylistViewControllerDelegate? + private let braveCore: BraveCoreMain? private let playerView: VideoView let isPrivateBrowsing: Bool @@ -74,7 +76,8 @@ class PlaylistListViewController: UIViewController { $0.allowsSelectionDuringEditing = true } - init(playerView: VideoView, isPrivateBrowsing: Bool) { + init(braveCore: BraveCoreMain?, playerView: VideoView, isPrivateBrowsing: Bool) { + self.braveCore = braveCore self.playerView = playerView self.isPrivateBrowsing = isPrivateBrowsing super.init(nibName: nil, bundle: nil) @@ -800,7 +803,7 @@ extension PlaylistListViewController { item: model, viewForInvisibleWebView: self.playerView.window ?? self.playerView.superview ?? self.playerView, - webLoaderFactory: LivePlaylistWebLoaderFactory() + webLoaderFactory: LivePlaylistWebLoaderFactory(braveCore: self.braveCore) ) try Task.checkCancellation() diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift index 2aff7a8c5dc9..b1e2f1b3d467 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift @@ -5,6 +5,7 @@ import AVFoundation import AVKit +import BraveCore import CarPlay import Combine import CoreData @@ -46,6 +47,7 @@ class PlaylistViewController: UIViewController { // MARK: Properties + private let braveCore: BraveCoreMain? private let player: MediaPlayer private let playerView = VideoView() private let mediaStreamer: PlaylistMediaStreamer @@ -54,6 +56,7 @@ class PlaylistViewController: UIViewController { private let splitController = UISplitViewController() private let folderController = PlaylistFolderController() private lazy var listController = PlaylistListViewController( + braveCore: braveCore, playerView: playerView, isPrivateBrowsing: isPrivateBrowsing ) @@ -69,6 +72,7 @@ class PlaylistViewController: UIViewController { private var folderSharingUrl: String? init( + braveCore: BraveCoreMain?, openInNewTab: ((URL?, Bool, Bool) -> Void)?, openPlaylistSettingsMenu: (() -> Void)?, profile: Profile?, @@ -78,12 +82,13 @@ class PlaylistViewController: UIViewController { isPrivateBrowsing: Bool ) { + self.braveCore = braveCore self.openInNewTab = openInNewTab self.openPlaylistSettingsMenu = openPlaylistSettingsMenu self.player = mediaPlayer self.mediaStreamer = PlaylistMediaStreamer( playerView: playerView, - webLoaderFactory: LivePlaylistWebLoaderFactory() + webLoaderFactory: LivePlaylistWebLoaderFactory(braveCore: braveCore) ) self.isPrivateBrowsing = isPrivateBrowsing self.folderSharingUrl = nil diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift index db7637584cb8..7ed38eea543d 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift @@ -5,6 +5,7 @@ import AVFoundation import BraveCore +import BraveShared import BraveShields import Data import Foundation @@ -17,24 +18,33 @@ import WebKit import os.log class LivePlaylistWebLoaderFactory: PlaylistWebLoaderFactory { + let braveCore: BraveCoreMain? + + init(braveCore: BraveCoreMain?) { + self.braveCore = braveCore + } + func makeWebLoader() -> PlaylistWebLoader { - LivePlaylistWebLoader() + LivePlaylistWebLoader(braveCore: braveCore) } } class LivePlaylistWebLoader: UIView, PlaylistWebLoader { fileprivate static var pageLoadTimeout = 300.0 private var pendingRequests = [String: URLRequest]() + private let braveCore: BraveCoreMain? - private let tab = Tab( - configuration: WKWebViewConfiguration().then { + private lazy var tab: Tab = Tab( + wkConfiguration: WKWebViewConfiguration().then { $0.processPool = WKProcessPool() $0.preferences = WKPreferences() $0.preferences.javaScriptCanOpenWindowsAutomatically = false $0.allowsInlineMediaPlayback = true $0.ignoresViewportScaleLimits = true }, - type: .private + configuration: braveCore?.nonPersistentWebViewConfiguration ?? .nonPersistent(), + type: .private, + contentScriptManager: .init(tabForWebView: { [weak self] _ in self?.tab }) ).then { $0.createWebview() $0.setScript(script: .playlistMediaSource, enabled: true) @@ -44,7 +54,8 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { private weak var certStore: CertStore? private var handler: ((PlaylistInfo?) -> Void)? - init() { + init(braveCore: BraveCoreMain?) { + self.braveCore = braveCore super.init(frame: .zero) guard let webView = tab.webView else { @@ -84,8 +95,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { self.certStore = browserViewController.profile.certStore let kvos: [KVOConstants] = [ .estimatedProgress, .loading, .canGoBack, - .canGoForward, .url, .title, - .hasOnlySecureContent, .serverTrust, + .canGoForward, .title, .visibleURL, .visibleSSLStatus, ] browserViewController.tab(tab, didCreateWebView: webView) @@ -113,7 +123,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { guard let webView = tab.webView else { return } webView.stopLoading() self.handler?(nil) - webView.loadHTMLString("PlayList", baseURL: nil) + webView.underlyingWebView?.loadHTMLString("PlayList", baseURL: nil) } private class PlaylistWebLoaderContentHelper: TabContentScript { @@ -128,7 +138,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { timeout = DispatchWorkItem(block: { [weak self] in guard let self = self else { return } self.webLoader?.handler?(nil) - self.webLoader?.tab.webView?.loadHTMLString( + self.webLoader?.tab.webView?.underlyingWebView?.loadHTMLString( "PlayList", baseURL: nil ) @@ -150,9 +160,9 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { static let userScript: WKUserScript? = nil static let playlistProcessDocumentLoad = PlaylistScriptHandler.playlistProcessDocumentLoad - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -166,7 +176,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { self.timeout?.cancel() self.timeout = nil self.webLoader?.handler?(nil) - self.webLoader?.tab.webView?.loadHTMLString( + self.webLoader?.tab.webView?.underlyingWebView?.loadHTMLString( "PlayList", baseURL: nil ) @@ -186,7 +196,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { timeout = DispatchWorkItem(block: { [weak self] in guard let self = self else { return } self.webLoader?.handler?(nil) - self.webLoader?.tab.webView?.loadHTMLString( + self.webLoader?.tab.webView?.underlyingWebView?.loadHTMLString( "PlayList", baseURL: nil ) @@ -215,7 +225,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { timeout = DispatchWorkItem(block: { [weak self] in guard let self = self else { return } self.webLoader?.handler?(nil) - self.webLoader?.tab.webView?.loadHTMLString( + self.webLoader?.tab.webView?.underlyingWebView?.loadHTMLString( "PlayList", baseURL: nil ) @@ -254,7 +264,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { // It may not have received all info necessary to play the item such as MetadataInfo // For now it works 100% of the time and it is safe to do it. If we come across such a website, that causes problems, // we'll need to find a different way of forcing the WebView to STOP loading metadata in the background - self.webLoader?.tab.webView?.loadHTMLString( + self.webLoader?.tab.webView?.underlyingWebView?.loadHTMLString( "PlayList", baseURL: nil ) @@ -264,7 +274,7 @@ class LivePlaylistWebLoader: UIView, PlaylistWebLoader { } } -extension LivePlaylistWebLoader: WKNavigationDelegate { +extension LivePlaylistWebLoader: CWVNavigationDelegate { // Recognize an Apple Maps URL. This will trigger the native app. But only if a search query is present. Otherwise // it could just be a visit to a regular page on maps.apple.com. fileprivate func isAppleMapsURL(_ url: URL) -> Bool { @@ -291,7 +301,7 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { return isHttpScheme && isAppStoreHost } - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + func webViewDidCommitNavigation(_ webView: CWVWebView) { webView.evaluateSafeJavaScript( functionName: "window.__firefox__.\(PlaylistWebLoaderContentHelper.playlistProcessDocumentLoad)()", @@ -301,37 +311,37 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { ) } - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - // There is a bug on some sites or something where the page may load TWICE OR there is a bug in WebKit where the page fails to load - // Either way, WebKit returns _WKRecoveryAttempterErrorKey with a WKReloadFrameErrorRecoveryAttempter - // Then it automatically reloads the page. In this case, we don't want to error and cancel loading and show the user an alert - // We want to continue waiting for the page to load and a proper response to come to us. - // If there is a real error, then we handle it and display an alert to the user. - if let error = error as? NSError { - if error.userInfo["_WKRecoveryAttempterErrorKey"] == nil { - self.handler?(nil) - } - return - } - + func webView(_ webView: CWVWebView, didFailNavigationWithError error: any Error) { self.handler?(nil) } func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - preferences: WKWebpagePreferences - ) async -> (WKNavigationActionPolicy, WKWebpagePreferences) { + _ webView: CWVWebView, + decidePolicyFor navigationAction: CWVNavigationAction, + decisionHandler: @escaping (CWVNavigationActionPolicy) -> Void + ) { + Task { + let policy = await self.webView(webView, decidePolicyFor: navigationAction) + decisionHandler(policy) + } + } + + func webView( + _ webView: CWVWebView, + decidePolicyFor navigationAction: CWVNavigationAction + ) async -> CWVNavigationActionPolicy { guard let url = navigationAction.request.url else { - return (.cancel, preferences) + return .cancel } if url.scheme == "about" || url.isBookmarklet { - return (.cancel, preferences) + return .cancel } - if navigationAction.isInternalUnprivileged && navigationAction.navigationType != .backForward { - return (.cancel, preferences) + if navigationAction.request.isInternalUnprivileged + && navigationAction.navigationType != .forwardBack + { + return .cancel } // Universal links do not work if the request originates from the app, manual handling is required. @@ -340,7 +350,7 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { { switch universalLink { case .buyVPN: - return (.cancel, preferences) + return .cancel } } @@ -349,7 +359,7 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { if url.scheme == "tel" || url.scheme == "facetime" || url.scheme == "facetime-audio" || url.scheme == "mailto" || isAppleMapsURL(url) || isStoreURL(url) { - return (.cancel, preferences) + return .cancel } // Ad-blocking checks @@ -362,12 +372,11 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { let domainForMainFrame = Domain.getOrCreate(forUrl: mainDocumentURL, persistent: false) - if let requestURL = navigationAction.request.url, - let targetFrame = navigationAction.targetFrame - { + if let requestURL = navigationAction.request.url { + // Check if custom user scripts must be added to or removed from the web view. tab.currentPageData?.addSubframeURL( forRequestURL: requestURL, - isForMainFrame: targetFrame.isMainFrame + isForMainFrame: navigationAction.navigationType.isMainFrame ) let scriptTypes = await tab.currentPageData?.makeUserScriptTypes( @@ -379,17 +388,12 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { } if ["http", "https", "data", "blob", "file"].contains(url.scheme) { - if navigationAction.targetFrame?.isMainFrame == true { - tab.updateUserAgent(webView, newURL: url) - } - pendingRequests[url.absoluteString] = navigationAction.request if let mainDocumentURL = navigationAction.request.mainDocumentURL, mainDocumentURL.schemelessAbsoluteString == url.schemelessAbsoluteString, !(InternalURL(url)?.isSessionRestore ?? false), - navigationAction.sourceFrame.isMainFrame - || navigationAction.targetFrame?.isMainFrame == true + navigationAction.navigationType.isMainFrame { // Identify specific block lists that need to be applied to the requesting domain @@ -407,22 +411,34 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { .noScript, considerAllShieldsOption: true ) - preferences.allowsContentJavaScript = isScriptsEnabled + // FIXME: Find a way to do this with CWVWebView + // preferences.allowsContentJavaScript = isScriptsEnabled } // Cookie Blocking code below tab.setScript(script: .cookieBlocking, enabled: Preferences.Privacy.blockAllCookies.value) - return (.allow, preferences) + return .allow } - return (.cancel, preferences) + return .cancel + } + + func webView( + _ webView: CWVWebView, + decidePolicyFor navigationResponse: CWVNavigationResponse, + decisionHandler: @escaping (CWVNavigationResponsePolicy) -> Void + ) { + Task { + let policy = await self.webView(webView, decidePolicyFor: navigationResponse) + decisionHandler(policy) + } } func webView( - _ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse - ) async -> WKNavigationResponsePolicy { + _ webView: CWVWebView, + decidePolicyFor navigationResponse: CWVNavigationResponse + ) async -> CWVNavigationResponsePolicy { let response = navigationResponse.response let responseURL = response.url @@ -444,20 +460,6 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { request = pendingRequests.removeValue(forKey: url.absoluteString) } - // TODO: REFACTOR to support Multiple Windows Better - if let browserController = webView.currentScene?.browserViewController { - // Check if this response should be handed off to Passbook. - if OpenPassBookHelper( - request: request, - response: response, - canShowInWebView: false, - forceDownload: false, - browserViewController: browserController - ) != nil { - return .cancel - } - } - if navigationResponse.isForMainFrame { if response.mimeType?.isKindOfHTML == false, request != nil { return .cancel @@ -471,57 +473,12 @@ extension LivePlaylistWebLoader: WKNavigationDelegate { return .allow } - nonisolated func webView( - _ webView: WKWebView, - respondTo challenge: URLAuthenticationChallenge - ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - // If this is a certificate challenge, see if the certificate has previously been - // accepted by the user. - let origin = "\(challenge.protectionSpace.host):\(challenge.protectionSpace.port)" - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust, - let cert = (SecTrustCopyCertificateChain(trust) as? [SecCertificate])?.first, - await certStore?.containsCertificate(cert, forOrigin: origin) == true - { - return (.useCredential, URLCredential(trust: trust)) - } - - // Certificate Pinning - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - if let serverTrust = challenge.protectionSpace.serverTrust { - let host = challenge.protectionSpace.host - let port = challenge.protectionSpace.port - - let result = BraveCertificateUtility.verifyTrust( - serverTrust, - host: host, - port: port - ) - let certificateChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] ?? [] - - // Cert is valid and should be pinned - if result == 0 { - return (.useCredential, URLCredential(trust: serverTrust)) - } - - // Cert is valid and should not be pinned - // Let the system handle it and we'll show an error if the system cannot validate it - if result == Int32.min { - return (.performDefaultHandling, nil) - } - - return (.cancelAuthenticationChallenge, nil) - } - } - - guard - challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic - || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest - || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM - else { - return (.performDefaultHandling, nil) - } - - return (.cancelAuthenticationChallenge, nil) + func webView( + _ webView: CWVWebView, + didRequestHTTPAuthFor protectionSpace: URLProtectionSpace, + proposedCredential: URLCredential, + completionHandler handler: @escaping (String?, String?) -> Void + ) { + handler(nil, nil) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCoordinator.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCoordinator.swift index 6d8c21cfb376..68633b738044 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCoordinator.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCoordinator.swift @@ -106,7 +106,7 @@ public class PlaylistCoordinator: NSObject { let mediaStreamer = PlaylistMediaStreamer( playerView: currentWindow ?? UIView(), - webLoaderFactory: LivePlaylistWebLoaderFactory() + webLoaderFactory: LivePlaylistWebLoaderFactory(braveCore: browserController?.braveCore) ) if FeatureList.kNewPlaylistUI.enabled { @@ -149,7 +149,7 @@ public class PlaylistCoordinator: NSObject { if FeatureList.kNewPlaylistUI.enabled { let mediaStreamer = PlaylistMediaStreamer( playerView: browserController!.view, - webLoaderFactory: LivePlaylistWebLoaderFactory() + webLoaderFactory: LivePlaylistWebLoaderFactory(braveCore: browserController?.braveCore) ) let player = self.playerModel @@ -187,6 +187,7 @@ public class PlaylistCoordinator: NSObject { let playlistController = self.playlistController ?? PlaylistViewController( + braveCore: browserController?.braveCore, openInNewTab: browserController?.openURLInNewTab, openPlaylistSettingsMenu: browserController?.openPlaylistSettingsMenu, profile: browserController?.profile, diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Tab.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Tab.swift index 12e9c7d5f521..25a3590feca8 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Tab.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Tab.swift @@ -17,6 +17,10 @@ import UserAgent import WebKit import os.log +#if DEBUG +import IsTesting +#endif + protocol TabContentScriptLoader { static func loadUserScript(named: String) -> String? static func secureScript(handlerName: String, securityToken: String, script: String) -> String @@ -37,9 +41,9 @@ protocol TabContentScript: TabContentScriptLoader { func verifyMessage(message: WKScriptMessage) -> Bool func verifyMessage(message: WKScriptMessage, securityToken: String) -> Bool - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) } @@ -51,8 +55,8 @@ protocol TabDelegate { func tab(_ tab: Tab, didSelectFindInPageFor selectedText: String) /// Triggered when "Search with Brave" is selected on selected web text func tab(_ tab: Tab, didSelectSearchWithBraveFor selectedText: String) - func tab(_ tab: Tab, didCreateWebView webView: WKWebView) - func tab(_ tab: Tab, willDeleteWebView webView: WKWebView) + func tab(_ tab: Tab, didCreateWebView webView: BraveWebView) + func tab(_ tab: Tab, willDeleteWebView webView: BraveWebView) func showRequestRewardsPanel(_ tab: Tab) func stopMediaPlayback(_ tab: Tab) func showWalletNotification(_ tab: Tab, origin: URLOrigin) @@ -115,62 +119,37 @@ class Tab: NSObject { private(set) var lastKnownSecureContentState: TabSecureContentState = .unknown func updateSecureContentState() async { - lastKnownSecureContentState = await secureContentState - } - @MainActor private var secureContentState: TabSecureContentState { - get async { - guard let webView = webView, let committedURL = self.committedURL else { - return .unknown - } - if let internalURL = InternalURL(committedURL), internalURL.isAboutHomeURL { - // New Tab Page is a special case, should be treated as `unknown` instead of `localhost` + guard let sslStatus = await webView?.visibleSSLStatus else { + lastKnownSecureContentState = .unknown + return + } + lastKnownSecureContentState = await { + switch sslStatus.securityStyle { + case .unknown: return .unknown - } - if webView.url != committedURL { - // URL has not been committed yet, so we will not evaluate the secure status of the page yet - return lastKnownSecureContentState - } - if let internalURL = InternalURL(committedURL) { - if internalURL.isErrorPage, ErrorPageHelper.certificateError(for: committedURL) != 0 { - return .invalidCert + case .authenticated: + if !sslStatus.hasOnlySecureContent { + return .mixedContent } - return .localhost - } - if committedURL.scheme?.lowercased() == "http" { - return .missingSSL - } - if let serverTrust = webView.serverTrust, - case let origin = committedURL.origin, !origin.isOpaque - { - let isMixedContent = !webView.hasOnlySecureContent - let result = await BraveCertificateUtils.verifyTrust( - serverTrust, - host: origin.host, - port: Int(origin.port) - ) - switch result { - case 0: - // Cert is valid - return isMixedContent ? .mixedContent : .secure - case Int(Int32.min): - // Cert is valid but should be validated by the system - // Let the system handle it and we'll show an error if the system cannot validate it - do { - try await BraveCertificateUtils.evaluateTrust(serverTrust, for: origin.host) - return isMixedContent ? .mixedContent : .secure - } catch { - return .invalidCert - } - default: - return .invalidCert + return .secure + case .authenticationBroken: + return .invalidCert + case .unauthenticated: + let webuiSchemes = ["brave", "chrome"] + if let lastCommittedURL = await webView?.lastCommittedURL, + InternalURL.isValid(url: lastCommittedURL) + || webuiSchemes.contains(lastCommittedURL.scheme ?? "") + { + return .localhost } + return .missingSSL + @unknown default: + return .unknown } - return .unknown - } + }() } - var sslPinningError: Error? - + private let browserPrefs: BrowserPrefs? private let _syncTab: BraveSyncTab? private let _faviconDriver: FaviconDriver? private var _walletEthProvider: BraveWalletEthereumProvider? @@ -324,10 +303,6 @@ class Tab: NSObject { // It is reset to initial values when the page navigation is finished. var rewardsReportingState = RewardsTabChangeReportingState() - /// This is the request that was upgraded to HTTPS - /// This allows us to rollback the upgrade when we encounter a 4xx+ - var upgradedHTTPSRequest: URLRequest? - /// The tabs new tab page controller. /// /// Should be setup in BVC then assigned here for future use. @@ -352,13 +327,13 @@ class Tab: NSObject { // There is no 'available macro' on props, we currently just need to store ownership. lazy var contentBlocker = ContentBlockerHelper(tab: self) - lazy var requestBlockingContentHelper = RequestBlockingContentScriptHandler(tab: self) + lazy var requestBlockingContentHelper = RequestBlockingContentScriptHandler() /// The last title shown by this tab. Used by the tab tray to show titles for zombie tabs. var lastTitle: String? var isDesktopSite: Bool { - webView?.customUserAgent?.lowercased().contains("mobile") == false + webView?.currentItemUserAgentType() == .desktop } var containsWebPage: Bool { @@ -370,11 +345,6 @@ class Tab: NSObject { return false } - /// In-memory dictionary of websites that were explicitly set to use either desktop or mobile user agent. - /// Key is url's base domain, value is desktop mode on or off. - /// Each tab has separate list of website overrides. - private var userAgentOverrides: [String: Bool] = [:] - var readerModeAvailableOrActive: Bool { if let readerMode = self.getContentScript(name: ReaderModeScriptHandler.scriptName) as? ReaderModeScriptHandler @@ -392,16 +362,19 @@ class Tab: NSObject { // If this tab has been opened from another, its parent will point to the tab from which it was opened weak var parent: Tab? - fileprivate var contentScriptManager = TabContentScriptManager() + fileprivate var contentScriptManager: TabContentScriptManager private var userScripts = Set() private var customUserScripts = Set() - fileprivate var configuration: WKWebViewConfiguration? + fileprivate var wkConfiguration: WKWebViewConfiguration? + fileprivate var configuration: CWVWebViewConfiguration? /// Any time a tab tries to make requests to display a Javascript Alert and we are not the active /// tab instance, queue it for later until we become foregrounded. fileprivate var alertQueue = [JSAlertInfo]() + var sampledTopPageColorNotifier: SampledTopPageColorNotifier? + var nightMode: Bool { didSet { var isNightModeEnabled = false @@ -436,14 +409,21 @@ class Tab: NSObject { } init( - configuration: WKWebViewConfiguration, + wkConfiguration: WKWebViewConfiguration?, + configuration: CWVWebViewConfiguration?, id: UUID = UUID(), type: TabType = .regular, - tabGeneratorAPI: BraveTabGeneratorAPI? = nil + contentScriptManager: TabContentScriptManager, + tabGeneratorAPI: BraveTabGeneratorAPI? = nil, + browserPrefs: BrowserPrefs? = nil ) { + self.wkConfiguration = wkConfiguration self.configuration = configuration self.favicon = Favicon.default self.id = id + self.contentScriptManager = contentScriptManager + self.browserPrefs = browserPrefs + rewardsId = UInt32.random(in: 1...UInt32.max) nightMode = Preferences.General.nightModeEnabled.value _syncTab = tabGeneratorAPI?.createBraveSyncTab(isOffTheRecord: type == .private) @@ -460,7 +440,54 @@ class Tab: NSObject { self.type = type } - weak var navigationDelegate: WKNavigationDelegate? { + init( + webView: TabWebView, + id: UUID = UUID(), + type: TabType = .regular, + contentScriptManager: TabContentScriptManager, + tabGeneratorAPI: BraveTabGeneratorAPI? = nil, + browserPrefs: BrowserPrefs? = nil + ) { + self.webView = webView + self.favicon = Favicon.default + self.id = id + self.type = type + self.contentScriptManager = contentScriptManager + self.browserPrefs = browserPrefs + rewardsId = UInt32.random(in: 1...UInt32.max) + nightMode = Preferences.General.nightModeEnabled.value + _syncTab = tabGeneratorAPI?.createBraveSyncTab(isOffTheRecord: type == .private) + + if let syncTab = _syncTab { + _faviconDriver = FaviconDriver(webState: syncTab.webState).then { + $0.setMaximumFaviconImageSize(CGSize(width: 1024, height: 1024)) + } + } else { + _faviconDriver = nil + } + + super.init() + + webView.delegate = self + } + + func childPopupTab( + configuration: CWVWebViewConfiguration, + tabGeneratorAPI: BraveTabGeneratorAPI? = nil, + browserPrefs: BrowserPrefs? = nil + ) -> Tab { + return Tab( + wkConfiguration: wkConfiguration!, + configuration: configuration, + id: UUID(), + type: type, + contentScriptManager: contentScriptManager, + tabGeneratorAPI: tabGeneratorAPI, + browserPrefs: browserPrefs + ) + } + + weak var navigationDelegate: CWVNavigationDelegate? { didSet { if let webView = webView { webView.navigationDelegate = navigationDelegate @@ -475,40 +502,62 @@ class Tab: NSObject { var braveSearchResultAdManager: BraveSearchResultAdManager? private lazy var refreshControl = UIRefreshControl().then { - $0.addTarget(self, action: #selector(reload), for: .valueChanged) + $0.addTarget(self, action: #selector(reloadFromRefreshControl), for: .valueChanged) } func createWebview() { + #if DEBUG + if isTesting { + // Tab now houses a `CWVWebView` which requires that global chromium state is created which + // currently cannot be done for Xcode tests, therefore we will never create an underlying + // `CWVWebView` if a tab is being referenced in a Xcode unit test. + return + } + #endif if webView == nil { - assert(configuration != nil, "Create webview can only be called once") - configuration!.userContentController = WKUserContentController() - configuration!.preferences = WKPreferences() - configuration!.preferences.javaScriptCanOpenWindowsAutomatically = false - configuration!.preferences.isFraudulentWebsiteWarningEnabled = + wkConfiguration!.userContentController = WKUserContentController() + wkConfiguration!.preferences = WKPreferences() + wkConfiguration!.preferences.javaScriptCanOpenWindowsAutomatically = false + wkConfiguration!.preferences.isFraudulentWebsiteWarningEnabled = Preferences.Shields.googleSafeBrowsing.value - configuration!.allowsInlineMediaPlayback = true + wkConfiguration!.allowsInlineMediaPlayback = true // Enables Zoom in website by ignoring their javascript based viewport Scale limits. - configuration!.ignoresViewportScaleLimits = true - configuration!.upgradeKnownHostsToHTTPS = ShieldPreferences.httpsUpgradeLevel.isEnabled - configuration!.enablePageTopColorSampling() + wkConfiguration!.ignoresViewportScaleLimits = true + wkConfiguration!.upgradeKnownHostsToHTTPS = browserPrefs?.httpsUpgradesEnabled ?? true + wkConfiguration!.enablePageTopColorSampling() - if configuration!.urlSchemeHandler(forURLScheme: InternalURL.scheme) == nil { - configuration!.setURLSchemeHandler( + if wkConfiguration!.urlSchemeHandler(forURLScheme: InternalURL.scheme) == nil { + wkConfiguration!.setURLSchemeHandler( InternalSchemeHandler(tab: self), forURLScheme: InternalURL.scheme ) } let webView = TabWebView( - frame: .zero, - tab: self, + // Set a non empty CGRect to avoid DCHECKs that occur when a load happens + // after state restoration, and before the view hierarchy is laid out for the + // first time. + // https://source.chromium.org/chromium/chromium/src/+/main:ios/web/web_state/ui/crw_web_request_controller.mm;l=518;drc=df887034106ef438611326745a7cd276eedd4953 + frame: .init(width: 1.0, height: 1.0), + wkConfiguration: parent == nil ? wkConfiguration! : nil, configuration: configuration!, isPrivate: isPrivate ) + webView.delegate = self + if parent != nil { + let userContentController = webView.wkConfiguration.userContentController + // Usually the configuration would be reset entirely but when we're opening up a child tab + // the `wkConfiguraton` is ignored entirely + userContentController.removeAllScriptMessageHandlers() + userContentController.removeAllContentRuleLists() + userContentController.removeAllUserScripts() + } + webView.accessibilityLabel = Strings.webContentAccessibilityLabel webView.allowsBackForwardNavigationGestures = true - webView.allowsLinkPreview = true + // FIXME: Link previews + // webView.underlyingWebView?.allowsLinkPreview = true // Turning off masking allows the web content to flow outside of the scrollView's frame // which allows the content appear beneath the toolbars in the BrowserViewController @@ -520,7 +569,7 @@ class Tab: NSObject { self.webView = webView self.webView?.addObserver( self, - forKeyPath: KVOConstants.url.keyPath, + forKeyPath: KVOConstants.visibleURL.keyPath, options: .new, context: nil ) @@ -541,7 +590,7 @@ class Tab: NSObject { } func resetWebView(config: WKWebViewConfiguration) { - configuration = config + wkConfiguration = config deleteWebView() contentScriptManager.helpers.removeAll() } @@ -575,19 +624,23 @@ class Tab: NSObject { tabId: id, interactionState: webView.sessionData ?? Data(), title: title, - url: webView.url ?? TabManager.ntpInteralURL + url: webView.lastCommittedURL ?? TabManager.ntpInteralURL ) } - func restore(_ webView: WKWebView, restorationData: (title: String, interactionState: Data)?) { + func restore(_ webView: BraveWebView, restorationData: (title: String, interactionState: Data)?) { // Pulls restored session data from a previous SavedTab to load into the Tab. If it's nil, a session restore // has already been triggered via custom URL, so we use the last request to trigger it again; otherwise, // we extract the information needed to restore the tabs and create a NSURLRequest with the custom session restore URL // to trigger the session restore via custom handlers - if let sessionInfo = restorationData { + if let sessionInfo = restorationData, + CWVWebView.isRestoreDataValid(sessionInfo.interactionState), + let coder = try? NSKeyedUnarchiver(forReadingFrom: sessionInfo.interactionState) + { restoring = true lastTitle = sessionInfo.title - webView.interactionState = sessionInfo.interactionState + coder.requiresSecureCoding = false + webView.decodeRestorableState(with: coder) restoring = false rewardsReportingState.wasRestored = true self.sessionData = nil @@ -600,8 +653,10 @@ class Tab: NSObject { } } - func restore(_ webView: WKWebView, requestRestorationData: (title: String, request: URLRequest)?) - { + func restore( + _ webView: BraveWebView, + requestRestorationData: (title: String, request: URLRequest)? + ) { if let sessionInfo = requestRestorationData { restoring = true lastTitle = sessionInfo.title @@ -621,7 +676,7 @@ class Tab: NSObject { contentScriptManager.uninstall(from: self) if let webView = webView { - webView.removeObserver(self, forKeyPath: KVOConstants.url.keyPath) + webView.removeObserver(self, forKeyPath: KVOConstants.visibleURL.keyPath) tabDelegate?.tab(self, willDeleteWebView: webView) } webView = nil @@ -657,16 +712,16 @@ class Tab: NSObject { return webView?.estimatedProgress ?? 0 } - var backList: [WKBackForwardListItem]? { + var backList: CWVBackForwardListItemArray? { return webView?.backForwardList.backList } - var forwardList: [WKBackForwardListItem]? { + var forwardList: CWVBackForwardListItemArray? { return webView?.backForwardList.forwardList } var historyList: [URL] { - func listToUrl(_ item: WKBackForwardListItem) -> URL { return item.url } + func listToUrl(_ item: CWVBackForwardListItem) -> URL { return item.url } var tabs = self.backList?.map(listToUrl) ?? [URL]() tabs.append(self.url!) return tabs @@ -677,19 +732,17 @@ class Tab: NSObject { } var displayTitle: String { - if let displayTabTitle = fetchDisplayTitle(using: url, title: title) { - syncTab?.setTitle(displayTabTitle) - return displayTabTitle - } - - // When picking a display title. Tabs with sessionData are pending a restore so show their old title. - // To prevent flickering of the display title. If a tab is restoring make sure to use its lastTitle. if let url = self.url, InternalURL(url)?.isAboutHomeURL ?? false, sessionData == nil, !restoring { syncTab?.setTitle(Strings.Hotkey.newTabTitle) return Strings.Hotkey.newTabTitle } + if let displayTabTitle = fetchDisplayTitle(using: url, title: title) { + syncTab?.setTitle(displayTabTitle) + return displayTabTitle + } + if let url = self.url, !InternalURL.isValid(url: url), let shownUrl = url.displayURL?.absoluteString, webView != nil { @@ -719,7 +772,8 @@ class Tab: NSObject { } var currentInitialURL: URL? { - return self.webView?.backForwardList.currentItem?.initialURL + // FIXME: This may be different than WKBackForwardItem.initialURL which may more closely resemble web::NavigationItem::GetOriginalRequestURL() + return self.webView?.backForwardList.currentItem?.url } var displayFavicon: Favicon? { @@ -789,41 +843,38 @@ class Tab: NSObject { _ = webView?.goForward() } - func goToBackForwardListItem(_ item: WKBackForwardListItem) { + func goToBackForwardListItem(_ item: CWVBackForwardListItem) { _ = webView?.go(to: item) } - @discardableResult func loadRequest(_ request: URLRequest) -> WKNavigation? { - if let webView = webView { - lastRequest = request - sslPinningError = nil - - if let url = request.url { - // Donate Custom Intent Open Website - if url.isSecureWebPage(), !isPrivate { - ActivityShortcutManager.shared.donateCustomIntent( - for: .openWebsite, - with: url.absoluteString - ) - } + func loadRequest(_ request: URLRequest) { + guard let webView = webView else { + return + } + lastRequest = request + + if let url = request.url { + // Donate Custom Intent Open Website + if url.isSecureWebPage(), !isPrivate { + ActivityShortcutManager.shared.donateCustomIntent( + for: .openWebsite, + with: url.absoluteString + ) } - - return webView.load(request) } - return nil + + webView.load(request) } func stop() { webView?.stopLoading() } - @objc func reload() { - // Clear the user agent before further navigation. - // Proper User Agent setting happens in BVC's WKNavigationDelegate. - // This prevents a bug with back-forward list, going back or forward and reloading the tab - // loaded wrong user agent. - webView?.customUserAgent = nil + @objc private func reloadFromRefreshControl() { + reload() + } + func reload(userAgentType: CWVUserAgentType? = nil) { defer { if let refreshControl = webView?.scrollView.refreshControl, refreshControl.isRefreshing @@ -834,40 +885,15 @@ class Tab: NSObject { tabDelegate?.didReloadTab(self) - // If the current page is an error page, and the reload button is tapped, load the original URL - if let url = webView?.url, let internalUrl = InternalURL(url), - let page = internalUrl.originalURLFromErrorPage - { - webView?.replaceLocation(with: page) - return - } - - if let _ = webView?.reloadFromOrigin() { + if let webView { + if let userAgentType { + webView.reload(withUserAgentType: userAgentType) + } else { + webView.reload() + } nightMode = Preferences.General.nightModeEnabled.value - Logger.module.debug("reloaded zombified tab from origin") return } - - if let webView = self.webView { - Logger.module.debug("restoring webView from scratch") - restore(webView, restorationData: sessionData) - } - } - - func updateUserAgent(_ webView: WKWebView, newURL: URL) { - guard let baseDomain = newURL.baseDomain else { return } - - let screenWidth = webView.currentScene?.screen.bounds.width ?? webView.bounds.size.width - if webView.traitCollection.horizontalSizeClass == .compact - && (webView.bounds.size.width < screenWidth / 2.0) - { - let desktopMode = userAgentOverrides[baseDomain] == true - webView.customUserAgent = desktopMode ? UserAgent.desktop : UserAgent.mobile - return - } - - let desktopMode = userAgentOverrides[baseDomain] ?? UserAgent.shouldUseDesktopMode - webView.customUserAgent = desktopMode ? UserAgent.desktop : UserAgent.mobile } func addContentScript(_ helper: TabContentScript, name: String, contentWorld: WKContentWorld) { @@ -948,17 +974,10 @@ class Tab: NSObject { /// Switches user agent Desktop -> Mobile or Mobile -> Desktop. func switchUserAgent() { - if let urlString = webView?.url?.baseDomain { - // The website was changed once already, need to flip the override.onScreenshotUpdated - if let siteOverride = userAgentOverrides[urlString] { - userAgentOverrides[urlString] = !siteOverride - } else { - // First time switch, adding the basedomain to dictionary with flipped value. - userAgentOverrides[urlString] = !UserAgent.shouldUseDesktopMode - } - } - - reload() + guard let webView else { return } + let userAgentType: CWVUserAgentType = + webView.currentItemUserAgentType() == .desktop ? .mobile : .desktop + reload(userAgentType: userAgentType) } func queueJavascriptAlertPrompt(_ alert: JSAlertInfo) { @@ -985,11 +1004,11 @@ class Tab: NSObject { context: UnsafeMutableRawPointer? ) { guard let webView = object as? BraveWebView, webView == self.webView, - let path = keyPath, path == KVOConstants.url.keyPath + let path = keyPath, path == KVOConstants.visibleURL.keyPath else { return assertionFailure("Unhandled KVO key: \(keyPath ?? "nil")") } - guard let url = self.webView?.url else { + guard let url = self.webView?.visibleURL else { return } @@ -999,7 +1018,7 @@ class Tab: NSObject { } func updatePullToRefreshVisibility() { - guard let url = webView?.url, let webView = webView else { return } + guard let url = webView?.lastCommittedURL, let webView = webView else { return } webView.scrollView.refreshControl = url.isLocalUtility || !Preferences.General.enablePullToRefresh.value ? nil : refreshControl } @@ -1044,32 +1063,42 @@ extension Tab: TabWebViewDelegate { } } -private class TabContentScriptManager: NSObject, WKScriptMessageHandlerWithReply { +class TabContentScriptManager: NSObject, WKScriptMessageHandlerWithReply { fileprivate var helpers = [String: TabContentScript]() + var tabForWebView: (WKWebView) -> Tab? + + init(tabForWebView: @escaping (WKWebView) -> Tab?) { + self.tabForWebView = tabForWebView + } + + convenience init(tabManager: TabManager) { + self.init(tabForWebView: { [weak tabManager] webView in + return tabManager?[webView] + }) + } func uninstall(from tab: Tab) { helpers.forEach { let name = type(of: $0.value).messageHandlerName - tab.webView?.configuration.userContentController.removeScriptMessageHandler(forName: name) + tab.webView?.wkConfiguration.userContentController + .removeScriptMessageHandler(forName: name) } } - @objc func userContentController( + func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage, - replyHandler: @escaping (Any?, String?) -> Void + replyHandler: @escaping @MainActor (Any?, String?) -> Void ) { - for helper in helpers.values { - let scriptMessageHandlerName = type(of: helper).messageHandlerName - if scriptMessageHandlerName == message.name { - helper.userContentController( - userContentController, - didReceiveScriptMessage: message, - replyHandler: replyHandler - ) - return - } + guard let webView = message.webView, let tab = tabForWebView(webView), + let helper = helpers.values.first(where: { + type(of: $0).messageHandlerName == message.name + }) + else { + replyHandler(nil, nil) + return } + helper.tab(tab, receivedScriptMessage: message, replyHandler: replyHandler) } func addContentScript( @@ -1079,7 +1108,7 @@ private class TabContentScriptManager: NSObject, WKScriptMessageHandlerWithReply contentWorld: WKContentWorld ) { if let _ = helpers[name] { - assertionFailure("Duplicate helper added: \(name)") + return } helpers[name] = helper @@ -1087,7 +1116,7 @@ private class TabContentScriptManager: NSObject, WKScriptMessageHandlerWithReply // If this helper handles script messages, then get the handler name and register it. The Tab // receives all messages and then dispatches them to the right TabHelper. let scriptMessageHandlerName = type(of: helper).messageHandlerName - tab.webView?.configuration.userContentController.addScriptMessageHandler( + tab.webView?.wkConfiguration.userContentController.addScriptMessageHandler( self, contentWorld: contentWorld, name: scriptMessageHandlerName @@ -1097,10 +1126,11 @@ private class TabContentScriptManager: NSObject, WKScriptMessageHandlerWithReply func removeContentScript(name: String, forTab tab: Tab, contentWorld: WKContentWorld) { if let helper = helpers[name] { let scriptMessageHandlerName = type(of: helper).messageHandlerName - tab.webView?.configuration.userContentController.removeScriptMessageHandler( - forName: scriptMessageHandlerName, - contentWorld: contentWorld - ) + tab.webView?.wkConfiguration.userContentController + .removeScriptMessageHandler( + forName: scriptMessageHandlerName, + contentWorld: contentWorld + ) helpers[name] = nil } } @@ -1125,16 +1155,19 @@ private protocol TabWebViewDelegate: AnyObject { class TabWebView: BraveWebView, MenuHelperInterface { fileprivate weak var delegate: TabWebViewDelegate? - private(set) weak var tab: Tab? - init( + override init( frame: CGRect, - tab: Tab, - configuration: WKWebViewConfiguration = WKWebViewConfiguration(), + wkConfiguration: WKWebViewConfiguration?, + configuration: CWVWebViewConfiguration, isPrivate: Bool = true ) { - self.tab = tab - super.init(frame: frame, configuration: configuration, isPrivate: isPrivate) + super.init( + frame: frame, + wkConfiguration: wkConfiguration, + configuration: configuration, + isPrivate: isPrivate + ) } override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { @@ -1180,17 +1213,11 @@ class TabWebView: BraveWebView, MenuHelperInterface { } } - // rdar://33283179 Apple bug where `serverTrust` is not defined as KVO when it should be - override func value(forUndefinedKey key: String) -> Any? { - if key == #keyPath(WKWebView.serverTrust) { - return serverTrust - } - - return super.value(forUndefinedKey: key) - } - private func getCurrentSelectedText(callback: @escaping (String?) -> Void) { - evaluateSafeJavaScript(functionName: "getSelection().toString", contentWorld: .defaultClient) { + evaluateSafeJavaScript( + functionName: "getSelection().toString", + contentWorld: .defaultClient + ) { result, _ in let selectedText = result as? String @@ -1235,47 +1262,52 @@ extension Tab { // The method we pass data to is undefined. // For such case we do not call that method or remove the search backup manager. - self.webView?.evaluateJavaScript("window.onFetchedBackupResults === undefined") { - result, - error in + self.webView?.evaluateSafeJavaScript( + functionName: "window.onFetchedBackupResults === undefined", + contentWorld: .page, + asFunction: false, + completion: { result, error in - if let error = error { - Logger.module.error( - "onFetchedBackupResults existence check error: \(error.localizedDescription, privacy: .public)" - ) - } + if let error = error { + Logger.module.error( + "onFetchedBackupResults existence check error: \(error.localizedDescription, privacy: .public)" + ) + } - guard let methodUndefined = result as? Bool else { - Logger.module.error( - "onFetchedBackupResults existence check, failed to unwrap bool result value" - ) - return - } + guard let methodUndefined = result as? Bool else { + Logger.module.error( + "onFetchedBackupResults existence check, failed to unwrap bool result value" + ) + return + } - if methodUndefined { - Logger.module.info("Search Backup results are ready but the page has not been loaded yet") - return - } + if methodUndefined { + Logger.module.info( + "Search Backup results are ready but the page has not been loaded yet" + ) + return + } - var queryResult = "null" + var queryResult = "null" - if let url = self.webView?.url, - BraveSearchManager.isValidURL(url), - let result = self.braveSearchManager?.fallbackQueryResult - { - queryResult = result - } + if let url = self.webView?.visibleURL, + BraveSearchManager.isValidURL(url), + let result = self.braveSearchManager?.fallbackQueryResult + { + queryResult = result + } - self.webView?.evaluateSafeJavaScript( - functionName: "window.onFetchedBackupResults", - args: [queryResult], - contentWorld: BraveSearchScriptHandler.scriptSandbox, - escapeArgs: false - ) + self.webView?.evaluateSafeJavaScript( + functionName: "window.onFetchedBackupResults", + args: [queryResult], + contentWorld: BraveSearchScriptHandler.scriptSandbox, + escapeArgs: false + ) - // Cleanup - self.braveSearchManager = nil - } + // Cleanup + self.braveSearchManager = nil + } + ) } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift index a80cb1b3304c..7f61e2434e1f 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift @@ -9,6 +9,7 @@ import Data import Favicon import Foundation import Growth +import IsTesting import Preferences import Shared import Storage @@ -97,6 +98,9 @@ class TabManager: NSObject { private var syncTabsTask: DispatchWorkItem? private var metricsHeartbeat: Timer? private let historyAPI: BraveHistoryAPI? + private let browserPrefs: BrowserPrefs? + private let defaultWebViewConfiguration: CWVWebViewConfiguration? + private let privateWebViewConfiguration: () -> CWVWebViewConfiguration? public let privateBrowsingManager: PrivateBrowsingManager private var forgetTasks: [TabType: [String: Task]] = [:] @@ -118,8 +122,7 @@ class TabManager: NSObject { windowId: UUID, prefs: Prefs, rewards: BraveRewards?, - tabGeneratorAPI: BraveTabGeneratorAPI?, - historyAPI: BraveHistoryAPI?, + braveCore: BraveCoreMain?, privateBrowsingManager: PrivateBrowsingManager ) { assert(Thread.isMainThread) @@ -128,10 +131,15 @@ class TabManager: NSObject { self.prefs = prefs self.navDelegate = TabManagerNavDelegate() self.rewards = rewards - self.tabGeneratorAPI = tabGeneratorAPI - self.historyAPI = historyAPI + self.tabGeneratorAPI = braveCore?.tabGeneratorAPI + self.historyAPI = braveCore?.historyAPI + self.browserPrefs = braveCore?.browserPrefs self.privateBrowsingManager = privateBrowsingManager self.tabEventHandlers = TabEventHandlers.create(with: prefs) + self.defaultWebViewConfiguration = braveCore?.defaultWebViewConfiguration + self.privateWebViewConfiguration = { [weak braveCore] in + braveCore?.nonPersistentWebViewConfiguration + } super.init() self.navDelegate.tabManager = self @@ -164,7 +172,7 @@ class TabManager: NSObject { syncTabsTask?.cancel() } - func addNavigationDelegate(_ delegate: WKNavigationDelegate) { + func addNavigationDelegate(_ delegate: CWVNavigationDelegate) { assert(Thread.isMainThread) self.navDelegate.insert(delegate) @@ -197,6 +205,16 @@ class TabManager: NSObject { subscript(webView: WKWebView) -> Tab? { assert(Thread.isMainThread) + for tab in allTabs where tab.webView?.underlyingWebView == webView { + return tab + } + + return nil + } + + subscript(webView: BraveWebView) -> Tab? { + assert(Thread.isMainThread) + for tab in allTabs where tab.webView === webView { return tab } @@ -204,6 +222,16 @@ class TabManager: NSObject { return nil } + subscript(webView: CWVWebView) -> Tab? { + assert(Thread.isMainThread) + + for tab in allTabs where tab.webView == webView { + return tab + } + + return nil + } + var currentDisplayedIndex: Int? { assert(Thread.isMainThread) @@ -372,19 +400,8 @@ class TabManager: NSObject { } UIImpactFeedbackGenerator(style: .light).vibrate() - selectedTab?.createWebview() selectedTab?.lastExecutedTime = Date.now() - if let selectedTab = selectedTab, - let webView = selectedTab.webView, - webView.url == nil - { - - selectedTab.url = selectedTab.url ?? TabManager.ntpInteralURL - restoreTab(selectedTab) - Logger.module.error("Force Restored a Zombie Tab?!") - } - delegates.forEach { $0.get()?.tabManager(self, didSelectedTabChange: tab, previous: previous) } if let tab = previous { TabEvent.post(.didLoseFocus, for: tab) @@ -450,13 +467,12 @@ class TabManager: NSObject { @MainActor func addPopupForParentTab( _ parentTab: Tab, - configuration: WKWebViewConfiguration + configuration: CWVWebViewConfiguration ) -> Tab { - let popup = Tab( + let popup = parentTab.childPopupTab( configuration: configuration, - id: UUID(), - type: parentTab.type, - tabGeneratorAPI: tabGeneratorAPI + tabGeneratorAPI: tabGeneratorAPI, + browserPrefs: browserPrefs ) configureTab( popup, @@ -543,11 +559,24 @@ class TabManager: NSObject { let tabId = id ?? UUID() let type: TabType = isPrivate ? .private : .regular + let webViewConfiguration: CWVWebViewConfiguration? + #if DEBUG + if isTesting { + webViewConfiguration = nil + } else { + webViewConfiguration = isPrivate ? privateWebViewConfiguration() : defaultWebViewConfiguration + } + #else + webViewConfiguration = isPrivate ? privateWebViewConfiguration() : defaultWebViewConfiguration + #endif let tab = Tab( - configuration: configuration, + wkConfiguration: configuration, + configuration: webViewConfiguration, id: tabId, type: type, - tabGeneratorAPI: tabGeneratorAPI + contentScriptManager: .init(tabManager: self), + tabGeneratorAPI: tabGeneratorAPI, + browserPrefs: browserPrefs ) configureTab( tab, @@ -870,7 +899,7 @@ class TabManager: NSObject { } @MainActor private func forgetData(for url: URL, in tab: Tab?) async { - await forgetData(for: [url], dataStore: tab?.webView?.configuration.websiteDataStore) + await forgetData(for: [url], dataStore: tab?.webView?.wkConfiguration.websiteDataStore) ContentBlockerManager.log.debug("Cleared website data for `\(url.baseDomain ?? "")`") if let etldP1 = url.baseDomain, let tab { @@ -1053,7 +1082,7 @@ class TabManager: NSObject { func removeAllBrowsingDataForTab(_ tab: Tab, completionHandler: @escaping () -> Void = {}) { let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes() - tab.webView?.configuration.websiteDataStore.removeData( + tab.webView?.wkConfiguration.websiteDataStore.removeData( ofTypes: dataTypes, modifiedSince: Date.distantPast, completionHandler: completionHandler @@ -1176,7 +1205,7 @@ class TabManager: NSObject { assert(Thread.isMainThread) let tab = allTabs.filter { - guard let webViewURL = $0.webView?.url, $0.isPrivate else { + guard let webViewURL = $0.webView?.lastCommittedURL, $0.isPrivate else { return false } @@ -1269,6 +1298,7 @@ class TabManager: NSObject { tab.lastTitle = savedTab.title tab.favicon = Favicon.default tab.setScreenshot(savedTab.screenshot) + tab.sessionData = (savedTab.title, savedTab.interactionState) Task { @MainActor in tab.favicon = try await FaviconFetcher.loadIcon( @@ -1295,6 +1325,7 @@ class TabManager: NSObject { tab.lastTitle = savedTab.title tab.favicon = Favicon.default tab.setScreenshot(savedTab.screenshot) + tab.sessionData = (savedTab.title, savedTab.interactionState) // Do not select the private tab since we always restore to regular mode! if savedTab.isSelected && !savedTab.isPrivate { @@ -1343,7 +1374,9 @@ class TabManager: NSObject { // Tab was created with no active webview session data. // Restore tab data from Core-Data URL, and configure it. - if sessionTab.interactionState.isEmpty { + if sessionTab.interactionState.isEmpty + || !CWVWebView.isRestoreDataValid(sessionTab.interactionState) + { tab.navigationDelegate = navDelegate if let tabURL = sessionTab.url { @@ -1504,7 +1537,7 @@ class TabManager: NSObject { // Convert any internal URLs to their real URL for the Recently Closed item var fetchedTabURL = tabUrl if let url = InternalURL(fetchedTabURL), - let actualURL = url.extractedUrlParam ?? url.originalURLFromErrorPage + let actualURL = url.extractedUrlParam { fetchedTabURL = actualURL } @@ -1518,26 +1551,19 @@ class TabManager: NSObject { } } -extension TabManager: WKNavigationDelegate { - - // Note the main frame JSContext (i.e. document, window) is not available yet. - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { +extension TabManager: CWVNavigationDelegate { + func webViewDidStartProvisionalNavigation(_ webView: CWVWebView) { if let tab = self[webView] { tab.contentBlocker.clearPageStats() } } - // The main frame JSContext is available, and DOM parsing has begun. - // Do not excute JS at this point that requires running prior to DOM parsing. - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + func webViewDidFinishNavigation(_ webView: CWVWebView) { // only store changes if this is not an error page // as we current handle tab restore as error page redirects then this ensures that we don't // call storeChanges unnecessarily on startup - if let url = webView.url { + if let url = webView.lastCommittedURL { // tab restore uses internal pages, // so don't call storeChanges unnecessarily on startup if InternalURL(url)?.isSessionRestore == true { @@ -1557,19 +1583,14 @@ extension TabManager: WKNavigationDelegate { } } - func tabForWebView(_ webView: WKWebView) -> Tab? { + func tabForWebView(_ webView: CWVWebView) -> Tab? { objc_sync_enter(self) defer { objc_sync_exit(self) } - return allTabs.first(where: { $0.webView === webView }) - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + return allTabs.first(where: { $0.webView == webView }) } - /// Called when the WKWebView's content process has gone away. If this happens for the currently selected tab - /// then we immediately reload it. - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + func webViewWebContentProcessDidTerminate(_ webView: CWVWebView) { if let tab = selectedTab, tab.webView == webView { webView.reload() } @@ -1591,7 +1612,8 @@ extension TabManager: PreferencesObserver { let allowPopups = !Preferences.General.blockPopups.value // Each tab may have its own configuration, so we should tell each of them in turn. allTabs.forEach { - $0.webView?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups + $0.webView?.wkConfiguration.preferences + .javaScriptCanOpenWindowsAutomatically = allowPopups } // The default tab configurations also need to change. configuration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift index 55ef3d5bece8..f4525d219fd9 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift @@ -1,163 +1,107 @@ +import BraveCore import Preferences import Shared import WebKit // WKNavigationDelegates must implement NSObjectProtocol -class TabManagerNavDelegate: NSObject, WKNavigationDelegate { - private var delegates = WeakList() +class TabManagerNavDelegate: NSObject, CWVNavigationDelegate { + private var delegates = WeakList() weak var tabManager: TabManager? - func insert(_ delegate: WKNavigationDelegate) { + func insert(_ delegate: CWVNavigationDelegate) { delegates.insert(delegate) } - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation) { + func webViewDidStartNavigation(_ webView: CWVWebView) { for delegate in delegates { - delegate.webView?(webView, didCommit: navigation) + delegate.webViewDidStartNavigation?(webView) } } - func webView(_ webView: WKWebView, didFail navigation: WKNavigation, withError error: Error) { + func webViewDidStartProvisionalNavigation(_ webView: CWVWebView) { for delegate in delegates { - delegate.webView?(webView, didFail: navigation, withError: error) + delegate.webViewDidStartProvisionalNavigation?(webView) } } - func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation, - withError error: Error - ) { + func webViewDidCommitNavigation(_ webView: CWVWebView) { for delegate in delegates { - delegate.webView?(webView, didFailProvisionalNavigation: navigation, withError: error) + delegate.webViewDidCommitNavigation?(webView) } } - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) { + func webViewDidFinishNavigation(_ webView: CWVWebView) { for delegate in delegates { - delegate.webView?(webView, didFinish: navigation) - } - } - - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - for delegate in delegates { - delegate.webViewWebContentProcessDidTerminate?(webView) - } - } - - func webView( - _ webView: WKWebView, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - let authenticatingDelegates = delegates.filter { wv in - return wv.responds( - to: #selector(WKNavigationDelegate.webView(_:didReceive:completionHandler:)) - ) + delegate.webViewDidFinishNavigation?(webView) } - - guard let firstAuthenticatingDelegate = authenticatingDelegates.first else { - completionHandler(.performDefaultHandling, nil) - return - } - - firstAuthenticatingDelegate.webView?( - webView, - didReceive: challenge, - completionHandler: completionHandler - ) } func webView( - _ webView: WKWebView, - didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation! + _ webView: CWVWebView, + decidePolicyFor navigationAction: CWVNavigationAction, + decisionHandler: @escaping (CWVNavigationActionPolicy) -> Void ) { - for delegate in delegates { - delegate.webView?(webView, didReceiveServerRedirectForProvisionalNavigation: navigation) - } - } - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - for delegate in delegates { - delegate.webView?(webView, didStartProvisionalNavigation: navigation) - } - } - - private func defaultAllowPolicy( - for navigationAction: WKNavigationAction - ) -> WKNavigationActionPolicy { - let isPrivateBrowsing = tabManager?.privateBrowsingManager.isPrivateBrowsing == true - func isYouTubeLoad() -> Bool { - guard let domain = navigationAction.request.mainDocumentURL?.baseDomain else { - return false - } - let domainsWithUniversalLinks: Set = ["youtube.com", "youtu.be"] - return domainsWithUniversalLinks.contains(domain) - } - if isPrivateBrowsing || !Preferences.General.followUniversalLinks.value - || (Preferences.General.keepYouTubeInBrave.value && isYouTubeLoad()) - { - // Stop Brave from opening universal links by using the private enum value - // `_WKNavigationActionPolicyAllowWithoutTryingAppLink` which is defined here: - // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/API/Cocoa/WKNavigationDelegatePrivate.h#L62 - let allowDecision = - WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2) ?? .allow - return allowDecision - } - return .allow - } + var res: CWVNavigationActionPolicy = .allow - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - preferences: WKWebpagePreferences, - decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void - ) { - var res = defaultAllowPolicy(for: navigationAction) - var pref = preferences + // Needed to resolve ambiguous delegate signatures: https://github.com/apple/swift/issues/45652#issuecomment-1149235081 + typealias CWVNavigationActionSignature = (CWVNavigationDelegate) -> ( + ( + CWVWebView, CWVNavigationAction, + @escaping (CWVNavigationActionPolicy) -> Void + ) -> Void + )? let group = DispatchGroup() - for delegate in delegates { - if !delegate.responds(to: #selector(webView(_:decidePolicyFor:preferences:decisionHandler:))) - { + if !delegate.responds( + to: #selector( + CWVNavigationDelegate.webView(_:decidePolicyFor:decisionHandler:) + as CWVNavigationActionSignature + ) + ) { continue } group.enter() delegate.webView?( webView, decidePolicyFor: navigationAction, - preferences: pref, - decisionHandler: { policy, preferences in + decisionHandler: { (policy: CWVNavigationActionPolicy) in if policy == .cancel { res = policy } - - if policy == .download { - res = policy - } - - pref = preferences - group.leave() } ) } group.notify(queue: .main) { - decisionHandler(res, pref) + decisionHandler(res) } } func webView( - _ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse, - decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void + _ webView: CWVWebView, + decidePolicyFor navigationResponse: CWVNavigationResponse, + decisionHandler: @escaping (CWVNavigationResponsePolicy) -> Void ) { - var res = WKNavigationResponsePolicy.allow + var res = CWVNavigationResponsePolicy.allow + + // Needed to resolve ambiguous delegate signatures: https://github.com/apple/swift/issues/45652#issuecomment-1149235081 + typealias CWVNavigationResponseSignature = (CWVNavigationDelegate) -> ( + ( + CWVWebView, CWVNavigationResponse, + @escaping (CWVNavigationResponsePolicy) -> Void + ) -> Void + )? + let group = DispatchGroup() for delegate in delegates { - if !delegate.responds(to: #selector(webView(_:decidePolicyFor:decisionHandler:))) { + if !delegate.responds( + to: #selector( + CWVNavigationDelegate.webView(_:decidePolicyFor:decisionHandler:) + as CWVNavigationResponseSignature + ) + ) { continue } group.enter() @@ -168,10 +112,6 @@ class TabManagerNavDelegate: NSObject, WKNavigationDelegate { if policy == .cancel { res = policy } - - if policy == .download { - res = policy - } group.leave() } ) @@ -187,29 +127,65 @@ class TabManagerNavDelegate: NSObject, WKNavigationDelegate { } } - func webView( - _ webView: WKWebView, - navigationAction: WKNavigationAction, - didBecome download: WKDownload - ) { + func webView(_ webView: CWVWebView, didFailNavigationWithError error: any Error) { + for delegate in delegates { + delegate.webView?(webView, didFailNavigationWithError: error) + } + } + + func webViewWebContentProcessDidTerminate(_ webView: CWVWebView) { + for delegate in delegates { + delegate.webViewWebContentProcessDidTerminate?(webView) + } + } + + func webView(_ webView: CWVWebView, didRequestDownloadWith task: CWVDownloadTask) { for delegate in delegates { - delegate.webView?(webView, navigationAction: navigationAction, didBecome: download) - if download.delegate != nil { - return + delegate.webView?(webView, didRequestDownloadWith: task) + } + } + + func webView(_ webView: CWVWebView, shouldBlockUniversalLinksFor request: URLRequest) -> Bool { + for delegate in delegates { + if let shouldBlock = delegate.webView?(webView, shouldBlockUniversalLinksFor: request), + shouldBlock + { + return true } } + return false + } + + func webView(_ webView: CWVWebView, shouldBlockJavaScriptFor request: URLRequest) -> Bool { + for delegate in delegates { + if let shouldBlock = delegate.webView?(webView, shouldBlockJavaScriptFor: request), + shouldBlock + { + return true + } + } + return false } func webView( - _ webView: WKWebView, - navigationResponse: WKNavigationResponse, - didBecome download: WKDownload + _ webView: CWVWebView, + didRequestHTTPAuthFor protectionSpace: URLProtectionSpace, + proposedCredential: URLCredential, + completionHandler handler: @escaping (String?, String?) -> Void ) { + var handled: Bool = false for delegate in delegates { - delegate.webView?(webView, navigationResponse: navigationResponse, didBecome: download) - if download.delegate != nil { - return - } + delegate.webView?( + webView, + didRequestHTTPAuthFor: protectionSpace, + proposedCredential: proposedCredential, + completionHandler: { username, password in + if !handled { + handled = true + handler(username, password) + } + } + ) } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift index a64b84e0b2bd..173d840d82a6 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift @@ -285,51 +285,48 @@ class UserScriptManager { } } - public func loadScripts(into webView: WKWebView, scripts: Set) { + public func loadScripts(into scriptController: WKUserContentController, scripts: Set) + { if Preferences.UserScript.blockAllScripts.value { return } var scripts = scripts - webView.configuration.userContentController.do { scriptController in - scriptController.removeAllUserScripts() - - // Inject all base scripts - self.baseScripts.forEach { - scriptController.addUserScript($0) - } + // Inject all base scripts + self.baseScripts.forEach { + scriptController.addUserScript($0) + } - // Inject specifically trackerProtectionStats BEFORE request blocking - // this is because it needs to hook requests before requestBlocking - if scripts.contains(.trackerProtectionStats), - let script = self.dynamicScripts[.trackerProtectionStats] - { - scripts.remove(.trackerProtectionStats) - scriptController.addUserScript(script) - } + // Inject specifically trackerProtectionStats BEFORE request blocking + // this is because it needs to hook requests before requestBlocking + if scripts.contains(.trackerProtectionStats), + let script = self.dynamicScripts[.trackerProtectionStats] + { + scripts.remove(.trackerProtectionStats) + scriptController.addUserScript(script) + } - // Inject specifically RequestBlocking BEFORE other scripts - // this is because it needs to hook requests before RewardsReporting - if scripts.contains(.requestBlocking), let script = self.dynamicScripts[.requestBlocking] { - scripts.remove(.requestBlocking) - scriptController.addUserScript(script) - } + // Inject specifically RequestBlocking BEFORE other scripts + // this is because it needs to hook requests before RewardsReporting + if scripts.contains(.requestBlocking), let script = self.dynamicScripts[.requestBlocking] { + scripts.remove(.requestBlocking) + scriptController.addUserScript(script) + } - // Inject all static scripts - self.staticScripts.forEach { - scriptController.addUserScript($0) - } + // Inject all static scripts + self.staticScripts.forEach { + scriptController.addUserScript($0) + } - // Inject all scripts that are dynamic, but always enabled - self.dynamicScripts.filter({ self.alwaysEnabledScripts.contains($0.key) }).forEach { - scriptController.addUserScript($0.value) - } + // Inject all scripts that are dynamic, but always enabled + self.dynamicScripts.filter({ self.alwaysEnabledScripts.contains($0.key) }).forEach { + scriptController.addUserScript($0.value) + } - // Inject all optional scripts - self.dynamicScripts.filter({ scripts.contains($0.key) }).forEach { - scriptController.addUserScript($0.value) - } + // Inject all optional scripts + self.dynamicScripts.filter({ scripts.contains($0.key) }).forEach { + scriptController.addUserScript($0.value) } } @@ -359,9 +356,10 @@ class UserScriptManager { ContentBlockerManager.log.debug( "Loaded \(userScripts.count + customScripts.count) script(s): \n\(logComponents.joined(separator: "\n"))" ) - loadScripts(into: webView, scripts: userScripts) + webView.updateScripts() + loadScripts(into: webView.wkConfiguration.userContentController, scripts: userScripts) - webView.configuration.userContentController.do { scriptController in + webView.wkConfiguration.userContentController.do { scriptController in // TODO: Somehow refactor wallet and get rid of this // Inject WALLET specific scripts diff --git a/ios/brave-ios/Sources/Brave/Frontend/Reader/ReadabilityService.swift b/ios/brave-ios/Sources/Brave/Frontend/Reader/ReadabilityService.swift index de668968f30e..5e072a5762da 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Reader/ReadabilityService.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Reader/ReadabilityService.swift @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import Foundation import WebKit @@ -16,12 +17,14 @@ class ReadabilityOperation: Operation { private var semaphore: DispatchSemaphore private var tab: Tab! private var readerModeCache: ReaderModeCache + private var braveCore: BraveCoreMain var result: ReadabilityOperationResult? - init(url: URL, readerModeCache: ReaderModeCache) { + init(url: URL, readerModeCache: ReaderModeCache, braveCore: BraveCoreMain) { self.url = url self.semaphore = DispatchSemaphore(value: 0) self.readerModeCache = readerModeCache + self.braveCore = braveCore } override func main() { @@ -34,11 +37,15 @@ class ReadabilityOperation: Operation { DispatchQueue.main.async { let configuration = WKWebViewConfiguration() - self.tab = Tab(configuration: configuration) + self.tab = Tab( + wkConfiguration: configuration, + configuration: self.braveCore.defaultWebViewConfiguration, + contentScriptManager: TabContentScriptManager(tabForWebView: { [weak self] _ in self?.tab }) + ) self.tab.createWebview() self.tab.navigationDelegate = self - let readerMode = ReaderModeScriptHandler(tab: self.tab) + let readerMode = ReaderModeScriptHandler() readerMode.delegate = self self.tab.addContentScript( readerMode, @@ -79,23 +86,14 @@ class ReadabilityOperation: Operation { } } -extension ReadabilityOperation: WKNavigationDelegate { - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - result = ReadabilityOperationResult.error(error as NSError) - semaphore.signal() - } - - func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation!, - withError error: Error - ) { +extension ReadabilityOperation: CWVNavigationDelegate { + func webView(_ webView: CWVWebView, didFailNavigationWithError error: any Error) { result = ReadabilityOperationResult.error(error as NSError) semaphore.signal() } - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - webView.evaluateSafeJavaScript( + func webViewDidFinishNavigation(_ webView: CWVWebView) { + tab.webView?.evaluateSafeJavaScript( functionName: "\(readerModeNamespace).checkReadability", contentWorld: ReaderModeScriptHandler.scriptSandbox ) @@ -139,7 +137,7 @@ class ReadabilityService { queue.maxConcurrentOperationCount = readabilityServiceDefaultConcurrency } - func process(_ url: URL, cache: ReaderModeCache) { - queue.addOperation(ReadabilityOperation(url: url, readerModeCache: cache)) + func process(_ url: URL, cache: ReaderModeCache, braveCore: BraveCoreMain) { + queue.addOperation(ReadabilityOperation(url: url, readerModeCache: cache, braveCore: braveCore)) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdvancedShieldSettings.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdvancedShieldSettings.swift index 4ef7f796c8e4..26cd1e716137 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdvancedShieldSettings.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdvancedShieldSettings.swift @@ -69,7 +69,8 @@ import os } @Published var httpsUpgradeLevel: HTTPSUpgradeLevel { didSet { - ShieldPreferences.httpsUpgradeLevel = httpsUpgradeLevel + browserPrefs.httpsOnlyModeEnabled = httpsUpgradeLevel.isStrict + browserPrefs.httpsUpgradesEnabled = httpsUpgradeLevel.isEnabled HttpsUpgradeServiceFactory.get(privateMode: false)?.clearAllowlist( fromStart: Date.distantPast, end: Date.distantFuture @@ -88,6 +89,7 @@ import os private var subscriptions: [AnyCancellable] = [] private let p3aUtilities: BraveP3AUtils private let deAmpPrefs: DeAmpPrefs + private let browserPrefs: BrowserPrefs private let debounceService: DebounceService? private let rewards: BraveRewards? private let clearDataCallback: ClearDataCallback @@ -104,13 +106,16 @@ import os ) { self.p3aUtilities = braveCore.p3aUtils self.deAmpPrefs = braveCore.deAmpPrefs + self.browserPrefs = braveCore.browserPrefs self.debounceService = debounceService self.tabManager = tabManager self.isP3AEnabled = p3aUtilities.isP3AEnabled self.rewards = rewards self.clearDataCallback = clearDataCallback self.adBlockAndTrackingPreventionLevel = ShieldPreferences.blockAdsAndTrackingLevel - self.httpsUpgradeLevel = ShieldPreferences.httpsUpgradeLevel + self.httpsUpgradeLevel = + browserPrefs.httpsOnlyModeEnabled + ? .strict : browserPrefs.httpsUpgradesEnabled ? .standard : .disabled self.isDeAmpEnabled = deAmpPrefs.isDeAmpEnabled self.isDebounceEnabled = debounceService?.isEnabled ?? false self.shredLevel = ShieldPreferences.shredLevel diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsContentViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsContentViewController.swift index 7f87ca881ec0..027d42f52883 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsContentViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsContentViewController.swift @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared import Shared import SnapKit @@ -13,7 +14,7 @@ let defaultTimeoutTimeInterval = 10.0 /// A controller that manages a single web view and provides a way for /// the user to navigate back to Settings. -class SettingsContentViewController: UIViewController, WKNavigationDelegate { +class SettingsContentViewController: UIViewController, CWVNavigationDelegate { let interstitialBackgroundColor: UIColor var settingsTitle: NSAttributedString? var url: URL! @@ -122,8 +123,11 @@ class SettingsContentViewController: UIViewController, WKNavigationDelegate { let configuration = WKWebViewConfiguration().then { $0.setURLSchemeHandler(InternalSchemeHandler(tab: nil), forURLScheme: InternalURL.scheme) } - let webView = BraveWebView(frame: frame, configuration: configuration) - webView.allowsLinkPreview = false + let webView = BraveWebView( + frame: frame, + wkConfiguration: configuration, + configuration: .default() + ) webView.navigationDelegate = self return webView } @@ -167,19 +171,13 @@ class SettingsContentViewController: UIViewController, WKNavigationDelegate { self.isError = true } - func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation!, - withError error: Error - ) { - didTimeOut() - } + // MARK: - CWVNavigationDelegate - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + func webView(_ webView: CWVWebView, didFailNavigationWithError error: any Error) { didTimeOut() } - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + func webViewDidFinishNavigation(_ webView: CWVWebView) { self.timer?.invalidate() self.timer = nil self.isLoaded = true diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift index 18efb3e6bad1..6e27e2a329bc 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -464,11 +464,19 @@ class SettingsViewController: TableViewController { ) if UIDevice.isIpad { + let defaultHostContentSettings = braveCore.defaultHostContentSettings + let defaultPageModeSwitch = SwitchAccessoryView( + initialValue: defaultHostContentSettings.defaultPageMode == .desktop, + valueChange: { value in + defaultHostContentSettings.defaultPageMode = value ? .desktop : .mobile + } + ) general.rows.append( - .boolRow( - title: Strings.alwaysRequestDesktopSite, - option: Preferences.UserAgent.alwaysRequestDesktopSite, - image: UIImage(braveSystemNamed: "leo.window.cursor") + Row( + text: Strings.alwaysRequestDesktopSite, + image: UIImage(braveSystemNamed: "leo.window.cursor"), + accessory: .view(defaultPageModeSwitch), + cellClass: MultilineSubtitleCell.self ) ) } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Shields/WKContentWorld+Sandbox.swift b/ios/brave-ios/Sources/Brave/Frontend/Shields/WKContentWorld+Sandbox.swift deleted file mode 100644 index 2e6fbbe960ac..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Shields/WKContentWorld+Sandbox.swift +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Foundation -import WebKit - -extension WKContentWorld { - static let cosmeticFiltersSandbox = WKContentWorld.world(name: "CosmeticFiltersSandbox") -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveSyncAPIExtensions.swift b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveSyncAPIExtensions.swift index 7fff51ee89a9..39fe0aa5325f 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveSyncAPIExtensions.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveSyncAPIExtensions.swift @@ -115,6 +115,10 @@ extension BraveSyncAPI { if Preferences.Chromium.syncOpenTabsEnabled.value { syncProfileService.userSelectedTypes.update(with: .TABS) } + + if Preferences.Chromium.syncAutofillEnabled.value { + syncProfileService.userSelectedTypes.update(with: .AUTOFILL) + } } /// Method to add observer for SyncService for onStateChanged and onSyncShutdown diff --git a/ios/brave-ios/Sources/Brave/Frontend/Sync/SyncSettingsTableViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Sync/SyncSettingsTableViewController.swift index dec178987e4e..aa9ac93f092f 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Sync/SyncSettingsTableViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Sync/SyncSettingsTableViewController.swift @@ -28,6 +28,7 @@ class SyncSettingsTableViewController: SyncViewController, UITableViewDelegate, case history case passwords case openTabs + case autofill var title: String { switch self { @@ -39,6 +40,9 @@ class SyncSettingsTableViewController: SyncViewController, UITableViewDelegate, return Strings.passwordsMenuItem case .openTabs: return Strings.openTabsMenuItem + case .autofill: + // FIXME: Needs localization + return "Phone numbers, emails, and addresses" } } } @@ -322,6 +326,8 @@ class SyncSettingsTableViewController: SyncViewController, UITableViewDelegate, if Preferences.Chromium.syncOpenTabsEnabled.value { tabManager.addRegularTabsToSyncChain() } + case .autofill: + Preferences.Chromium.syncAutofillEnabled.value = !toggleExistingStatus } syncAPI.enableSyncTypes(syncProfileService: syncProfileService) @@ -575,6 +581,8 @@ extension SyncSettingsTableViewController { return Preferences.Chromium.syncPasswordsEnabled.value case .openTabs: return Preferences.Chromium.syncOpenTabsEnabled.value + case .autofill: + return Preferences.Chromium.syncAutofillEnabled.value } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/BlockedDomainScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/BlockedDomainScriptHandler.swift index bfdac0f56e41..26dbd0df2b39 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/BlockedDomainScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/BlockedDomainScriptHandler.swift @@ -8,21 +8,15 @@ import Shared import WebKit class BlockedDomainScriptHandler: TabContentScript { - private weak var tab: Tab? - - required init(tab: Tab) { - self.tab = tab - } - static let scriptName = "BlockedDomainScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" static let scriptSandbox: WKContentWorld = .page static let userScript: WKUserScript? = nil - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -41,16 +35,16 @@ class BlockedDomainScriptHandler: TabContentScript { switch action { case "didProceed": - blockedDomainDidProceed() + blockedDomainDidProceed(tab: tab) case "didGoBack": - blockedDomainDidGoBack() + blockedDomainDidGoBack(tab: tab) default: assertionFailure("Unhandled action `\(action)`") } } - private func blockedDomainDidProceed() { - guard let url = tab?.url?.strippedInternalURL, let etldP1 = url.baseDomain else { + private func blockedDomainDidProceed(tab: Tab) { + guard let url = tab.url?.strippedInternalURL, let etldP1 = url.baseDomain else { assertionFailure( "There should be no way this method can be triggered if the tab is not on an internal url" ) @@ -58,27 +52,27 @@ class BlockedDomainScriptHandler: TabContentScript { } let request = URLRequest(url: url) - tab?.proceedAnywaysDomainList.insert(etldP1) - tab?.loadRequest(request) + tab.proceedAnywaysDomainList.insert(etldP1) + tab.loadRequest(request) } - private func blockedDomainDidGoBack() { - guard let url = tab?.url?.strippedInternalURL else { + private func blockedDomainDidGoBack(tab: Tab) { + guard let url = tab.url?.strippedInternalURL else { assertionFailure( "There should be no way this method can be triggered if the tab is not on an internal url" ) return } - guard let listItem = tab?.backList?.reversed().first(where: { $0.url != url }) else { + guard let listItem = tab.backList?.reversed().first(where: { $0.url != url }) else { // How is this even possible? // All testing indicates no, so we will not handle. // If we find it is, then we need to disable or hide the "Go Back" button in these cases. // But this would require heavy changes or ugly mechanisms to InternalSchemeHandler. - tab?.goBack() + tab.goBack() return } - tab?.goToBackForwardListItem(listItem) + tab.goToBackForwardListItem(listItem) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/ErrorPageScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/ErrorPageScriptHandler.swift deleted file mode 100644 index 5ca06d61e87a..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/ErrorPageScriptHandler.swift +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Shared -import WebKit - -private let messageOpenInSafari = "openInSafari" -private let messageCertVisitOnce = "certVisitOnce" - -extension ErrorPageHelper: TabContentScript { - - static let scriptName = "ErrorPageScript" - static let scriptId = UUID().uuidString - static let messageHandlerName = "\(scriptName)_\(messageUUID)" - static let scriptSandbox: WKContentWorld = .page - static let userScript: WKUserScript? = nil - - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void - ) { - defer { replyHandler(nil, nil) } - - if !verifyMessage(message: message) { - assertionFailure("Missing required security token.") - return - } - - guard let errorURL = message.frameInfo.request.url, - let internalUrl = InternalURL(errorURL), - internalUrl.isErrorPage, - let originalURL = internalUrl.originalURLFromErrorPage, - let res = message.body as? [String: String], - let type = res["type"] - else { return } - - switch type { - case messageOpenInSafari: - UIApplication.shared.open(originalURL, options: [:]) - case messageCertVisitOnce: - if let cert = CertificateErrorPageHandler.certsFromErrorURL(errorURL)?.first, - let host = originalURL.host - { - let origin = "\(host):\(originalURL.port ?? 443)" - addCertificate(cert, forOrigin: origin) - message.webView?.replaceLocation(with: originalURL) - // webview.reload will not change the error URL back to the original URL - } - default: - assertionFailure("Unknown error message") - } - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/HTTPBlockedScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/HTTPBlockedScriptHandler.swift deleted file mode 100644 index 53ada8f4bccb..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/HTTPBlockedScriptHandler.swift +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveCore -import Foundation -import Shared -import WebKit - -class HTTPBlockedScriptHandler: TabContentScript { - private weak var tab: Tab? - private weak var tabManager: TabManager? - - required init(tab: Tab, tabManager: TabManager) { - self.tab = tab - self.tabManager = tabManager - } - - static let scriptName = "HTTPBlockedScript" - static let scriptId = UUID().uuidString - static let messageHandlerName = "\(scriptName)_\(messageUUID)" - static let scriptSandbox: WKContentWorld = .page - static let userScript: WKUserScript? = nil - - @MainActor func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void - ) { - defer { replyHandler(nil, nil) } - - if !verifyMessage(message: message, securityToken: UserScriptManager.securityToken) { - assertionFailure("Missing required security token.") - return - } - - guard let params = message.body as? [String: AnyObject], - let action = params["action"] as? String - else { - assertionFailure("Missing required params.") - return - } - - switch action { - case "didProceed": - didProceed() - case "didGoBack": - didGoBack() - default: - assertionFailure("Unhandled action `\(action)`") - } - } - - private func didProceed() { - guard let tab, let url = tab.upgradedHTTPSRequest?.url ?? tab.url?.strippedInternalURL else { - // assertionFailure( - // "There should be no way this method can be triggered if the tab is not on an internal url" - // ) - return - } - - // When restoring the page, `upgradedHTTPSRequest` will be nil - // So we default to the embedded internal page URL - let request = tab.upgradedHTTPSRequest ?? URLRequest(url: url) - if let httpsUpgradeService = HttpsUpgradeServiceFactory.get(privateMode: tab.isPrivate), - let host = url.host(percentEncoded: false) - { - httpsUpgradeService.allowHttp(forHost: host) - } - tab.loadRequest(request) - } - - @MainActor private func didGoBack() { - guard let tab else { return } - tab.upgradedHTTPSRequest = nil - if tab.backList?.isEmpty == true { - // interstitial was opened in a new tab - tabManager?.addTabToRecentlyClosed(tab) - tabManager?.removeTab(tab) - } else { - tab.goBack() - } - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/SessionRestoreScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/SessionRestoreScriptHandler.swift index d859819bba57..16e6ec097864 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/SessionRestoreScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/SessionRestoreScriptHandler.swift @@ -12,11 +12,6 @@ protocol SessionRestoreScriptHandlerDelegate: AnyObject { class SessionRestoreScriptHandler: TabContentScript { weak var delegate: SessionRestoreScriptHandlerDelegate? - fileprivate weak var tab: Tab? - - required init(tab: Tab) { - self.tab = tab - } static let scriptName = "SessionRestoreScript" static let scriptId = UUID().uuidString @@ -38,9 +33,9 @@ class SessionRestoreScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -50,7 +45,7 @@ class SessionRestoreScriptHandler: TabContentScript { return } - if let tab = tab, let params = message.body as? [String: AnyObject] { + if let params = message.body as? [String: AnyObject] { if params["name"] as? String == "didRestoreSession" { DispatchQueue.main.async { self.delegate?.sessionRestore(self, didRestoreSessionForTab: tab) diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/Web3NameServiceScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/Web3NameServiceScriptHandler.swift index 0c647b46a66b..08b2726c5f59 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/Web3NameServiceScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Internal/Web3NameServiceScriptHandler.swift @@ -26,11 +26,6 @@ class Web3NameServiceScriptHandler: TabContentScript { weak var delegate: Web3NameServiceScriptHandlerDelegate? var originalURL: URL? - fileprivate weak var tab: Tab? - - required init(tab: Tab) { - self.tab = tab - } static let scriptName = "Web3NameServiceScript" static let scriptId = UUID().uuidString @@ -38,9 +33,9 @@ class Web3NameServiceScriptHandler: TabContentScript { static let scriptSandbox: WKContentWorld = .page static let userScript: WKUserScript? = nil - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchResultAdScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchResultAdScriptHandler.swift index d4eb19b4df18..457ba9398606 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchResultAdScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchResultAdScriptHandler.swift @@ -28,12 +28,6 @@ class BraveSearchResultAdScriptHandler: TabContentScript { let creatives: [SearchResultAd] } - fileprivate weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "BraveSearchResultAdScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -54,9 +48,9 @@ class BraveSearchResultAdScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -66,9 +60,7 @@ class BraveSearchResultAdScriptHandler: TabContentScript { return } - guard let tab = tab, - let braveSearchResultAdManager = tab.braveSearchResultAdManager - else { + guard let braveSearchResultAdManager = tab.braveSearchResultAdManager else { Logger.module.error("Failed to get brave search result ad handler") return } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift index e933d4a9744c..06be38e8c2e7 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift @@ -11,7 +11,6 @@ import WebKit import os.log class BraveSearchScriptHandler: TabContentScript { - private weak var tab: Tab? private let profile: Profile private weak var rewards: BraveRewards? @@ -23,8 +22,7 @@ class BraveSearchScriptHandler: TabContentScript { /// How many times user is shown the default browser prompt in total, this does not reset between app launches. private let maxCountOfDefaultBrowserPromptsTotal = 10 - required init(tab: Tab, profile: Profile, rewards: BraveRewards) { - self.tab = tab + required init(profile: Profile, rewards: BraveRewards) { self.profile = profile self.rewards = rewards } @@ -62,9 +60,9 @@ class BraveSearchScriptHandler: TabContentScript { let methodId: Int } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -93,7 +91,7 @@ class BraveSearchScriptHandler: TabContentScript { switch method { case Method.canSetBraveSearchAsDefault.rawValue: - handleCanSetBraveSearchAsDefault(replyHandler: replyHandler) + handleCanSetBraveSearchAsDefault(tab: tab, replyHandler: replyHandler) case Method.setBraveSearchDefault.rawValue: handleSetBraveSearchDefault(replyHandler: replyHandler) default: @@ -101,8 +99,8 @@ class BraveSearchScriptHandler: TabContentScript { } } - private func handleCanSetBraveSearchAsDefault(replyHandler: (Any?, String?) -> Void) { - if tab?.isPrivate == true { + private func handleCanSetBraveSearchAsDefault(tab: Tab, replyHandler: (Any?, String?) -> Void) { + if tab.isPrivate == true { Logger.module.debug("Private mode detected, skipping setting Brave Search as a default") replyHandler(false, nil) return diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSkusScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSkusScriptHandler.swift index e5e2143f8f43..83e5c547d6d4 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSkusScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSkusScriptHandler.swift @@ -13,18 +13,6 @@ import WebKit import os.log class BraveSkusScriptHandler: TabContentScript { - private weak var tab: Tab? - private let skusManager: BraveSkusManager - - required init?(tab: Tab) { - self.tab = tab - guard let skusManager = BraveSkusManager(isPrivateMode: tab.isPrivate) else { - return nil - } - - self.skusManager = skusManager - } - static let scriptName = "BraveSkusScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -47,9 +35,9 @@ class BraveSkusScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -76,7 +64,12 @@ class BraveSkusScriptHandler: TabContentScript { Task { @MainActor in do { - let result = try await processRequest(message: message, method: method, for: requestHost) + let result = try await processRequest( + tab: tab, + message: message, + method: method, + for: requestHost + ) replyHandler(result, nil) } catch { Logger.module.error("Brave skus error processing request: \(error)") @@ -87,10 +80,15 @@ class BraveSkusScriptHandler: TabContentScript { @MainActor private func processRequest( + tab: Tab, message: WKScriptMessage, method: Method, for skusDomain: String ) async throws -> Any? { + guard let skusManager = BraveSkusManager(isPrivateMode: tab.isPrivate) else { + return nil + } + switch method { case .refreshOrder: let order = try OrderMessage.from(message: message) diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift index c9fb9f548a93..31a4cdfe30fc 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift @@ -14,23 +14,16 @@ import os.log import BraveTalk class BraveTalkScriptHandler: TabContentScript { - private weak var tab: Tab? private weak var rewards: BraveRewards? private var rewardsEnabledReplyHandler: ((Any?, String?) -> Void)? private let launchNativeBraveTalk: (_ tab: Tab?, _ room: String, _ token: String) -> Void required init( - tab: Tab, rewards: BraveRewards, launchNativeBraveTalk: @escaping (_ tab: Tab?, _ room: String, _ token: String) -> Void ) { - self.tab = tab self.rewards = rewards self.launchNativeBraveTalk = launchNativeBraveTalk - - tab.rewardsEnabledCallback = { [weak self] success in - self?.rewardsEnabledReplyHandler?(success, nil) - } } static let scriptName = "BraveTalkScript" @@ -85,9 +78,9 @@ class BraveTalkScriptHandler: TabContentScript { } } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -114,7 +107,7 @@ class BraveTalkScriptHandler: TabContentScript { switch payload.kind { case .braveRequestAdsEnabled: - handleBraveRequestAdsEnabled(replyHandler) + handleBraveRequestAdsEnabled(tab: tab, replyHandler) case .launchNativeBraveTalk(let url): guard let components = URLComponents(string: url), case let room = String(components.path.dropFirst(1)), @@ -127,8 +120,11 @@ class BraveTalkScriptHandler: TabContentScript { } } - private func handleBraveRequestAdsEnabled(_ replyHandler: @escaping (Any?, String?) -> Void) { - guard let rewards = rewards, tab?.isPrivate != true else { + private func handleBraveRequestAdsEnabled( + tab: Tab, + _ replyHandler: @escaping (Any?, String?) -> Void + ) { + guard let rewards = rewards, tab.isPrivate != true else { replyHandler(false, nil) return } @@ -140,10 +136,11 @@ class BraveTalkScriptHandler: TabContentScript { // If rewards are disabled we show a Rewards panel, // The `rewardsEnabledReplyHandler` will be called from other place. - if let tab = tab { - rewardsEnabledReplyHandler = replyHandler - tab.tabDelegate?.showRequestRewardsPanel(tab) + rewardsEnabledReplyHandler = replyHandler + tab.rewardsEnabledCallback = { [weak self] success in + self?.rewardsEnabledReplyHandler?(success, nil) } + tab.tabDelegate?.showRequestRewardsPanel(tab) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift index 95162142be31..a30577fe082f 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift @@ -54,14 +54,14 @@ extension ContentBlockerHelper: TabContentScript { blockedRequests.removeAll() } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } - guard let currentTabURL = tab?.webView?.url else { + guard let currentTabURL = tab.webView?.lastCommittedURL else { assertionFailure("Missing tab or webView") return } @@ -128,7 +128,7 @@ extension ContentBlockerHelper: TabContentScript { if blockedType == .ad, Preferences.PrivacyReports.captureShieldsData.value, let domainURL = URL(string: domainURLString), let blockedResourceHost = requestURL.baseDomain, - tab?.isPrivate != true + !tab.isPrivate { PrivacyReportsManager.pendingBlockedRequests.append( (blockedResourceHost, domainURL, Date()) diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/CosmeticFiltersScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/CosmeticFiltersScriptHandler.swift index c00ede362823..acfbab669881 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/CosmeticFiltersScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/CosmeticFiltersScriptHandler.swift @@ -30,15 +30,9 @@ class CosmeticFiltersScriptHandler: TabContentScript { static let scriptSandbox: WKContentWorld = .defaultClient static let userScript: WKUserScript? = nil - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -59,7 +53,7 @@ class CosmeticFiltersScriptHandler: TabContentScript { Task { @MainActor in let domain = Domain.getOrCreate( forUrl: frameURL, - persistent: self.tab?.isPrivate == true ? false : true + persistent: !tab.isPrivate ) let cachedEngines = AdBlockGroupsManager.shared.cachedEngines(for: domain) @@ -76,7 +70,7 @@ class CosmeticFiltersScriptHandler: TabContentScript { return nil } - return (selectors, cachedEngine.type.isAlwaysAggressive) + return await (selectors, cachedEngine.type.isAlwaysAggressive) } catch { Logger.module.error("\(error.localizedDescription)") return nil diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/EthereumProviderScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/EthereumProviderScriptHandler.swift index e42e46c1dcdd..c604a7c0262f 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/EthereumProviderScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/EthereumProviderScriptHandler.swift @@ -19,12 +19,6 @@ class EthereumProviderScriptHandler: TabContentScript { "eth_newPendingTransactionFilter", ] - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "WalletEthereumProviderScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -64,9 +58,9 @@ class EthereumProviderScriptHandler: TabContentScript { } } - @MainActor func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -74,8 +68,7 @@ class EthereumProviderScriptHandler: TabContentScript { return } - guard let tab = tab, - !tab.isPrivate, + guard !tab.isPrivate, let provider = tab.walletEthProvider, // Fail if there is no last committed URL yet !message.frameInfo.securityOrigin.host.isEmpty, diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistFolderSharingScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistFolderSharingScriptHandler.swift index 1a5c61e1ea69..2c4c6c598006 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistFolderSharingScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistFolderSharingScriptHandler.swift @@ -15,14 +15,8 @@ protocol PlaylistFolderSharingScriptHandlerDelegate: AnyObject { } class PlaylistFolderSharingScriptHandler: NSObject, TabContentScript { - fileprivate weak var tab: Tab? public weak var delegate: PlaylistFolderSharingScriptHandlerDelegate? - init(tab: Tab) { - self.tab = tab - super.init() - } - static let scriptName = "PlaylistFolderSharingScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -43,10 +37,10 @@ class PlaylistFolderSharingScriptHandler: NSObject, TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistScriptHandler.swift index fbd305eba9fa..023650e23614 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PlaylistScriptHandler.swift @@ -27,7 +27,6 @@ protocol PlaylistScriptHandlerDelegate: NSObject { } class PlaylistScriptHandler: NSObject, TabContentScript { - fileprivate weak var tab: Tab? public weak var delegate: PlaylistScriptHandlerDelegate? private var url: URL? private var urlObserver: NSObjectProtocol? @@ -35,21 +34,20 @@ class PlaylistScriptHandler: NSObject, TabContentScript { private static let queue = DispatchQueue(label: "com.playlisthelper.queue", qos: .userInitiated) init(tab: Tab) { - self.tab = tab self.url = tab.url super.init() urlObserver = tab.webView?.observe( - \.url, + \.visibleURL, options: [.new], - changeHandler: { [weak self] _, change in + changeHandler: { [weak self, weak tab] _, change in guard let self = self, let url = change.newValue else { return } if self.url != url { self.url = url self.asset?.cancelLoading() self.asset = nil - self.delegate?.updatePlaylistURLBar(tab: self.tab, state: .none, item: nil) + self.delegate?.updatePlaylistURLBar(tab: tab, state: .none, item: nil) } } ) @@ -63,7 +61,6 @@ class PlaylistScriptHandler: NSObject, TabContentScript { deinit { asset?.cancelLoading() - delegate?.updatePlaylistURLBar(tab: tab, state: .none, item: nil) } static let playlistLongPressed = "playlistLongPressed_\(uniqueID)" @@ -100,10 +97,10 @@ class PlaylistScriptHandler: NSObject, TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -122,22 +119,27 @@ class PlaylistScriptHandler: NSObject, TabContentScript { } Self.processPlaylistInfo( + tab: tab, handler: self, item: PlaylistInfo.from(message: message) ) } - private class func processPlaylistInfo(handler: PlaylistScriptHandler, item: PlaylistInfo?) { + private class func processPlaylistInfo( + tab: Tab, + handler: PlaylistScriptHandler, + item: PlaylistInfo? + ) { guard var item = item, !item.src.isEmpty else { DispatchQueue.main.async { - handler.delegate?.updatePlaylistURLBar(tab: handler.tab, state: .none, item: nil) + handler.delegate?.updatePlaylistURLBar(tab: tab, state: .none, item: nil) } return } if handler.url?.baseDomain != "soundcloud.com", item.isInvisible { DispatchQueue.main.async { - handler.delegate?.updatePlaylistURLBar(tab: handler.tab, state: .none, item: nil) + handler.delegate?.updatePlaylistURLBar(tab: tab, state: .none, item: nil) } return } @@ -147,8 +149,8 @@ class PlaylistScriptHandler: NSObject, TabContentScript { item = PlaylistInfo( name: item.name, src: item.src, - pageSrc: handler.tab?.webView?.url?.absoluteString ?? item.pageSrc, - pageTitle: handler.tab?.webView?.title ?? item.pageTitle, + pageSrc: tab.webView?.lastCommittedURL?.absoluteString ?? item.pageSrc, + pageTitle: tab.webView?.title ?? item.pageTitle, mimeType: item.mimeType, duration: item.duration, lastPlayedOffset: 0.0, @@ -164,7 +166,7 @@ class PlaylistScriptHandler: NSObject, TabContentScript { if item.duration <= 0.0 && !item.detected || item.src.isEmpty { DispatchQueue.main.async { - handler.delegate?.updatePlaylistURLBar(tab: handler.tab, state: .none, item: nil) + handler.delegate?.updatePlaylistURLBar(tab: tab, state: .none, item: nil) } return } @@ -177,14 +179,14 @@ class PlaylistScriptHandler: NSObject, TabContentScript { if PlaylistItem.itemExists(pageSrc: item.pageSrc) { // Item already exists, so just update the database with new token or URL. - handler.updateItem(item, detected: item.detected) + handler.updateItem(item, detected: item.detected, tab: tab) } else if item.detected { // Automatic Detection - delegate.updatePlaylistURLBar(tab: handler.tab, state: .newItem, item: item) - delegate.showPlaylistOnboarding(tab: handler.tab) + delegate.updatePlaylistURLBar(tab: tab, state: .newItem, item: item) + delegate.showPlaylistOnboarding(tab: tab) } else { // Long-Press - delegate.showPlaylistAlert(tab: handler.tab, state: .newItem, item: item) + delegate.showPlaylistAlert(tab: tab, state: .newItem, item: item) } } } @@ -207,9 +209,9 @@ class PlaylistScriptHandler: NSObject, TabContentScript { return await PlaylistMediaStreamer.loadAssetPlayability(asset: asset) } - private func updateItem(_ item: PlaylistInfo, detected: Bool) { + private func updateItem(_ item: PlaylistInfo, detected: Bool, tab: Tab) { if detected { - self.delegate?.updatePlaylistURLBar(tab: self.tab, state: .existingItem, item: item) + self.delegate?.updatePlaylistURLBar(tab: tab, state: .existingItem, item: item) } PlaylistItem.updateItem(item) { [weak self] in @@ -219,9 +221,9 @@ class PlaylistScriptHandler: NSObject, TabContentScript { if let delegate = self.delegate { if detected { - delegate.updatePlaylistURLBar(tab: self.tab, state: .existingItem, item: item) + delegate.updatePlaylistURLBar(tab: tab, state: .existingItem, item: item) } else { - delegate.showPlaylistToast(tab: self.tab, state: .existingItem, item: item) + delegate.showPlaylistToast(tab: tab, state: .existingItem, item: item) } } } @@ -232,7 +234,7 @@ extension PlaylistScriptHandler: UIGestureRecognizerDelegate { @objc func onLongPressedWebView(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began, - let webView = tab?.webView, + let webView = gestureRecognizer.view as? BraveWebView, Preferences.Playlist.enableLongPressAddToPlaylist.value { @@ -277,7 +279,7 @@ extension PlaylistScriptHandler: UIGestureRecognizerDelegate { extension PlaylistScriptHandler { static func getCurrentTime( - webView: WKWebView, + webView: BraveWebView, nodeTag: String, completion: @escaping (Double) -> Void ) { @@ -330,7 +332,7 @@ extension PlaylistScriptHandler { extension PlaylistScriptHandler { static func updatePlaylistTab(tab: Tab, item: PlaylistInfo?) { if let handler = tab.getContentScript(name: Self.scriptName) as? PlaylistScriptHandler { - Self.processPlaylistInfo(handler: handler, item: item) + Self.processPlaylistInfo(tab: tab, handler: handler, item: item) } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PrintScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PrintScriptHandler.swift index 7f6b042025cc..06c3ffc0daf4 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PrintScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/PrintScriptHandler.swift @@ -9,15 +9,13 @@ import os.log class PrintScriptHandler: TabContentScript { private weak var browserController: BrowserViewController? - private weak var tab: Tab? private var isPresentingController = false private var printCounter = 0 private var isBlocking = false private var currentDomain: String? - required init(browserController: BrowserViewController, tab: Tab) { + required init(browserController: BrowserViewController) { self.browserController = browserController - self.tab = tab } static let scriptName = "PrintScript" @@ -40,10 +38,10 @@ class PrintScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -52,7 +50,7 @@ class PrintScriptHandler: TabContentScript { return } - if let tab = tab, let webView = tab.webView, let url = webView.url { + if let webView = tab.webView, let url = webView.lastCommittedURL { // If the main-frame's URL has changed if let domain = url.baseDomain, domain != currentDomain, message.frameInfo.isMainFrame { isBlocking = false diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ReadyStateScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ReadyStateScriptHandler.swift index 1cf03c3702ce..68967edd5e4e 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ReadyStateScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ReadyStateScriptHandler.swift @@ -45,13 +45,8 @@ struct ReadyState: Codable { } class ReadyStateScriptHandler: TabContentScript { - private weak var tab: Tab? private var debounceTimer: Timer? - required init(tab: Tab) { - self.tab = tab - } - static let scriptName = "ReadyStateScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -72,12 +67,11 @@ class ReadyStateScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { - defer { replyHandler(nil, nil) } if !verifyMessage(message: message) { @@ -90,6 +84,6 @@ class ReadyStateScriptHandler: TabContentScript { return } - tab?.onPageReadyStateChanged?(readyState.state) + tab.onPageReadyStateChanged?(readyState.state) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift index 1194b5a72e84..29256ab62951 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift @@ -44,18 +44,12 @@ class RequestBlockingContentScriptHandler: TabContentScript { ) }() - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { - guard let tab = tab, let currentTabURL = tab.webView?.url else { + guard let currentTabURL = tab.webView?.lastCommittedURL else { assertionFailure("Should have a tab set") return } @@ -93,7 +87,7 @@ class RequestBlockingContentScriptHandler: TabContentScript { // We simply check the known subframeURLs on this page. guard tab.url?.baseDomain == sourceURL.baseDomain - || self.tab?.currentPageData?.allSubframeURLs.contains(sourceURL) == true + || tab.currentPageData?.allSubframeURLs.contains(sourceURL) == true else { replyHandler(shouldBlock, nil) return diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift index 64d2fc5227a6..9d6b75c3abb9 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift @@ -10,11 +10,9 @@ import os.log class RewardsReportingScriptHandler: TabContentScript { let rewards: BraveRewards - weak var tab: Tab? - init(rewards: BraveRewards, tab: Tab) { + init(rewards: BraveRewards) { self.rewards = rewards - self.tab = tab } static let scriptName = "RewardsReportingScript" @@ -38,10 +36,10 @@ class RewardsReportingScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } struct Content: Decodable { @@ -51,7 +49,7 @@ class RewardsReportingScriptHandler: TabContentScript { var referrerUrl: String? } - if tab?.isPrivate == true || !rewards.isEnabled { + if tab.isPrivate || !rewards.isEnabled { return } @@ -69,7 +67,7 @@ class RewardsReportingScriptHandler: TabContentScript { let json = try JSONSerialization.data(withJSONObject: body, options: []) var content = try JSONDecoder().decode(Content.self, from: json) - guard let tab = tab, let tabURL = tab.url else { return } + guard let tabURL = tab.url else { return } if content.url.hasPrefix("//") { content.url = "\(tabURL.scheme ?? "http"):\(content.url)" diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/SolanaProviderScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/SolanaProviderScriptHandler.swift index ef7d11b4d56f..2a0dacbbcf36 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/SolanaProviderScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/SolanaProviderScriptHandler.swift @@ -52,12 +52,6 @@ class SolanaProviderScriptHandler: TabContentScript { ) }() - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - private struct MessageBody: Decodable { enum Method: String, Decodable { case connect @@ -77,9 +71,9 @@ class SolanaProviderScriptHandler: TabContentScript { } } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -87,8 +81,7 @@ class SolanaProviderScriptHandler: TabContentScript { return } - guard let tab = tab, - !tab.isPrivate, + guard !tab.isPrivate, let provider = tab.walletSolProvider, // Fail if there is no last committed URL yet !message.frameInfo.securityOrigin.host.isEmpty, @@ -114,19 +107,19 @@ class SolanaProviderScriptHandler: TabContentScript { Task { @MainActor in switch body.method { case .connect: - let (publicKey, error) = await connect(args: body.args) + let (publicKey, error) = await connect(tab: tab, args: body.args) replyHandler(publicKey, error) if let publicKey = publicKey as? String { - await emitConnectEvent(publicKey: publicKey) + await emitConnectEvent(tab: tab, publicKey: publicKey) } case .disconnect: provider.disconnect() replyHandler("{:}", nil) case .signAndSendTransaction: - let (result, error) = await signAndSendTransaction(args: body.args) + let (result, error) = await signAndSendTransaction(tab: tab, args: body.args) replyHandler(result, error) case .signMessage: - let (result, error) = await signMessage(args: body.args) + let (result, error) = await signMessage(tab: tab, args: body.args) replyHandler(result, error) case .request: guard let args = body.args, @@ -136,26 +129,26 @@ class SolanaProviderScriptHandler: TabContentScript { replyHandler(nil, buildErrorJson(status: .invalidParams, errorMessage: "Invalid args")) return } - let (result, error) = await request(args: body.args) + let (result, error) = await request(tab: tab, args: body.args) replyHandler(result, error) if method == Keys.connect.rawValue, let publicKey = result as? String { - await emitConnectEvent(publicKey: publicKey) + await emitConnectEvent(tab: tab, publicKey: publicKey) } else if method == Keys.disconnect.rawValue { tab.emitSolanaEvent(.disconnect) } case .signTransaction: - let (result, error) = await signTransaction(args: body.args) + let (result, error) = await signTransaction(tab: tab, args: body.args) replyHandler(result, error) case .signAllTransactions: - let (result, error) = await signAllTransactions(args: body.args) + let (result, error) = await signAllTransactions(tab: tab, args: body.args) replyHandler(result, error) } } } /// Given optional args `{onlyIfTrusted: Bool}`, will return the base 58 encoded public key for success or the error dictionary for failures. - @MainActor func connect(args: String?) async -> (Any?, String?) { - guard let tab = tab, let provider = tab.walletSolProvider else { + @MainActor func connect(tab: Tab, args: String?) async -> (Any?, String?) { + guard let provider = tab.walletSolProvider else { return ( nil, buildErrorJson(status: .internalError, errorMessage: Strings.Wallet.internalErrorMessage) @@ -177,12 +170,12 @@ class SolanaProviderScriptHandler: TabContentScript { /// Given args `{serializedMessage: [Uint8], signatures: [Buffer], sendOptions: [:]}`, will return /// dictionary `{publicKey: , signature: }` for success /// or an error dictionary for failures. - @MainActor func signAndSendTransaction(args: String?) async -> (Any?, String?) { + @MainActor func signAndSendTransaction(tab: Tab, args: String?) async -> (Any?, String?) { guard let args = args, let arguments = MojoBase.Value(jsonString: args)?.dictionaryValue, let serializedMessage = arguments[Keys.serializedMessage.rawValue], let signatures = arguments[Keys.signatures.rawValue], - let provider = tab?.walletSolProvider + let provider = tab.walletSolProvider else { return (nil, buildErrorJson(status: .invalidParams, errorMessage: "Invalid args")) } @@ -208,11 +201,11 @@ class SolanaProviderScriptHandler: TabContentScript { /// Given args `{[[UInt8], String]}` (second arg optional), will return /// `{publicKey: , signature: }` for success flow /// or an error dictionary for failures - @MainActor func signMessage(args: String?) async -> (Any?, String?) { + @MainActor func signMessage(tab: Tab, args: String?) async -> (Any?, String?) { guard let args = args, let argsList = MojoBase.Value(jsonString: args)?.listValue, let blobMsg = argsList.first?.numberArray, - let provider = tab?.walletSolProvider + let provider = tab.walletSolProvider else { return (nil, buildErrorJson(status: .invalidParams, errorMessage: "Invalid args")) } @@ -239,11 +232,10 @@ class SolanaProviderScriptHandler: TabContentScript { /// Given a request arg `{method: String, params: {}}`, will encode the response as a json object for success /// or provide an error dictionary for failures - @MainActor func request(args: String?) async -> (Any?, String?) { + @MainActor func request(tab: Tab, args: String?) async -> (Any?, String?) { guard let args = args, var argDict = MojoBase.Value(jsonString: args)?.dictionaryValue, let method = argDict[Keys.method.rawValue]?.stringValue, - let tab = tab, let provider = tab.walletSolProvider else { return (nil, buildErrorJson(status: .invalidParams, errorMessage: "Invalid args")) @@ -279,12 +271,12 @@ class SolanaProviderScriptHandler: TabContentScript { /// Given args `{serializedMessage: Buffer, signatures: {publicKey: String, signature: Buffer}}`, /// will encoded the response as a json object for success or provide an error dictionary for failures - @MainActor func signTransaction(args: String?) async -> (Any?, String?) { + @MainActor func signTransaction(tab: Tab, args: String?) async -> (Any?, String?) { guard let args = args, let arguments = MojoBase.Value(jsonString: args)?.dictionaryValue, let serializedMessage = arguments[Keys.serializedMessage.rawValue], let signatures = arguments[Keys.signatures.rawValue], - let provider = tab?.walletSolProvider + let provider = tab.walletSolProvider else { return (nil, buildErrorJson(status: .invalidParams, errorMessage: "Invalid args")) } @@ -311,10 +303,10 @@ class SolanaProviderScriptHandler: TabContentScript { /// Given args `[{serializedMessage: Buffer, signatures: {publicKey: String, signature: Buffer}}]`, /// will encoded the response as a json object for success or provide an error dictionary for failures - @MainActor func signAllTransactions(args: String?) async -> (Any?, String?) { + @MainActor func signAllTransactions(tab: Tab, args: String?) async -> (Any?, String?) { guard let args = args, let transactions = MojoBase.Value(jsonString: args)?.listValue, - let provider = tab?.walletSolProvider + let provider = tab.walletSolProvider else { return (nil, buildErrorJson(status: .invalidParams, errorMessage: "Invalid args")) } @@ -406,8 +398,8 @@ class SolanaProviderScriptHandler: TabContentScript { ) } - @MainActor private func emitConnectEvent(publicKey: String) async { - if let webView = tab?.webView { + @MainActor private func emitConnectEvent(tab: Tab, publicKey: String) async { + if let webView = tab.webView { let script = "window.solana.emit('connect', new \(UserScriptManager.walletSolanaNameSpace).solanaWeb3.PublicKey('\(publicKey.htmlEntityEncodedString)'))" await webView.evaluateSafeJavaScript( diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/URLPartinessScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/URLPartinessScriptHandler.swift index 67ab9673c8e2..cf050153c273 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/URLPartinessScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/URLPartinessScriptHandler.swift @@ -29,15 +29,9 @@ class URLPartinessScriptHandler: TabContentScript { static let scriptSandbox: WKContentWorld = .defaultClient static let userScript: WKUserScript? = nil - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/YoutubeQualityScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/YoutubeQualityScriptHandler.swift index cb2397c54718..f79230a4cdfa 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/YoutubeQualityScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/YoutubeQualityScriptHandler.swift @@ -10,17 +10,15 @@ import Shared import WebKit class YoutubeQualityScriptHandler: NSObject, TabContentScript { - private weak var tab: Tab? private var url: URL? private var urlObserver: NSObjectProtocol? init(tab: Tab) { - self.tab = tab self.url = tab.url super.init() urlObserver = tab.webView?.observe( - \.url, + \.visibleURL, options: [.new], changeHandler: { [weak self] object, change in guard let self = self, let url = change.newValue else { return } @@ -78,10 +76,10 @@ class YoutubeQualityScriptHandler: NSObject, TabContentScript { ) } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { assertionFailure("Missing required security token.") diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/AdsMediaReportingScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/AdsMediaReportingScriptHandler.swift index f89070b54ee6..d95ec5d6b5c5 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/AdsMediaReportingScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/AdsMediaReportingScriptHandler.swift @@ -10,11 +10,9 @@ import os.log class AdsMediaReportingScriptHandler: TabContentScript { let rewards: BraveRewards - weak var tab: Tab? - init(rewards: BraveRewards, tab: Tab) { + init(rewards: BraveRewards) { self.rewards = rewards - self.tab = tab } static let scriptName = "AdsMediaReportingScript" @@ -23,10 +21,10 @@ class AdsMediaReportingScriptHandler: TabContentScript { static let scriptSandbox: WKContentWorld = .defaultClient static let userScript: WKUserScript? = nil - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -40,7 +38,6 @@ class AdsMediaReportingScriptHandler: TabContentScript { } if let isPlaying = body["data"] as? Bool { - guard let tab = tab else { return } if isPlaying { rewards.reportMediaStarted(tabId: Int(tab.rewardsId)) } else { diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveLeoScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveLeoScriptHandler.swift index c8d99dd72610..01edc15f64f7 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveLeoScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveLeoScriptHandler.swift @@ -4,18 +4,12 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. import AIChat +import BraveCore +import BraveShared import Foundation -import WebKit import os.log class BraveLeoScriptHandler: NSObject, TabContentScript { - fileprivate weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - super.init() - } - static let getMainArticle = "getMainArticle\(uniqueID)" static let getPDFDocument = "getPDFDocument\(uniqueID)" @@ -44,10 +38,10 @@ class BraveLeoScriptHandler: NSObject, TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -61,7 +55,7 @@ class BraveLeoScriptHandler: NSObject, TabContentScript { extension BraveLeoScriptHandler: AIChatJavascript { @MainActor - static func getPageContentType(webView: WKWebView) async -> String? { + static func getPageContentType(webView: CWVWebView) async -> String? { return try? await webView.evaluateSafeJavaScriptThrowing( functionName: "document.contentType", contentWorld: Self.scriptSandbox, @@ -71,7 +65,7 @@ extension BraveLeoScriptHandler: AIChatJavascript { } @MainActor - static func getMainArticle(webView: WKWebView) async -> String? { + static func getMainArticle(webView: CWVWebView) async -> String? { do { let articleText = try await webView.evaluateSafeJavaScriptThrowing( @@ -88,8 +82,8 @@ extension BraveLeoScriptHandler: AIChatJavascript { } @MainActor - static func getPDFDocument(webView: WKWebView) async -> String? { - // po webView.perform(Selector("_methodDescription")) + static func getPDFDocument(webView: CWVWebView) async -> String? { + guard let webView = webView.underlyingWebView else { return nil } if webView.responds(to: Selector(("_dataForDisplayedPDF"))), let pdfData = webView.perform(Selector(("_dataForDisplayedPDF"))).takeUnretainedValue() as? Data @@ -131,7 +125,7 @@ extension BraveLeoScriptHandler: AIChatJavascript { } @MainActor - static func getPrintViewPDF(webView: WKWebView) async -> Data { + static func getPrintViewPDF(webView: CWVWebView) async -> Data { // No article text. Attempt to parse the page as a PDF/Image let render = UIPrintPageRenderer() render.addPrintFormatter(webView.viewPrintFormatter(), startingAtPageAt: 0) diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/CustomSearchScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/CustomSearchScriptHandler.swift index b4379e26c608..438b70629e85 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/CustomSearchScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/CustomSearchScriptHandler.swift @@ -6,12 +6,6 @@ import Foundation import WebKit class CustomSearchScriptHandler: TabContentScript { - fileprivate weak var tab: Tab? - - required init(tab: Tab) { - self.tab = tab - } - static let scriptName = "CustomSearchHelper" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -32,10 +26,10 @@ class CustomSearchScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { replyHandler(nil, nil) // We don't listen to messages because the BVC calls the searchHelper script by itself. diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DarkReaderScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DarkReaderScriptHandler.swift index 5d3be1949660..36a680066983 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DarkReaderScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DarkReaderScriptHandler.swift @@ -3,15 +3,13 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import Foundation import Preferences import Shared import WebKit public class DarkReaderScriptHandler: TabContentScript { - - private weak var tab: Tab? - #if USE_NIGHTMODE_COLOURS private static let configuration = [ "brightness": 100, @@ -22,14 +20,10 @@ public class DarkReaderScriptHandler: TabContentScript { private static let configuration: [String: Int] = [:] #endif - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "DarkReaderScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" - static let scriptSandbox: WKContentWorld = .world(name: "DarkReaderContentWorld") + static let scriptSandbox: WKContentWorld = .defaultClient static let userScript: WKUserScript? = { guard var script = loadUserScript(named: scriptName) else { return nil @@ -46,16 +40,16 @@ public class DarkReaderScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { // Do nothing. There's no message handling for Dark-Reader } /// Enables DarkReader - static func enable(for webView: WKWebView) { + static func enable(for webView: CWVWebView) { webView.evaluateSafeJavaScript( functionName: "DarkReader.enable", args: configuration.isEmpty ? [] : [configuration], @@ -64,7 +58,7 @@ public class DarkReaderScriptHandler: TabContentScript { } /// Disables DarkReader - static func disable(for webView: WKWebView) { + static func disable(for webView: CWVWebView) { webView.evaluateSafeJavaScript( functionName: "DarkReader.disable", args: [], diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DeAmpScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DeAmpScriptHandler.swift index 56827be23774..76434b4a9201 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DeAmpScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DeAmpScriptHandler.swift @@ -13,12 +13,6 @@ public class DeAmpScriptHandler: TabContentScript { let destURL: URL } - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "DeAmpScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -39,9 +33,9 @@ public class DeAmpScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { if !verifyMessage(message: message) { @@ -59,7 +53,7 @@ public class DeAmpScriptHandler: TabContentScript { // Also check that our window url does not match the previously committed url // or that previousURL is nil which indicates as circular loop caused by a server side redirect let shouldRedirect = - dto.destURL != tab?.previousComittedURL && tab?.committedURL != tab?.previousComittedURL + dto.destURL != tab.previousComittedURL && tab.committedURL != tab.previousComittedURL replyHandler(shouldRedirect, nil) } catch { assertionFailure("Invalid type of message. Fix the `RequestBlocking.js` script") diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DownloadContentScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DownloadContentScriptHandler.swift index 6c35b1c5b62c..21c78cc29403 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DownloadContentScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/DownloadContentScriptHandler.swift @@ -18,13 +18,11 @@ private struct BlobDownloadInfo: Codable { } class DownloadContentScriptHandler: TabContentScript { - private weak var tab: Tab? private weak var browserViewController: BrowserViewController? private static var blobUrlForDownload: URL? - init(browserController: BrowserViewController, tab: Tab) { + init(browserController: BrowserViewController) { self.browserViewController = browserController - self.tab = tab } static let scriptName = "DownloadContentScript" @@ -47,9 +45,9 @@ class DownloadContentScriptHandler: TabContentScript { return true } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -73,7 +71,6 @@ class DownloadContentScriptHandler: TabContentScript { } defer { - browserViewController?.pendingDownloadWebView = nil Self.blobUrlForDownload = nil } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FaviconScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FaviconScriptHandler.swift index 9c4d79d9dbc3..cb35d10a59bb 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FaviconScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FaviconScriptHandler.swift @@ -11,13 +11,6 @@ import WebKit import os.log class FaviconScriptHandler: NSObject, TabContentScript { - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - super.init() - } - static let scriptName = "FaviconScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -39,13 +32,12 @@ class FaviconScriptHandler: NSObject, TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } - guard let tab = tab else { return } Task { @MainActor in // Assign default favicon diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FocusScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FocusScriptHandler.swift index 16c862e62856..654cedeff8ad 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FocusScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/FocusScriptHandler.swift @@ -8,22 +8,16 @@ import WebKit import os.log class FocusScriptHandler: TabContentScript { - fileprivate weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "FocusScript" static let scriptId = UUID().uuidString static let messageHandlerName = "focusHelper" static let scriptSandbox: WKContentWorld = .defaultClient static let userScript: WKUserScript? = nil - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -48,9 +42,9 @@ class FocusScriptHandler: TabContentScript { switch eventType { case "focus": - tab?.isEditing = true + tab.isEditing = true case "blur": - tab?.isEditing = false + tab.isEditing = false default: return Logger.module.error("FocusHelper.js sent unhandled eventType") } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/LoginsScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/LoginsScriptHandler.swift index f78039b018b8..0e33f1ef7586 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/LoginsScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/LoginsScriptHandler.swift @@ -12,14 +12,12 @@ import WebKit import os.log class LoginsScriptHandler: TabContentScript { - private weak var tab: Tab? private let profile: Profile private let passwordAPI: BravePasswordAPI private var snackBar: SnackBar? - required init(tab: Tab, profile: Profile, passwordAPI: BravePasswordAPI) { - self.tab = tab + required init(profile: Profile, passwordAPI: BravePasswordAPI) { self.profile = profile self.passwordAPI = passwordAPI } @@ -30,10 +28,10 @@ class LoginsScriptHandler: TabContentScript { static let messageHandlerName = "loginsScriptMessageHandler" static let userScript: WKUserScript? = nil - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } if !verifyMessage(message: message, securityToken: UserScriptManager.securityToken) { @@ -71,19 +69,20 @@ class LoginsScriptHandler: TabContentScript { formSubmitURL: res["formSubmitURL"] as? String ?? "", logins: logins, requestId: requestId, - frameInfo: message.frameInfo + frameInfo: message.frameInfo, + tab: tab ) } } } else if type == "submit" { if Preferences.General.saveLogins.value { - updateORSaveCredentials(for: url, script: res) + updateORSaveCredentials(for: url, script: res, tab: tab) } } } } - private func updateORSaveCredentials(for url: URL, script: [String: Any]) { + private func updateORSaveCredentials(for url: URL, script: [String: Any], tab: Tab) { guard let scriptCredentials = passwordAPI.fetchFromScript(url, script: script), let username = scriptCredentials.usernameValue, scriptCredentials.usernameElement != nil, @@ -112,20 +111,20 @@ class LoginsScriptHandler: TabContentScript { return } - self.showUpdatePrompt(from: login, to: scriptCredentials) + self.showUpdatePrompt(from: login, to: scriptCredentials, tab: tab) return } else { - self.showAddPrompt(for: scriptCredentials) + self.showAddPrompt(for: scriptCredentials, tab: tab) return } } - self.showAddPrompt(for: scriptCredentials) + self.showAddPrompt(for: scriptCredentials, tab: tab) } } - private func showAddPrompt(for login: PasswordForm) { - addSnackBarForPrompt(for: login, isUpdating: false) { [weak self] in + private func showAddPrompt(for login: PasswordForm, tab: Tab) { + addSnackBarForPrompt(for: login, tab: tab, isUpdating: false) { [weak self] in guard let self = self else { return } DispatchQueue.main.async { @@ -134,8 +133,8 @@ class LoginsScriptHandler: TabContentScript { } } - private func showUpdatePrompt(from old: PasswordForm, to new: PasswordForm) { - addSnackBarForPrompt(for: new, isUpdating: true) { [weak self] in + private func showUpdatePrompt(from old: PasswordForm, to new: PasswordForm, tab: Tab) { + addSnackBarForPrompt(for: new, tab: tab, isUpdating: true) { [weak self] in guard let self = self else { return } self.passwordAPI.updateLogin(new, oldPasswordForm: old) @@ -144,6 +143,7 @@ class LoginsScriptHandler: TabContentScript { private func addSnackBarForPrompt( for login: PasswordForm, + tab: Tab, isUpdating: Bool, _ completion: @escaping () -> Void ) { @@ -153,7 +153,7 @@ class LoginsScriptHandler: TabContentScript { // Remove the existing prompt if let existingPrompt = self.snackBar { - tab?.removeSnackbar(existingPrompt) + tab.removeSnackbar(existingPrompt) } let promptMessage = String( @@ -174,7 +174,7 @@ class LoginsScriptHandler: TabContentScript { ? Strings.loginsHelperDontUpdateButtonTitle : Strings.loginsHelperDontSaveButtonTitle, accessibilityIdentifier: "UpdateLoginPrompt.dontSaveUpdateButton" ) { [unowned self] bar in - self.tab?.removeSnackbar(bar) + tab.removeSnackbar(bar) self.snackBar = nil return } @@ -184,7 +184,7 @@ class LoginsScriptHandler: TabContentScript { ? Strings.loginsHelperUpdateButtonTitle : Strings.loginsHelperSaveLoginButtonTitle, accessibilityIdentifier: "UpdateLoginPrompt.saveUpdateButton" ) { [unowned self] bar in - self.tab?.removeSnackbar(bar) + tab.removeSnackbar(bar) self.snackBar = nil completion() @@ -194,7 +194,7 @@ class LoginsScriptHandler: TabContentScript { snackBar?.addButton(saveORUpdate) if let bar = snackBar { - tab?.addSnackbar(bar) + tab.addSnackbar(bar) } } @@ -202,7 +202,8 @@ class LoginsScriptHandler: TabContentScript { formSubmitURL: String, logins: [PasswordForm], requestId: String, - frameInfo: WKFrameInfo + frameInfo: WKFrameInfo, + tab: Tab ) { let securityOrigin = frameInfo.securityOrigin @@ -216,7 +217,7 @@ class LoginsScriptHandler: TabContentScript { // Check for current tab has a url to begin with // and the frame is not modified - guard let currentURL = tab?.webView?.url, + guard let currentURL = tab.webView?.lastCommittedURL, LoginsScriptHandler.checkIsSameFrame( url: currentURL, frameScheme: securityOrigin.protocol, @@ -244,7 +245,7 @@ class LoginsScriptHandler: TabContentScript { return } - self.tab?.webView?.evaluateSafeJavaScript( + tab.webView?.evaluateSafeJavaScript( functionName: "window.__firefox__.logins.inject", args: [jsonString], contentWorld: LoginsScriptHandler.scriptSandbox, diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ReaderModeScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ReaderModeScriptHandler.swift index 06abb5700df8..ecab4d4d7c16 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ReaderModeScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ReaderModeScriptHandler.swift @@ -267,48 +267,35 @@ let readerModeNamespace = "window.__firefox__.reader" class ReaderModeScriptHandler: TabContentScript { weak var delegate: ReaderModeScriptHandlerDelegate? - fileprivate weak var tab: Tab? var state: ReaderModeState = ReaderModeState.unavailable fileprivate var originalURL: URL? - required init(tab: Tab) { - self.tab = tab - } - static let scriptName = "ReaderModeScript" static let scriptId = UUID().uuidString static let messageHandlerName = "readerModeMessageHandler" static let scriptSandbox: WKContentWorld = .defaultClient static let userScript: WKUserScript? = nil - fileprivate func handleReaderPageEvent(_ readerPageEvent: ReaderPageEvent) { + fileprivate func handleReaderPageEvent(_ readerPageEvent: ReaderPageEvent, tab: Tab) { switch readerPageEvent { case .pageShow: - if let tab = tab { - delegate?.readerMode(self, didDisplayReaderizedContentForTab: tab) - } + delegate?.readerMode(self, didDisplayReaderizedContentForTab: tab) } } - fileprivate func handleReaderModeStateChange(_ state: ReaderModeState) { + fileprivate func handleReaderModeStateChange(_ state: ReaderModeState, tab: Tab) { self.state = state - guard let tab = tab else { - return - } delegate?.readerMode(self, didChangeReaderModeState: state, forTab: tab) } - fileprivate func handleReaderContentParsed(_ readabilityResult: ReadabilityResult) { - guard let tab = tab else { - return - } + fileprivate func handleReaderContentParsed(_ readabilityResult: ReadabilityResult, tab: Tab) { delegate?.readerMode(self, didParseReadabilityResult: readabilityResult, forTab: tab) } - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -326,34 +313,32 @@ class ReaderModeScriptHandler: TabContentScript { switch messageType { case .pageEvent: if let readerPageEvent = ReaderPageEvent(rawValue: msg["Value"] as? String ?? "Invalid") { - handleReaderPageEvent(readerPageEvent) + handleReaderPageEvent(readerPageEvent, tab: tab) } case .stateChange: if let readerModeState = ReaderModeState(rawValue: msg["Value"] as? String ?? "Invalid") { - handleReaderModeStateChange(readerModeState) + handleReaderModeStateChange(readerModeState, tab: tab) } case .contentParsed: if let readabilityResult = ReadabilityResult(object: msg["Value"] as AnyObject?) { - handleReaderContentParsed(readabilityResult) + handleReaderContentParsed(readabilityResult, tab: tab) } else { - handleReaderModeStateChange(.unavailable) + handleReaderModeStateChange(.unavailable, tab: tab) } } } } } - var style: ReaderModeStyle = defaultReaderModeStyle { - didSet { - if state == ReaderModeState.active { - tab?.webView?.evaluateSafeJavaScript( - functionName: "\(readerModeNamespace).setStyle", - args: [style.encode()], - contentWorld: Self.scriptSandbox, - escapeArgs: false - ) { (object, error) -> Void in - return - } + func setStyle(_ style: ReaderModeStyle, in tab: Tab) { + if state == ReaderModeState.active { + tab.webView?.evaluateSafeJavaScript( + functionName: "\(readerModeNamespace).setStyle", + args: [style.encode()], + contentWorld: Self.scriptSandbox, + escapeArgs: false + ) { (object, error) -> Void in + return } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ResourceDownloadScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ResourceDownloadScriptHandler.swift index 51d6f58c8ec5..910b5dce8c4a 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ResourceDownloadScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/ResourceDownloadScriptHandler.swift @@ -34,12 +34,6 @@ struct DownloadedResourceResponse: Decodable { } class ResourceDownloadScriptHandler: TabContentScript { - fileprivate weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "ResourceDownloaderScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -61,17 +55,17 @@ class ResourceDownloadScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { Task { @MainActor in do { let response = try DownloadedResourceResponse.from(message: message) - await tab?.temporaryDocument?.onDocumentDownloaded(document: response, error: nil) + await tab.temporaryDocument?.onDocumentDownloaded(document: response, error: nil) } catch { - await tab?.temporaryDocument?.onDocumentDownloaded(document: nil, error: error) + await tab.temporaryDocument?.onDocumentDownloaded(document: nil, error: error) } replyHandler(nil, nil) } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift index 817106c14435..6154c96396cb 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift @@ -18,12 +18,6 @@ class SiteStateListenerScriptHandler: TabContentScript { let data: MessageDTOData } - private weak var tab: Tab? - - init(tab: Tab) { - self.tab = tab - } - static let scriptName = "SiteStateListenerScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -45,9 +39,9 @@ class SiteStateListenerScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } @@ -57,7 +51,7 @@ class SiteStateListenerScriptHandler: TabContentScript { return } - guard let tab = tab, let webView = tab.webView else { + guard let webView = tab.webView else { assertionFailure("Should have a tab set") return } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/WindowRenderScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/WindowRenderScriptHandler.swift index 34682992340f..ac3262afc791 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/WindowRenderScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/WindowRenderScriptHandler.swift @@ -7,12 +7,6 @@ import Shared import WebKit class WindowRenderScriptHandler: TabContentScript { - private weak var tab: Tab? - - required init(tab: Tab) { - self.tab = tab - } - static let scriptName = "WindowRenderScript" static let scriptId = UUID().uuidString static let messageHandlerName = "\(scriptName)_\(messageUUID)" @@ -35,10 +29,10 @@ class WindowRenderScriptHandler: TabContentScript { ) }() - func userContentController( - _ userContentController: WKUserContentController, - didReceiveScriptMessage message: WKScriptMessage, - replyHandler: (Any?, String?) -> Void + func tab( + _ tab: Tab, + receivedScriptMessage message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void ) { // Do nothing with the messages received. // For now.. It's useful for debugging though. diff --git a/ios/brave-ios/Sources/Brave/Migration/Migration.swift b/ios/brave-ios/Sources/Brave/Migration/Migration.swift index fa49ebc5d639..7d430a2a40d6 100644 --- a/ios/brave-ios/Sources/Brave/Migration/Migration.swift +++ b/ios/brave-ios/Sources/Brave/Migration/Migration.swift @@ -25,7 +25,6 @@ public class Migration { Preferences.migratePreferences(keyPrefix: keyPrefix) Preferences.migrateWalletPreferences() Preferences.migrateAdAndTrackingProtection() - Preferences.migrateHTTPSUpgradeLevel() Preferences.migrateBackgroundSponsoredImages() Preferences.migrateBookmarksButtonInToolbar() @@ -39,8 +38,11 @@ public class Migration { Preferences.Playlist.firstLoadAutoPlay.value = true } + migrateHTTPSUpgradeLevel() migrateDeAmpPreferences() migrateDebouncePreferences() + migrateHTTPSUpgradePreferences() + migrateChromiumWebViewsPreferencesAndData() // Adding Observer to enable sync types NotificationCenter.default.addObserver( @@ -69,6 +71,14 @@ public class Migration { Preferences.Shields.autoRedirectTrackingURLsDeprecated.value = nil } + private func migrateHTTPSUpgradePreferences() { + if ShieldPreferences.httpsUpgradeLevelRawDeprecated.value.isEmpty { return } + let httpsUpgradeLevel = ShieldPreferences.httpsUpgradeLevelDeprecated + braveCore.browserPrefs.httpsOnlyModeEnabled = httpsUpgradeLevel.isStrict + braveCore.browserPrefs.httpsUpgradesEnabled = httpsUpgradeLevel.isEnabled + ShieldPreferences.httpsUpgradeLevelRawDeprecated.value = "" + } + @objc private func enableUserSelectedTypesForSync() { guard braveCore.syncAPI.isInSyncGroup else { Logger.module.info("Sync is not active") @@ -160,6 +170,40 @@ public class Migration { ) } } + + /// Migrates preferences and data to support Chromium web views + func migrateChromiumWebViewsPreferencesAndData() { + // FIXME: Product change: Chromium loads mobile on iPad by default, may need to switch this to just migrate always and not based on saved value + Preferences.UserAgent.alwaysRequestDesktopSite.migrate { value in + // We only want to set the default content setting if explicitly set + if value { + self.braveCore.defaultHostContentSettings.defaultPageMode = .desktop + } + } + } + + func migrateHTTPSUpgradeLevel() { + let browserPrefs = braveCore.browserPrefs + // If the feature flag for https by default is off but we've already stored a user pref for it + // then assign that enabled level a preference so that if a user toggles HTTPS Everywhere off + // and on it will correctly set the underlying upgarde level to the level they had set when + // the feature flag was on. + if !FeatureList.kBraveHttpsByDefault.enabled || !FeatureList.kHttpsOnlyMode.enabled, + browserPrefs.httpsUpgradesEnabled || browserPrefs.httpsOnlyModeEnabled + { + ShieldPreferences.httpsUpgradePriorEnabledLevel = + browserPrefs.httpsOnlyModeEnabled + ? .strict : browserPrefs.httpsUpgradesEnabled ? .standard : .disabled + } + guard !Preferences.Migration.httpsUpgradesLivelCompleted.value else { return } + + // Migrate old tracking protection setting to new BraveShields setting + Preferences.DeprecatedPreferences.httpsEverywhere.migrate { isEnabled in + browserPrefs.httpsUpgradesEnabled = isEnabled + } + + Preferences.Migration.adBlockAndTrackingProtectionShieldLevelCompleted.value = true + } } extension Migration { @@ -186,7 +230,7 @@ extension Migration { } extension Preferences { - private final class DeprecatedPreferences { + fileprivate final class DeprecatedPreferences { static let blockAdsAndTracking = Option( key: "shields.block-ads-and-tracking", default: true @@ -371,26 +415,6 @@ extension Preferences { Migration.adBlockAndTrackingProtectionShieldLevelCompleted.value = true } - fileprivate class func migrateHTTPSUpgradeLevel() { - // If the feature flag for https by default is off but we've already stored a user pref for it - // then assign that enabled level a preference so that if a user toggles HTTPS Everywhere off - // and on it will correctly set the underlying upgarde level to the level they had set when - // the feature flag was on. - if !FeatureList.kBraveHttpsByDefault.enabled || !FeatureList.kHttpsOnlyMode.enabled, - ShieldPreferences.httpsUpgradeLevel.isEnabled - { - ShieldPreferences.httpsUpgradePriorEnabledLevel = ShieldPreferences.httpsUpgradeLevel - } - guard !Migration.httpsUpgradesLivelCompleted.value else { return } - - // Migrate old tracking protection setting to new BraveShields setting - DeprecatedPreferences.httpsEverywhere.migrate { isEnabled in - ShieldPreferences.httpsUpgradeLevel = isEnabled ? .standard : .disabled - } - - Migration.adBlockAndTrackingProtectionShieldLevelCompleted.value = true - } - /// Migrate Wallet Preferences from version <1.43 fileprivate class func migrateWalletPreferences() { guard Preferences.Migration.walletProviderAccountRequestCompleted.value != true else { return } diff --git a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift index f67f01c29b2d..07889b377518 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift @@ -68,14 +68,14 @@ class ContentBlockerHelper { // Remove unwanted rule lists for ruleList in setRuleLists.subtracting(ruleLists) { // It's added but we don't want it. So we remove it. - tab?.webView?.configuration.userContentController.remove(ruleList) + tab?.webView?.wkConfiguration.userContentController.remove(ruleList) setRuleLists.remove(ruleList) removedIds.append(ruleList.identifier) } // Add missing rule lists for ruleList in ruleLists.subtracting(setRuleLists) { - tab?.webView?.configuration.userContentController.add(ruleList) + tab?.webView?.wkConfiguration.userContentController.add(ruleList) setRuleLists.insert(ruleList) addedIds.append(ruleList.identifier) } diff --git a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift index 6d71fbdbaa91..f445acf44b97 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift @@ -49,7 +49,6 @@ import os.log case blockAds case blockCookies case blockTrackers - case upgradeMixedContent func mode(isAggressiveMode: Bool) -> BlockingMode { switch self { @@ -59,7 +58,7 @@ import os.log } else { return .standard } - case .blockCookies, .blockTrackers, .upgradeMixedContent: + case .blockCookies, .blockTrackers: return .general } } @@ -69,7 +68,6 @@ import os.log case .blockAds: return "block-ads" case .blockCookies: return "block-cookies" case .blockTrackers: return "block-trackers" - case .upgradeMixedContent: return "mixed-content-upgrade" } } } @@ -577,9 +575,6 @@ import os.log results.insert(.blockCookies) } - // Always upgrade mixed content - results.insert(.upgradeMixedContent) - return results } diff --git a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/Lists/mixed-content-upgrade.json b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/Lists/mixed-content-upgrade.json deleted file mode 100644 index c5fc1dd9ee53..000000000000 --- a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/Lists/mixed-content-upgrade.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "trigger": { - "url-filter": "http://.*", - "if-top-url": ["https://.*"], - "resource-type": ["image", "media"] - }, - "action": { - "type": "make-https" - } - } -] diff --git a/ios/brave-ios/Sources/BraveShared/Extensions/CWVWebViewExtensions.swift b/ios/brave-ios/Sources/BraveShared/Extensions/CWVWebViewExtensions.swift new file mode 100644 index 000000000000..91a99fcc0647 --- /dev/null +++ b/ios/brave-ios/Sources/BraveShared/Extensions/CWVWebViewExtensions.swift @@ -0,0 +1,254 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveCore +import Foundation + +// Adds Sequence conformance to the `CWVBackForwardListItemArray` which under the hood is not +// actually an `NSArray` and only conforms to the `NSFastEnumeration` protocol +extension CWVBackForwardListItemArray: Sequence { + public typealias Element = CWVBackForwardListItem + + // A custom Iterator is used because `NSFastEnumerationIterator.Element` is `Any`, and we can't + // specialize it. + public struct Iterator: IteratorProtocol { + public typealias Element = CWVBackForwardListItem + + private var enumerator: NSFastEnumerationIterator + + fileprivate init(_ enumerable: CWVBackForwardListItemArray) { + self.enumerator = NSFastEnumerationIterator(enumerable) + } + + public mutating func next() -> Element? { + return enumerator.next() as? Element + } + } + + public func makeIterator() -> Iterator { + Iterator(self) + } +} + +// Helper types to properly discern between core types and qualifiers within a PageTransition +// mimics ui/base/page_transition_types.cc +extension CWVNavigationType: CustomDebugStringConvertible { + // CWVNavigationType is marked with NS_OPTIONS_SET in the Obj-C side which turns this into a + // struct with static lets for each case on the Swift side, but since `CWVNavigationTypeLink` is + // equal to `0`, no symbol is generated for it because its implied that 0 should be an empty set. + // This adds it back since link is a core type which should be treated like an enum case + public static let link: CWVNavigationType = .init(rawValue: 0) + + /// The core navigation type, which only one value will exist from the list of CWVNavigationType + /// cases + public var coreType: CWVNavigationType { + .init(rawValue: self.rawValue & ~CWVNavigationType.qualifierMask.rawValue) + } + + /// Qualifiers that are stored within this type. + public var qualifiers: CWVNavigationType { + .init(rawValue: self.rawValue & CWVNavigationType.qualifierMask.rawValue) + } + + public static func == (lhs: CWVNavigationType, rhs: CWVNavigationType) -> Bool { + return lhs.coreType.rawValue == rhs.coreType.rawValue + } + + public func contains(_ member: CWVNavigationType) -> Bool { + assert( + member.rawValue > CWVNavigationType.lastCore.rawValue, + "\(member) is a core type, not a qualifier, replace with an equality check" + ) + return qualifiers.rawValue & member.rawValue != 0 + } + + // FIXME: Probably need to implement rest of Set-methods + + public var isMainFrame: Bool { + self != .autoSubframe && self != .manualSubframe + } + + public var isRedirect: Bool { + rawValue & CWVNavigationType.isRedirectMask.rawValue != 0 + } + + public var isNewNavigation: Bool { + self != .reload && contains(.forwardBack) + } + + public var isWebTriggerable: Bool { + switch coreType { + case .link, .autoSubframe, .manualSubframe, .formSubmit: + return true + default: + return false + } + } + + public var debugDescription: String { + switch coreType { + case .link: return "link" + case .typed: return "typed" + case .autoBookmark: return "auto_bookmark" + case .autoSubframe: return "auto_subframe" + case .manualSubframe: return "manual_subframe" + case .generated: return "generated" + case .autoToplevel: return "auto_toplevel" + case .formSubmit: return "form_submit" + case .reload: return "reload" + case .keyword: return "keyword" + case .keywordGenerated: return "keyword_generated" + default: return "" + } + } +} + +extension CWVUserAgentType: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .none: return "none" + case .automatic: return "automatic" + case .mobile: return "mobile" + case .desktop: return "desktop" + default: return "" + } + } +} + +public enum JavascriptError: Error { + case invalid +} + +extension CWVWebView { + // Use JS to redirect the page without adding a history entry + public func replaceLocation(with url: URL) { + let apostropheEncoded = "%27" + let safeUrl = url.absoluteString.replacingOccurrences(of: "'", with: apostropheEncoded) + evaluateSafeJavaScript( + functionName: "location.replace", + args: ["'\(safeUrl)'"], + contentWorld: .defaultClient, + escapeArgs: false, + asFunction: true, + completion: nil + ) + } + + public func generateJSFunctionString( + functionName: String, + args: [Any?], + escapeArgs: Bool = true + ) -> (javascript: String, error: Error?) { + var sanitizedArgs = [String]() + for arg in args { + if let arg = arg { + do { + if let arg = arg as? String { + sanitizedArgs.append(escapeArgs ? "'\(arg.htmlEntityEncodedString)'" : "\(arg)") + } else { + let data = try JSONSerialization.data(withJSONObject: arg, options: [.fragmentsAllowed]) + + if let str = String(data: data, encoding: .utf8) { + sanitizedArgs.append(str) + } else { + throw JavascriptError.invalid + } + } + } catch { + return ("", error) + } + } else { + sanitizedArgs.append("null") + } + } + + if args.count != sanitizedArgs.count { + assertionFailure("Javascript parsing failed.") + return ("", JavascriptError.invalid) + } + + return ("\(functionName)(\(sanitizedArgs.joined(separator: ", ")))", nil) + } + + public func evaluateSafeJavaScript( + functionName: String, + args: [Any] = [], + contentWorld: WKContentWorld, + escapeArgs: Bool = true, + asFunction: Bool = true, + completion: ((Any?, Error?) -> Void)? = nil + ) { + var javascript = functionName + + if asFunction { + let js = generateJSFunctionString( + functionName: functionName, + args: args, + escapeArgs: escapeArgs + ) + if js.error != nil { + if let completionHandler = completion { + completionHandler(nil, js.error) + } + return + } + javascript = js.javascript + } + + DispatchQueue.main.async { + self.evaluateJavaScript( + javascript, + contentWorld: contentWorld, + completionHandler: { value, error in + completion?(value, error) + } + ) + } + } + + @discardableResult @MainActor public func evaluateSafeJavaScript( + functionName: String, + args: [Any] = [], + contentWorld: WKContentWorld, + escapeArgs: Bool = true, + asFunction: Bool = true + ) async -> (Any?, Error?) { + await withCheckedContinuation { continuation in + evaluateSafeJavaScript( + functionName: functionName, + args: args, + contentWorld: contentWorld, + escapeArgs: escapeArgs, + asFunction: asFunction + ) { value, error in + continuation.resume(returning: (value, error)) + } + } + } + + @discardableResult + @MainActor public func evaluateSafeJavaScriptThrowing( + functionName: String, + args: [Any] = [], + frame: WKFrameInfo? = nil, + contentWorld: WKContentWorld, + escapeArgs: Bool = true, + asFunction: Bool = true + ) async throws -> Any? { + let result = await evaluateSafeJavaScript( + functionName: functionName, + args: args, + contentWorld: contentWorld, + escapeArgs: escapeArgs, + asFunction: asFunction + ) + + if let error = result.1 { + throw error + } else { + return result.0 + } + } +} diff --git a/ios/brave-ios/Sources/BraveShared/Extensions/URLExtensions.swift b/ios/brave-ios/Sources/BraveShared/Extensions/URLExtensions.swift index e7b7173bda71..da27f77f15f3 100644 --- a/ios/brave-ios/Sources/BraveShared/Extensions/URLExtensions.swift +++ b/ios/brave-ios/Sources/BraveShared/Extensions/URLExtensions.swift @@ -30,14 +30,10 @@ extension URL { public var strippedInternalURL: URL? { if let internalURL = InternalURL(self) { switch internalURL.urlType { - case .errorPage: - return internalURL.originalURLFromErrorPage case .web3Page, .sessionRestorePage, .aboutHomePage: return internalURL.extractedUrlParam case .blockedPage: return decodeEmbeddedInternalURL(for: .blocked) - case .httpBlockedPage: - return decodeEmbeddedInternalURL(for: .httpBlocked) case .readerModePage: return decodeEmbeddedInternalURL(for: .readermode) default: @@ -63,13 +59,8 @@ extension URL { .havingRemovedAuthorisationComponents() } - if let internalUrl = InternalURL(self), internalUrl.isErrorPage { - return internalUrl.originalURLFromErrorPage?.displayURL - } - if let internalUrl = InternalURL(self), - internalUrl.isSessionRestore || internalUrl.isWeb3URL || internalUrl.isHTTPBlockedPage - || internalUrl.isBlockedPage + internalUrl.isSessionRestore || internalUrl.isWeb3URL || internalUrl.isBlockedPage { return internalUrl.extractedUrlParam?.displayURL } @@ -80,9 +71,6 @@ extension URL { if !InternalURL.isValid(url: self) { let url = self.havingRemovedAuthorisationComponents() - if let internalUrl = InternalURL(url), internalUrl.isErrorPage { - return internalUrl.originalURLFromErrorPage?.displayURL - } return url } @@ -142,9 +130,7 @@ extension InternalURL { enum URLType { case blockedPage - case httpBlockedPage case sessionRestorePage - case errorPage case readerModePage case aboutHomePage case web3Page @@ -152,21 +138,10 @@ extension InternalURL { } var urlType: URLType { - // This needs to be before `isBlockedPage` - // because http-blocked has the word "blocked" in it - // We should refactor this code because its really iffy. - if isHTTPBlockedPage { - return .httpBlockedPage - } - if isBlockedPage { return .blockedPage } - if isErrorPage { - return .errorPage - } - if isWeb3URL { return .web3Page } diff --git a/ios/brave-ios/Sources/BraveShields/ShieldPreferences.swift b/ios/brave-ios/Sources/BraveShields/ShieldPreferences.swift index c1a79f68c804..a7b034321696 100644 --- a/ios/brave-ios/Sources/BraveShields/ShieldPreferences.swift +++ b/ios/brave-ios/Sources/BraveShields/ShieldPreferences.swift @@ -20,7 +20,7 @@ public class ShieldPreferences { /// Get the level of the https upgrade setting as a stored preference /// - Warning: You should not access this directly but through ``httpsUpgradeLevel`` - public static var httpsUpgradeLevelRaw = Preferences.Option( + public static var httpsUpgradeLevelRawDeprecated = Preferences.Option( key: "shields.https-upgrade-level", default: defaultHTTPsUpgradeLevel.rawValue ) @@ -49,11 +49,11 @@ public class ShieldPreferences { } /// Get the level of HTTPS upgrades - public static var httpsUpgradeLevel: HTTPSUpgradeLevel { + public static var httpsUpgradeLevelDeprecated: HTTPSUpgradeLevel { get { - HTTPSUpgradeLevel(rawValue: httpsUpgradeLevelRaw.value) ?? defaultHTTPsUpgradeLevel + HTTPSUpgradeLevel(rawValue: httpsUpgradeLevelRawDeprecated.value) ?? defaultHTTPsUpgradeLevel } - set { httpsUpgradeLevelRaw.value = newValue.rawValue } + set { httpsUpgradeLevelRawDeprecated.value = newValue.rawValue } } /// Get the prior enabled level of HTTPS upgrades diff --git a/ios/brave-ios/Sources/Favicon/FaviconFetcher.swift b/ios/brave-ios/Sources/Favicon/FaviconFetcher.swift index b70869ef844f..0d3ba36c9265 100644 --- a/ios/brave-ios/Sources/Favicon/FaviconFetcher.swift +++ b/ios/brave-ios/Sources/Favicon/FaviconFetcher.swift @@ -123,7 +123,7 @@ public class FaviconFetcher { // Handle internal URLs var url = url if let internalURL = InternalURL(url), - let realUrl = internalURL.originalURLFromErrorPage ?? internalURL.extractedUrlParam + let realUrl = internalURL.extractedUrlParam { url = realUrl } diff --git a/ios/brave-ios/Sources/IsTesting/IsTesting.swift b/ios/brave-ios/Sources/IsTesting/IsTesting.swift new file mode 100644 index 000000000000..f45a411a7ce1 --- /dev/null +++ b/ios/brave-ios/Sources/IsTesting/IsTesting.swift @@ -0,0 +1,23 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation + +/// Whether or not the current process is running tests. +/// +/// https://github.com/pointfreeco/swift-issue-reporting/blob/main/Sources/IssueReporting/IsTesting.swift +public let isTesting = ProcessInfo.processInfo.isTesting + +extension ProcessInfo { + fileprivate var isTesting: Bool { + if environment.keys.contains("XCTestBundlePath") { return true } + if environment.keys.contains("XCTestConfigurationFilePath") { return true } + if environment.keys.contains("XCTestSessionIdentifier") { return true } + return arguments.contains { argument in + let path = URL(fileURLWithPath: argument) + return path.lastPathComponent == "xctest" || path.pathExtension == "xctest" + } + } +} diff --git a/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift b/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift index a34a35f8de9a..16171146ab08 100644 --- a/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift +++ b/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift @@ -251,6 +251,11 @@ extension Preferences { key: "chromium.last.bookmark.folder.node.id", default: nil ) + /// The sync type open tabs enabled the device in sync chain + public static let syncAutofillEnabled = Option( + key: "chromium.sync.syncAutofillEnabled", + default: false + ) } public final class Debug { diff --git a/ios/brave-ios/Sources/Shared/AppConstants.swift b/ios/brave-ios/Sources/Shared/AppConstants.swift index 372b02d3aba3..59fa6e5f05f2 100644 --- a/ios/brave-ios/Sources/Shared/AppConstants.swift +++ b/ios/brave-ios/Sources/Shared/AppConstants.swift @@ -43,15 +43,14 @@ public struct KVOConstants: Equatable { self.keyPath = keyPath } + // Safe for CWVWebView public static let loading: Self = .init(keyPath: "loading") - public static let estimatedProgress: Self = .init(keyPath: "estimatedProgress") - public static let url: Self = .init(keyPath: "URL") - public static let title: Self = .init(keyPath: "title") public static let canGoBack: Self = .init(keyPath: "canGoBack") public static let canGoForward: Self = .init(keyPath: "canGoForward") - public static let hasOnlySecureContent: Self = .init(keyPath: "hasOnlySecureContent") - public static let serverTrust: Self = .init(keyPath: "serverTrust") - public static let _sampledPageTopColor: Self = .init(keyPath: "_sampl\("edPageTopC")olor") + public static let estimatedProgress: Self = .init(keyPath: "estimatedProgress") + public static let visibleSSLStatus: Self = .init(keyPath: "visibleSSLStatus") + public static let visibleURL: Self = .init(keyPath: "visibleURL") + public static let title: Self = .init(keyPath: "title") } public struct AppConstants { diff --git a/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift b/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift index 544c7a1e9889..ec9247d67cd3 100644 --- a/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift +++ b/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift @@ -252,7 +252,7 @@ extension URL { return false } let utilityURLs = [ - "/\(InternalURL.Path.errorpage.rawValue)", "/\(InternalURL.Path.sessionrestore.rawValue)", + "/\(InternalURL.Path.sessionrestore.rawValue)", "/about/home", "/\(InternalURL.Path.readermode.rawValue)", ] return utilityURLs.contains { self.path.hasPrefix($0) } @@ -464,11 +464,9 @@ public struct InternalURL { public static let baseUrl = "\(scheme)://\(host)" public enum Path: String { - case errorpage case sessionrestore case readermode = "reader-mode" case blocked - case httpBlocked = "http-blocked" case basicAuth = "basic-auth" func matches(_ string: String) -> Bool { @@ -538,22 +536,10 @@ public struct InternalURL { return url.absoluteString.hasPrefix(sessionRestoreHistoryItemBaseUrl) } - public var isErrorPage: Bool { - // Error pages can be nested in session restore URLs, and session restore handler will forward them to the error page handler - let path = - url.absoluteString.hasPrefix(sessionRestoreHistoryItemBaseUrl) - ? extractedUrlParam?.path : url.path - return InternalURL.Path.errorpage.matches(path ?? "") - } - public var isBlockedPage: Bool { return InternalURL.Path.blocked.matches(url.path) } - public var isHTTPBlockedPage: Bool { - return InternalURL.Path.httpBlocked.matches(url.path) - } - public var isReaderModePage: Bool { return InternalURL.Path.readermode.matches(url.path) } @@ -562,16 +548,6 @@ public struct InternalURL { return InternalURL.Path.basicAuth.matches(url.path) } - public var originalURLFromErrorPage: URL? { - if !url.absoluteString.hasPrefix(sessionRestoreHistoryItemBaseUrl) { - return isErrorPage ? extractedUrlParam : nil - } - if let urlParam = extractedUrlParam, let nested = InternalURL(urlParam), nested.isErrorPage { - return nested.extractedUrlParam - } - return nil - } - public var extractedUrlParam: URL? { if let nestedUrl = url.getQuery()[InternalURL.Param.url.rawValue]?.unescape() { return URL(string: nestedUrl) @@ -621,9 +597,7 @@ public struct InternalURL { } public var displayURL: URL? { - if isErrorPage { - return originalURLFromErrorPage - } else if isReaderModePage { + if isReaderModePage { return extractedUrlParam } return nil diff --git a/ios/brave-ios/Sources/Shared/Extensions/WKWebViewExtensions.swift b/ios/brave-ios/Sources/Shared/Extensions/WKWebViewExtensions.swift index 8bf8f3d88b93..271fe580801d 100644 --- a/ios/brave-ios/Sources/Shared/Extensions/WKWebViewExtensions.swift +++ b/ios/brave-ios/Sources/Shared/Extensions/WKWebViewExtensions.swift @@ -11,7 +11,22 @@ enum JavascriptError: Error { case invalid } +// FIXME: Delete these JS methods once Playlist web view & error pages are ported over extension WKWebView { + // Use JS to redirect the page without adding a history entry + public func replaceLocation(with url: URL) { + let apostropheEncoded = "%27" + let safeUrl = url.absoluteString.replacingOccurrences(of: "'", with: apostropheEncoded) + evaluateSafeJavaScript( + functionName: "location.replace", + args: ["'\(safeUrl)'"], + contentWorld: .defaultClient, + escapeArgs: false, + asFunction: true, + completion: nil + ) + } + public func generateJSFunctionString( functionName: String, args: [Any?], diff --git a/ios/brave-ios/Sources/Shared/SharedStrings.swift b/ios/brave-ios/Sources/Shared/SharedStrings.swift index 7e720d1ff293..f66886c06602 100644 --- a/ios/brave-ios/Sources/Shared/SharedStrings.swift +++ b/ios/brave-ios/Sources/Shared/SharedStrings.swift @@ -207,6 +207,20 @@ extension Strings { comment: "The label text in the Download Cancelled toast for showing confirmation that the download was cancelled." ) + public static let downloadAlreadyInProgressToastLabelTitle = NSLocalizedString( + "DownloadAlreadyInProgressToastLabelTitle", + bundle: .module, + value: "Start a new download?", + comment: + "The label title in the Download Toast when there's already a download currently in progress" + ) + public static let downloadAlreadyInProgressToastLabelText = NSLocalizedString( + "DownloadAlreadyInProgressToastLabelText", + bundle: .module, + value: "This will stop all progress for your current download.", + comment: + "The label text in the Download Toast when there's already a download currently in progress" + ) public static let downloadFailedToastLabelText = NSLocalizedString( "DownloadFailedToastLabelText", bundle: .module, diff --git a/ios/brave-ios/Sources/UserAgent/UserAgent.swift b/ios/brave-ios/Sources/UserAgent/UserAgent.swift index c8037f1d8d3d..ad04d7e74404 100644 --- a/ios/brave-ios/Sources/UserAgent/UserAgent.swift +++ b/ios/brave-ios/Sources/UserAgent/UserAgent.swift @@ -6,6 +6,8 @@ import Foundation import Preferences import UIKit +// FIXME: Delete this, expose WebClient's UserAgent methods from BraveCore instead +@available(*, deprecated) public struct UserAgent { public static let mobile = UserAgentBuilder().build(desktopMode: false) public static let desktop = UserAgentBuilder().build(desktopMode: true) diff --git a/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift b/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift index e38194afa769..cfe4b4012684 100644 --- a/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift +++ b/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift @@ -11,6 +11,8 @@ extension Preferences { public enum UserAgent { /// Sets Desktop UA for iPad by default (iOS 13+ & iPad only). /// Do not read it directly, prefer to use `UserAgent.shouldUseDesktopMode` instead. + /// + /// - Note: This preference is deprecated and no longer respected when using Chromium web views public static let alwaysRequestDesktopSite = Option( key: "general.always-request-desktop-site", default: UIDevice.current.userInterfaceIdiom == .pad diff --git a/ios/brave-ios/Tests/BraveSharedTests/NSURLExtensionsTests.swift b/ios/brave-ios/Tests/BraveSharedTests/NSURLExtensionsTests.swift index 9f68c45e11ef..e0805c756563 100644 --- a/ios/brave-ios/Tests/BraveSharedTests/NSURLExtensionsTests.swift +++ b/ios/brave-ios/Tests/BraveSharedTests/NSURLExtensionsTests.swift @@ -304,15 +304,11 @@ class NSURLExtensionsTests: XCTestCase { func testisAboutHomeURL() { let goodurls = [ - "\(InternalURL.baseUrl)/about/home/#panel=0", - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=internal%3A//local%3A/about/home/%23panel%3D1", - + "\(InternalURL.baseUrl)/about/home/#panel=0" ] let badurls = [ "http://google.com", "http://localhost:6571/sessionrestore.html", - "http://localhost:6571/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com", - "http://localhost:6571/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com/about/home/%23panel%3D1", ] goodurls.forEach { @@ -339,8 +335,6 @@ class NSURLExtensionsTests: XCTestCase { let badurls = [ "http://google.com", "http://localhost:6571/sessionrestore.html", - "http://localhost:6571/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com", - "http://localhost:6571/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com/about/home/%23panel%3D1", ] goodurls.forEach { @@ -360,73 +354,6 @@ class NSURLExtensionsTests: XCTestCase { } } - func testisErrorPage() { - let goodurls = [ - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com", - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=blah", - ] - let badurls = [ - "http://google.com", - "http://localhost:6571/sessionrestore.html", - "http://localhost:6571/about/home/#panel=0", - ] - - goodurls.forEach { - if let url = URL(string: $0) { - XCTAssertTrue(InternalURL(url)?.isErrorPage == true, $0) - - } else { - XCTAssert(false, "Invalid URL: \($0)") - } - } - - badurls.forEach { - if let url = URL(string: $0) { - XCTAssertFalse(InternalURL(url)?.isErrorPage == true, $0) - - } else { - XCTAssert(false, "Invalid URL: \($0)") - } - } - } - - func testoriginalURLFromErrorURL() { - let goodurls = [ - ( - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com", - URL(string: "http://mozilla.com") - ), - ( - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=internal%3A//local/about/home/%23panel%3D1", - URL(string: "internal://local/about/home/#panel=1") - ), - ] - let badurls = [ - "http://google.com", - "http://localhost:6571/sessionrestore.html", - "http://localhost:6571/about/home/#panel=0", - "http://localhost:6571/\(InternalURL.Path.errorpage.rawValue)/error.html", - ] - - goodurls.forEach { - if let url = URL(string: $0.0) { - XCTAssertEqual(InternalURL(url)?.originalURLFromErrorPage, $0.1) - - } else { - XCTAssert(false, "Invalid URL: \($0)") - } - } - - badurls.forEach { - if let url = URL(string: $0) { - XCTAssertNil(InternalURL(url)?.originalURLFromErrorPage) - - } else { - XCTAssert(false, "Invalid URL: \($0)") - } - } - } - func testhavingRemovedAuthorisationComponents() { let goodurls = [ ( @@ -462,7 +389,6 @@ class NSURLExtensionsTests: XCTestCase { let goodurls = [ "http://localhost:6571/reader-mode/page", "http://LOCALhost:6571/\(InternalURL.Path.sessionrestore)/sessionrestore.html", - "http://127.0.0.1:6571/\(InternalURL.Path.errorpage)/error.html", ] let badurls = [ "http://google.com", @@ -549,19 +475,11 @@ class NSURLExtensionsTests: XCTestCase { "\(InternalURL.baseUrl)/\(InternalURL.Path.readermode.rawValue)?url=https%3A%2F%2Fen%2Em%2Ewikipedia%2Eorg%2Fwiki%2F", "https://en.m.wikipedia.org/wiki/" ), - ( - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com", - "http://mozilla.com" - ), ("https://mail.example.co.uk/index.html", "https://mail.example.co.uk/index.html"), ( "\(InternalURL.baseUrl)/\(InternalURL.Path.readermode.rawValue)?url=http%3A//mozilla.com", "http://mozilla.com" ), - ( - "\(InternalURL.scheme)://user:pass@\(InternalURL.host)/\(InternalURL.Path.errorpage.rawValue)/error.html?url=http%3A//mozilla.com", - "http://mozilla.com" - ), ] let badurls = [ "\(InternalURL.baseUrl)/\(InternalURL.Path.readermode.rawValue)/page?url=https%3A%2F%2Fen%2Em%2Ewikipedia%2Eorg%2Fwiki%2F" @@ -618,35 +536,6 @@ class NSURLExtensionsTests: XCTestCase { XCTAssertEqual("http://foo.com/bar/?ppp=123&rrr=aaa", urlE.absoluteString) } - func testLocalQueryParamHandling() { - let urlA = URL(string: "http://brave.com?url=https://foo.com") - let urlB = URL(string: "http://brave.com/?url=https://foo.com") - let urlC = URL(string: "http://brave.com?url=https://foo.com/meh") - let urlD = URL( - string: "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage)/foo.hmtl?url=https://foo.com" - ) - let urlE = URL( - string: - "\(InternalURL.baseUrl)/\(InternalURL.Path.errorpage)/foo.hmtl?url=https://foo.com/meh" - ) - - for url in [urlA, urlB, urlC, urlD, urlE] { - if url == nil { - XCTAssertTrue(false, "Cannot parse URL") - return - } - } - - XCTAssertNotEqual(InternalURL(urlA!)?.originalURLFromErrorPage, urlA) - XCTAssertNotEqual(InternalURL(urlB!)?.originalURLFromErrorPage, urlB) - XCTAssertNotEqual(InternalURL(urlC!)?.originalURLFromErrorPage, urlC) - XCTAssertEqual(InternalURL(urlD!)?.originalURLFromErrorPage?.absoluteString, "https://foo.com") - XCTAssertEqual( - InternalURL(urlE!)?.originalURLFromErrorPage?.absoluteString, - "https://foo.com/meh" - ) - } - func testAppendPathComponentsHelper() { var urlA = URL(string: "http://foo.com/bar/")! var urlB = URL(string: "http://bar.com/noo")! diff --git a/ios/brave-ios/Tests/BraveSharedTests/URLExtensionTests.swift b/ios/brave-ios/Tests/BraveSharedTests/URLExtensionTests.swift index 7ed457576c2f..224a662788d8 100644 --- a/ios/brave-ios/Tests/BraveSharedTests/URLExtensionTests.swift +++ b/ios/brave-ios/Tests/BraveSharedTests/URLExtensionTests.swift @@ -55,10 +55,6 @@ class URLExtensionTests: XCTestCase { embeddedURL.encodeEmbeddedInternalURL(for: .blocked)?.strippedInternalURL, embeddedURL ) - XCTAssertEqual( - embeddedURL.encodeEmbeddedInternalURL(for: .httpBlocked)?.strippedInternalURL, - embeddedURL - ) XCTAssertNil(embeddedURL.strippedInternalURL) } @@ -126,10 +122,6 @@ class URLExtensionTests: XCTestCase { embeddedURL.encodeEmbeddedInternalURL(for: .blocked)?.displayURL, embeddedURL ) - XCTAssertEqual( - embeddedURL.encodeEmbeddedInternalURL(for: .httpBlocked)?.displayURL, - embeddedURL - ) XCTAssertEqual( embeddedURL.displayURL, embeddedURL diff --git a/ios/brave-ios/Tests/ClientTests/ClientTests.swift b/ios/brave-ios/Tests/ClientTests/ClientTests.swift index c9ac70457e93..66f65d20ede7 100644 --- a/ios/brave-ios/Tests/ClientTests/ClientTests.swift +++ b/ios/brave-ios/Tests/ClientTests/ClientTests.swift @@ -22,7 +22,6 @@ class ClientTests: XCTestCase { (AboutHomeHandler.path, AboutHomeHandler()), (AboutLicenseHandler.path, AboutLicenseHandler()), (SessionRestoreHandler.path, SessionRestoreHandler()), - (ErrorPageHandler.path, ErrorPageHandler()), (ReaderModeHandler.path, ReaderModeHandler(profile: BrowserProfile(localName: "profile"))), ] diff --git a/ios/brave-ios/Tests/ClientTests/DownloadQueueTests.swift b/ios/brave-ios/Tests/ClientTests/DownloadQueueTests.swift deleted file mode 100644 index 0280a64bfbc6..000000000000 --- a/ios/brave-ios/Tests/ClientTests/DownloadQueueTests.swift +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import XCTest - -@testable import Brave - -class DownloadQueueTests: XCTestCase { - - func test_enqueue_whenItHasDownloads_sendsCorrectMessage() { - let (sut, delegate) = makeSUT() - let download = DownloadSpy() - sut.downloads = [Download(), Download()] - - sut.enqueue(download) - - XCTAssertEqual(delegate.receivedMessages, [.didStartDownload]) - XCTAssertEqual(download.receivedMessages, [.resume]) - XCTAssertNotNil(download.delegate as? DownloadQueue) - XCTAssertEqual(sut.downloads.count, 3) - } - - func test_cancelAll_sendsCancelMessageForIncompleteDownloads() { - let (sut, _) = makeSUT() - let download1 = DownloadSpy() - let download2 = DownloadSpy() - let download3 = DownloadSpy() - sut.downloads = [download1, download2, download3] - - sut.cancelAll() - - XCTAssertFalse(download1.isComplete) - XCTAssertFalse(download2.isComplete) - XCTAssertFalse(download3.isComplete) - XCTAssertEqual(download1.receivedMessages, [.cancel]) - XCTAssertEqual(download2.receivedMessages, [.cancel]) - XCTAssertEqual(download3.receivedMessages, [.cancel]) - } - - func test_pauseAll_sendsPauseMessageForIncompleteDownloads() { - let (sut, _) = makeSUT() - let download1 = DownloadSpy() - let download2 = DownloadSpy() - let download3 = DownloadSpy() - sut.downloads = [download1, download2, download3] - - sut.pauseAll() - - XCTAssertFalse(download1.isComplete) - XCTAssertFalse(download2.isComplete) - XCTAssertFalse(download3.isComplete) - XCTAssertEqual(download1.receivedMessages, [.pause]) - XCTAssertEqual(download2.receivedMessages, [.pause]) - XCTAssertEqual(download3.receivedMessages, [.pause]) - } - - func test_resumeAll_sendsResumeMessageForIncompleteDownloads() { - let (sut, _) = makeSUT() - let download1 = DownloadSpy() - let download2 = DownloadSpy() - let download3 = DownloadSpy() - sut.downloads = [download1, download2, download3] - - sut.resumeAll() - - XCTAssertFalse(download1.isComplete) - XCTAssertFalse(download2.isComplete) - XCTAssertFalse(download3.isComplete) - XCTAssertEqual(download1.receivedMessages, [.resume]) - XCTAssertEqual(download2.receivedMessages, [.resume]) - XCTAssertEqual(download3.receivedMessages, [.resume]) - } - - func test_downloadDidCompleteWithError_whenErrorIsEmpty_doNothing() { - let (sut, delegate) = makeSUT() - let download = Download() - sut.downloads = [download] - - sut.download(download, didCompleteWithError: nil) - - XCTAssertEqual(delegate.receivedMessages, []) - } - - func test_downloadDidCompleteWithError_whenDownloadsAreEmpty_doNothing() { - let (sut, delegate) = makeSUT() - let download = Download() - sut.downloads = [] - - let error = NSError(domain: "download.error", code: 0) - sut.download(download, didCompleteWithError: error) - - XCTAssertEqual(delegate.receivedMessages, []) - } - - func test_downloadDidCompleteWithError_whenItHasMoreThanOneDownload_doNothing() { - let (sut, delegate) = makeSUT() - let download1 = Download() - let download2 = Download() - sut.downloads = [download1, download2] - - let error = NSError(domain: "download.error", code: 0) - sut.download(download1, didCompleteWithError: error) - - XCTAssertEqual(delegate.receivedMessages, []) - } - - func test_downloadDidCompleteWithError_sendsCorrectMessage() { - let (sut, delegate) = makeSUT() - let download = Download() - sut.downloads = [download] - - let error = NSError(domain: "download.error", code: 0) - sut.download(download, didCompleteWithError: error) - - XCTAssertEqual(delegate.receivedMessages, [.didCompleteWithError(error: .downloadError)]) - } - - func test_downloadDidDownloadBytes_sendsCorrectMessage() { - let (sut, delegate) = makeSUT() - let download = Download() - - sut.download(download, didDownloadBytes: 1000) - sut.download(download, didDownloadBytes: 1000) - sut.download(download, didDownloadBytes: 1000) - - XCTAssertEqual( - delegate.receivedMessages, - [ - .didDownloadCombinedBytes(bytes: 1000), - .didDownloadCombinedBytes(bytes: 2000), - .didDownloadCombinedBytes(bytes: 3000), - ] - ) - } - - func test_downloadDidFinishDownloadingTo_whenDownloadsAreEmpty_doNothing() { - let (sut, delegate) = makeSUT() - let download = Download() - sut.downloads = [] - - let location = URL(string: "https://some-location")! - sut.download(download, didFinishDownloadingTo: location) - - XCTAssertEqual(delegate.receivedMessages, []) - } - - func test_downloadDidFinishDownloadingTo_whenItHasDownload_removesCorrectDownload() { - let (sut, _) = makeSUT() - let download1 = Download() - let download2 = Download() - sut.downloads = [download1, download2] - - let location = URL(string: "https://some-location")! - sut.download(download1, didFinishDownloadingTo: location) - - XCTAssertEqual(sut.downloads, [download2]) - } - - func test_downloadDidFinishDownloadingTo_whenItHasDownloads_sendsCorrectMessage() { - let (sut, delegate) = makeSUT() - let download1 = Download() - let download2 = Download() - sut.downloads = [download1, download2] - - let location = URL(string: "https://some-location")! - sut.download(download1, didFinishDownloadingTo: location) - - XCTAssertEqual(delegate.receivedMessages, [.didFinishDownloadingTo(location: location)]) - } - - func test_downloadDidFinishDownloadingTo_whenLastDonwload_sendsCorrectMessage() { - let (sut, delegate) = makeSUT() - let download = Download() - sut.downloads = [download] - - let location = URL(string: "https://some-location")! - sut.download(download, didFinishDownloadingTo: location) - - XCTAssertEqual( - delegate.receivedMessages, - [ - .didFinishDownloadingTo(location: location), - .didCompleteWithError(error: nil), - ] - ) - } -} - -// MARK: - Tests Helpers - -private func makeSUT() -> (sut: DownloadQueue, delegate: DownloadQueueDelegateSpy) { - let delegate = DownloadQueueDelegateSpy() - let sut = DownloadQueue() - sut.delegate = delegate - - return (sut, delegate) -} - -private class DownloadSpy: Download { - enum Message { - case resume - case cancel - case pause - } - - var receivedMessages: [Message] = [] - - override func resume() { - receivedMessages.append(.resume) - } - - override func cancel() { - receivedMessages.append(.cancel) - } - - override func pause() { - receivedMessages.append(.pause) - } -} - -private class DownloadQueueDelegateSpy: DownloadQueueDelegate { - enum DownloadQueueError: Error { - case downloadError - } - - enum Message: Equatable { - case didStartDownload - case didCompleteWithError(error: DownloadQueueError?) - case didDownloadCombinedBytes(bytes: Int64) - case didFinishDownloadingTo(location: URL) - } - - var receivedMessages: [Message] = [] - - func downloadQueue(_ downloadQueue: DownloadQueue, didStartDownload download: Download) { - receivedMessages.append(.didStartDownload) - } - - func downloadQueue( - _ downloadQueue: DownloadQueue, - didDownloadCombinedBytes combinedBytesDownloaded: Int64, - combinedTotalBytesExpected: Int64? - ) { - receivedMessages.append(.didDownloadCombinedBytes(bytes: combinedBytesDownloaded)) - } - - func downloadQueue( - _ downloadQueue: DownloadQueue, - download: Download, - didFinishDownloadingTo location: URL - ) { - receivedMessages.append(.didFinishDownloadingTo(location: location)) - } - - func downloadQueue(_ downloadQueue: DownloadQueue, didCompleteWithError error: Error?) { - receivedMessages.append(.didCompleteWithError(error: error != nil ? .downloadError : nil)) - } -} diff --git a/ios/brave-ios/Tests/ClientTests/Helpers/DownloadHelperTests.swift b/ios/brave-ios/Tests/ClientTests/Helpers/DownloadHelperTests.swift deleted file mode 100644 index b6f96f0daeb2..000000000000 --- a/ios/brave-ios/Tests/ClientTests/Helpers/DownloadHelperTests.swift +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Foundation -import WebKit -import XCTest - -@testable import Brave - -class DownloadHelperTests: XCTestCase { - - func test_init_whenMIMETypeIsNil_initializeCorrectly() { - let response = anyResponse(mimeType: nil) - - var sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: false, - forceDownload: true - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: false, - forceDownload: false - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: true - ) - XCTAssertNotNil(sut) - } - - func test_init_whenMIMETypeIsNotOctetStream_initializeCorrectly() { - for mimeType in allMIMETypes() { - if mimeType == MIMEType.octetStream { continue } - - let response = anyResponse(mimeType: mimeType) - - var sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - XCTAssertNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: false, - forceDownload: true - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: false, - forceDownload: false - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: true - ) - XCTAssertNotNil(sut) - } - } - - func test_init_whenMIMETypeIsOctetStream_initializeCorrectly() { - let response = anyResponse(mimeType: MIMEType.octetStream) - - var sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: false, - forceDownload: true - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: true - ) - XCTAssertNotNil(sut) - - sut = DownloadHelper( - request: anyRequest(), - response: response, - cookieStore: cookieStore(), - canShowInWebView: false, - forceDownload: false - ) - XCTAssertNotNil(sut) - } - - func test_downloadAlert_whenRequestURLIsWrong_deliversEmptyResult() { - let request = anyRequest(urlString: "wrong-url.com") - let sut = DownloadHelper( - request: request, - response: anyResponse(mimeType: nil), - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - - let downloadAlert = sut?.downloadAlert(from: UIView(), okAction: { _ in }) - - XCTAssertNil(downloadAlert) - } - - func test_downloadAlert_deliversCorrectTitle() { - let request = anyRequest(urlString: "http://some-domain.com/some-image.jpg") - let sut = DownloadHelper( - request: request, - response: anyResponse(mimeType: nil), - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - - let downloadAlert = sut?.downloadAlert(from: UIView(), okAction: { _ in }) - - XCTAssertEqual(downloadAlert!.title!, "some-image.jpg - some-domain.com") - } - - func test_downloadAlert_okActionButtonSendsCorrectMessage() { - let request = anyRequest() - let sut = DownloadHelper( - request: request, - response: anyResponse(mimeType: nil), - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - let okActionIndex = 0 - - var receivedMessages = [String]() - let okAction: (HTTPDownload) -> Void = { download in - receivedMessages.append(download.request.url!.absoluteString) - } - let downloadAlert = sut?.downloadAlert(from: UIView(), okAction: okAction) - downloadAlert?.tapButton(atIndex: okActionIndex) - - XCTAssertEqual(receivedMessages, [request.url!.absoluteString]) - } - - func test_downloadAlert_deliversCorrectCancelButton() { - let sut = DownloadHelper( - request: anyRequest(), - response: anyResponse(mimeType: nil), - cookieStore: cookieStore(), - canShowInWebView: true, - forceDownload: false - ) - - let downloadAlert = sut?.downloadAlert(from: UIView(), okAction: { _ in }) - - XCTAssertEqual(downloadAlert!.actions.count, 2) - XCTAssertEqual(downloadAlert!.actions.last!.style, .cancel) - } - - // MARK: - Helpers - - private func anyRequest(urlString: String = "http://any-url.com") -> URLRequest { - return URLRequest( - url: URL(string: urlString)!, - cachePolicy: anyCachePolicy(), - timeoutInterval: 60.0 - ) - } - - private func anyResponse(mimeType: String?) -> URLResponse { - return URLResponse( - url: URL(string: "http://any-url.com")!, - mimeType: mimeType, - expectedContentLength: 10, - textEncodingName: nil - ) - } - - private func cookieStore() -> WKHTTPCookieStore { - return WKWebsiteDataStore.`default`().httpCookieStore - } - - private func anyCachePolicy() -> URLRequest.CachePolicy { - return .useProtocolCachePolicy - } - - private func allMIMETypes() -> [String] { - return [ - MIMEType.bitmap, - MIMEType.css, - MIMEType.gif, - MIMEType.javaScript, - MIMEType.jpeg, - MIMEType.html, - MIMEType.octetStream, - MIMEType.passbook, - MIMEType.pdf, - MIMEType.plainText, - MIMEType.png, - MIMEType.webP, - MIMEType.xHTML, - ] - } -} - -extension UIAlertController { - fileprivate typealias AlertHandler = @convention(block) (UIAlertAction) -> Void - - fileprivate func tapButton(atIndex index: Int) { - guard let block = actions[index].value(forKey: "handler") else { return } - let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self) - handler(actions[index]) - } -} diff --git a/ios/brave-ios/Tests/ClientTests/NavigationRouterTests.swift b/ios/brave-ios/Tests/ClientTests/NavigationRouterTests.swift index a1789a344da4..c39b81b52d77 100644 --- a/ios/brave-ios/Tests/ClientTests/NavigationRouterTests.swift +++ b/ios/brave-ios/Tests/ClientTests/NavigationRouterTests.swift @@ -11,7 +11,7 @@ import XCTest class NavigationRouterTests: XCTestCase { - private let privateBrowsingManager = PrivateBrowsingManager() + private let privateBrowsingManager = PrivateBrowsingManager(braveCore: nil) override func setUp() { super.setUp() diff --git a/ios/brave-ios/Tests/ClientTests/SolanaProviderScriptHandlerTests.swift b/ios/brave-ios/Tests/ClientTests/SolanaProviderScriptHandlerTests.swift index de46220b83d3..f435562aea43 100644 --- a/ios/brave-ios/Tests/ClientTests/SolanaProviderScriptHandlerTests.swift +++ b/ios/brave-ios/Tests/ClientTests/SolanaProviderScriptHandlerTests.swift @@ -19,7 +19,7 @@ import XCTest provider._connect = { params, completion in completion(.internalError, Strings.Wallet.internalErrorMessage, "") } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -43,7 +43,7 @@ import XCTest provider._connect = { [kTestPublicKey] params, completion in completion(.success, "", kTestPublicKey) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -69,7 +69,7 @@ import XCTest XCTAssertNotNil(params) // onlyIfTrusted provided as an arg, should be non-nil completion(.success, "", kTestPublicKey) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -100,7 +100,7 @@ import XCTest provider._signAndSendTransaction = { signTransactionParam, sendOptions, completion in completion(.internalError, Strings.Wallet.internalErrorMessage, [:]) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -143,7 +143,7 @@ import XCTest ] ) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -185,7 +185,7 @@ import XCTest ] ) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -214,7 +214,7 @@ import XCTest provider._signMessage = { _, _, completion in completion(.internalError, Strings.Wallet.internalErrorMessage, [:]) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -254,7 +254,7 @@ import XCTest ] ) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -299,7 +299,7 @@ import XCTest ] ) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -340,7 +340,7 @@ import XCTest provider._signTransaction = { _, completion in completion(.internalError, Strings.Wallet.internalErrorMessage, [], .legacy) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -379,7 +379,7 @@ import XCTest provider._signTransaction = { [kSerializedTx] _, completion in completion(.success, "", kSerializedTx.map(NSNumber.init(value:)), .legacy) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -418,7 +418,7 @@ import XCTest provider._signTransaction = { [kSerializedTx] _, completion in completion(.success, "", kSerializedTx.map(NSNumber.init(value:)), .V0) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -457,7 +457,7 @@ import XCTest provider._signAllTransactions = { _, completion in completion(.internalError, Strings.Wallet.internalErrorMessage, [], []) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -501,7 +501,7 @@ import XCTest [NSNumber(value: BraveWallet.SolanaMessageVersion.legacy.rawValue)] ) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider @@ -547,7 +547,7 @@ import XCTest [NSNumber(value: BraveWallet.SolanaMessageVersion.V0.rawValue)] ) } - let tab = Tab(configuration: .init()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let solProviderHelper = SolanaProviderScriptHandler(tab: tab) tab.walletSolProvider = provider diff --git a/ios/brave-ios/Tests/ClientTests/StringExtensionsTests.swift b/ios/brave-ios/Tests/ClientTests/StringExtensionsTests.swift index bdb0472067ab..e0be55d54904 100644 --- a/ios/brave-ios/Tests/ClientTests/StringExtensionsTests.swift +++ b/ios/brave-ios/Tests/ClientTests/StringExtensionsTests.swift @@ -54,24 +54,6 @@ import XCTest XCTAssertEqual("test test".capitalizeFirstLetter, "Test test") } - func testRemoveUnicodeFromFilename() { - let files = [ - "foo-\u{200F}cod.jpg", - "regedt\u{202e}gpj.apk", - ] - - let nounicodes = [ - "foo-cod.jpg", - "regedtgpj.apk", - ] - - for (file, nounicode) in zip(files, nounicodes) { - XCTAssert(file != nounicode) - let strip = HTTPDownload.stripUnicode(fromFilename: file) - XCTAssert(strip == nounicode) - } - } - func testWithSecureUrlScheme() { XCTAssertEqual("test".withSecureUrlScheme, "https://test") XCTAssertEqual("http://test".withSecureUrlScheme, "https://test") diff --git a/ios/brave-ios/Tests/ClientTests/TabEventHandlerTests.swift b/ios/brave-ios/Tests/ClientTests/TabEventHandlerTests.swift index 559bb4b91ff5..fb09714eedb8 100644 --- a/ios/brave-ios/Tests/ClientTests/TabEventHandlerTests.swift +++ b/ios/brave-ios/Tests/ClientTests/TabEventHandlerTests.swift @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import Foundation import WebKit import XCTest @@ -11,7 +12,7 @@ import XCTest @MainActor class TabEventHandlerTests: XCTestCase { func testEventDelivery() { - let tab = Tab(configuration: WKWebViewConfiguration()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let handler = DummyHandler() XCTAssertNil(handler.isFocused) @@ -24,7 +25,7 @@ import XCTest } func testUnregistration() { - let tab = Tab(configuration: WKWebViewConfiguration()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let handler = DummyHandler() XCTAssertNil(handler.isFocused) @@ -39,7 +40,7 @@ import XCTest } func testOnlyRegisteredForEvents() { - let tab = Tab(configuration: WKWebViewConfiguration()) + let tab = Tab(wkConfiguration: nil, configuration: nil) let handler = DummyHandler() handler.doUnregister() @@ -57,7 +58,7 @@ import XCTest } func testOnlyRegisteredForEvents111() { - let tab = Tab(configuration: WKWebViewConfiguration(), type: .private) + let tab = Tab(wkConfiguration: nil, configuration: nil) let urlTest1 = URL(string: "https://www.brave.com") let urlTest2 = URL(string: "http://localhost:8080") diff --git a/ios/brave-ios/Tests/ClientTests/TabManagerNavDelegateTests.swift b/ios/brave-ios/Tests/ClientTests/TabManagerNavDelegateTests.swift deleted file mode 100644 index e09f74f8d85d..000000000000 --- a/ios/brave-ios/Tests/ClientTests/TabManagerNavDelegateTests.swift +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import WebKit -import XCTest - -@testable import Brave - -@MainActor class TabManagerNavDelegateTests: XCTestCase { - var navigation: WKNavigation! - override func setUp() { - super.setUp() - navigation = WKNavigation() - } - - func test_webViewDidCommit_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webView(anyWebView(), didCommit: navigation) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDidCommit]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDidCommit]) - } - - func test_webViewDidFail_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webView(anyWebView(), didFail: navigation, withError: anyError()) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDidFail]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDidFail]) - } - - func test_webViewDidFailProvisionalNavigation_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webView(anyWebView(), didFailProvisionalNavigation: navigation, withError: anyError()) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDidFailProvisionalNavigation]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDidFailProvisionalNavigation]) - } - - func test_webViewDidFinish_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webView(anyWebView(), didFinish: navigation) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDidFinish]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDidFinish]) - } - - func test_webViewWebContentProcessDidTerminate_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webViewWebContentProcessDidTerminate(anyWebView()) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewWebContentProcessDidTerminate]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewWebContentProcessDidTerminate]) - } - - func test_webViewDidReceiveServerRedirectForProvisionalNavigation_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webView(anyWebView(), didReceiveServerRedirectForProvisionalNavigation: navigation) - - XCTAssertEqual( - delegate1.receivedMessages, - [.webViewDidReceiveServerRedirectForProvisionalNavigation] - ) - XCTAssertEqual( - delegate2.receivedMessages, - [.webViewDidReceiveServerRedirectForProvisionalNavigation] - ) - } - - func test_webViewDidStartProvisionalNavigation_sendsCorrectMessage() { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - sut.webView(anyWebView(), didStartProvisionalNavigation: navigation) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDidStartProvisionalNavigation]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDidStartProvisionalNavigation]) - } - - @MainActor - func test_webViewDecidePolicyFor_actionPolicy_sendsCorrectMessage() async { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - let e = expectation(description: "decisionHandler") - sut.webView( - anyWebView(), - decidePolicyFor: WKNavigationAction(), - preferences: WKWebpagePreferences(), - decisionHandler: { _, _ in e.fulfill() } - ) - await fulfillment(of: [e]) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDecidePolicyWithActionPolicy]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDecidePolicyWithActionPolicy]) - } - - @MainActor - func test_webViewDecidePolicyFor_responsePolicy_sendsCorrectMessage() async { - let sut = TabManagerNavDelegate() - let delegate1 = WKNavigationDelegateSpy() - let delegate2 = WKNavigationDelegateSpy() - - sut.insert(delegate1) - sut.insert(delegate2) - let e = expectation(description: "decisionHandler") - sut.webView( - anyWebView(), - decidePolicyFor: WKNavigationResponse(), - decisionHandler: { _ in e.fulfill() } - ) - await fulfillment(of: [e]) - - XCTAssertEqual(delegate1.receivedMessages, [.webViewDecidePolicyWithResponsePolicy]) - XCTAssertEqual(delegate2.receivedMessages, [.webViewDecidePolicyWithResponsePolicy]) - } -} - -// MARK: - Helpers - -private func anyWebView() -> WKWebView { - return WKWebView(frame: CGRect(width: 100, height: 100)) -} - -private func anyError() -> NSError { - return NSError(domain: "any error", code: 0) -} - -private class WKNavigationDelegateSpy: NSObject, WKNavigationDelegate { - enum Message { - case webViewDidCommit - case webViewDidFail - case webViewDidFailProvisionalNavigation - case webViewDidFinish - case webViewWebContentProcessDidTerminate - case webViewDidReceiveServerRedirectForProvisionalNavigation - case webViewDidStartProvisionalNavigation - case webViewDecidePolicyWithActionPolicy - case webViewDecidePolicyWithResponsePolicy - } - - var receivedMessages = [Message]() - - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { - receivedMessages.append(.webViewDidCommit) - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - receivedMessages.append(.webViewDidFail) - } - - func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation!, - withError error: Error - ) { - receivedMessages.append(.webViewDidFailProvisionalNavigation) - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - receivedMessages.append(.webViewDidFinish) - } - - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - receivedMessages.append(.webViewWebContentProcessDidTerminate) - } - - func webView( - _ webView: WKWebView, - didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation! - ) { - receivedMessages.append(.webViewDidReceiveServerRedirectForProvisionalNavigation) - } - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - receivedMessages.append(.webViewDidStartProvisionalNavigation) - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse - ) async -> WKNavigationResponsePolicy { - receivedMessages.append(.webViewDecidePolicyWithResponsePolicy) - return .allow - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - preferences: WKWebpagePreferences - ) async -> (WKNavigationActionPolicy, WKWebpagePreferences) { - receivedMessages.append(.webViewDecidePolicyWithActionPolicy) - return (.allow, preferences) - } -} diff --git a/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift b/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift index 7a1e633893f7..7e17c3fef2df 100644 --- a/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift +++ b/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift @@ -116,7 +116,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { let didAdd = MethodSpy(functionName: "tabManager(_:didAddTab:)") var manager: TabManager! - private let privateBrowsingManager = PrivateBrowsingManager() + private let privateBrowsingManager = PrivateBrowsingManager(braveCore: nil) private let testWindowId = UUID() override func setUp() { @@ -128,8 +128,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { windowId: testWindowId, prefs: profile.prefs, rewards: nil, - tabGeneratorAPI: nil, - historyAPI: nil, + braveCore: nil, privateBrowsingManager: privateBrowsingManager ) privateBrowsingManager.isPrivateBrowsing = false @@ -157,7 +156,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { // test that non-private tabs are saved to the db // add some non-private tabs to the tab manager for _ in 0..<3 { - let tab = Tab(configuration: configuration, type: .private) + let tab = Tab(wkConfiguration: nil, configuration: nil, type: .private) tab.url = URL(string: "http://yahoo.com")! manager.configureTab( tab, @@ -179,7 +178,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { manager.addDelegate(delegate) delegate.expect([willAdd, didAdd]) - manager.addTab(isPrivate: false) + manager.addTab(zombie: true, isPrivate: false) delegate.verify("Not all delegate methods were called") } @@ -187,7 +186,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) manager.addDelegate(delegate) @@ -208,7 +207,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about - let tab = manager.addTab(isPrivate: true) + let tab = manager.addTab(zombie: true, isPrivate: true) manager.selectTab(tab) manager.addDelegate(delegate) @@ -229,9 +228,9 @@ open class MockTabManagerDelegate: TabManagerDelegate { setPersistentPrivateMode(true) // create one private and one normal tab - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) XCTAssertEqual( TabType.of(manager.selectedTab).isPrivate, @@ -256,7 +255,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { "The regular tab should stil be around" ) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) XCTAssertEqual(manager.tabs(withType: .private).count, 2, "There should be two private tabs") manager.willSwitchTabMode(leavingPBM: true) XCTAssertEqual( @@ -265,14 +264,14 @@ open class MockTabManagerDelegate: TabManagerDelegate { "After willSwitchTabMode there should be 2 private tabs" ) - manager.selectTab(manager.addTab(isPrivate: true)) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) XCTAssertEqual( manager.tabs(withType: .private).count, 4, "Private tabs should not be deleted when another one is added" ) - manager.selectTab(manager.addTab(isPrivate: false)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: false)) XCTAssertEqual( manager.tabs(withType: .private).count, 4, @@ -291,9 +290,9 @@ open class MockTabManagerDelegate: TabManagerDelegate { setPersistentPrivateMode(false) // create one private and one normal tab - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) XCTAssertEqual( TabType.of(manager.selectedTab).isPrivate, @@ -318,7 +317,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { "The regular tab should stil be around" ) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should be one new private tab") manager.willSwitchTabMode(leavingPBM: true) XCTAssertEqual( @@ -327,14 +326,14 @@ open class MockTabManagerDelegate: TabManagerDelegate { "After willSwitchTabMode there should be no more private tabs" ) - manager.selectTab(manager.addTab(isPrivate: true)) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) XCTAssertEqual( manager.tabs(withType: .private).count, 2, "Private tabs should not be deleted when another one is added" ) - manager.selectTab(manager.addTab(isPrivate: false)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: false)) XCTAssertEqual( manager.tabs(withType: .private).count, 0, @@ -350,10 +349,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { func testTogglePBMDeletePersistent() { setPersistentPrivateMode(true) - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.selectTab(manager.addTab(isPrivate: false)) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: false)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) manager.willSwitchTabMode(leavingPBM: false) XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should be 1 private tab") @@ -368,10 +367,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { func testTogglePBMDeleteNonPersistent() { setPersistentPrivateMode(false) - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.selectTab(manager.addTab(isPrivate: false)) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: false)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) manager.willSwitchTabMode(leavingPBM: false) XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should be 1 private tab") @@ -385,10 +384,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.addTab(isPrivate: false) - let deleteTab = manager.addTab(isPrivate: false) + manager.addTab(zombie: true, isPrivate: false) + let deleteTab = manager.addTab(zombie: true, isPrivate: false) manager.addDelegate(delegate) delegate.expect([willRemove, didRemove]) @@ -401,7 +400,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() func addTab(_ visit: Bool) -> Tab { - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) if visit { tab.lastExecutedTime = Date.now() } @@ -440,7 +439,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about - (0..<10).forEach { _ in manager.addTab(isPrivate: false) } + (0..<10).forEach { _ in manager.addTab(zombie: true, isPrivate: false) } manager.selectTab(manager.allTabs.last) let deleteTab = manager.allTabs.last let newSelectedTab = manager.allTabs[8] @@ -466,10 +465,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() // create one private and one normal tab - let tab = manager.addTab(isPrivate: false) - let newTab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) + let newTab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) manager.addDelegate(delegate) // Double check a few things @@ -522,10 +521,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() // create one private and one normal tab - let tab = manager.addTab(isPrivate: false) - let newTab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) + let newTab = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(tab) - manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(zombie: true, isPrivate: true)) manager.addDelegate(delegate) // Double check a few things @@ -573,7 +572,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { let delegate = MockTabManagerDelegate() //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about - (0..<10).forEach { _ in manager.addTab(isPrivate: false) } + (0..<10).forEach { _ in manager.addTab(zombie: true, isPrivate: false) } manager.selectTab(manager.allTabs.first) let deleteTab = manager.allTabs.first let newSelectedTab = manager.allTabs[1] @@ -598,10 +597,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { // We add 2 tabs. Then a private one before adding another normal tab and selecting it. // Make sure that when the last one is deleted we dont switch to the private tab - manager.addTab(isPrivate: false) - let newSelected = manager.addTab(isPrivate: false) - manager.addTab(isPrivate: true) - let deleted = manager.addTab(isPrivate: false) + manager.addTab(zombie: true, isPrivate: false) + let newSelected = manager.addTab(zombie: true, isPrivate: false) + manager.addTab(zombie: true, isPrivate: true) + let deleted = manager.addTab(zombie: true, isPrivate: false) manager.selectTab(manager.allTabs.last) manager.addDelegate(delegate) @@ -623,10 +622,10 @@ open class MockTabManagerDelegate: TabManagerDelegate { // We add 2 tabs. Then a private one before adding another normal tab and selecting the first. // Make sure that when the last one is deleted we dont switch to the private tab - let deleted = manager.addTab(isPrivate: false) - let newSelected = manager.addTab(isPrivate: false) - manager.addTab(isPrivate: true) - manager.addTab(isPrivate: false) + let deleted = manager.addTab(zombie: true, isPrivate: false) + let newSelected = manager.addTab(zombie: true, isPrivate: false) + manager.addTab(zombie: true, isPrivate: true) + manager.addTab(zombie: true, isPrivate: false) manager.selectTab(manager.allTabs.first) manager.addDelegate(delegate) @@ -645,7 +644,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { func testRemoveOnlyTab() { let delegate = MockTabManagerDelegate() - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) manager.addDelegate(delegate) delegate.expect([ @@ -661,7 +660,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { } func testRemoveAllTabs() { - (0..<10).forEach { _ in manager.addTab(isPrivate: false) } + (0..<10).forEach { _ in manager.addTab(zombie: true, isPrivate: false) } manager.removeAll() XCTAssertFalse(manager.allTabs.isEmpty, "Should create a new tab when all others are removed") @@ -669,7 +668,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { } func testRemoveAllOtherTabs() { - (0..<10).forEach { index in manager.addTab(isPrivate: false) } + (0..<10).forEach { index in manager.addTab(zombie: true, isPrivate: false) } manager.removeAllForCurrentMode(isActiveTabIncluded: false) XCTAssertFalse( @@ -684,9 +683,9 @@ open class MockTabManagerDelegate: TabManagerDelegate { func testMoveTabToEnd() { let firstTab = manager.addTabAndSelect(isPrivate: false) - let secondTab = manager.addTab(isPrivate: false) - manager.addTab(isPrivate: true) - let thirdTab = manager.addTab(isPrivate: false) + let secondTab = manager.addTab(zombie: true, isPrivate: false) + manager.addTab(zombie: true, isPrivate: true) + let thirdTab = manager.addTab(zombie: true, isPrivate: false) manager.moveTab(firstTab, toIndex: manager.tabs(withType: .regular).count - 1) @@ -702,9 +701,9 @@ open class MockTabManagerDelegate: TabManagerDelegate { func testMoveTabToStart() { let firstTab = manager.addTabAndSelect(isPrivate: false) - let secondTab = manager.addTab(isPrivate: false) - manager.addTab(isPrivate: true) - let thirdTab = manager.addTab(isPrivate: false) + let secondTab = manager.addTab(zombie: true, isPrivate: false) + manager.addTab(zombie: true, isPrivate: true) + let thirdTab = manager.addTab(zombie: true, isPrivate: false) manager.moveTab(thirdTab, toIndex: 0) @@ -719,11 +718,11 @@ open class MockTabManagerDelegate: TabManagerDelegate { } func testMoveTabToMiddle() { - let firstTab = manager.addTab(isPrivate: false) - let secondTab = manager.addTab(isPrivate: false) - manager.addTab(isPrivate: true) + let firstTab = manager.addTab(zombie: true, isPrivate: false) + let secondTab = manager.addTab(zombie: true, isPrivate: false) + manager.addTab(zombie: true, isPrivate: true) let thirdTab = manager.addTabAndSelect(isPrivate: false) - let forthTab = manager.addTab(isPrivate: false) + let forthTab = manager.addTab(zombie: true, isPrivate: false) manager.moveTab(forthTab, toIndex: 1) @@ -756,7 +755,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { forNotification: .NSManagedObjectContextDidSave, object: nil ) - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) wait(for: [tabAddExpectation], timeout: 5) delegate.verify("Not all delegate methods were called") @@ -772,7 +771,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { DataController.shared.initializeOnce() delegate.expect([willAdd, didAdd]) - manager.addTab(isPrivate: true) + manager.addTab(zombie: true, isPrivate: true) delegate.verify("Not all delegate methods were called") let storedTabs = SessionTab.all() @@ -795,12 +794,12 @@ open class MockTabManagerDelegate: TabManagerDelegate { wait(for: [windowCreateExpectation], timeout: 5) delegate.expect([willAdd, didAdd, willAdd, didAdd]) - manager.addTab(isPrivate: true) + manager.addTab(zombie: true, isPrivate: true) let tabAddExpectation = expectation( forNotification: .NSManagedObjectContextDidSave, object: nil ) - let tab = manager.addTab(isPrivate: false) + let tab = manager.addTab(zombie: true, isPrivate: false) wait(for: [tabAddExpectation], timeout: 5) delegate.verify("Not all delegate methods were called") diff --git a/ios/brave-ios/Tests/ClientTests/TabSessionTests.swift b/ios/brave-ios/Tests/ClientTests/TabSessionTests.swift deleted file mode 100644 index 668427cfd6dc..000000000000 --- a/ios/brave-ios/Tests/ClientTests/TabSessionTests.swift +++ /dev/null @@ -1,854 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveShared -import Data -import ObjectiveC.runtime -import Shared -import Storage -import WebKit -import XCTest - -@testable import Brave - -extension WKWebView { - fileprivate class func swizzleMe() { - let originalSelector = #selector(WKWebView.init(frame:configuration:)) - let swizzledSelector = #selector(WKWebView.reInit(frame:configuration:)) - - let originalMethod = class_getInstanceMethod(self, originalSelector)! - let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)! - - let didAddMethod = class_addMethod( - self, - originalSelector, - method_getImplementation(swizzledMethod), - method_getTypeEncoding(swizzledMethod) - ) - - if didAddMethod { - class_replaceMethod( - self, - swizzledSelector, - method_getImplementation(originalMethod), - method_getTypeEncoding(originalMethod) - ) - } else { - method_exchangeImplementations(originalMethod, swizzledMethod) - } - } - - @objc - fileprivate func reInit(frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView { - configuration.setValue(true, forKey: "alwaysRunsAtForegroundPriority") - return reInit(frame: frame, configuration: configuration) - } -} - -extension HTTPCookie { - fileprivate class func filter(cookies: [HTTPCookie], for url: URL) -> [HTTPCookie]? { - guard let host = url.host?.lowercased() else { return nil } - return cookies.filter({ $0.validFor(host: host) }) - } - - private func validFor(host: String) -> Bool { - guard domain.hasPrefix(".") else { return host == domain } - return host == domain.dropFirst() || host.hasSuffix(domain) - } -} - -private class WebViewNavigationAdapter: NSObject, WKNavigationDelegate { - private let didFailListener: ((Error) -> Void)? - private let didFinishListener: (() -> Void)? - - init(didFailListener: ((Error) -> Void)? = nil, didFinishListener: (() -> Void)? = nil) { - self.didFailListener = didFailListener - self.didFinishListener = didFinishListener - super.init() - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - decisionHandler(.allow) - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse, - decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void - ) { - decisionHandler(.allow) - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - didFinishListener?() - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - didFailListener?(error) - } -} - -@MainActor class TabSessionTests: XCTestCase { - private var tabManager: TabManager! - private let maxTimeout = 60.0 - private let privateBrowsingManager = PrivateBrowsingManager() - - override class func setUp() { - super.setUp() - // WKWebView.swizzleMe() - } - - override func setUp() { - super.setUp() - - DataController.shared.initializeOnce() - tabManager = { () -> TabManager in - let profile = BrowserProfile(localName: "profile") - return TabManager( - windowId: UUID(), - prefs: profile.prefs, - rewards: nil, - tabGeneratorAPI: nil, - historyAPI: nil, - privateBrowsingManager: privateBrowsingManager - ) - }() - } - - override func tearDown() { - super.tearDown() - - destroyData {} - tabManager = nil - } - - private func destroyData(_ completion: @escaping () -> Void) { - tabManager.removeAll() - tabManager.reset() - - HTTPCookieStorage.shared.removeCookies(since: .distantPast) - - WKWebsiteDataStore.default().removeData( - ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), - modifiedSince: .distantPast, - completionHandler: { - completion() - } - ) - } - - func testPrivateTabSessionSharing() { - let dataStoreExpectation = XCTestExpectation(description: "dataStorePersistence") - var webViewNavigationAdapter = WebViewNavigationAdapter() - - destroyData { - let urls = [ - "https://bing.com", - "https://google.com", - "https://yandex.ru", - "https://yahoo.com", - ] - - self.tabManager.addTabsForURLs( - urls.compactMap({ URL(string: $0) }), - zombie: false, - isPrivate: true - ) - if self.tabManager.allTabs.count != 4 { - XCTFail("Error: Not all Tabs are created equally") - return dataStoreExpectation.fulfill() - } - - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if webView.configuration.websiteDataStore.isPersistent { - XCTFail("Private Tab is storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - - group.notify(queue: .main) { - if self.tabManager.allTabs.count != 4 { - XCTFail("Error: Not all Tabs are alive equally") - return dataStoreExpectation.fulfill() - } - - // All requests finished loading.. time to check the cookies.. - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView died unexpectedly") - return dataStoreExpectation.fulfill() - } - - group.enter() - webView.configuration.websiteDataStore.fetchDataRecords( - ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), - completionHandler: { records in - XCTAssertFalse(records.isEmpty, "Error: Data Store not shared amongst private tabs!") - - let recordNames = Set( - records.compactMap({ URL(string: "http://\($0.displayName)")?.host }) - ) - let urlNames = Set(urls.compactMap({ URL(string: $0)?.host })) - - XCTAssertTrue(urlNames.isSubset(of: recordNames), "Data Store records do not match!") - group.leave() - } - ) - } - - group.notify(queue: .main) { - dataStoreExpectation.fulfill() - } - } - } - wait(for: [dataStoreExpectation], timeout: maxTimeout) - } - - func testNormalTabSessionSharing() { - let dataStoreExpectation = XCTestExpectation(description: "dataStorePersistence") - var webViewNavigationAdapter = WebViewNavigationAdapter() - - destroyData { - let urls = [ - "https://stackoverflow.com", - "https://discordapp.com", - "https://apple.com", - "https://slack.com", - ] - - self.tabManager.addTabsForURLs( - urls.compactMap({ URL(string: $0) }), - zombie: false, - isPrivate: false - ) - self.tabManager.removeTabs( - self.tabManager.allTabs.filter({ $0.url?.absoluteString.contains("localhost") ?? false }) - ) - if self.tabManager.allTabs.count != 4 { - XCTFail("Error: Not all Tabs are created equally") - return dataStoreExpectation.fulfill() - } - - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if !webView.configuration.websiteDataStore.isPersistent { - XCTFail("Normal Tab is not storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - - group.notify(queue: .main) { - if self.tabManager.allTabs.count != 4 { - XCTFail("Error: Not all Tabs are alive equally") - return dataStoreExpectation.fulfill() - } - - // All requests finished loading.. time to check the cookies.. - let group = DispatchGroup() - if self.tabManager.allTabs.isEmpty { - XCTFail("Tabs somehow destroyed") - return dataStoreExpectation.fulfill() - } - - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView died unexpectedly") - return dataStoreExpectation.fulfill() - } - - group.enter() - webView.configuration.websiteDataStore.fetchDataRecords( - ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), - completionHandler: { records in - XCTAssertFalse(records.isEmpty, "Error: Data Store not shared amongst normal tabs!") - - let recordNames = Set( - records.compactMap({ URL(string: "http://\($0.displayName)")?.host }) - ) - let urlNames = Set(urls.compactMap({ URL(string: $0)?.host })) - - XCTAssertTrue(urlNames.isSubset(of: recordNames), "Data Store records do not match!") - group.leave() - } - ) - } - - group.notify(queue: .main) { - dataStoreExpectation.fulfill() - } - } - } - wait(for: [dataStoreExpectation], timeout: maxTimeout) - } - - func testPrivateTabNonPersistence() { - let dataStoreExpectation = XCTestExpectation(description: "dataStorePersistence") - var webViewNavigationAdapter = WebViewNavigationAdapter() - - destroyData { - let urls = [ - "https://github.com", - "https://bitbucket.com", - "https://youtube.com", - "https://msn.com", - ] - - self.tabManager.addTabsForURLs( - urls.compactMap({ URL(string: $0) }), - zombie: false, - isPrivate: true - ) - if self.tabManager.allTabs.count != 4 { - XCTFail("Error: Not all Tabs are created equally") - dataStoreExpectation.fulfill() - } - - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if webView.configuration.websiteDataStore.isPersistent { - XCTFail("Private Tab is storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - - group.notify(queue: .main) { - // All requests finished loading.. kill all tabs.. check the cookies.. - self.tabManager.removeAll() - - // When the tab manager destroys tabs, if there are no tabs left, it adds a new zombie tab. - // Hence the count of 1. - if self.tabManager.allTabs.count != 1 { - XCTFail("Error: Not all Tabs are destroyed equally") - return dataStoreExpectation.fulfill() - } - - // Now that all private tabs have been destroyed, we need to create a new one and check the storage. - // We'd verify if the new tab has the old data after all private tabs were destroyed. - let group = DispatchGroup() - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - self.tabManager.addTabsForURLs( - [URL(string: "https://brave.com")!], - zombie: false, - isPrivate: true - ) - if self.tabManager.tabs(withType: .private).isEmpty { - XCTFail("Error: Private tab not created") - return dataStoreExpectation.fulfill() - } - - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if webView.configuration.websiteDataStore.isPersistent { - XCTFail("Private Tab is storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - group.notify(queue: .main) { - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView died unexpectedly") - return dataStoreExpectation.fulfill() - } - - group.enter() - webView.configuration.websiteDataStore.fetchDataRecords( - ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), - completionHandler: { records in - XCTAssertFalse( - records.isEmpty, - "Error: Data Store does not contain the latest webpage's data!" - ) - - let recordNames = records.compactMap({ - URL(string: "http://\($0.displayName)")?.host - }) - - for url in urls.compactMap({ URL(string: $0)?.host }) { - if recordNames.contains(url) { - XCTFail("Error: Data Store is NOT ephemeral!") - } - } - - group.leave() - } - ) - } - - group.notify(queue: .main) { - dataStoreExpectation.fulfill() - } - } - } - } - - wait(for: [dataStoreExpectation], timeout: maxTimeout) - } - - func testTabsPrivateToNormal() { - let dataStoreExpectation = XCTestExpectation(description: "dataStorePersistence") - var webViewNavigationAdapter = WebViewNavigationAdapter() - - destroyData { - let url = URL(string: "https://www.insanelymac.com")! - let otherURL = URL(string: "https://www.macrumors.com")! - - self.tabManager.addTabsForURLs([url], zombie: false, isPrivate: true) - if self.tabManager.allTabs.count != 1 { - XCTFail("Error: Not all Tabs are created equally") - return dataStoreExpectation.fulfill() - } - - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if webView.configuration.websiteDataStore.isPersistent { - XCTFail("Private Tab is storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - - // All requests finished loading.. switch to normal mode.. check the cookies.. - group.notify(queue: .main) { - let group = DispatchGroup() - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - self.tabManager.addTabsForURLs([otherURL], zombie: false, isPrivate: false) - - // When the tab manager switches to normal mode from private - // It should kill all private tabs.. - if !self.tabManager.tabs(withType: .private).isEmpty { - XCTFail("Error: Private tabs not destroyed when switching to normal mode!") - return dataStoreExpectation.fulfill() - } - - if self.tabManager.tabs(withType: .regular).isEmpty { - XCTFail("Error: Normal tab not created") - return dataStoreExpectation.fulfill() - } - - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if webView.configuration.websiteDataStore.isPersistent == false { - XCTFail("Normal Tab is not storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - group.notify(queue: .main) { - let group = DispatchGroup() - for tab in self.tabManager.tabs(withType: .regular) { - guard let webView = tab.webView else { - XCTFail("Webview died unexpectedly") - return dataStoreExpectation.fulfill() - } - - group.enter() - webView.configuration.websiteDataStore.fetchDataRecords( - ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), - completionHandler: { records in - XCTAssertFalse(records.isEmpty, "Error: Data Store not persistent in normal mode!") - - let recordNames = Set( - records.compactMap({ URL(string: "http://\($0.displayName)")?.host }) - ) - let urlNames = Set([url.host ?? "FailedTestDomain"]) - - XCTAssertFalse( - urlNames.isSubset(of: recordNames), - "Data Store leaking from private tab to normal tab!" - ) - - group.leave() - } - ) - } - - group.notify(queue: .main) { - dataStoreExpectation.fulfill() - } - } - } - } - - wait(for: [dataStoreExpectation], timeout: maxTimeout) - } - - func testIsSecretTokenAvailable() { - let url = URL(string: "https://www.brave.com")! - let expectation = self.expectation(description: "No secret tokens found") - - var webViewNavigationAdapter = WebViewNavigationAdapter() - self.tabManager.addTabsForURLs([url], zombie: false, isPrivate: false) - - let javascript = - """ - (function() { - var found = '' - var parsed = [] - - // search all user known functions for tokens - var functions = [] - if (typeof navigator.credentials !== 'undefined') { - if (typeof navigator.credentials.create !== 'undefined') { - functions.push(navigator.credentials.create) - } - - if (typeof navigator.credentials.get !== 'undefined') { - functions.push(navigator.credentials.get) - } - } - - // search all enumerable objects for tokens - var objects = [ - document, - window - ] - - if (typeof registerResponse !== 'undefined') { - objects.push(registerResponse) - } - - if (typeof signResponse !== 'undefined') { - objects.push(signResponse) - } - - if (typeof navigator.credentials !== 'undefined') { - objects.push(navigator.credentials) - } - - function isTokenPresent(value) { - if (typeof value == "undefined" || typeof value.indexOf == "undefined") { - return false; - } - if (value.indexOf("\(UserScriptManager.securityToken)") >= 0 || value.indexOf("\(UserScriptManager.securityToken)") >= 0) { - return true; - } - return false; - } - - function secretTokensInObject(obj) { - if (obj == undefined) { - return '' - } - - if (isTokenPresent(obj.name) || isTokenPresent(obj.toString())) { - return '' + obj - } - for (const [key, value] of Object.entries(obj)) { - if (isTokenPresent(key + '') || isTokenPresent(value + '')) { - return key + ': ' + value; - } - - if (typeof value == 'function' && isTokenPresent(value.toString())) { - return 'function: ' + value - } - - if (typeof value == 'object' && value != null) { - if (isTokenPresent(value.constructor.name)) { - return 'Object: ' + value.constructor.name - } else if (value != obj && parsed.indexOf(value) == -1 && objects.indexOf(value) == -1) { - objects.push(value) - } - } - } - parsed.push(obj) - return ''; - } - - function secretTokensInFunctions(func) { - if (func == undefined) { - return ''; - } - - if (isTokenPresent(func.toString())) { - return func.toString(); - } - return '' - } - - while (objects.length > 0 && found.length == 0) { - found = secretTokensInObject(objects.shift()) - } - - while (functions.length > 0 && found.length == 0) { - found = secretTokensInFunctions(functions.shift()) - } - return found - })(); - """ - - let scripts = Set(UserScriptManager.ScriptType.allCases) - let customScripts = Set([.farblingProtection(etld: "fake.com")]) - - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - // include all scripts - UserScriptManager.shared.loadCustomScripts( - into: tab, - userScripts: scripts, - customScripts: customScripts - ) - - group.enter() - } - - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - - group.notify(queue: .main) { - let tab = self.tabManager.selectedTab - guard let webView = tab?.webView else { - XCTFail("WebView is not created yet") - return - } - webView.evaluateSafeJavaScript( - functionName: javascript, - contentWorld: .page, - asFunction: false - ) { result, error in - guard let keys = result as? String else { - XCTFail("Javascript error while finding secret tokens") - expectation.fulfill() - return - } - if !keys.isEmpty { - XCTFail("Secret tokens found!" + keys) - } - expectation.fulfill() - } - } - - waitForExpectations(timeout: 60, handler: nil) - } - - func testTabsNormalToPrivate() { - let dataStoreExpectation = XCTestExpectation(description: "dataStorePersistence") - var webViewNavigationAdapter = WebViewNavigationAdapter() - - destroyData { - let url = URL(string: "https://twitter.com")! - let otherURL = URL(string: "https://instagram.com")! - - self.tabManager.addTabsForURLs([url], zombie: false, isPrivate: false) - if self.tabManager.allTabs.count != 1 { - XCTFail("Error: Not all Tabs are created equally") - return dataStoreExpectation.fulfill() - } - - let group = DispatchGroup() - for tab in self.tabManager.allTabs { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if !webView.configuration.websiteDataStore.isPersistent { - XCTFail("Normal Tab is not storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - - // All requests finished loading.. switch to private mode.. check the cookies.. - group.notify(queue: .main) { - let group = DispatchGroup() - webViewNavigationAdapter = WebViewNavigationAdapter( - didFailListener: { _ in - group.leave() - }, - didFinishListener: { - group.leave() - } - ) - - self.tabManager.addNavigationDelegate(webViewNavigationAdapter) - self.tabManager.addTabsForURLs([otherURL], zombie: false, isPrivate: true) - - // When the tab manager switches to private mode from normal - // It should keep both private and normal tabs - if self.tabManager.tabs(withType: .private).isEmpty { - XCTFail("Error: Private tabs not created") - return dataStoreExpectation.fulfill() - } - - if self.tabManager.tabs(withType: .regular).isEmpty { - XCTFail("Error: Normal tab not created") - return dataStoreExpectation.fulfill() - } - - for tab in self.tabManager.tabs(withType: .private) { - guard let webView = tab.webView else { - XCTFail("WebView is not created yet") - return dataStoreExpectation.fulfill() - } - - if webView.configuration.websiteDataStore.isPersistent { - XCTFail("Private Tab is storing data persistently!") - return dataStoreExpectation.fulfill() - } - - group.enter() - } - - group.notify(queue: .main) { - let group = DispatchGroup() - - for tab in self.tabManager.tabs(withType: .private) { - guard let webView = tab.webView else { - XCTFail("WebView unexpectedly died") - return dataStoreExpectation.fulfill() - } - - group.enter() - webView.configuration.websiteDataStore.fetchDataRecords( - ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), - completionHandler: { records in - let recordNames = Set( - records.compactMap({ URL(string: "http://\($0.displayName)")?.host }) - ) - let urlNames = Set([url.host ?? "FailedTestDomain"]) - - XCTAssertFalse( - urlNames.isSubset(of: recordNames), - "Data Store leaking from normal tab to private tab!" - ) - - group.leave() - } - ) - } - - group.notify(queue: .main) { - dataStoreExpectation.fulfill() - } - } - } - } - - wait(for: [dataStoreExpectation], timeout: maxTimeout) - } -} diff --git a/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift b/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift index 77d52e7acb3a..fa0e8867a8a9 100644 --- a/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift +++ b/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift @@ -33,7 +33,7 @@ class MockScriptsViewController: UIViewController { super.viewDidLoad() // Will load some base scripts into this webview - userScriptManager.loadScripts(into: webView, scripts: []) + userScriptManager.loadScripts(into: webView.configuration.userContentController, scripts: []) self.view.addSubview(webView) webView.snp.makeConstraints { make in diff --git a/ios/brave-ios/Tests/ClientTests/WKWebViewExtensionsTest.swift b/ios/brave-ios/Tests/ClientTests/WKWebViewExtensionsTest.swift index b367c0778fba..4574c1c12af8 100644 --- a/ios/brave-ios/Tests/ClientTests/WKWebViewExtensionsTest.swift +++ b/ios/brave-ios/Tests/ClientTests/WKWebViewExtensionsTest.swift @@ -12,7 +12,7 @@ import XCTest class WKWebViewExtensionsTest: XCTestCase { func testGenerateJavascriptFunctionString() { - let webView = BraveWebView(frame: .zero, isPrivate: false) + let webView = WKWebView(frame: .zero) var js = webView.generateJSFunctionString(functionName: "demo_function", args: []) XCTAssertNil(js.error) XCTAssertEqual(js.javascript, "demo_function()") diff --git a/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift b/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift deleted file mode 100644 index f1422efbcc7b..000000000000 --- a/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift +++ /dev/null @@ -1,171 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Preferences -import Shared -import WebKit -import XCTest - -@testable import Brave -@testable import UserAgent - -class UserAgentTests: XCTestCase { - - override func setUp() { - super.setUp() - Preferences.UserAgent.alwaysRequestDesktopSite.reset() - } - - let desktopUARegex: (String) -> Bool = { ua in - let range = ua.range( - of: - "^Mozilla/5\\.0 \\(Macintosh; Intel Mac OS X [0-9_]+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) Version/[0-9\\.]+ Safari/[0-9\\.]+$", - options: .regularExpression - ) - return range != nil - } - - let mobileUARegex: (String) -> Bool = { ua in - let cpuPart = - UIDevice.isPhone - ? "\\(iPhone; CPU iPhone OS [0-9_]+ like Mac OS X\\)" - : "\\(iPad; CPU OS [0-9_]+ like Mac OS X\\)" - - let range = ua.range( - of: - "^Mozilla/5\\.0 \(cpuPart) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) Version/[0-9\\.]+ Mobile/[A-Za-z0-9]+ Safari/[0-9\\.]+$", - options: .regularExpression - ) - return range != nil - } - - // Simple test to make sure the WKWebView UA matches the expected FxiOS pattern. - func testBraveWebViewUserAgentOnPhone() { - if UIDevice.current.userInterfaceIdiom != .phone { return } - - XCTAssertTrue(mobileUARegex(UserAgent.mobile), "User agent computes correctly.") - - let expectation = self.expectation(description: "Found Firefox user agent") - - let webView = BraveWebView(frame: .zero, isPrivate: false) - - webView.evaluateSafeJavaScript( - functionName: "navigator.userAgent", - contentWorld: .page, - asFunction: false - ) { result, error in - let userAgent = result as! String - if !self.mobileUARegex(userAgent) || self.desktopUARegex(userAgent) { - XCTFail("User agent did not match expected pattern! \(userAgent)") - } - expectation.fulfill() - } - - waitForExpectations(timeout: 60, handler: nil) - } - - // WKWebView doesn't give us all UA parts of Safari - // we are able to compare only the first part. - func testFirstUAPart() { - let expectation = self.expectation(description: "First part of UA comparison") - - let webView = BraveWebView(frame: .zero, isPrivate: false) - let wkWebView = WKWebView() - - webView.evaluateSafeJavaScript( - functionName: "navigator.userAgent", - args: [], - contentWorld: .page, - asFunction: false - ) { result, error in - - guard let braveFirstPartOfUA = (result as? String)?.components(separatedBy: "Gecko") else { - XCTFail("Could not unwrap BraveWebView UA") - return - } - - wkWebView.evaluateSafeJavaScript( - functionName: "navigator.userAgent", - contentWorld: .page, - asFunction: false - ) { wkResult, wkError in - guard - let wkWebViewFirstPartOfUA = (result as? String)? - .components(separatedBy: "Gecko") - else { - XCTFail("Could not unwrap WKWebView UA") - return - } - - if braveFirstPartOfUA == wkWebViewFirstPartOfUA { - expectation.fulfill() - } else { - XCTFail("BraveWebView and WKWebView user agents do not match.") - } - } - } - - waitForExpectations(timeout: 60, handler: nil) - } - - // MARK: iPad only tests - must run on iPad - - func testDesktopUserAgent() { - // Must run on iPad iOS 13+ - if UIDevice.current.userInterfaceIdiom != .pad - || ProcessInfo().operatingSystemVersion.majorVersion < 13 - { - return - } - - XCTAssertTrue(desktopUARegex(UserAgent.desktop), "User agent computes correctly.") - - let expectation = self.expectation(description: "Found Firefox user agent") - let webView = BraveWebView(frame: .zero, isPrivate: false) - - webView.evaluateSafeJavaScript( - functionName: "navigator.userAgent", - contentWorld: .page, - asFunction: false - ) { result, error in - let userAgent = result as! String - if self.mobileUARegex(userAgent) || !self.desktopUARegex(userAgent) { - XCTFail("User agent did not match expected pattern! \(userAgent)") - } - expectation.fulfill() - } - - waitForExpectations(timeout: 60, handler: nil) - } - - func testIpadMobileUserAgent() { - // Must run on iPad iOS 13+ - if UIDevice.current.userInterfaceIdiom != .pad - || ProcessInfo().operatingSystemVersion.majorVersion < 13 - { - return - } - - Preferences.UserAgent.alwaysRequestDesktopSite.value = false - - XCTAssertTrue(mobileUARegex(UserAgent.mobile), "User agent computes correctly.") - - let expectation = self.expectation(description: "Found Firefox user agent") - let webView = BraveWebView(frame: .zero, isPrivate: false) - - webView.evaluateSafeJavaScript( - functionName: "navigator.userAgent", - contentWorld: .page, - asFunction: false - ) { result, error in - let userAgent = result as! String - if !self.mobileUARegex(userAgent) || self.desktopUARegex(userAgent) { - XCTFail("User agent did not match expected pattern! \(userAgent)") - } - expectation.fulfill() - } - - waitForExpectations(timeout: 60, handler: nil) - } -} diff --git a/ios/browser/BUILD.gn b/ios/browser/BUILD.gn index fa037761dfc3..87af823af728 100644 --- a/ios/browser/BUILD.gn +++ b/ios/browser/BUILD.gn @@ -6,14 +6,7 @@ import("//build/config/ios/rules.gni") import("//ios/build/config.gni") -source_set("browser") { - sources = [ - "brave_web_client.h", - "brave_web_client.mm", - "brave_web_main_parts.h", - "brave_web_main_parts.mm", - ] - +group("browser") { deps = [ "api/ai_chat", "api/bookmarks", @@ -24,6 +17,7 @@ source_set("browser") { "api/brave_shields", "api/brave_wallet:wallet_mojom_wrappers", "api/certificate", + "api/content_settings", "api/credential_provider", "api/developer_options_code", "api/favicon", @@ -37,6 +31,7 @@ source_set("browser") { "api/opentabs", "api/p3a", "api/password", + "api/prefs", "api/session_restore", "api/storekit_receipt", "api/sync", @@ -45,40 +40,13 @@ source_set("browser") { "api/web", "api/web_image", "brave_wallet", - "//base", "//brave/browser/sync", - "//brave/components/ai_chat/core/browser", - "//brave/components/brave_component_updater/browser", - "//brave/components/brave_wallet/browser", "//brave/components/constants", "//brave/ios/browser/application_context", "//brave/ios/browser/brave_wallet", "//brave/ios/browser/profile/model", "//brave/ios/browser/ui/webui", - "//components/component_updater/installer_policies", - "//components/flags_ui", - "//components/metrics", - "//components/metrics_services_manager", - "//components/variations", - "//components/variations/service", - "//ios/chrome/browser/application_context/model", - "//ios/chrome/browser/flags", - "//ios/chrome/browser/flags:client_side", - "//ios/chrome/browser/profile/model:keyed_service_factories", - "//ios/chrome/browser/providers/raccoon:chromium_raccoon", - "//ios/chrome/browser/shared/model/browser_state", - "//ios/chrome/browser/shared/model/paths", + "//brave/ios/browser/web", "//ios/chrome/browser/shared/model/prefs", - "//ios/chrome/browser/shared/model/profile", - "//ios/chrome/browser/shared/model/url:constants", - "//ios/chrome/browser/web/model:web_internal", - "//ios/components/webui:url_constants", - "//ios/public/provider/chrome/browser/url_rewriters:url_rewriters_api", - "//ios/web/public", - "//ios/web/public/init", - "//ios/web/public/navigation", - "//ios/web/public/thread", - "//ui/base", - "//url", ] } diff --git a/ios/browser/DEPS b/ios/browser/DEPS index 91232cc9a9b0..d9bff1fb063d 100644 --- a/ios/browser/DEPS +++ b/ios/browser/DEPS @@ -9,6 +9,7 @@ include_rules = [ "+ios/web/js_messaging", "+ios/web/web_state", "+ios/web/webui", + "+ios/web_view", "+ios/public/provider/chrome/browser/url_rewriters", "+services/network/public/cpp", ] diff --git a/ios/browser/api/content_settings/BUILD.gn b/ios/browser/api/content_settings/BUILD.gn new file mode 100644 index 000000000000..4f2686a4e133 --- /dev/null +++ b/ios/browser/api/content_settings/BUILD.gn @@ -0,0 +1,16 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +source_set("content_settings") { + sources = [ + "default_host_content_settings.h", + "default_host_content_settings.mm", + "default_host_content_settings_internal.h", + ] + deps = [ + "//base", + "//components/content_settings/core/browser", + ] +} diff --git a/ios/browser/api/content_settings/default_host_content_settings.h b/ios/browser/api/content_settings/default_host_content_settings.h new file mode 100644 index 000000000000..4fb47c3f03a8 --- /dev/null +++ b/ios/browser/api/content_settings/default_host_content_settings.h @@ -0,0 +1,30 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_BROWSER_API_CONTENT_SETTINGS_DEFAULT_HOST_CONTENT_SETTINGS_H_ +#define BRAVE_IOS_BROWSER_API_CONTENT_SETTINGS_DEFAULT_HOST_CONTENT_SETTINGS_H_ + +#import + +NS_ASSUME_NONNULL_BEGIN + +// The mode in which pages should be loaded. +typedef NS_ENUM(NSUInteger, DefaultPageMode) { + DefaultPageModeMobile, + DefaultPageModeDesktop, +}; + +/// A list of default website preferences. +OBJC_EXPORT +@interface DefaultHostContentSettings : NSObject + +/// The default page mode in which pages should be loaded. +@property(nonatomic) DefaultPageMode defaultPageMode; + +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_IOS_BROWSER_API_CONTENT_SETTINGS_DEFAULT_HOST_CONTENT_SETTINGS_H_ diff --git a/ios/browser/api/content_settings/default_host_content_settings.mm b/ios/browser/api/content_settings/default_host_content_settings.mm new file mode 100644 index 000000000000..3e9f775b6254 --- /dev/null +++ b/ios/browser/api/content_settings/default_host_content_settings.mm @@ -0,0 +1,36 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/ios/browser/api/content_settings/default_host_content_settings.h" + +#include "base/memory/scoped_refptr.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" + +@implementation DefaultHostContentSettings { + scoped_refptr _settingsMap; +} + +- (instancetype)initWithSettingsMap:(HostContentSettingsMap*)settingsMap { + if ((self = [super init])) { + _settingsMap = settingsMap; + } + return self; +} + +- (DefaultPageMode)defaultPageMode { + auto setting = _settingsMap->GetDefaultContentSetting( + ContentSettingsType::REQUEST_DESKTOP_SITE, nullptr); + return setting == CONTENT_SETTING_ALLOW ? DefaultPageModeDesktop + : DefaultPageModeMobile; +} + +- (void)setDefaultPageMode:(DefaultPageMode)defaultPageMode { + _settingsMap->SetDefaultContentSetting( + ContentSettingsType::REQUEST_DESKTOP_SITE, + defaultPageMode == DefaultPageModeDesktop ? CONTENT_SETTING_ALLOW + : CONTENT_SETTING_BLOCK); +} + +@end diff --git a/ios/browser/api/content_settings/default_host_content_settings_internal.h b/ios/browser/api/content_settings/default_host_content_settings_internal.h new file mode 100644 index 000000000000..e239d615a2d8 --- /dev/null +++ b/ios/browser/api/content_settings/default_host_content_settings_internal.h @@ -0,0 +1,18 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_BROWSER_API_CONTENT_SETTINGS_DEFAULT_HOST_CONTENT_SETTINGS_INTERNAL_H_ +#define BRAVE_IOS_BROWSER_API_CONTENT_SETTINGS_DEFAULT_HOST_CONTENT_SETTINGS_INTERNAL_H_ + +#include "brave/ios/browser/api/content_settings/default_host_content_settings.h" + +class HostContentSettingsMap; + +@interface DefaultHostContentSettings () +- (instancetype)initWithSettingsMap:(HostContentSettingsMap*)settingsMap + NS_DESIGNATED_INITIALIZER; +@end + +#endif // BRAVE_IOS_BROWSER_API_CONTENT_SETTINGS_DEFAULT_HOST_CONTENT_SETTINGS_INTERNAL_H_ diff --git a/ios/browser/api/https_upgrade_exceptions/headers.gni b/ios/browser/api/content_settings/headers.gni similarity index 63% rename from ios/browser/api/https_upgrade_exceptions/headers.gni rename to ios/browser/api/content_settings/headers.gni index b075c1ae253a..b2eaa46d4976 100644 --- a/ios/browser/api/https_upgrade_exceptions/headers.gni +++ b/ios/browser/api/content_settings/headers.gni @@ -3,4 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at https://mozilla.org/MPL/2.0/. -browser_api_https_upgrade_exceptions_service_public_headers = [ "//brave/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.h" ] +content_settings_public_headers = [ + "//brave/ios/browser/api/content_settings/default_host_content_settings.h", +] diff --git a/ios/browser/api/https_upgrade_exceptions/BUILD.gn b/ios/browser/api/https_upgrade_exceptions/BUILD.gn deleted file mode 100644 index 230408dc90a9..000000000000 --- a/ios/browser/api/https_upgrade_exceptions/BUILD.gn +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2024 The Brave Authors. All rights reserved. -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at https://mozilla.org/MPL/2.0/. - -source_set("https_upgrade_exceptions") { - sources = [ - "https_upgrade_exceptions_service+private.h", - "https_upgrade_exceptions_service.h", - "https_upgrade_exceptions_service.mm", - ] - deps = [ - "//base", - "//brave/components/https_upgrade_exceptions/browser", - "//brave/ios/browser/application_context", - "//ios/chrome/browser/shared/model/application_context", - "//net", - "//url", - ] - frameworks = [ "Foundation.framework" ] -} diff --git a/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service+private.h b/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service+private.h deleted file mode 100644 index e2b33eb35574..000000000000 --- a/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service+private.h +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2024 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at https://mozilla.org/MPL/2.0/. - -#ifndef BRAVE_IOS_BROWSER_API_HTTPS_UPGRADE_EXCEPTIONS_HTTPS_UPGRADE_EXCEPTIONS_SERVICE_PRIVATE_H_ -#define BRAVE_IOS_BROWSER_API_HTTPS_UPGRADE_EXCEPTIONS_HTTPS_UPGRADE_EXCEPTIONS_SERVICE_PRIVATE_H_ - -#include "brave/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.h" - -@interface HTTPSUpgradeExceptionsService (Private) -@end - -#endif // BRAVE_IOS_BROWSER_API_HTTPS_UPGRADE_EXCEPTIONS_HTTPS_UPGRADE_EXCEPTIONS_SERVICE_PRIVATE_H_ diff --git a/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.h b/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.h deleted file mode 100644 index 9504897cc558..000000000000 --- a/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2024 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at https://mozilla.org/MPL/2.0/. - -#ifndef BRAVE_IOS_BROWSER_API_HTTPS_UPGRADE_EXCEPTIONS_HTTPS_UPGRADE_EXCEPTIONS_SERVICE_H_ -#define BRAVE_IOS_BROWSER_API_HTTPS_UPGRADE_EXCEPTIONS_HTTPS_UPGRADE_EXCEPTIONS_SERVICE_H_ - -#import - -NS_ASSUME_NONNULL_BEGIN - -OBJC_EXPORT -@interface HTTPSUpgradeExceptionsService : NSObject -/// Tells us if the "HTTPS by default" feature is enabled -@property(readonly) bool isHttpsByDefaultFeatureEnabled; - -- (instancetype)init; -/// This returns if a url can be upgraded to HTTPS -- (bool)canUpgradeToHTTPSForURL:(NSURL*)url; -@end - -NS_ASSUME_NONNULL_END - -#endif // BRAVE_IOS_BROWSER_API_HTTPS_UPGRADE_EXCEPTIONS_HTTPS_UPGRADE_EXCEPTIONS_SERVICE_H_ diff --git a/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.mm b/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.mm deleted file mode 100644 index f486314e1e51..000000000000 --- a/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service.mm +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2024 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at https://mozilla.org/MPL/2.0/. - -#include "brave/ios/browser/api/https_upgrade_exceptions/https_upgrade_exceptions_service+private.h" - -#include "base/containers/contains.h" -#include "base/feature_list.h" -#include "base/memory/raw_ptr.h" -#include "brave/components/https_upgrade_exceptions/browser/https_upgrade_exceptions_service.h" -#include "brave/ios/browser/application_context/brave_application_context_impl.h" -#include "ios/chrome/browser/shared/model/application_context/application_context.h" -#include "net/base/apple/url_conversions.h" -#include "net/base/features.h" -#include "url/gurl.h" -#include "url/origin.h" - -@interface HTTPSUpgradeExceptionsService () { -} - -@property(nonatomic) bool isHttpsByDefaultFeatureEnabled; -@end - -@implementation HTTPSUpgradeExceptionsService -- (instancetype)init { - if ((self = [super init])) { - } - return self; -} - -- (bool)isHttpsByDefaultFeatureEnabled { - return base::FeatureList::IsEnabled(net::features::kBraveHttpsByDefault); -} - -- (bool)canUpgradeToHTTPSForURL:(NSURL*)url { - GURL gurl = net::GURLWithNSURL(url); - if (![self isHttpsByDefaultFeatureEnabled]) { - return false; - } - - // Check url validity - if (!gurl.SchemeIs("http") || !gurl.is_valid()) { - return false; - } - - BraveApplicationContextImpl* braveContext = - static_cast(GetApplicationContext()); - return braveContext->https_upgrade_exceptions_service()->CanUpgradeToHTTPS( - gurl); -} -@end diff --git a/ios/browser/api/prefs/BUILD.gn b/ios/browser/api/prefs/BUILD.gn new file mode 100644 index 000000000000..2f3af052bea8 --- /dev/null +++ b/ios/browser/api/prefs/BUILD.gn @@ -0,0 +1,17 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +source_set("prefs") { + sources = [ + "browser_prefs+private.h", + "browser_prefs.h", + "browser_prefs.mm", + ] + deps = [ + "//base", + "//components/prefs", + "//ios/chrome/browser/shared/model/prefs:pref_names", + ] +} diff --git a/ios/browser/api/prefs/browser_prefs+private.h b/ios/browser/api/prefs/browser_prefs+private.h new file mode 100644 index 000000000000..875820730c77 --- /dev/null +++ b/ios/browser/api/prefs/browser_prefs+private.h @@ -0,0 +1,16 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_BROWSER_API_PREFS_BROWSER_PREFS_PRIVATE_H_ +#define BRAVE_IOS_BROWSER_API_PREFS_BROWSER_PREFS_PRIVATE_H_ + +#include "brave/ios/browser/api/prefs/browser_prefs.h" +#include "components/prefs/pref_service.h" + +@interface BrowserPrefs () +- (instancetype)initWithPrefService:(PrefService*)prefs; +@end + +#endif // BRAVE_IOS_BROWSER_API_PREFS_BROWSER_PREFS_PRIVATE_H_ diff --git a/ios/browser/api/prefs/browser_prefs.h b/ios/browser/api/prefs/browser_prefs.h new file mode 100644 index 000000000000..26bd38109bf5 --- /dev/null +++ b/ios/browser/api/prefs/browser_prefs.h @@ -0,0 +1,20 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_BROWSER_API_PREFS_BROWSER_PREFS_H_ +#define BRAVE_IOS_BROWSER_API_PREFS_BROWSER_PREFS_H_ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BrowserPrefs : NSObject +@property(nonatomic) BOOL httpsUpgradesEnabled; +@property(nonatomic) BOOL httpsOnlyModeEnabled; +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_IOS_BROWSER_API_PREFS_BROWSER_PREFS_H_ diff --git a/ios/browser/api/prefs/browser_prefs.mm b/ios/browser/api/prefs/browser_prefs.mm new file mode 100644 index 000000000000..a2c6f861716e --- /dev/null +++ b/ios/browser/api/prefs/browser_prefs.mm @@ -0,0 +1,38 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "base/memory/raw_ptr.h" +#include "brave/ios/browser/api/prefs/browser_prefs+private.h" +#include "components/prefs/pref_service.h" +#include "ios/chrome/browser/shared/model/prefs/pref_names.h" + +@implementation BrowserPrefs { + raw_ptr _prefs; +} + +- (instancetype)initWithPrefService:(PrefService*)prefs { + if ((self = [super init])) { + _prefs = prefs; + } + return self; +} + +- (BOOL)httpsUpgradesEnabled { + return _prefs->GetBoolean(prefs::kHttpsUpgradesEnabled); +} + +- (void)setHttpsUpgradesEnabled:(BOOL)httpsUpgradesEnabled { + _prefs->SetBoolean(prefs::kHttpsUpgradesEnabled, httpsUpgradesEnabled); +} + +- (BOOL)httpsOnlyModeEnabled { + return _prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled); +} + +- (void)setHttpsOnlyModeEnabled:(BOOL)httpsOnlyModeEnabled { + _prefs->SetBoolean(prefs::kHttpsOnlyModeEnabled, httpsOnlyModeEnabled); +} + +@end diff --git a/ios/browser/api/prefs/headers.gni b/ios/browser/api/prefs/headers.gni new file mode 100644 index 000000000000..dfa5c719f558 --- /dev/null +++ b/ios/browser/api/prefs/headers.gni @@ -0,0 +1,6 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +prefs_public_headers = [ "//brave/ios/browser/api/prefs/browser_prefs.h" ] diff --git a/ios/browser/api/url/url_formatter.mm b/ios/browser/api/url/url_formatter.mm index 5bc4097ee90d..04314c350ef0 100644 --- a/ios/browser/api/url/url_formatter.mm +++ b/ios/browser/api/url/url_formatter.mm @@ -31,6 +31,26 @@ // MARK: - Implementation +namespace { +constexpr char16_t kChromeSchema16[] = u"chrome://"; +constexpr char16_t kBraveSchema16[] = u"brave://"; +} // namespace + +namespace brave_utils { + +bool ReplaceChromeToBraveScheme(std::u16string& url_string) { + if (base::StartsWith(url_string, kChromeSchema16, + base::CompareCase::INSENSITIVE_ASCII)) { + base::ReplaceFirstSubstringAfterOffset(&url_string, 0, kChromeSchema16, + kBraveSchema16); + return true; + } + + return false; +} + +} // namespace brave_utils + @implementation BraveURLFormatter + (NSString*)formatURLOriginForSecurityDisplay:(NSString*)origin schemeDisplay: @@ -38,6 +58,7 @@ + (NSString*)formatURLOriginForSecurityDisplay:(NSString*)origin std::u16string result = url_formatter::FormatUrlForSecurityDisplay( GURL(base::SysNSStringToUTF8(origin)), static_cast(schemeDisplay)); + brave_utils::ReplaceChromeToBraveScheme(result); return base::SysUTF16ToNSString(result) ?: @""; } @@ -46,6 +67,7 @@ + (NSString*)formatURLOriginForDisplayOmitSchemePathAndTrivialSubdomains: std::u16string result = url_formatter::FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains( GURL(base::SysNSStringToUTF8(origin))); + brave_utils::ReplaceChromeToBraveScheme(result); return base::SysUTF16ToNSString(result) ?: @""; } @@ -57,6 +79,7 @@ + (NSString*)formatURL:(NSString*)url static_cast(formatTypes), static_cast(unescapeOptions), nullptr, nullptr, nullptr); + brave_utils::ReplaceChromeToBraveScheme(result); return base::SysUTF16ToNSString(result) ?: @""; } @end diff --git a/ios/browser/brave_web_client.mm b/ios/browser/brave_web_client.mm deleted file mode 100644 index fa0e605e9ca0..000000000000 --- a/ios/browser/brave_web_client.mm +++ /dev/null @@ -1,73 +0,0 @@ -/* Copyright (c) 2020 The Brave Authors. All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#import "brave/ios/browser/brave_web_client.h" - -#include "base/functional/bind.h" -#include "brave/components/constants/url_constants.h" -#include "brave/ios/browser/brave_web_main_parts.h" -#include "ios/chrome/browser/shared/model/url/chrome_url_constants.h" -#include "ios/components/webui/web_ui_url_constants.h" -#import "ios/public/provider/chrome/browser/url_rewriters/url_rewriters_api.h" -#import "ios/web/public/navigation/browser_url_rewriter.h" -#include "url/gurl.h" - -#if !defined(__has_feature) || !__has_feature(objc_arc) -#error "This file requires ARC support." -#endif - -BraveWebClient::BraveWebClient() {} - -BraveWebClient::~BraveWebClient() { -} - -std::unique_ptr BraveWebClient::CreateWebMainParts() { - return std::make_unique( - *base::CommandLine::ForCurrentProcess()); -} - -void BraveWebClient::SetUserAgent(const std::string& user_agent) { - user_agent_ = user_agent; -} - -std::string BraveWebClient::GetUserAgent(web::UserAgentType type) const { - if (user_agent_.empty()) { - return ChromeWebClient::GetUserAgent(type); - } - return user_agent_; -} - -void BraveWebClient::AddAdditionalSchemes(Schemes* schemes) const { - ChromeWebClient::AddAdditionalSchemes(schemes); - - schemes->standard_schemes.push_back(kBraveUIScheme); - schemes->secure_schemes.push_back(kBraveUIScheme); -} - -bool BraveWebClient::IsAppSpecificURL(const GURL& url) const { - return ChromeWebClient::IsAppSpecificURL(url) || url.SchemeIs(kBraveUIScheme); -} - -bool WillHandleBraveURLRedirect(GURL* url, web::BrowserState* browser_state) { - if (url->SchemeIs(kBraveUIScheme)) { - GURL::Replacements replacements; - replacements.SetSchemeStr(kChromeUIScheme); - *url = url->ReplaceComponents(replacements); - } - return false; -} - -std::vector BraveWebClient::GetJavaScriptFeatures( - web::BrowserState* browser_state) const { - // We don't use Chromium web views for anything but WebUI at the moment, so - // we don't need any JS features added. - return {}; -} - -void BraveWebClient::PostBrowserURLRewriterCreation( - web::BrowserURLRewriter* rewriter) { - rewriter->AddURLRewriter(&WillHandleBraveURLRedirect); - ChromeWebClient::PostBrowserURLRewriterCreation(rewriter); -} diff --git a/ios/browser/web/BUILD.gn b/ios/browser/web/BUILD.gn new file mode 100644 index 000000000000..0fca6ce23107 --- /dev/null +++ b/ios/browser/web/BUILD.gn @@ -0,0 +1,41 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +source_set("web") { + sources = [ + "brave_web_client.h", + "brave_web_client.mm", + "brave_web_main_parts.h", + "brave_web_main_parts.mm", + "javascript_block_util.mm", + "universal_link_block_util.mm", + "web_view_tab_helper_util.mm", + ] + deps = [ + "//base", + "//brave/components/ai_chat/core/browser", + "//brave/components/brave_component_updater/browser", + "//brave/components/brave_wallet/browser", + "//brave/components/constants", + "//brave/ios/browser/application_context", + "//components/component_updater/installer_policies", + "//components/translate/ios/browser", + "//ios/chrome/browser/application_context/model", + "//ios/chrome/browser/providers/raccoon:chromium_raccoon", + "//ios/chrome/browser/shared/model/application_context", + "//ios/chrome/browser/shared/model/paths", + "//ios/chrome/browser/shared/model/url", + "//ios/chrome/browser/shared/model/url:constants", + "//ios/chrome/browser/tabs/model", + "//ios/chrome/browser/web/model:web_internal", + "//ios/components/security_interstitials", + "//ios/components/security_interstitials/lookalikes", + "//ios/components/security_interstitials/safe_browsing", + "//ios/components/webui:url_constants", + "//ios/public/provider/chrome/browser/url_rewriters:url_rewriters_api", + "//ios/web_view:web_view_sources", + ] + frameworks = [ "Foundation.framework" ] +} diff --git a/ios/browser/brave_web_client.h b/ios/browser/web/brave_web_client.h similarity index 56% rename from ios/browser/brave_web_client.h rename to ios/browser/web/brave_web_client.h index f71eb62626af..28f071a482cc 100644 --- a/ios/browser/brave_web_client.h +++ b/ios/browser/web/brave_web_client.h @@ -1,10 +1,10 @@ -/* Copyright (c) 2020 The Brave Authors. All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. -#ifndef BRAVE_IOS_BROWSER_BRAVE_WEB_CLIENT_H_ -#define BRAVE_IOS_BROWSER_BRAVE_WEB_CLIENT_H_ +#ifndef BRAVE_IOS_BROWSER_WEB_BRAVE_WEB_CLIENT_H_ +#define BRAVE_IOS_BROWSER_WEB_BRAVE_WEB_CLIENT_H_ #include #include @@ -19,24 +19,23 @@ class BraveWebClient : public ChromeWebClient { BraveWebClient& operator=(const BraveWebClient&) = delete; ~BraveWebClient() override; - void SetUserAgent(const std::string& user_agent); - // WebClient implementation. std::unique_ptr CreateWebMainParts() override; std::string GetUserAgent(web::UserAgentType type) const override; + web::UserAgentType GetDefaultUserAgent(web::WebState* web_state, + const GURL& url) const override; void AddAdditionalSchemes(Schemes* schemes) const override; - bool IsAppSpecificURL(const GURL& url) const override; std::vector GetJavaScriptFeatures( web::BrowserState* browser_state) const override; + bool EnableLongPressUIContextMenu() const override; + bool EnableWebInspector(web::BrowserState* browser_state) const override; + void PostBrowserURLRewriterCreation( web::BrowserURLRewriter* rewriter) override; - - private: - std::string user_agent_; }; -#endif // BRAVE_IOS_BROWSER_BRAVE_WEB_CLIENT_H_ +#endif // BRAVE_IOS_BROWSER_WEB_BRAVE_WEB_CLIENT_H_ diff --git a/ios/browser/web/brave_web_client.mm b/ios/browser/web/brave_web_client.mm new file mode 100644 index 000000000000..f0bdf4c00736 --- /dev/null +++ b/ios/browser/web/brave_web_client.mm @@ -0,0 +1,100 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#import "brave/ios/browser/web/brave_web_client.h" + +#include "base/check.h" +#include "base/functional/bind.h" +#include "base/ios/ns_error_util.h" +#include "base/strings/sys_string_conversions.h" +#include "brave/components/constants/url_constants.h" +#include "brave/ios/browser/web/brave_web_main_parts.h" +#import "components/translate/ios/browser/translate_java_script_feature.h" +#include "ios/chrome/browser/shared/model/url/chrome_url_constants.h" +#include "ios/chrome/browser/web/model/chrome_web_client.h" +#import "ios/components/security_interstitials/ios_security_interstitial_java_script_feature.h" +#import "ios/components/security_interstitials/lookalikes/lookalike_url_error.h" +#import "ios/components/security_interstitials/safe_browsing/safe_browsing_error.h" +#include "ios/components/webui/web_ui_url_constants.h" +#import "ios/public/provider/chrome/browser/url_rewriters/url_rewriters_api.h" +#import "ios/web/public/navigation/browser_url_rewriter.h" +#import "ios/web_view/internal/cwv_ssl_error_handler_internal.h" +#import "ios/web_view/internal/cwv_web_view_internal.h" +#import "ios/web_view/public/cwv_navigation_delegate.h" +#import "net/base/apple/url_conversions.h" +#include "url/gurl.h" + +BraveWebClient::BraveWebClient() {} + +BraveWebClient::~BraveWebClient() {} + +std::unique_ptr BraveWebClient::CreateWebMainParts() { + return std::make_unique( + *base::CommandLine::ForCurrentProcess()); +} + +std::string BraveWebClient::GetUserAgent(web::UserAgentType type) const { + // FIXME: Decide on whether or not to adjust the product type (defaults to + // CriOS/) + return ChromeWebClient::GetUserAgent(type); +} + +web::UserAgentType BraveWebClient::GetDefaultUserAgent(web::WebState* web_state, + const GURL& url) const { + // FIXME: Add a way to force desktop mode always via prefs + // FIXME: Possibly handle desktop by default on iPad here? + return ChromeWebClient::GetDefaultUserAgent(web_state, url); +} + +void BraveWebClient::AddAdditionalSchemes(Schemes* schemes) const { + ChromeWebClient::AddAdditionalSchemes(schemes); + + schemes->standard_schemes.push_back(kBraveUIScheme); + schemes->secure_schemes.push_back(kBraveUIScheme); +} + +bool BraveWebClient::IsAppSpecificURL(const GURL& url) const { + // temporarily add `internal://` scheme handling until those pages can be + // ported to WebUI + return ChromeWebClient::IsAppSpecificURL(url) || + url.SchemeIs(kBraveUIScheme) || url.SchemeIs("internal"); +} + +bool WillHandleBraveURLRedirect(GURL* url, web::BrowserState* browser_state) { + if (url->SchemeIs(kBraveUIScheme)) { + GURL::Replacements replacements; + replacements.SetSchemeStr(kChromeUIScheme); + *url = url->ReplaceComponents(replacements); + } + return false; +} + +std::vector BraveWebClient::GetJavaScriptFeatures( + web::BrowserState* browser_state) const { + // Disable majority of ChromeWebClient JS features + std::vector features; + // FIXME: Add any JavaScriptFeature's from Chromium as needed + features.push_back( + security_interstitials::IOSSecurityInterstitialJavaScriptFeature:: + GetInstance()); + features.push_back(translate::TranslateJavaScriptFeature::GetInstance()); + return features; +} + +void BraveWebClient::PostBrowserURLRewriterCreation( + web::BrowserURLRewriter* rewriter) { + rewriter->AddURLRewriter(&WillHandleBraveURLRedirect); + ChromeWebClient::PostBrowserURLRewriterCreation(rewriter); +} + +bool BraveWebClient::EnableLongPressUIContextMenu() const { + return CWVWebView.chromeContextMenuEnabled; +} + +bool BraveWebClient::EnableWebInspector( + web::BrowserState* browser_state) const { + // FIXME: Probably better to just use prefs::kWebInspectorEnabled + return CWVWebView.webInspectorEnabled; +} diff --git a/ios/browser/brave_web_main_parts.h b/ios/browser/web/brave_web_main_parts.h similarity index 55% rename from ios/browser/brave_web_main_parts.h rename to ios/browser/web/brave_web_main_parts.h index 75f134f7dcf2..df1278811903 100644 --- a/ios/browser/brave_web_main_parts.h +++ b/ios/browser/web/brave_web_main_parts.h @@ -1,10 +1,10 @@ -/* Copyright (c) 2019 The Brave Authors. All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ +// Copyright (c) 2019 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. -#ifndef BRAVE_IOS_BROWSER_BRAVE_WEB_MAIN_PARTS_H_ -#define BRAVE_IOS_BROWSER_BRAVE_WEB_MAIN_PARTS_H_ +#ifndef BRAVE_IOS_BROWSER_WEB_BRAVE_WEB_MAIN_PARTS_H_ +#define BRAVE_IOS_BROWSER_WEB_BRAVE_WEB_MAIN_PARTS_H_ #include "ios/chrome/browser/web/model/chrome_main_parts.h" @@ -21,4 +21,4 @@ class BraveWebMainParts : public IOSChromeMainParts { void PreMainMessageLoopRun() override; }; -#endif // BRAVE_IOS_BROWSER_BRAVE_WEB_MAIN_PARTS_H_ +#endif // BRAVE_IOS_BROWSER_WEB_BRAVE_WEB_MAIN_PARTS_H_ diff --git a/ios/browser/brave_web_main_parts.mm b/ios/browser/web/brave_web_main_parts.mm similarity index 61% rename from ios/browser/brave_web_main_parts.mm rename to ios/browser/web/brave_web_main_parts.mm index 25d36396b643..6c11c0037a3e 100644 --- a/ios/browser/brave_web_main_parts.mm +++ b/ios/browser/web/brave_web_main_parts.mm @@ -1,11 +1,11 @@ -/* Copyright (c) 2020 The Brave Authors. All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ +// Copyright (c) 2019 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. -#include "brave/ios/browser/brave_web_main_parts.h" +#include "brave/ios/browser/web/brave_web_main_parts.h" -#include "base/metrics/user_metrics.h" +#include "base/command_line.h" #include "base/path_service.h" #include "base/strings/sys_string_conversions.h" #include "base/task/sequenced_task_runner.h" @@ -14,30 +14,12 @@ #include "brave/components/brave_component_updater/browser/brave_on_demand_updater.h" #include "brave/components/brave_wallet/browser/wallet_data_files_installer.h" #include "brave/ios/browser/application_context/brave_application_context_impl.h" -#include "brave/ios/browser/profile/model/brave_keyed_service_factories.h" #include "components/component_updater/installer_policies/safety_tips_component_installer.h" -#include "components/flags_ui/pref_service_flags_storage.h" -#include "components/metrics/metrics_service.h" -#include "components/metrics_services_manager/metrics_services_manager.h" -#include "components/variations/service/variations_service.h" -#include "components/variations/synthetic_trial_registry.h" -#include "components/variations/synthetic_trials_active_group_id_provider.h" -#include "components/variations/variations_ids_provider.h" -#include "components/variations/variations_switches.h" #include "ios/chrome/browser/application_context/model/application_context_impl.h" -#include "ios/chrome/browser/flags/about_flags.h" -#include "ios/chrome/browser/profile/model/keyed_service_factories.h" #include "ios/chrome/browser/shared/model/paths/paths.h" -#include "ios/chrome/browser/shared/model/profile/profile_manager_ios.h" -#include "ios/web/public/thread/web_task_traits.h" -#include "ios/web/public/thread/web_thread.h" #include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/resource/resource_bundle.h" -#if !defined(__has_feature) || !__has_feature(objc_arc) -#error "This file requires ARC support." -#endif - namespace { void RegisterComponentsForUpdate( component_updater::ComponentUpdateService* cus) { @@ -59,8 +41,7 @@ void RegisterComponentsForUpdate( IOSChromeMainParts::PreCreateMainMessageLoop(); // Add Brave Resource Pack - base::FilePath brave_pack_path; - base::PathService::Get(base::DIR_ASSETS, &brave_pack_path); + auto brave_pack_path = base::PathService::CheckedGet(base::DIR_ASSETS); brave_pack_path = brave_pack_path.AppendASCII("brave_resources.pak"); ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath( brave_pack_path, ui::kScaleFactorNone); diff --git a/ios/browser/web/javascript_block_util.mm b/ios/browser/web/javascript_block_util.mm new file mode 100644 index 000000000000..31e8aa6609f7 --- /dev/null +++ b/ios/browser/web/javascript_block_util.mm @@ -0,0 +1,32 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#import + +#include "ios/chrome/browser/shared/model/application_context/application_context.h" +#include "ios/web/public/web_state.h" +#import "ios/web_view/internal/cwv_web_view_internal.h" +#import "ios/web_view/public/cwv_navigation_delegate.h" + +namespace brave { + +void ShouldBlockJavaScript(web::WebState* webState, + NSURLRequest* request, + WKWebpagePreferences* preferences) { + CWVWebView* webView = [CWVWebView webViewForWebState:webState]; + id navigationDelegate = webView.navigationDelegate; + + if ([navigationDelegate respondsToSelector:@selector + (webView:shouldBlockJavaScriptForRequest:)]) { + BOOL shouldBlockJavaScript = [navigationDelegate webView:webView + shouldBlockJavaScriptForRequest:request]; + if (shouldBlockJavaScript && preferences) { + // Only ever update it to false + preferences.allowsContentJavaScript = false; + } + } +} + +} // namespace brave diff --git a/ios/browser/web/universal_link_block_util.mm b/ios/browser/web/universal_link_block_util.mm new file mode 100644 index 000000000000..b9417235ff3e --- /dev/null +++ b/ios/browser/web/universal_link_block_util.mm @@ -0,0 +1,30 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/chrome/browser/shared/model/application_context/application_context.h" +#include "ios/web/public/web_state.h" +#import "ios/web_view/internal/cwv_web_view_internal.h" +#import "ios/web_view/public/cwv_navigation_delegate.h" + +namespace brave { + +void ShouldBlockUniversalLinks(web::WebState* webState, + NSURLRequest* request, + bool* forceBlockUniversalLinks) { + CWVWebView* webView = [CWVWebView webViewForWebState:webState]; + id navigationDelegate = webView.navigationDelegate; + + if ([navigationDelegate respondsToSelector:@selector + (webView:shouldBlockUniversalLinksForRequest:)]) { + BOOL shouldBlockUniversalLinks = [navigationDelegate webView:webView + shouldBlockUniversalLinksForRequest:request]; + if (forceBlockUniversalLinks && shouldBlockUniversalLinks) { + // Only ever update it to true + *forceBlockUniversalLinks = shouldBlockUniversalLinks; + } + } +} + +} // namespace brave diff --git a/ios/browser/web/web_view_tab_helper_util.mm b/ios/browser/web/web_view_tab_helper_util.mm new file mode 100644 index 000000000000..58a9e804d7d6 --- /dev/null +++ b/ios/browser/web/web_view_tab_helper_util.mm @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/chrome/browser/tabs/model/tab_helper_util.h" + +namespace ios_web_view { + +void AttachTabHelpers(web::WebState* web_state) { + AttachTabHelpers(web_state, TabHelperFilter::kEmpty); +} + +} // namespace ios_web_view diff --git a/ios/web_view/BUILD.gn b/ios/web_view/BUILD.gn new file mode 100644 index 000000000000..75e781c0d714 --- /dev/null +++ b/ios/web_view/BUILD.gn @@ -0,0 +1,51 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//brave/ios/web_view/headers.gni") + +config("config") { + # TODO(crbug.com/40120082): This will only guarantee ios/web_view source files + # are extension safe. We also need to pass this flag to all all dependencies. + common_flags = [ "-fapplication-extension" ] + cflags_objc = common_flags + cflags_objcc = common_flags + defines = [ "CWV_IMPLEMENTATION" ] + frameworks = [ + "CoreGraphics.framework", + "Foundation.framework", + "MobileCoreServices.framework", + "UIKit.framework", + ] + + # For our own public headers to be able to import the official public headers + include_dirs = [ "//ios/web_view/public" ] +} + +source_set("web_view") { + # sources = ios_web_view_public_headers + sources = [ + "internal/cwv_ssl_status_extras.mm", + "internal/cwv_web_view_extras.mm", + "internal/cwv_x509_certificate_extras.mm", + "internal/web_view_global_state_util_stub.mm", + "public/cwv_ssl_status_extras.h", + "public/cwv_web_view_extras.h", + "public/cwv_x509_certificate_extras.h", + ] + deps = [ + "//base", + "//components/ssl_errors", + "//ios/web/js_messaging", + "//ios/web/js_messaging:java_script_feature_manager_header", + "//ios/web/js_messaging:web_view_js_utils", + "//ios/web/web_state", + "//ios/web/web_state:web_state_impl_header", + "//ios/web/web_state/ui", + "//ios/web_view:web_view_sources", + "//net", + "//url", + ] + configs += [ ":config" ] +} diff --git a/ios/web_view/DEPS b/ios/web_view/DEPS new file mode 100644 index 000000000000..74d72de28cdb --- /dev/null +++ b/ios/web_view/DEPS @@ -0,0 +1,5 @@ +include_rules = [ + "+components/ssl_errors", + "+ios/web", + "+ios/web_view", +] diff --git a/ios/web_view/headers.gni b/ios/web_view/headers.gni new file mode 100644 index 000000000000..96ef3aaba54a --- /dev/null +++ b/ios/web_view/headers.gni @@ -0,0 +1,69 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +# This file exposes the //ios/web_view embedder as a regular source_set as well +# as any additions found in //brave/ios/web_view/public + +ios_web_view_public_headers = [ + "//brave/ios/web_view/public/cwv_ssl_status_extras.h", + "//brave/ios/web_view/public/cwv_web_view_extras.h", + "//brave/ios/web_view/public/cwv_x509_certificate_extras.h", + + "//ios/web_view/public/cwv_autofill_controller.h", + "//ios/web_view/public/cwv_autofill_controller_delegate.h", + "//ios/web_view/public/cwv_autofill_data_manager.h", + "//ios/web_view/public/cwv_autofill_data_manager_observer.h", + "//ios/web_view/public/cwv_autofill_form.h", + "//ios/web_view/public/cwv_autofill_profile.h", + "//ios/web_view/public/cwv_autofill_suggestion.h", + "//ios/web_view/public/cwv_back_forward_list.h", + "//ios/web_view/public/cwv_back_forward_list_item.h", + "//ios/web_view/public/cwv_cert_status.h", + "//ios/web_view/public/cwv_credential_provider_extension_utils.h", + "//ios/web_view/public/cwv_credit_card.h", + "//ios/web_view/public/cwv_credit_card_saver.h", + "//ios/web_view/public/cwv_credit_card_verifier.h", + "//ios/web_view/public/cwv_defines.h", + "//ios/web_view/public/cwv_download_task.h", + "//ios/web_view/public/cwv_export.h", + "//ios/web_view/public/cwv_favicon.h", + "//ios/web_view/public/cwv_find_in_page_controller.h", + "//ios/web_view/public/cwv_html_element.h", + "//ios/web_view/public/cwv_identity.h", + "//ios/web_view/public/cwv_leak_check_credential.h", + "//ios/web_view/public/cwv_leak_check_service.h", + "//ios/web_view/public/cwv_leak_check_service_observer.h", + "//ios/web_view/public/cwv_lookalike_url_handler.h", + "//ios/web_view/public/cwv_navigation_action.h", + "//ios/web_view/public/cwv_navigation_delegate.h", + "//ios/web_view/public/cwv_navigation_response.h", + "//ios/web_view/public/cwv_navigation_type.h", + "//ios/web_view/public/cwv_password.h", + "//ios/web_view/public/cwv_preferences.h", + "//ios/web_view/public/cwv_preview_element_info.h", + "//ios/web_view/public/cwv_reuse_check_service.h", + "//ios/web_view/public/cwv_ssl_error_handler.h", + "//ios/web_view/public/cwv_ssl_status.h", + "//ios/web_view/public/cwv_suggestion_type.h", + "//ios/web_view/public/cwv_sync_controller.h", + "//ios/web_view/public/cwv_sync_controller_data_source.h", + "//ios/web_view/public/cwv_sync_controller_delegate.h", + "//ios/web_view/public/cwv_sync_errors.h", + "//ios/web_view/public/cwv_translation_controller.h", + "//ios/web_view/public/cwv_translation_controller_delegate.h", + "//ios/web_view/public/cwv_translation_language.h", + "//ios/web_view/public/cwv_translation_policy.h", + "//ios/web_view/public/cwv_trusted_vault_observer.h", + "//ios/web_view/public/cwv_trusted_vault_provider.h", + "//ios/web_view/public/cwv_trusted_vault_utils.h", + "//ios/web_view/public/cwv_ui_delegate.h", + "//ios/web_view/public/cwv_unsafe_url_handler.h", + "//ios/web_view/public/cwv_user_content_controller.h", + "//ios/web_view/public/cwv_user_script.h", + "//ios/web_view/public/cwv_weak_check_utils.h", + "//ios/web_view/public/cwv_web_view.h", + "//ios/web_view/public/cwv_web_view_configuration.h", + "//ios/web_view/public/cwv_x509_certificate.h", +] diff --git a/ios/web_view/internal/cwv_ssl_status_extras.mm b/ios/web_view/internal/cwv_ssl_status_extras.mm new file mode 100644 index 000000000000..17bc2ba3d90c --- /dev/null +++ b/ios/web_view/internal/cwv_ssl_status_extras.mm @@ -0,0 +1,55 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/ios/web_view/public/cwv_ssl_status_extras.h" + +#include + +#include "base/strings/sys_string_conversions.h" +#include "components/ssl_errors/error_info.h" +#include "ios/web_view/internal/cwv_ssl_status_internal.h" +#include "net/base/apple/url_conversions.h" +#include "net/cert/cert_status_flags.h" +#include "url/gurl.h" + +@interface CWVSSLErrorInformation () +@property(nonatomic, copy) NSString* details; +@property(nonatomic, copy) NSString* shortDescription; +@end + +@implementation CWVSSLErrorInformation + +- (instancetype)initWithErrorInfo:(ssl_errors::ErrorInfo)errorInfo { + if ((self = [super init])) { + self.details = base::SysUTF16ToNSString(errorInfo.details()); + self.shortDescription = + base::SysUTF16ToNSString(errorInfo.short_description()); + } + return self; +} + +@end + +@implementation CWVSSLStatus (Extras) + +- (BOOL)isCertStatusError { + return net::IsCertStatusError(self.internalStatus.cert_status); +} + +- (NSArray*)certStatusErrorsForURL:(NSURL*)url { + std::vector errors; + ssl_errors::ErrorInfo::GetErrorsForCertStatus( + self.internalStatus.certificate, self.internalStatus.cert_status, + net::GURLWithNSURL(url), &errors); + NSMutableArray* bridgedErrors = + [[NSMutableArray alloc] init]; + for (size_t i = 0; i < errors.size(); ++i) { + [bridgedErrors + addObject:[[CWVSSLErrorInformation alloc] initWithErrorInfo:errors[i]]]; + } + return [bridgedErrors copy]; +} + +@end diff --git a/ios/web_view/internal/cwv_web_view_extras.mm b/ios/web_view/internal/cwv_web_view_extras.mm new file mode 100644 index 000000000000..2c52423f5f45 --- /dev/null +++ b/ios/web_view/internal/cwv_web_view_extras.mm @@ -0,0 +1,187 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/ios/web_view/public/cwv_web_view_extras.h" + +#include "base/apple/foundation_util.h" +#include "base/json/json_writer.h" +#include "base/strings/sys_string_conversions.h" +#include "ios/web/common/user_agent.h" +#include "ios/web/js_messaging/java_script_feature_manager.h" +#include "ios/web/js_messaging/web_frame_internal.h" +#include "ios/web/js_messaging/web_view_js_utils.h" +#include "ios/web/public/js_messaging/web_frames_manager.h" +#include "ios/web/web_state/ui/crw_web_controller.h" +#include "ios/web/web_state/ui/wk_web_view_configuration_provider.h" +#include "ios/web/web_state/web_state_impl.h" +#include "ios/web_view/internal/cwv_web_view_configuration_internal.h" +#include "ios/web_view/internal/cwv_web_view_internal.h" +#include "ios/web_view/internal/web_view_browser_state.h" +#include "ios/web_view/public/cwv_navigation_delegate.h" + +const CWVUserAgentType CWVUserAgentTypeNone = + static_cast(web::UserAgentType::NONE); +const CWVUserAgentType CWVUserAgentTypeAutomatic = + static_cast(web::UserAgentType::AUTOMATIC); +const CWVUserAgentType CWVUserAgentTypeMobile = + static_cast(web::UserAgentType::MOBILE); +const CWVUserAgentType CWVUserAgentTypeDesktop = + static_cast(web::UserAgentType::DESKTOP); + +@implementation CWVWebView (Extras) + ++ (BOOL)isRestoreDataValid:(NSData*)data { + return [self _isRestoreDataValid:data]; +} + +// Converts base::Value expected to be a dictionary or list to NSDictionary or +// NSArray, respectively. +id NSObjectFromCollectionValue(const base::Value* value) { + DCHECK(value->is_dict() || value->is_list()) + << "Incorrect value type: " << value->type(); + + std::string json; + const bool success = base::JSONWriter::Write(*value, &json); + DCHECK(success) << "Failed to convert base::Value to JSON"; + + NSData* json_data = [NSData dataWithBytes:json.c_str() length:json.length()]; + id ns_object = [NSJSONSerialization JSONObjectWithData:json_data + options:kNilOptions + error:nil]; + DCHECK(ns_object) << "Failed to convert JSON to Collection"; + return ns_object; +} + +// Converts base::Value to an appropriate Obj-C object. +// |value| must not be null. +id NSObjectFromValue(const base::Value* value) { + switch (value->type()) { + case base::Value::Type::NONE: + return nil; + case base::Value::Type::BOOLEAN: + return @(value->GetBool()); + case base::Value::Type::INTEGER: + return @(value->GetInt()); + case base::Value::Type::DOUBLE: + return @(value->GetDouble()); + case base::Value::Type::STRING: + return base::SysUTF8ToNSString(value->GetString()); + case base::Value::Type::BINARY: + // Unsupported. + return nil; + case base::Value::Type::DICT: + case base::Value::Type::LIST: + return NSObjectFromCollectionValue(value); + } + return nil; +} + +- (void)updateScripts { + // This runs `UpdateScripts` on the configuration provider which we will need + // to call in-place of `-[WKUserContentController removeAllUserScripts]` until + // all Brave JavaScript features are ported over to actual Chromium + // JavascriptFeature types and added to BraveWebClient + web::WKWebViewConfigurationProvider& config_provider = + web::WKWebViewConfigurationProvider::FromBrowserState( + self.webState->GetBrowserState()); + config_provider.UpdateScripts(); +} + +- (void)createPDF:(void (^)(NSData* _Nullable))completionHandler { + self.webState->CreateFullPagePdf(base::BindOnce(completionHandler)); +} + +- (void)takeSnapshotWithRect:(CGRect)rect + completionHandler:(void (^)(UIImage* _Nullable))completionHandler { + self.webState->TakeSnapshot(rect, base::BindRepeating(completionHandler)); +} + +- (CWVUserAgentType)currentItemUserAgentType { + return static_cast(self.webState->GetNavigationManager() + ->GetVisibleItem() + ->GetUserAgentType()); +} + +- (void)reloadWithUserAgentType:(CWVUserAgentType)userAgentType { + self.webState->GetNavigationManager()->ReloadWithUserAgentType( + static_cast(userAgentType)); +} + +- (void)evaluateJavaScript:(NSString*)javaScriptString + contentWorld:(WKContentWorld*)contentWorld + completionHandler:(void (^)(id result, NSError* error))completion { + web::WebFrame* mainFrame = + self.webState->GetPageWorldWebFramesManager()->GetMainWebFrame(); + if (!mainFrame) { + if (completion) { + completion(nil, [NSError errorWithDomain:@"org.chromium.chromewebview" + code:0 + userInfo:nil]); + } + return; + } + web::WebFrameInternal* webFrame = mainFrame->GetWebFrameInternal(); + auto worlds = web::JavaScriptFeatureManager::FromBrowserState( + self.configuration.browserState) + ->GetAllContentWorlds(); + + auto jsContentWorld = + std::find_if(worlds.begin(), worlds.end(), [&contentWorld](auto world) { + return world->GetWKContentWorld() == contentWorld; + }); + if (jsContentWorld == worlds.end()) { + if (completion) { + completion(nil, [NSError errorWithDomain:@"org.chromium.chromewebview" + code:0 + userInfo:nil]); + } + return; + } + + webFrame->ExecuteJavaScriptInContentWorld( + base::SysNSStringToUTF16(javaScriptString), *jsContentWorld, + base::BindOnce(^(const base::Value* result, NSError* error) { + if (completion) { + id jsResult = nil; + if (!error && result) { + jsResult = NSObjectFromValue(result); + } + completion(jsResult, error); + } + })); +} + +- (WKWebView*)underlyingWebView { + CRWWebController* web_controller = + web::WebStateImpl::FromWebState(self.webState)->GetWebController(); + return web_controller.webView; +} + +- (WKWebViewConfiguration*)WKConfiguration { + web::WKWebViewConfigurationProvider& config_provider = + web::WKWebViewConfigurationProvider::FromBrowserState( + self.webState->GetBrowserState()); + return config_provider.GetWebViewConfiguration(); +} + +#pragma mark - CRWWebStateDelegate + +- (void)webState:(web::WebState*)webState + didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace + proposedCredential:(NSURLCredential*)proposedCredential + completionHandler:(void (^)(NSString* username, + NSString* password))handler { + SEL selector = @selector(webView: + didRequestHTTPAuthForProtectionSpace:proposedCredential:completionHandler + :); + if ([self.navigationDelegate respondsToSelector:selector]) { + [self.navigationDelegate webView:self + didRequestHTTPAuthForProtectionSpace:protectionSpace + proposedCredential:proposedCredential + completionHandler:handler]; + } +} + +@end diff --git a/ios/web_view/internal/cwv_x509_certificate_extras.mm b/ios/web_view/internal/cwv_x509_certificate_extras.mm new file mode 100644 index 000000000000..3b7d3985da32 --- /dev/null +++ b/ios/web_view/internal/cwv_x509_certificate_extras.mm @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web_view/internal/cwv_x509_certificate_internal.h" +#include "net/cert/x509_util_apple.h" + +@implementation CWVX509Certificate (Extras) + +- (SecCertificateRef)certificateRef { + return net::x509_util::CreateSecCertificateFromX509Certificate( + self.internalCertificate.get()) + .release(); // Swift bridging should handle the lifetime +} + +@end diff --git a/ios/web_view/internal/web_view_global_state_util_stub.mm b/ios/web_view/internal/web_view_global_state_util_stub.mm new file mode 100644 index 000000000000..1cf726521d8a --- /dev/null +++ b/ios/web_view/internal/web_view_global_state_util_stub.mm @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web_view/internal/web_view_global_state_util.h" + +namespace ios_web_view { + +void InitializeGlobalState() { + // We already provide global initialization with BraveCoreMain +} + +} // namespace ios_web_view diff --git a/ios/web_view/public/cwv_ssl_status_extras.h b/ios/web_view/public/cwv_ssl_status_extras.h new file mode 100644 index 000000000000..31e4e0e732c1 --- /dev/null +++ b/ios/web_view/public/cwv_ssl_status_extras.h @@ -0,0 +1,46 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_SSL_STATUS_EXTRAS_H_ +#define BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_SSL_STATUS_EXTRAS_H_ + +#import + +#import "cwv_ssl_status.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Describes an error that happened while showing a page over SSL. +OBJC_EXPORT +@interface CWVSSLErrorInformation : NSObject + +/// A description of the error. +@property(readonly) NSString* details; + +/// A short message describing the error (1 line). +@property(readonly) NSString* shortDescription; + +@end + +OBJC_EXPORT +@interface CWVSSLStatus (Extras) + +/// Whether or not `certStatus` is an error +/// +/// It is possible to have `securityStyle` be `authenticationBroken` and +/// non-error `certStatus` for WKWebView because `securityStyle` and +/// `certStatus` are calculated using different API, which may lead to different +/// cert verification results. Check this before using error information from +/// `certStatusErrorsForURL` +@property(readonly) BOOL isCertStatusError; + +/// A list of error details for a given URL using this SSL certificate +- (NSArray*)certStatusErrorsForURL:(NSURL*)url; + +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_SSL_STATUS_EXTRAS_H_ diff --git a/ios/web_view/public/cwv_web_view_extras.h b/ios/web_view/public/cwv_web_view_extras.h new file mode 100644 index 000000000000..ac5ad5006419 --- /dev/null +++ b/ios/web_view/public/cwv_web_view_extras.h @@ -0,0 +1,52 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_WEB_VIEW_EXTRAS_H_ +#define BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_WEB_VIEW_EXTRAS_H_ + +#import +#import +#import + +#import "cwv_export.h" +#import "cwv_web_view.h" + +typedef NSInteger CWVUserAgentType NS_TYPED_ENUM; +OBJC_EXPORT const CWVUserAgentType CWVUserAgentTypeNone; +OBJC_EXPORT const CWVUserAgentType CWVUserAgentTypeAutomatic; +OBJC_EXPORT const CWVUserAgentType CWVUserAgentTypeMobile; +OBJC_EXPORT const CWVUserAgentType CWVUserAgentTypeDesktop; + +NS_ASSUME_NONNULL_BEGIN + +CWV_EXPORT +@interface CWVWebView (Extras) + +/// Determines if the data used to restore a CWVWebView is a WebState cache and +/// is valid. +/// +/// Used for migration purposes, can be removed in the future ++ (BOOL)isRestoreDataValid:(NSData*)data; + +- (void)updateScripts; +- (void)createPDF:(void (^)(NSData* _Nullable))completionHandler; +- (void)takeSnapshotWithRect:(CGRect)rect + completionHandler:(void (^)(UIImage* _Nullable))completionHandler; +- (CWVUserAgentType)currentItemUserAgentType; +- (void)reloadWithUserAgentType:(CWVUserAgentType)userAgentType; +- (void)evaluateJavaScript:(NSString*)javaScriptString + contentWorld:(WKContentWorld*)contentWorld + completionHandler: + (nullable void (^)(id _Nullable result, + NSError* _Nullable error))completion; + +@property(readonly, nullable) WKWebView* underlyingWebView; +@property(readonly) WKWebViewConfiguration* WKConfiguration; + +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_WEB_VIEW_EXTRAS_H_ diff --git a/ios/web_view/public/cwv_x509_certificate_extras.h b/ios/web_view/public/cwv_x509_certificate_extras.h new file mode 100644 index 000000000000..45925ec04ee5 --- /dev/null +++ b/ios/web_view/public/cwv_x509_certificate_extras.h @@ -0,0 +1,15 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_X509_CERTIFICATE_EXTRAS_H_ +#define BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_X509_CERTIFICATE_EXTRAS_H_ + +#include "cwv_x509_certificate.h" // NOLINT + +@interface CWVX509Certificate (Extras) +@property(readonly, nullable) SecCertificateRef certificateRef; +@end + +#endif // BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_X509_CERTIFICATE_EXTRAS_H_ diff --git a/ios/web_view/sources.gni b/ios/web_view/sources.gni new file mode 100644 index 000000000000..9931b2ac9151 --- /dev/null +++ b/ios/web_view/sources.gni @@ -0,0 +1,22 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +# Source exclusions for //ios/web_view +# - Excludes custom WebUI implementations as we use //ios/chrome's +# - Excludes the custom WebClient & WebMainParts as we don't use the +# //ios/web_view global state setup +brave_ios_web_view_excluded_sources = [ + "internal/web_view_web_client.h", + "internal/web_view_web_client.mm", + "internal/web_view_web_main_parts.h", + "internal/web_view_web_main_parts.mm", + "internal/webui/web_view_web_ui_ios_controller_factory.h", + "internal/webui/web_view_web_ui_ios_controller_factory.mm", + "internal/webui/web_view_web_ui_provider.mm", +] + +# Dep exclusions for //ios/web_view +# - Excludes the bundled resources since these are already bundled +brave_ios_web_view_excluded_public_deps = [ ":web_view_resources" ] diff --git a/patches/ios-web-navigation-crw_wk_navigation_handler.mm.patch b/patches/ios-web-navigation-crw_wk_navigation_handler.mm.patch new file mode 100644 index 000000000000..33d4bb3b62ba --- /dev/null +++ b/patches/ios-web-navigation-crw_wk_navigation_handler.mm.patch @@ -0,0 +1,44 @@ +diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm +index 443d5684fceb98e1362b8d337b64d0a36c2186c8..569ae27d0ef10d2aef5a9c4df571570fd522c09d 100644 +--- a/ios/web/navigation/crw_wk_navigation_handler.mm ++++ b/ios/web/navigation/crw_wk_navigation_handler.mm +@@ -256,6 +256,7 @@ void LogPresentingErrorPageFailedWithError(NSError* error) { + BOOL isMainFrameNavigationAction = [self isMainFrameNavigationAction:action]; + auto decisionHandler = ^(WKNavigationActionPolicy policy) { + preferences.preferredContentMode = contentMode; ++ BRAVE_SHOULD_BLOCK_JAVASCRIPT + #if defined(__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0 + if (@available(iOS 16.0, *)) { + if ((policy == WKNavigationActionPolicyAllow) && +@@ -287,6 +288,7 @@ void LogPresentingErrorPageFailedWithError(NSError* error) { + + BOOL forceBlockUniversalLinks = self.blockUniversalLinksOnNextDecidePolicy; + self.blockUniversalLinksOnNextDecidePolicy = NO; ++ BRAVE_SHOULD_BLOCK_UNIVERSAL_LINKS + + _webProcessCrashed = NO; + if (self.beingDestroyed) { +@@ -698,6 +700,7 @@ void LogPresentingErrorPageFailedWithError(NSError* error) { + code:error.code + userInfo:userInfo]; + } ++ BRAVE_DID_FAIL_PROVISIONAL_NAVIGATION + + // Handle load cancellation for directly cancelled navigations without + // handling their potential errors. Otherwise, handle the error. +@@ -1712,6 +1715,7 @@ void LogPresentingErrorPageFailedWithError(NSError* error) { + web::CertVerificationError(is_recoverable, certStatus)); + } + } ++ BRAVE_PROCESS_AUTH_CHALLENGE + completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); + } + +@@ -1773,6 +1777,7 @@ void LogPresentingErrorPageFailedWithError(NSError* error) { + // automatically be retried by the web view, so early return in this case. + return; + } ++ BRAVE_HANDLE_LOAD_ERROR + + NSError* contextError = web::NetErrorFromError(error); + if (policyDecisionCancellationError) { diff --git a/patches/ios-web-web_state-ui-wk_web_view_configuration_provider.patch b/patches/ios-web-web_state-ui-wk_web_view_configuration_provider.patch new file mode 100644 index 000000000000..bce32a3f54e1 --- /dev/null +++ b/patches/ios-web-web_state-ui-wk_web_view_configuration_provider.patch @@ -0,0 +1,12 @@ +diff --git a/ios/web/web_state/ui/wk_web_view_configuration_provider.mm b/ios/web/web_state/ui/wk_web_view_configuration_provider.mm +index a3e5cdad9454cda0a49986b2a67f1b11741ad7d3..538b2a93c65fc3912a2fb5cabde42ef640e9c9b0 100644 +--- a/ios/web/web_state/ui/wk_web_view_configuration_provider.mm ++++ b/ios/web/web_state/ui/wk_web_view_configuration_provider.mm +@@ -143,6 +143,7 @@ void WKWebViewConfigurationProvider::ResetWithWebViewConfiguration( + fetchDataRecordsOfTypes:data_types + completionHandler:^(NSArray* records){ + }]; ++ BRAVE_RESET_WITH_WEB_VIEW_CONFIGURATION + } + + WKWebViewConfiguration* diff --git a/patches/ios-web_view-BUILD.gn.patch b/patches/ios-web_view-BUILD.gn.patch new file mode 100644 index 000000000000..b4904d8c4204 --- /dev/null +++ b/patches/ios-web_view-BUILD.gn.patch @@ -0,0 +1,12 @@ +diff --git a/ios/web_view/BUILD.gn b/ios/web_view/BUILD.gn +index ad1314a46740124186248000ed93f734ec73bcb0..97354ade551cd41a506e839fe1d7a2f75a0b2eea 100644 +--- a/ios/web_view/BUILD.gn ++++ b/ios/web_view/BUILD.gn +@@ -429,6 +429,7 @@ source_set("web_view_sources") { + public_deps = ios_web_view_deps + deps = [ "//components/version_info:channel" ] + configs += [ ":config" ] ++ import("//brave/ios/web_view/sources.gni") sources -= brave_ios_web_view_excluded_sources public_deps -= brave_ios_web_view_excluded_public_deps + } + + action("web_view_umbrella_header") { diff --git a/patches/ios-web_view-internal-cwv_web_view.mm.patch b/patches/ios-web_view-internal-cwv_web_view.mm.patch new file mode 100644 index 000000000000..7f6252d2f2df --- /dev/null +++ b/patches/ios-web_view-internal-cwv_web_view.mm.patch @@ -0,0 +1,28 @@ +diff --git a/ios/web_view/internal/cwv_web_view.mm b/ios/web_view/internal/cwv_web_view.mm +index 6823b138f7536ee41ddd11f5f176f9ad88474fa7..5139865a547ceb633a51d514d2b6d5373a128d29 100644 +--- a/ios/web_view/internal/cwv_web_view.mm ++++ b/ios/web_view/internal/cwv_web_view.mm +@@ -970,6 +970,7 @@ WEB_STATE_USER_DATA_KEY_IMPL(WebViewHolder) + #pragma mark - Translation + + - (CWVTranslationController*)translationController { ++ BRAVE_NOP_SERVICE + if (!_translationController) { + _translationController = [self newTranslationController]; + } +@@ -990,6 +991,7 @@ WEB_STATE_USER_DATA_KEY_IMPL(WebViewHolder) + #pragma mark - Autofill + + - (CWVAutofillController*)autofillController { ++ BRAVE_NOP_SERVICE + if (!_autofillController) { + _autofillController = [self newAutofillController]; + } +@@ -1129,6 +1131,7 @@ WEB_STATE_USER_DATA_KEY_IMPL(WebViewHolder) + } + + [self attachSecurityInterstitialHelpersToWebStateIfNecessary]; ++ BRAVE_ATTACH_TAB_HELPERS + WebViewHolder::CreateForWebState(_webState.get()); + WebViewHolder::FromWebState(_webState.get())->set_web_view(self); + diff --git a/patches/ios-web_view-public-cwv_navigation_delegate.h.patch b/patches/ios-web_view-public-cwv_navigation_delegate.h.patch new file mode 100644 index 000000000000..b8ac38515f33 --- /dev/null +++ b/patches/ios-web_view-public-cwv_navigation_delegate.h.patch @@ -0,0 +1,14 @@ +diff --git a/ios/web_view/public/cwv_navigation_delegate.h b/ios/web_view/public/cwv_navigation_delegate.h +index 0ebe5c3f82c4ce1dae858200f340c6986e558eaf..504cb917329505172de44d3f6960a1d1950a37f7 100644 +--- a/ios/web_view/public/cwv_navigation_delegate.h ++++ b/ios/web_view/public/cwv_navigation_delegate.h +@@ -130,6 +130,9 @@ typedef NS_ENUM(NSInteger, CWVNavigationResponsePolicy) { + // (usually by crashing, though possibly by other means). + - (void)webViewWebContentProcessDidTerminate:(CWVWebView*)webView; + ++- (BOOL)webView:(CWVWebView*)webView shouldBlockUniversalLinksForRequest:(NSURLRequest*)request; ++- (BOOL)webView:(CWVWebView*)webView shouldBlockJavaScriptForRequest:(NSURLRequest*)request; ++- (void)webView:(CWVWebView*)webView didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace proposedCredential:(NSURLCredential*)proposedCredential completionHandler:(void (^)(NSString* _Nullable username, NSString* _Nullable password))handler; + @end + + NS_ASSUME_NONNULL_END diff --git a/patches/ios-web_view-public-cwv_web_view.h.patch b/patches/ios-web_view-public-cwv_web_view.h.patch new file mode 100644 index 000000000000..2ea1abc9eab8 --- /dev/null +++ b/patches/ios-web_view-public-cwv_web_view.h.patch @@ -0,0 +1,13 @@ +diff --git a/ios/web_view/public/cwv_web_view.h b/ios/web_view/public/cwv_web_view.h +index c17c6eb810f3122705cc0124e68e1b4a32cd10b8..c76bc2193fb3230585e719e35db730d24eee60e1 100644 +--- a/ios/web_view/public/cwv_web_view.h ++++ b/ios/web_view/public/cwv_web_view.h +@@ -71,7 +71,7 @@ CWV_EXPORT + // (not implemented for CWVWebView) + // |visibleURL| is the bad cert page URL. |lastCommittedURL| is the previous + // page URL. +-@property(nonatomic, readonly) NSURL* visibleURL; ++@property(nonatomic, readonly, nullable) NSURL* visibleURL; + + // A human-friendly string which represents the location of the document + // currently being loaded. KVO compliant.