blob: 34472675e60c1864d326ca9d80c9b7dc0d9923b2 [file] [log] [blame]
// Copyright 2021 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/subapps/sub_apps.h"
#include <utility>
#include "base/check.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_sub_apps_add_params.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_sub_apps_list_result.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_sub_apps_result_code.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/navigator.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
using mojom::blink::SubAppsService;
using mojom::blink::SubAppsServiceAddParameters;
using mojom::blink::SubAppsServiceAddParametersPtr;
using mojom::blink::SubAppsServiceAddResultPtr;
using mojom::blink::SubAppsServiceListResultEntryPtr;
using mojom::blink::SubAppsServiceListResultPtr;
using mojom::blink::SubAppsServiceRemoveResultPtr;
using mojom::blink::SubAppsServiceResultCode;
namespace {
const int kMaximumNumberOfSubappsPerAddCall = 7;
Vector<std::pair<String, V8SubAppsResultCode>> AddResultsFromMojo(
Vector<SubAppsServiceAddResultPtr> add_results_mojo) {
Vector<std::pair<String, V8SubAppsResultCode>> add_results_idl;
for (auto& add_result : add_results_mojo) {
auto result_code =
add_result->result_code == SubAppsServiceResultCode::kSuccess
? V8SubAppsResultCode(V8SubAppsResultCode::Enum::kSuccess)
: V8SubAppsResultCode(V8SubAppsResultCode::Enum::kFailure);
add_results_idl.emplace_back(add_result->unhashed_app_id_path, result_code);
}
return add_results_idl;
}
Vector<std::pair<String, V8SubAppsResultCode>> RemoveResultsFromMojo(
Vector<SubAppsServiceRemoveResultPtr> remove_results_mojo) {
Vector<std::pair<String, V8SubAppsResultCode>> results;
for (auto& remove_result : remove_results_mojo) {
auto result_code =
remove_result->result_code == SubAppsServiceResultCode::kSuccess
? V8SubAppsResultCode(V8SubAppsResultCode::Enum::kSuccess)
: V8SubAppsResultCode(V8SubAppsResultCode::Enum::kFailure);
results.emplace_back(remove_result->unhashed_app_id_path, result_code);
}
return results;
}
Vector<SubAppsServiceAddParametersPtr> AddOptionsToMojo(
HeapVector<std::pair<String, Member<SubAppsAddParams>>>
sub_apps_to_add_idl) {
Vector<SubAppsServiceAddParametersPtr> sub_apps_to_add_mojo;
for (auto& [unhashed_app_id_path, add_params] : sub_apps_to_add_idl) {
sub_apps_to_add_mojo.emplace_back(SubAppsServiceAddParameters::New(
unhashed_app_id_path, add_params->installURL()));
}
return sub_apps_to_add_mojo;
}
HeapVector<std::pair<String, Member<SubAppsListResult>>> ListResultsFromMojo(
Vector<SubAppsServiceListResultEntryPtr> sub_apps_list_mojo) {
HeapVector<std::pair<String, Member<SubAppsListResult>>> sub_apps_list_idl;
for (auto& sub_app_entry : sub_apps_list_mojo) {
SubAppsListResult* list_result = SubAppsListResult::Create();
list_result->setAppName(std::move(sub_app_entry->app_name));
sub_apps_list_idl.emplace_back(
std::move(sub_app_entry->unhashed_app_id_path), list_result);
}
return sub_apps_list_idl;
}
} // namespace
// static
const char SubApps::kSupplementName[] = "SubApps";
SubApps::SubApps(Navigator& navigator)
: Supplement<Navigator>(navigator),
service_(navigator.GetExecutionContext()) {}
// static
SubApps* SubApps::subApps(Navigator& navigator) {
SubApps* subapps = Supplement<Navigator>::From<SubApps>(navigator);
if (!subapps) {
subapps = MakeGarbageCollected<SubApps>(navigator);
ProvideTo(navigator, subapps);
}
return subapps;
}
void SubApps::Trace(Visitor* visitor) const {
ScriptWrappable::Trace(visitor);
Supplement<Navigator>::Trace(visitor);
visitor->Trace(service_);
}
HeapMojoRemote<SubAppsService>& SubApps::GetService() {
if (!service_.is_bound()) {
auto* context = GetSupplementable()->GetExecutionContext();
context->GetBrowserInterfaceBroker().GetInterface(
service_.BindNewPipeAndPassReceiver(
context->GetTaskRunner(TaskType::kMiscPlatformAPI)));
// In case the other endpoint gets disconnected, we want to reset our end of
// the pipe as well so that we don't remain connected to a half-open pipe.
service_.set_disconnect_handler(
WTF::BindOnce(&SubApps::OnConnectionError, WrapWeakPersistent(this)));
}
return service_;
}
void SubApps::OnConnectionError() {
service_.reset();
}
ScriptPromise SubApps::add(
ScriptState* script_state,
const HeapVector<std::pair<String, Member<SubAppsAddParams>>>&
sub_apps_to_add,
ExceptionState& exception_state) {
// [SecureContext] from the IDL ensures this.
DCHECK(ExecutionContext::From(script_state)->IsSecureContext());
if (!CheckPreconditionsMaybeThrow(exception_state)) {
return ScriptPromise();
}
LocalFrame* frame = GetSupplementable()->DomWindow()->GetFrame();
// TODO(crbug.com/1326843): Maybe we don't need user activation if
// the right policy is set.
if (!LocalFrame::ConsumeTransientUserActivation(frame)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotAllowedError,
"Unable to add sub-app. This API can only be called shortly after a "
"user activation.");
return ScriptPromise();
}
// TODO(crbug.com/1326843): Maybe we don't need to limit add() if the
// right policy is set, we mainly want to avoid overwhelming the user with
// a permissions prompt that lists dozens of apps to install.
if (sub_apps_to_add.size() > kMaximumNumberOfSubappsPerAddCall) {
exception_state.ThrowDOMException(
DOMExceptionCode::kDataError,
"Unable to add sub-apps. The maximum number of apps added per call "
"is " +
String::Number(kMaximumNumberOfSubappsPerAddCall) + ", but " +
String::Number(sub_apps_to_add.size()) + " were provided.");
return ScriptPromise();
}
// Check that the arguments are root-relative paths.
for (const auto& [unhashed_app_id_path, add_params] : sub_apps_to_add) {
if (KURL(unhashed_app_id_path).IsValid() ||
KURL(add_params->installURL()).IsValid()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Arguments must be root-relative paths.");
return ScriptPromise();
}
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
GetService()->Add(
AddOptionsToMojo(std::move(sub_apps_to_add)),
resolver->WrapCallbackInScriptScope(
WTF::BindOnce([](ScriptPromiseResolver* resolver,
Vector<SubAppsServiceAddResultPtr> results_mojo) {
for (const auto& add_result : results_mojo) {
if (add_result->result_code ==
SubAppsServiceResultCode::kFailure) {
return resolver->Reject(
AddResultsFromMojo(std::move(results_mojo)));
}
}
resolver->Resolve(AddResultsFromMojo(std::move(results_mojo)));
})));
return resolver->Promise();
}
ScriptPromise SubApps::list(ScriptState* script_state,
ExceptionState& exception_state) {
if (!CheckPreconditionsMaybeThrow(exception_state)) {
return ScriptPromise();
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
GetService()->List(resolver->WrapCallbackInScriptScope(WTF::BindOnce(
[](ScriptPromiseResolver* resolver, SubAppsServiceListResultPtr result) {
if (result->result_code == SubAppsServiceResultCode::kSuccess) {
resolver->Resolve(
ListResultsFromMojo(std::move(result->sub_apps_list)));
} else {
resolver->Reject(V8ThrowDOMException::CreateOrDie(
resolver->GetScriptState()->GetIsolate(),
DOMExceptionCode::kOperationError,
"Unable to list sub-apps. Check whether the calling app is "
"installed."));
}
})));
return resolver->Promise();
}
ScriptPromise SubApps::remove(ScriptState* script_state,
const Vector<String>& unhashed_app_id_paths,
ExceptionState& exception_state) {
if (!CheckPreconditionsMaybeThrow(exception_state)) {
return ScriptPromise();
}
// Check that the arguments are root-relative paths.
for (const auto& unhashed_app_id_path : unhashed_app_id_paths) {
if (KURL(unhashed_app_id_path).IsValid()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Arguments must be root-relative paths.");
return ScriptPromise();
}
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
GetService()->Remove(
unhashed_app_id_paths,
resolver->WrapCallbackInScriptScope(
WTF::BindOnce([](ScriptPromiseResolver* resolver,
Vector<SubAppsServiceRemoveResultPtr> results_mojo) {
for (const auto& remove_result : results_mojo) {
if (remove_result->result_code ==
SubAppsServiceResultCode::kFailure) {
return resolver->Reject(
RemoveResultsFromMojo(std::move(results_mojo)));
}
}
resolver->Resolve(RemoveResultsFromMojo(std::move(results_mojo)));
})));
return resolver->Promise();
}
bool SubApps::CheckPreconditionsMaybeThrow(ExceptionState& exception_state) {
Navigator* const navigator = GetSupplementable();
if (!navigator->DomWindow()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotFoundError,
"The object is no longer associated to a document.");
return false;
}
if (!navigator->DomWindow()->GetFrame()->IsMainFrame() ||
navigator->DomWindow()->GetFrame()->GetPage()->IsPrerendering() ||
navigator->DomWindow()->GetFrame()->IsInFencedFrameTree()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"API is only supported in primary top-level browsing contexts.");
return false;
}
return true;
}
} // namespace blink