// 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 "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);
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);
}
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);
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);
}
bool JavaScriptFeature::ExecuteJavaScript(
WebFrame* web_frame,
const std::u16string& script,
ExecuteJavaScriptCallbackWithError callback) {
DCHECK(web_frame);
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));
}
} // namespace web