chromium/ios/web/public/js_messaging/java_script_feature.h

// 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.

#ifndef IOS_WEB_PUBLIC_JS_MESSAGING_JAVA_SCRIPT_FEATURE_H_
#define IOS_WEB_PUBLIC_JS_MESSAGING_JAVA_SCRIPT_FEATURE_H_

#import <Foundation/Foundation.h>

#import <optional>
#include <string>
#include <vector>

#import "base/functional/callback.h"
#import "base/memory/weak_ptr.h"
#import "base/values.h"
#import "ios/web/public/js_messaging/content_world.h"
#include "ios/web/public/js_messaging/web_frame.h"

namespace base {
class TimeDelta;
}  // namespace base

namespace web {

class FuzzerEnvWithJavaScriptFeature;
class ScriptMessage;
class WebState;
class WebFrame;
class WebFramesManager;

// Describes a feature implemented in Javascript and native<->JS communication
// (if any). It is intended to be instantiated directly for simple features
// requiring injection only, but should subclassed into feature specific classes
// to handle JS<->native communication.
// NOTE: As implemented within //ios/web, JavaScriptFeature instances holds no
// state itself and can be used application-wide across browser states. However,
// this is not guaranteed of JavaScriptFeature subclasses.
class JavaScriptFeature {
  // `FuzzerEnvWithJavaScriptFeature` stores subclasses of `JavaScriptFeature`
  // and invokes `ScriptMessageReceived` function in a public API. So fuzzers
  // can call `ScriptMessageReceived` functions without friending with each
  // subclass.
  friend class FuzzerEnvWithJavaScriptFeature;

 public:
  // A script to be injected into webpage frames which support this feature.
  class FeatureScript {
   public:
    // The time at which this script will be injected into the page.
    enum class InjectionTime {
      kDocumentStart = 0,
      kDocumentEnd,
    };

    // Describes whether or not this script should be re-injected when the
    // document is re-created.
    enum class ReinjectionBehavior {
      // The script will only be injected once per window.
      kInjectOncePerWindow = 0,
      // The script will be re-injected when the document is re-created.
      // NOTE: This is necessary to re-add event listeners and to re-inject
      // modifications to the DOM and `document` JS object. Note, however, that
      // this option can also overwrite or duplicate state which was already
      // previously added to the window's state.
      kReinjectOnDocumentRecreation,
    };

    // The frames which this script will be injected into.
    enum class TargetFrames {
      kAllFrames = 0,
      kMainFrame,
    };

    // Mapping of placeholder to their replacement value.
    using PlaceholderReplacements = NSDictionary<NSString*, NSString*>*;

    // Callback used to perform placeholder replacement in the script. The
    // returned value is a dictionary mapping "placeholder" to the "value"
    // that needs it to be substituted by with in the script.
    using PlaceholderReplacementsCallback =
        base::RepeatingCallback<PlaceholderReplacements()>;

    // Creates a FeatureScript with the script file from the application bundle
    // with `filename` to be injected at `injection_time` into `target_frames`
    // using `reinjection_behavior`. If `replacements` is provided, it will be
    // used to replace placeholder with the corresponding string values.
    static FeatureScript CreateWithFilename(
        const std::string& filename,
        InjectionTime injection_time,
        TargetFrames target_frames,
        ReinjectionBehavior reinjection_behavior =
            ReinjectionBehavior::kInjectOncePerWindow,
        const PlaceholderReplacementsCallback& replacements_callback =
            PlaceholderReplacementsCallback());

    // Creates a FeatureScript with the string `script` to be injected at
    // `injection_time` into `target_frames` using `reinjection_behavior`. If
    // `replacements` is provided, it will be used to replace placeholder with
    // the corresponding string values.
    static FeatureScript CreateWithString(
        const std::string& script,
        InjectionTime injection_time,
        TargetFrames target_frames,
        ReinjectionBehavior reinjection_behavior =
            ReinjectionBehavior::kInjectOncePerWindow,
        const PlaceholderReplacementsCallback& replacements_callback =
            PlaceholderReplacementsCallback());

    FeatureScript(const FeatureScript& other);
    FeatureScript& operator=(const FeatureScript&);

    FeatureScript(FeatureScript&&);
    FeatureScript& operator=(FeatureScript&&);

    // Returns the JavaScript string of the script with `script_filename_`.
    NSString* GetScriptString() const;

    InjectionTime GetInjectionTime() const { return injection_time_; }
    TargetFrames GetTargetFrames() const { return target_frames_; }

    ~FeatureScript();

   private:
    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);

    // Returns `script` after swapping the placeholders with their value as
    // instructed by `replacements_callback_`.
    NSString* ReplacePlaceholders(NSString* script) const;

