// 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.
#import "ios/chrome/browser/link_to_text/model/link_to_text_java_script_feature.h"
#import "base/barrier_callback.h"
#import "base/no_destructor.h"
#import "base/ranges/algorithm.h"
#import "base/timer/elapsed_timer.h"
#import "components/shared_highlighting/core/common/disabled_sites.h"
#import "components/shared_highlighting/core/common/shared_highlighting_features.h"
#import "ios/chrome/browser/link_to_text/model/link_to_text_constants.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"
#import "url/gurl.h"
namespace {
const char kScriptName[] = "link_to_text";
const char kGetLinkToTextFunction[] = "linkToText.getLinkToText";
bool IsKnownAmpCache(web::WebFrame* frame) {
GURL origin = frame->GetSecurityOrigin();
// Source:
// https://github.com/ampproject/amphtml/blob/main/build-system/global-configs/caches.json
return origin.DomainIs("ampproject.org") || origin.DomainIs("bing-amp.com");
}
LinkToTextResponse* ParseResponse(base::WeakPtr<web::WebState> web_state,
const base::ElapsedTimer& timer,
const base::Value* value) {
return [LinkToTextResponse linkToTextResponseWithValue:value
webState:web_state.get()
latency:timer.Elapsed()];
}
} // namespace
LinkToTextJavaScriptFeature::LinkToTextJavaScriptFeature()
: JavaScriptFeature(
web::ContentWorld::kIsolatedWorld,
{FeatureScript::CreateWithFilename(
kScriptName,
FeatureScript::InjectionTime::kDocumentStart,
FeatureScript::TargetFrames::kAllFrames,
FeatureScript::ReinjectionBehavior::kInjectOncePerWindow)}),
weak_ptr_factory_(this) {}
LinkToTextJavaScriptFeature::~LinkToTextJavaScriptFeature() = default;
// static
LinkToTextJavaScriptFeature* LinkToTextJavaScriptFeature::GetInstance() {
static base::NoDestructor<LinkToTextJavaScriptFeature> instance;
return instance.get();
}
void LinkToTextJavaScriptFeature::GetLinkToText(
web::WebState* web_state,
base::OnceCallback<void(LinkToTextResponse*)> callback) {
base::ElapsedTimer link_generation_timer;
RunGenerationJS(
web_state->GetPageWorldWebFramesManager()->GetMainWebFrame(),
base::BindOnce(&LinkToTextJavaScriptFeature::HandleResponse,
weak_ptr_factory_.GetWeakPtr(), web_state->GetWeakPtr(),
std::move(link_generation_timer), std::move(callback)));
}
void LinkToTextJavaScriptFeature::RunGenerationJS(
web::WebFrame* frame,
base::OnceCallback<void(const base::Value*)> callback) {
CallJavaScriptFunction(frame, kGetLinkToTextFunction, /* parameters= */ {},
std::move(callback),
link_to_text::kLinkGenerationTimeout);
}
// static
bool LinkToTextJavaScriptFeature::ShouldAttemptIframeGeneration(
std::optional<shared_highlighting::LinkGenerationError> error,
const GURL& main_frame_url) {
if (!base::FeatureList::IsEnabled(
shared_highlighting::kSharedHighlightingAmp)) {
return false;
}
if (!error ||
error.value() !=
shared_highlighting::LinkGenerationError::kIncorrectSelector) {
return false;
}
return shared_highlighting::SupportsLinkGenerationInIframe(main_frame_url);
}
void LinkToTextJavaScriptFeature::HandleResponse(
base::WeakPtr<web::WebState> web_state,
base::ElapsedTimer link_generation_timer,
base::OnceCallback<void(LinkToTextResponse*)> final_callback,
const base::Value* response) {
LinkToTextResponse* parsed_response = [LinkToTextResponse
linkToTextResponseWithValue:response
webState:web_state.get()
latency:link_generation_timer.Elapsed()];
std::optional<shared_highlighting::LinkGenerationError> error =
[parsed_response error];
std::vector<web::WebFrame*> amp_frames;
if (web_state &&
ShouldAttemptIframeGeneration(error, web_state->GetLastCommittedURL())) {
base::ranges::copy_if(
web_state->GetPageWorldWebFramesManager()->GetAllWebFrames(),
std::back_inserter(amp_frames), IsKnownAmpCache);
}
// Empty indicates we're not attempting AMP generation (e.g., succeeded or
// conclusively failed on main frame, feature is disabled, no AMP frames
// found, etc.) so run the callback immediately.
if (amp_frames.empty()) {
std::move(final_callback).Run(parsed_response);
return;
}
// The response will be parsed immediately after the call finishes, and the
// result will be held on to by the BarrierCallback until all the calls have
// returned. Because LinkToTextResponse* is managed by ARC, this is OK even
// though the original base::Value* will go out of scope.
const auto parse_value = base::BindRepeating(
&ParseResponse, web_state, std::move(link_generation_timer));
const auto accumulate_subframe_results =
base::BarrierCallback<LinkToTextResponse*>(
amp_frames.size(),
base::BindOnce(
&LinkToTextJavaScriptFeature::HandleResponseFromSubframe,
weak_ptr_factory_.GetWeakPtr(), std::move(final_callback)));
for (auto* frame : amp_frames) {
RunGenerationJS(frame, parse_value.Then(accumulate_subframe_results));
}
}
void LinkToTextJavaScriptFeature::HandleResponseFromSubframe(
base::OnceCallback<void(LinkToTextResponse*)> final_callback,
std::vector<LinkToTextResponse*> parsed_responses) {
DCHECK(!parsed_responses.empty());
// First, see if we succeeded in any frame.
auto success_response = base::ranges::find_if_not(
parsed_responses,
[](LinkToTextResponse* response) { return response.payload == nil; });
if (success_response != parsed_responses.end()) {
std::move(final_callback).Run(*success_response);
return;
}
// If not, look for a frame where we failed with an error other than Incorrect
// Selector. There should be at most one of these (since every frame with no
// user selection should return Incorrect Selector).
auto error_response = base::ranges::find_if_not(
parsed_responses, [](LinkToTextResponse* response) {
return [response error].value() ==
shared_highlighting::LinkGenerationError::kIncorrectSelector;
});
if (error_response != parsed_responses.end()) {
std::move(final_callback).Run(*error_response);
return;
}
// All the frames have Incorrect Selector, so just use the first one.
std::move(final_callback).Run(parsed_responses[0]);
}