// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/broadcastchannel/broadcast_channel.h"

#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/frame/back_forward_cache_controller.mojom-blink.h"
#include "third_party/blink/public/mojom/navigation/renderer_eviction_reason.mojom-blink.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/core/event_target_names.h"
#include "third_party/blink/renderer/core/events/message_event.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

namespace blink {

namespace {

// To ensure proper ordering of messages sent to/from multiple BroadcastChannel
// instances in the same thread, this uses one BroadcastChannelProvider
// connection as basis for all connections to channels from the same thread. The
// actual connections used to send/receive messages are then created using
// associated interfaces, ensuring proper message ordering. Note that this
// approach only works in the case of workers, since each worker has it's own
// thread.
mojo::Remote<mojom::blink::BroadcastChannelProvider>&
GetWorkerThreadSpecificProvider(WorkerGlobalScope& worker_global_scope) {
  DEFINE_THREAD_SAFE_STATIC_LOCAL(
      ThreadSpecific<mojo::Remote<mojom::blink::BroadcastChannelProvider>>,
      provider, ());
  if (!provider.IsSet()) {
    worker_global_scope.GetBrowserInterfaceBroker().GetInterface(
        provider->BindNewPipeAndPassReceiver());
  }
  return *provider;
}

}  // namespace

// static
BroadcastChannel* BroadcastChannel::Create(ExecutionContext* execution_context,
                                           const String& name) {
  LocalDOMWindow* window = DynamicTo<LocalDOMWindow>(execution_context);
  if (window && window->IsCrossSiteSubframe())
    UseCounter::Count(window, WebFeature::kThirdPartyBroadcastChannel);

  if (auto* worker_global_scope =
          DynamicTo<WorkerGlobalScope>(execution_context)) {
    if (worker_global_scope->Url().ProtocolIsData()) {
      UseCounter::Count(worker_global_scope,
                        WebFeature::kDataUrlWorkerBroadcastChannel);
    }
  }

  return MakeGarbageCollected<BroadcastChannel>(execution_context, name);
}

BroadcastChannel::~BroadcastChannel() = default;

void BroadcastChannel::Dispose() {
  CloseInternal();
}

void BroadcastChannel::postMessage(const ScriptValue& message,
                                   ExceptionState& exception_state) {
  // If the receiver is not bound because `close` was called on this
  // BroadcastChannel instance, raise an exception per the spec. Otherwise,
  // in cases like the instance being created in an iframe that is now detached,
  // just ignore the postMessage call.
  if (!receiver_.is_bound()) {
    if (explicitly_closed_) {
      exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                        "Channel is closed");
    }
    return;
  }

  // Silently ignore the postMessage call if this BroadcastChannel instance is
  // associated with a closing worker. This case needs to be handled explicitly
  // because the mojo connection to the worker won't be torn down until the
  // worker actually goes away.
  ExecutionContext* execution_context = GetExecutionContext();
  if (execution_context->IsWorkerGlobalScope()) {
    WorkerGlobalScope* worker_global_scope =
        DynamicTo<WorkerGlobalScope>(execution_context);
    DCHECK(worker_global_scope);
    if (worker_global_scope->IsClosing()) {
      return;
    }
  }

  scoped_refptr<SerializedScriptValue> value = SerializedScriptValue::Serialize(
      message.GetIsolate(), message.V8Value(),
      SerializedScriptValue::SerializeOptions(), exception_state);
  if (exception_state.HadException())
    return;

  // Defer postMessage() from a prerendered page until page activation.
  // https://wicg.github.io/nav-speculation/prerendering.html#patch-broadcast-channel
  if (execution_context->IsWindow()) {
    Document* document = To<LocalDOMWindow>(execution_context)->document();
    if (document->IsPrerendering()) {
      document->AddPostPrerenderingActivationStep(blink::BindOnce(
          &BroadcastChannel::PostMessageInternal, WrapWeakPersistent(this),
          std::move(value),
          execution_context->GetSecurityOrigin()->IsolatedCopy(),
          execution_context->GetAgentClusterID()));
      return;
    }
  }

  PostMessageInternal(std::move(value),
                      execution_context->GetSecurityOrigin()->IsolatedCopy(),
                      execution_context->GetAgentClusterID());
}

void BroadcastChannel::PostMessageInternal(
    scoped_refptr<SerializedScriptValue> value,
    scoped_refptr<SecurityOrigin> sender_origin,
    const base::UnguessableToken sender_agent_cluster_id) {
  if (!receiver_.is_bound())
    return;
  BlinkCloneableMessage msg;
  msg.message = std::move(value);
  msg.sender_origin = std::move(sender_origin);
  msg.sender_agent_cluster_id = sender_agent_cluster_id;
  msg.locked_to_sender_agent_cluster = msg.message->IsLockedToAgentCluster();
  remote_client_->OnMessage(std::move(msg));
}