    std::optional<std::string> script_filename_;
    std::optional<std::string> script_;
    NSString* injection_token_;
    InjectionTime injection_time_;
    TargetFrames target_frames_;
    ReinjectionBehavior reinjection_behavior_;
    PlaceholderReplacementsCallback replacements_callback_;
  };

  // Constructs a new feature instance inside the world described by
  // `supported_world`. Each FeatureScript within `feature_scripts` will be
  // configured within that same world.
  // NOTE: Features should use `kIsolatedWorld` whenever possible to allow for
  // isolation between the feature and the loaded webpage JavaScript.
  JavaScriptFeature(ContentWorld supported_world,
                    std::vector<FeatureScript> feature_scripts);
  // Same as above constructor with the addition of dependent features. If
  // `dependent_features` are given, they will be setup in the world specified
  // prior to configuring this feaure.
  JavaScriptFeature(ContentWorld supported_world,
                    std::vector<FeatureScript> feature_scripts,
                    std::vector<const JavaScriptFeature*> dependent_features);
  virtual ~JavaScriptFeature();

  // Returns the supported content world for this feature.
  ContentWorld GetSupportedContentWorld() const;

  // Returns the WebFramesManager associated with `web_state` for the content
  // world which this feature instance has been configured. This ensures that
  // the WebFrames within WebFramesManager match the environment where the
  // scripts of this feature are executed. This is partifularly important if
  // frameIds are used which are not consistent across content worlds.
  // NOTE: This helper only works for features which are defined to live in a
  // specific content world. To obtain a WebFramesManager for a feature that is
  // configured with ContentWorld::kAllContentWorlds, obtain the frames manager
  // from a higher level feature or obtain the WebFramesManager from the
  // WebState and specify the content world directly.
  WebFramesManager* GetWebFramesManager(WebState* web_state);

  // Returns a vector of scripts used by this feature.
  virtual std::vector<FeatureScript> GetScripts() const;
  // Returns a vector of features which this one depends upon being available.
  virtual std::vector<const JavaScriptFeature*> GetDependentFeatures() const;

  // Returns the script message handler name which this feature will receive
  // messages from JavaScript. Returning null will not register any handler.
  virtual std::optional<std::string> GetScriptMessageHandlerName() const;

  using ScriptMessageHandler =
      base::RepeatingCallback<void(WebState* web_state,
                                   const ScriptMessage& message)>;
  // Returns the script message handler callback if
  // `GetScriptMessageHandlerName()` returns a handler name.
  std::optional<ScriptMessageHandler> GetScriptMessageHandler() const;

  JavaScriptFeature(const JavaScriptFeature&) = delete;

 protected:
  explicit JavaScriptFeature(ContentWorld supported_world);

  // Calls `function_name` with `parameters` in `web_frame` within the content
  // world that this feature has been configured. `web_frame` must not be null.
  // See WebFrame::CallJavaScriptFunction for more details.
  bool CallJavaScriptFunction(WebFrame* web_frame,
                              const std::string& function_name,
                              const base::Value::List& parameters);

  // Calls `function_name` with `parameters` in `web_frame` within the content
  // world that this feature has been configured. `callback` will be called with
  // the return value of the function if it completes within `timeout`.
  // `web_frame` must not be null.
  // See WebFrame::CallJavaScriptFunction for more details.
  bool CallJavaScriptFunction(
      WebFrame* web_frame,
      const std::string& function_name,
      const base::Value::List& parameters,
      base::OnceCallback<void(const base::Value*)> callback,
      base::TimeDelta timeout);

  // Use of this function is DISCOURAGED. Prefer the `CallJavaScriptFunction`
  // family of functions instead to keep the API clear and well defined.
  // Executes `script` in `web_frame` within the content world that this feature
  // has been configured.
  // See WebFrame::ExecuteJavaScript for more details on `callback`.
  bool ExecuteJavaScript(WebFrame* web_frame,
                         const std::u16string& script,
                         ExecuteJavaScriptCallbackWithError callback);

  // Callback for script messages registered through `GetScriptMessageHandler`.
  // `ScriptMessageReceived` is called when `web_state` receives a `message`.
  // `web_state` will always be non-null.
  virtual void ScriptMessageReceived(WebState* web_state,
                                     const ScriptMessage& message);

 private:
  ContentWorld supported_world_;
  const std::vector<FeatureScript> scripts_;
  const std::vector<const JavaScriptFeature*> dependent_features_;
  base::WeakPtrFactory<JavaScriptFeature> weak_factory_;
};

}  // namespace web

#endif  // IOS_WEB_PUBLIC_JS_MESSAGING_JAVA_SCRIPT_FEATURE_H_