chromium/ios/web/js_messaging/java_script_content_world.mm

// 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/js_messaging/java_script_content_world.h"

#import <optional>

#import "base/check_op.h"
#import "base/containers/contains.h"
#import "base/debug/crash_logging.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/web/javascript_flags.h"
#import "ios/web/js_messaging/web_view_web_state_map.h"
#import "ios/web/public/browser_state.h"
#import "ios/web/public/js_messaging/content_world.h"
#import "ios/web/public/js_messaging/java_script_feature.h"
#import "ios/web/public/js_messaging/script_message.h"
#import "ios/web/public/js_messaging/web_view_js_utils.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#import "ios/web/web_state/web_state_impl.h"
#import "net/base/apple/url_conversions.h"

#if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS)
#import "base/command_line.h"
#import "base/strings/string_split.h"
#import "ios/web/public/js_messaging/java_script_feature_util.h"
#import "ios/web/switches.h"
#endif

namespace web {

namespace {

WKUserScriptInjectionTime InjectionTimeToWKUserScriptInjectionTime(
    JavaScriptFeature::FeatureScript::InjectionTime injection_time) {
  switch (injection_time) {
    case JavaScriptFeature::FeatureScript::InjectionTime::kDocumentStart:
      return WKUserScriptInjectionTimeAtDocumentStart;
    case JavaScriptFeature::FeatureScript::InjectionTime::kDocumentEnd:
      return WKUserScriptInjectionTimeAtDocumentEnd;
  }
  NOTREACHED_IN_MIGRATION();
  return WKUserScriptInjectionTimeAtDocumentStart;
}

// Returns the WKUserContentController associated with `browser_state`.
// NOTE: Only fetch the WKUserContentController once at construction. Although
// it is not guaranteed to remain constant over the lifetime of the
// application, the entire JavaScriptcontentWorld will be recreated when it
// changes. Calling WKWebViewConfigurationProvider::GetWebViewConfiguration on
// a configuration provider during destruction will cause partial
// re-initialization during tear down.
WKUserContentController* GetUserContentController(BrowserState* browser_state) {
  return WKWebViewConfigurationProvider::FromBrowserState(browser_state)
      .GetWebViewConfiguration()
      .userContentController;
}

}  // namespace

JavaScriptContentWorld::JavaScriptContentWorld(BrowserState* browser_state,
                                               WKContentWorld* content_world)
    : browser_state_(browser_state),
      user_content_controller_(GetUserContentController(browser_state)),
      content_world_(content_world),
      weak_factory_(this) {
  DCHECK(content_world_);

#if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS)
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    int num_flags_enabled = 0;
    bool disable_all_scripts =
        base::CommandLine::ForCurrentProcess()->HasSwitch(
            web::switches::kDisableAllInjectedScripts);
    if (disable_all_scripts) {
      num_flags_enabled++;
      LOG(WARNING) << "\n\n###########\nFlag set: "
                   << web::switches::kDisableAllInjectedScripts
                   << "\n###########\n\n";
    }

    bool disable_feature_scripts =
        base::CommandLine::ForCurrentProcess()->HasSwitch(
            web::switches::kDisableInjectedFeatureScripts);
    if (disable_feature_scripts) {
      num_flags_enabled++;
      LOG(WARNING) << "\n\n###########\nFlag set: "
                   << web::switches::kDisableInjectedFeatureScripts
                   << "\n###########\n\n";
    }

    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            web::switches::kDisableListedScripts)) {
      num_flags_enabled++;
      LOG(WARNING) << "\n\n###########\nFlag set: "
                   << web::switches::kDisableListedScripts
                   << "\n###########\n\n";
    }

    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            web::switches::kEnableListedScripts)) {
      num_flags_enabled++;
      LOG(WARNING) << "\n\n###########\nFlag set: "
                   << web::switches::kEnableListedScripts
                   << "\n###########\n\n";
    }

    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            web::switches::kDisableListedJavascriptFeatures)) {
      num_flags_enabled++;
      LOG(WARNING) << "\n\n###########\nFlag set: "
                   << web::switches::kDisableListedJavascriptFeatures
                   << "\n###########\n\n";
    }

    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            web::switches::kEnableListedJavascriptFeatures)) {
      num_flags_enabled++;
      LOG(WARNING) << "\n\n###########\nFlag set: "
                   << web::switches::kEnableListedJavascriptFeatures
                   << "\n###########\n\n";
    }

    if (num_flags_enabled > 1) {
      LOG(ERROR) << "Multiple JavaScript flags set, results undefined. Ensure "
                    "only one is set and re-run.";
      abort();
    }
  });
#endif
}

WKContentWorld* JavaScriptContentWorld::GetWKContentWorld() {
  return content_world_;
}

JavaScriptContentWorld::~JavaScriptContentWorld() {}

bool JavaScriptContentWorld::HasFeature(const JavaScriptFeature* feature) {
  return base::Contains(features_, feature);
}