void BroadcastChannel::close() {
  explicitly_closed_ = true;
  CloseInternal();
}

void BroadcastChannel::CloseInternal() {
  remote_client_.reset();
  if (receiver_.is_bound())
    receiver_.reset();
  if (associated_remote_.is_bound())
    associated_remote_.reset();
  feature_handle_for_scheduler_.reset();
}

const AtomicString& BroadcastChannel::InterfaceName() const {
  return event_target_names::kBroadcastChannel;
}

bool BroadcastChannel::HasPendingActivity() const {
  return receiver_.is_bound() && HasEventListeners(event_type_names::kMessage);
}

void BroadcastChannel::ContextDestroyed() {
  CloseInternal();
}

void BroadcastChannel::Trace(Visitor* visitor) const {
  ExecutionContextLifecycleObserver::Trace(visitor);
  EventTarget::Trace(visitor);
  visitor->Trace(receiver_);
  visitor->Trace(remote_client_);
  visitor->Trace(associated_remote_);
}

void BroadcastChannel::OnMessage(BlinkCloneableMessage message) {
  auto* context = GetExecutionContext();

  // Queue a task to dispatch the event.
  MessageEvent* event;
  if ((!message.locked_to_sender_agent_cluster ||
       context->IsSameAgentCluster(message.sender_agent_cluster_id)) &&
      message.message->CanDeserializeIn(context)) {
    event = MessageEvent::Create(nullptr, std::move(message.message),
                                 context->GetSecurityOrigin(),
                                 MessageEvent::kMessageIsSameOrigin,
                                 /* last_event_id=*/{}, /* source=*/nullptr);
  } else {
    event = MessageEvent::CreateError(context->GetSecurityOrigin());
  }

  if (base::FeatureList::IsEnabled(features::kBFCacheOpenBroadcastChannel) &&
      context->is_in_back_forward_cache()) {
    LocalDOMWindow* window = DynamicTo<LocalDOMWindow>(context);
    CHECK(window);
    if (LocalFrame* frame = window->GetFrame()) {
      base::UmaHistogramEnumeration(
          "BackForwardCache.Eviction.Renderer",
          mojom::blink::RendererEvictionReason::kBroadcastChannelOnMessage);
      // We don't need to report the source location of a broadcast channel.
      frame->GetBackForwardCacheControllerHostRemote()
          .EvictFromBackForwardCache(
              mojom::blink::RendererEvictionReason::kBroadcastChannelOnMessage,
              /*source=*/nullptr);
    }
    return;
  }
  // <specdef
  // href="https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-postmessage">
  // <spec>The tasks must use the DOM manipulation task source, and, for
  // those where the event loop specified by the target BroadcastChannel
  // object's BroadcastChannel settings object is a window event loop,
  // must be associated with the responsible document specified by that
  // target BroadcastChannel object's BroadcastChannel settings object.
  // </spec>
  DispatchEvent(*event);
}

void BroadcastChannel::OnError() {
  CloseInternal();
}

BroadcastChannel::BroadcastChannel(ExecutionContext* execution_context,
                                   const String& name)
    : BroadcastChannel(execution_context,
                       name,
                       mojo::NullAssociatedReceiver(),
                       mojo::NullAssociatedRemote()) {}

BroadcastChannel::BroadcastChannel(
    base::PassKey<StorageAccessHandle>,
    ExecutionContext* execution_context,
    const String& name,
    mojom::blink::BroadcastChannelProvider* provider)
    : ActiveScriptWrappable<BroadcastChannel>({}),
      ExecutionContextLifecycleObserver(execution_context),
      name_(name),
      receiver_(this, execution_context),
      remote_client_(execution_context),
      associated_remote_(execution_context) {
  if (!base::FeatureList::IsEnabled(features::kBFCacheOpenBroadcastChannel)) {
    feature_handle_for_scheduler_ =
        execution_context->GetScheduler()->RegisterFeature(
            SchedulingPolicy::Feature::kBroadcastChannel,
            {SchedulingPolicy::DisableBackForwardCache()});
  }
  provider->ConnectToChannel(
      name_,
      receiver_.BindNewEndpointAndPassRemote(
          execution_context->GetTaskRunner(TaskType::kInternalDefault)),
      remote_client_.BindNewEndpointAndPassReceiver(
          execution_context->GetTaskRunner(TaskType::kInternalDefault)));
  SetupDisconnectHandlers();
}

