| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/web/public/js_messaging/java_script_feature.h" |
| |
| #import <Foundation/Foundation.h> |
| |
| #import "base/functional/bind.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "base/time/time.h" |
| #import "build/blink_buildflags.h" |
| #import "ios/web/javascript_flags.h" |
| #import "ios/web/js_messaging/java_script_content_world.h" |
| #import "ios/web/js_messaging/java_script_feature_manager.h" |
| #import "ios/web/js_messaging/page_script_util.h" |
| #import "ios/web/js_messaging/web_frame_internal.h" |
| #import "ios/web/public/js_messaging/content_world.h" |
| #import "ios/web/public/js_messaging/web_frame.h" |
| #import "ios/web/public/js_messaging/web_frames_manager.h" |
| #import "ios/web/public/web_state.h" |
| |
| #if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS) |
| #import "base/command_line.h" |
| #import "base/strings/string_split.h" |
| #import "ios/web/switches.h" |
| #endif |
| |
| namespace { |
| |
| // Returns a JavaScript safe string based on `script_filename`. This is used as |
| // a unique identifier for a given script and passed to |
| // `MakeScriptInjectableOnce` which ensures JS isn't executed multiple times due |
| // to duplicate injection. |
| NSString* InjectionTokenForScript(NSString* script_filename) { |
| NSMutableCharacterSet* valid_characters = |
| [NSMutableCharacterSet alphanumericCharacterSet]; |
| [valid_characters addCharactersInString:@"$_"]; |
| NSCharacterSet* invalid_characters = valid_characters.invertedSet; |
| NSString* token = |
| [[script_filename componentsSeparatedByCharactersInSet:invalid_characters] |
| componentsJoinedByString:@""]; |
| DCHECK_GT(token.length, 0ul); |
| return token; |
| } |
| |
| bool IsScriptEnabled(NSString* script_token) { |
| #if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS) |
| bool disable_all_scripts = base::CommandLine::ForCurrentProcess()->HasSwitch( |
| web::switches::kDisableAllInjectedScripts); |
| if (disable_all_scripts) { |
| return false; |
| } |
| |
| bool disable_feature_scripts = |
| base::CommandLine::ForCurrentProcess()->HasSwitch( |
| web::switches::kDisableInjectedFeatureScripts); |
| if (disable_feature_scripts) { |
| return [[NSSet setWithArray:@[ @"gcrweb", @"common", @"message" ]] |
| containsObject:script_token]; |
| } |
| |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| web::switches::kDisableListedScripts)) { |
| std::string token = base::SysNSStringToUTF8(script_token); |
| auto disable_scripts_flag = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| web::switches::kDisableListedScripts); |
| auto disable_scripts = |
| base::SplitStringPiece(disable_scripts_flag, ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (std::find(disable_scripts.begin(), disable_scripts.end(), |
| token.c_str()) != disable_scripts.end()) { |
| // `token` found in passed switch value. |
| return false; |
| } |
| return true; |
| } |
| |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| web::switches::kEnableListedScripts)) { |
| std::string token = base::SysNSStringToUTF8(script_token); |
| auto enable_scripts_flag = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| web::switches::kEnableListedScripts); |
| auto enable_scripts = |
| base::SplitStringPiece(enable_scripts_flag, ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (std::find(enable_scripts.begin(), enable_scripts.end(), |
| token.c_str()) != enable_scripts.end()) { |
| // `token` found in passed switch value. |
| return true; |
| } |
| return false; |
| } |
| #endif |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| namespace web { |
| |
| #pragma mark - JavaScriptFeature::FeatureScript |
| |
| JavaScriptFeature::FeatureScript |
| JavaScriptFeature::FeatureScript::CreateWithFilename( |
| const std::string& filename, |
| InjectionTime injection_time, |
| TargetFrames target_frames, |
| ReinjectionBehavior reinjection_behavior, |
| const PlaceholderReplacementsCallback& replacements_callback) { |
| NSString* injection_token = |
| InjectionTokenForScript(base::SysUTF8ToNSString(filename)); |
| return JavaScriptFeature::FeatureScript( |
| filename, /*script=*/std::nullopt, injection_token, injection_time, |
| target_frames, reinjection_behavior, replacements_callback); |
| } |
| |
| JavaScriptFeature::FeatureScript |
| JavaScriptFeature::FeatureScript::CreateWithString( |
| const std::string& script, |
| InjectionTime injection_time, |
| TargetFrames target_frames, |
| ReinjectionBehavior reinjection_behavior, |
| const PlaceholderReplacementsCallback& replacements_callback) { |
| NSString* unique_id = [[NSProcessInfo processInfo] globallyUniqueString]; |
| NSString* injection_token = InjectionTokenForScript(unique_id); |
| return JavaScriptFeature::FeatureScript( |
| /*filename=*/std::nullopt, script, injection_token, injection_time, |
| target_frames, reinjection_behavior, replacements_callback); |
| } |
| |
| JavaScriptFeature::FeatureScript::FeatureScript( |
| std::optional<std::string> filename, |
| std::optional<std::string> script, |
| NSString* injection_token, |
| InjectionTime injection_time, |
| TargetFrames target_frames, |
| ReinjectionBehavior reinjection_behavior, |
| const PlaceholderReplacementsCallback& replacements_callback) |
| : script_filename_(filename), |
| script_(script), |
| injection_token_(injection_token), |
| injection_time_(injection_time), |
| target_frames_(target_frames), |
| reinjection_behavior_(reinjection_behavior), |
| replacements_callback_(replacements_callback) {} |
| |
| JavaScriptFeature::FeatureScript::FeatureScript(const FeatureScript&) = default; |
| |
| JavaScriptFeature::FeatureScript& JavaScriptFeature::FeatureScript::operator=( |
| const FeatureScript&) = default; |
| |
| JavaScriptFeature::FeatureScript::FeatureScript(FeatureScript&&) = default; |
| |
| JavaScriptFeature::FeatureScript& JavaScriptFeature::FeatureScript::operator=( |
| FeatureScript&&) = default; |
| |
| JavaScriptFeature::FeatureScript::~FeatureScript() = default; |
| |
| NSString* JavaScriptFeature::FeatureScript::GetScriptString() const { |
| if (!IsScriptEnabled(injection_token_)) { |
| return @""; |
| } |
| |
| NSString* script = nil; |
| if (script_) { |
| script = base::SysUTF8ToNSString(script_.value()); |
| } else { |
| CHECK(script_filename_); |
| script = GetPageScript(base::SysUTF8ToNSString(*script_filename_)); |
| } |
| |
| if (reinjection_behavior_ == |
| ReinjectionBehavior::kReinjectOnDocumentRecreation) { |
| return ReplacePlaceholders(script); |
| } |
| // WKUserScript instances will automatically be re-injected by WebKit when the |
| // document is re-created, even though the JavaScript context will not be |
| // re-created. So the script needs to be wrapped in `MakeScriptInjectableOnce` |
| // so that is is not re-injected. |
| return MakeScriptInjectableOnce(injection_token_, |
| ReplacePlaceholders(script)); |
| } |
| |
| NSString* JavaScriptFeature::FeatureScript::ReplacePlaceholders( |
| NSString* script) const { |
| if (replacements_callback_.is_null()) { |
| return script; |
| } |
| |
| PlaceholderReplacements replacements = replacements_callback_.Run(); |
| if (!replacements) { |
| return script; |
| } |
| |
| for (NSString* key in replacements) { |
| script = [script stringByReplacingOccurrencesOfString:key |
| withString:replacements[key]]; |
| } |
| |
| return script; |
| } |
| |
| #pragma mark - JavaScriptFeature |
| |
| JavaScriptFeature::JavaScriptFeature(ContentWorld supported_world) |
| : supported_world_(supported_world), weak_factory_(this) {} |
| |
| JavaScriptFeature::JavaScriptFeature(ContentWorld supported_world, |
| std::vector<FeatureScript> feature_scripts) |
| : supported_world_(supported_world), |
| scripts_(feature_scripts), |
| weak_factory_(this) {} |
| |
| JavaScriptFeature::JavaScriptFeature( |
| ContentWorld supported_world, |
| std::vector<FeatureScript> feature_scripts, |
| std::vector<const JavaScriptFeature*> dependent_features) |
| : supported_world_(supported_world), |
| scripts_(feature_scripts), |
| dependent_features_(dependent_features), |
| weak_factory_(this) {} |
| |
| JavaScriptFeature::~JavaScriptFeature() = default; |
| |
| ContentWorld JavaScriptFeature::GetSupportedContentWorld() const { |
| return supported_world_; |
| } |
| |
| WebFramesManager* JavaScriptFeature::GetWebFramesManager(WebState* web_state) { |
| return web_state->GetWebFramesManager(GetSupportedContentWorld()); |
| } |
| |
| std::vector<JavaScriptFeature::FeatureScript> JavaScriptFeature::GetScripts() |
| const { |
| return scripts_; |
| } |
| |
| std::vector<const JavaScriptFeature*> JavaScriptFeature::GetDependentFeatures() |
| const { |
| return dependent_features_; |
| } |
| |
| std::optional<std::string> JavaScriptFeature::GetScriptMessageHandlerName() |
| const { |
| return std::nullopt; |
| } |
| |
| std::optional<JavaScriptFeature::ScriptMessageHandler> |
| JavaScriptFeature::GetScriptMessageHandler() const { |
| if (!GetScriptMessageHandlerName()) { |
| return std::nullopt; |
| } |
| |
| return base::BindRepeating(&JavaScriptFeature::ScriptMessageReceived, |
| weak_factory_.GetMutableWeakPtr()); |
| } |
| |
| void JavaScriptFeature::ScriptMessageReceived(WebState* web_state, |
| const ScriptMessage& message) {} |
| |
| bool JavaScriptFeature::CallJavaScriptFunction( |
| WebFrame* web_frame, |
| const std::string& function_name, |
| const base::Value::List& parameters) { |
| DCHECK(web_frame); |
| |
| #if BUILDFLAG(USE_BLINK) |
| // TODO(crbug.com/40254930): Call the ContentJavascriptFeatureManager instead. |
| return false; |
| #else |
| JavaScriptFeatureManager* feature_manager = |
| JavaScriptFeatureManager::FromBrowserState(web_frame->GetBrowserState()); |
| DCHECK(feature_manager); |
| |
| JavaScriptContentWorld* content_world = |
| feature_manager->GetContentWorldForFeature(this); |
| #if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS) |
| // If this JavaScript feature was not registered due to a JavaScript debug |
| // flag, do not attempt to call `function_name`. |
| if (!content_world) { |
| return false; |
| } |
| #endif |
| DCHECK(content_world); |
| |
| return web_frame->GetWebFrameInternal()->CallJavaScriptFunctionInContentWorld( |
| function_name, parameters, content_world); |
| #endif |
| } |
| |
| bool JavaScriptFeature::CallJavaScriptFunction( |
| WebFrame* web_frame, |
| const std::string& function_name, |
| const base::Value::List& parameters, |
| base::OnceCallback<void(const base::Value*)> callback, |
| base::TimeDelta timeout) { |
| DCHECK(web_frame); |
| |
| #if BUILDFLAG(USE_BLINK) |
| // TODO(crbug.com/40254930): Call the ContentJavascriptFeatureManager instead. |
| return false; |
| #else |
| JavaScriptFeatureManager* feature_manager = |
| JavaScriptFeatureManager::FromBrowserState(web_frame->GetBrowserState()); |
| DCHECK(feature_manager); |
| |
| JavaScriptContentWorld* content_world = |
| feature_manager->GetContentWorldForFeature(this); |
| #if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS) |
| // If this JavaScript feature was not registered due to a JavaScript debug |
| // flag, do not attempt to call `function_name`. |
| if (!content_world) { |
| return false; |
| } |
| #endif |
| DCHECK(content_world); |
| |
| return web_frame->GetWebFrameInternal()->CallJavaScriptFunctionInContentWorld( |
| function_name, parameters, content_world, std::move(callback), timeout); |
| #endif |
| } |
| |
| bool JavaScriptFeature::ExecuteJavaScript( |
| WebFrame* web_frame, |
| const std::u16string& script, |
| ExecuteJavaScriptCallbackWithError callback) { |
| DCHECK(web_frame); |
| |
| #if BUILDFLAG(USE_BLINK) |
| // TODO(crbug.com/40254930): Call the ContentJavascriptFeatureManager instead. |
| return false; |
| #else |
| JavaScriptFeatureManager* feature_manager = |
| JavaScriptFeatureManager::FromBrowserState(web_frame->GetBrowserState()); |
| DCHECK(feature_manager); |
| |
| JavaScriptContentWorld* content_world = |
| feature_manager->GetContentWorldForFeature(this); |
| #if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS) |
| // If this JavaScript feature was not registered due to a JavaScript debug |
| // flag, do not attempt to call `function_name`. |
| if (!content_world) { |
| return false; |
| } |
| #endif |
| DCHECK(content_world); |
| |
| return web_frame->GetWebFrameInternal()->ExecuteJavaScriptInContentWorld( |
| script, content_world, std::move(callback)); |
| #endif |
| } |
| |
| } // namespace web |