void JavaScriptContentWorld::AddFeature(const JavaScriptFeature* feature) {
#if BUILDFLAG(ENABLE_IOS_JAVASCRIPT_FLAGS)
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          web::switches::kDisableListedJavascriptFeatures)) {
    std::optional<std::string> message_handler_name =
        feature->GetScriptMessageHandlerName();
    if (message_handler_name) {
      auto disable_features_flag =
          base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
              web::switches::kDisableListedJavascriptFeatures);
      auto disable_features = base::SplitStringPiece(disable_features_flag, ",",
                                                     base::TRIM_WHITESPACE,
                                                     base::SPLIT_WANT_NONEMPTY);
      if (std::find(disable_features.begin(), disable_features.end(),
                    message_handler_name.value().c_str()) !=
          disable_features.end()) {
        // `feature`'s message handler name was found in passed switch value.
        return;
      }
    }
  }

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          web::switches::kEnableListedJavascriptFeatures)) {
    std::optional<std::string> message_handler_name =
        feature->GetScriptMessageHandlerName();
    if (feature != java_script_features::GetBaseJavaScriptFeature() &&
        feature != java_script_features::GetCommonJavaScriptFeature() &&
        feature != java_script_features::GetMessageJavaScriptFeature()) {
      if (!message_handler_name) {
        return;
      }

      auto enable_features_flag =
          base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
              web::switches::kEnableListedJavascriptFeatures);
      auto enable_features = base::SplitStringPiece(enable_features_flag, ",",
                                                    base::TRIM_WHITESPACE,
                                                    base::SPLIT_WANT_NONEMPTY);
      if (std::find(enable_features.begin(), enable_features.end(),
                    message_handler_name.value().c_str()) ==
          enable_features.end()) {
        // `feature`'s message handler name was NOT found in passed switch
        // value.
        return;
      }
    }
  }
#endif

  if (HasFeature(feature)) {
    // `feature` has already been added to this content world.
    return;
  }

  // Ensure `feature` supports this `content_world_`.
  if (content_world_ == WKContentWorld.pageWorld) {
    // A feature specifying kIsolatedWorld can not be added to the page
    // content world.
    DCHECK_NE(feature->GetSupportedContentWorld(),
              ContentWorld::kIsolatedWorld);
  } else {
    // A feature specifying kPageContentWorld can not be added to an isolated
    // world.
    DCHECK_NE(feature->GetSupportedContentWorld(),
              ContentWorld::kPageContentWorld);
  }

  features_.insert(feature);

  // Add dependent features first.
  for (const JavaScriptFeature* dep_feature : feature->GetDependentFeatures()) {
    AddFeature(dep_feature);
  }

  // Setup user scripts.
  for (const JavaScriptFeature::FeatureScript& feature_script :
       feature->GetScripts()) {
    WKUserScriptInjectionTime injection_time =
        InjectionTimeToWKUserScriptInjectionTime(
            feature_script.GetInjectionTime());

    bool main_frame_only =
        feature_script.GetTargetFrames() !=
        JavaScriptFeature::FeatureScript::TargetFrames::kAllFrames;

    if (content_world_) {
      WKUserScript* user_script =
          [[WKUserScript alloc] initWithSource:feature_script.GetScriptString()
                                 injectionTime:injection_time
                              forMainFrameOnly:main_frame_only
                                inContentWorld:content_world_];
      [user_content_controller_ addUserScript:user_script];
    } else {
      WKUserScript* user_script =
          [[WKUserScript alloc] initWithSource:feature_script.GetScriptString()
                                 injectionTime:injection_time
                              forMainFrameOnly:main_frame_only];
      [user_content_controller_ addUserScript:user_script];
    }
  }

  // Setup Javascript message callback.
  auto optional_handler_name = feature->GetScriptMessageHandlerName();
  if (optional_handler_name) {
    auto handler = feature->GetScriptMessageHandler();
    DCHECK(handler);

    NSString* handler_name =
        base::SysUTF8ToNSString(optional_handler_name.value());

    std::unique_ptr<ScopedWKScriptMessageHandler> script_message_handler;
    if (content_world_) {
      script_message_handler = std::make_unique<ScopedWKScriptMessageHandler>(
          user_content_controller_, handler_name, content_world_,
          base::BindRepeating(&JavaScriptContentWorld::ScriptMessageReceived,
                              weak_factory_.GetWeakPtr(), handler.value(),
                              browser_state_));
    } else {
      script_message_handler = std::make_unique<ScopedWKScriptMessageHandler>(
          user_content_controller_, handler_name,
          base::BindRepeating(&JavaScriptContentWorld::ScriptMessageReceived,
                              weak_factory_.GetWeakPtr(), handler.value(),
                              browser_state_));
    }
    script_message_handlers_[feature] = std::move(script_message_handler);
  }
}

void JavaScriptContentWorld::ScriptMessageReceived(
    JavaScriptFeature::ScriptMessageHandler handler,
    BrowserState* browser_state,
    WKScriptMessage* script_message) {
  SCOPED_CRASH_KEY_STRING32("ScriptMessage", "name",
                            base::SysNSStringToUTF8(script_message.name));

  web::WebViewWebStateMap* map =
      web::WebViewWebStateMap::FromBrowserState(browser_state);
  web::WebState* web_state = map->GetWebStateForWebView(script_message.webView);

  // Drop messages if they are no longer associated with a WebState.
  if (!web_state) {
    return;
  }

  CRWWebController* web_controller =
      web::WebStateImpl::FromWebState(web_state)->GetWebController();
  if (!web_controller) {
    return;
  }

  NSURL* ns_url = script_message.frameInfo.request.URL;
  std::optional<GURL> url;
  if (ns_url) {
    url = net::GURLWithNSURL(ns_url);
  }

  ScriptMessage message(web::ValueResultFromWKResult(script_message.body),
                        web_controller.isUserInteracting,
                        script_message.frameInfo.mainFrame, url);

  handler.Run(web_state, message);
}

}  // namespace web