BroadcastChannel::BroadcastChannel(
    base::PassKey<BroadcastChannelTester>,
    ExecutionContext* execution_context,
    const String& name,
    mojo::PendingAssociatedReceiver<mojom::blink::BroadcastChannelClient>
        receiver,
    mojo::PendingAssociatedRemote<mojom::blink::BroadcastChannelClient> remote)
    : BroadcastChannel(execution_context,
                       name,
                       std::move(receiver),
                       std::move(remote)) {}

BroadcastChannel::BroadcastChannel(
    ExecutionContext* execution_context,
    const String& name,
    mojo::PendingAssociatedReceiver<mojom::blink::BroadcastChannelClient>
        receiver,
    mojo::PendingAssociatedRemote<mojom::blink::BroadcastChannelClient> remote)
    : ActiveScriptWrappable<BroadcastChannel>({}),
      ExecutionContextLifecycleObserver(execution_context),
      name_(name),
      receiver_(this, execution_context),
      remote_client_(execution_context),
      associated_remote_(execution_context) {
  if (!base::FeatureList::IsEnabled(features::kBFCacheOpenBroadcastChannel)) {
    feature_handle_for_scheduler_ =
        execution_context->GetScheduler()->RegisterFeature(
            SchedulingPolicy::Feature::kBroadcastChannel,
            {SchedulingPolicy::DisableBackForwardCache()});
  }
  // Note: We cannot associate per-frame task runner here, but postTask
  //       to it manually via EnqueueEvent, since the current expectation
  //       is to receive messages even after close for which queued before
  //       close.
  //       https://github.com/whatwg/html/issues/1319
  //       Relying on Mojo binding will cancel the enqueued messages
  //       at close().

  // The BroadcastChannel spec indicates that messages should be delivered to
  // BroadcastChannel objects in the order in which they were created, so it's
  // important that the ordering of ConnectToChannel messages (used to create
  // the corresponding state in the browser process) is preserved. We accomplish
  // this using two approaches, depending on the context:
  //
  //  - In the frame case, we create a new navigation associated remote for each
  //    BroadcastChannel instance and leverage it to ensure in-order delivery
  //    and delivery to the RenderFrameHostImpl object that corresponds to the
  //    current frame.
  //
  //  - In the worker case, since each worker runs in its own thread, we use a
  //    shared remote for all BroadcastChannel objects created on that thread to
  //    ensure in-order delivery of messages to the appropriate *WorkerHost
  //    object.
  auto receiver_task_runner =
      execution_context->GetTaskRunner(TaskType::kInternalDefault);
  auto client_task_runner =
      execution_context->GetTaskRunner(TaskType::kInternalDefault);
  if (receiver.is_valid() && remote.is_valid()) {
    receiver_.Bind(std::move(receiver), receiver_task_runner);
    remote_client_.Bind(std::move(remote), client_task_runner);
  } else if (auto* window = DynamicTo<LocalDOMWindow>(execution_context)) {
    LocalFrame* frame = window->GetFrame();
    if (!frame)
      return;

    frame->GetRemoteNavigationAssociatedInterfaces()->GetInterface(
        associated_remote_.BindNewEndpointAndPassReceiver(
            execution_context->GetTaskRunner(TaskType::kInternalDefault)));
    associated_remote_->ConnectToChannel(
        name_, receiver_.BindNewEndpointAndPassRemote(receiver_task_runner),
        remote_client_.BindNewEndpointAndPassReceiver(client_task_runner));
  } else if (auto* worker_global_scope =
                 DynamicTo<WorkerGlobalScope>(execution_context)) {
    if (worker_global_scope->IsClosing())
      return;

    mojo::Remote<mojom::blink::BroadcastChannelProvider>& provider =
        GetWorkerThreadSpecificProvider(*worker_global_scope);
    provider->ConnectToChannel(
        name_, receiver_.BindNewEndpointAndPassRemote(receiver_task_runner),
        remote_client_.BindNewEndpointAndPassReceiver(client_task_runner));
  } else {
    NOTREACHED();
  }

  SetupDisconnectHandlers();
}

void BroadcastChannel::SetupDisconnectHandlers() {
  receiver_.set_disconnect_handler(
      BindOnce(&BroadcastChannel::OnError, WrapWeakPersistent(this)));
  remote_client_.set_disconnect_handler(
      BindOnce(&BroadcastChannel::OnError, WrapWeakPersistent(this)));
}

bool BroadcastChannel::IsRemoteClientConnectedForTesting() const {
  return remote_client_.is_connected();
}

}  // namespace blink
