blob: c3b668fb097008c6f7ae4442d9906d20650458c4 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/translate/content/renderer/translate_agent.h"
#include <utility>
#include "base/bind.h"
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/json/string_escape.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/translate/core/common/translate_constants.h"
#include "components/translate/core/common/translate_metrics.h"
#include "components/translate/core/common/translate_util.h"
#include "components/translate/core/language_detection/language_detection_util.h"
#include "content/public/common/content_constants.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/web_isolated_world_info.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_language_detection_details.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_script_source.h"
#include "url/gurl.h"
#include "v8/include/v8.h"
using blink::WebDocument;
using blink::WebLanguageDetectionDetails;
using blink::WebLocalFrame;
using blink::WebScriptSource;
using blink::WebSecurityOrigin;
using blink::WebString;
using blink::WebVector;
namespace {
// The delay in milliseconds that we'll wait before checking to see if the
// translate library injected in the page is ready.
const int kTranslateInitCheckDelayMs = 150;
// The maximum number of times we'll check to see if the translate library
// injected in the page is ready.
const int kMaxTranslateInitCheckAttempts = 5;
// The delay we wait in milliseconds before checking whether the translation has
// finished.
const int kTranslateStatusCheckDelayMs = 400;
// Language name passed to the Translate element for it to detect the language.
const char kAutoDetectionLanguage[] = "auto";
// Isolated world sets following content-security-policy.
const char kContentSecurityPolicy[] = "script-src 'self' 'unsafe-eval'";
} // namespace
namespace translate {
////////////////////////////////////////////////////////////////////////////////
// TranslateAgent, public:
TranslateAgent::TranslateAgent(content::RenderFrame* render_frame,
int world_id,
const std::string& extension_scheme)
: content::RenderFrameObserver(render_frame),
world_id_(world_id),
extension_scheme_(extension_scheme) {
translate_task_runner_ = this->render_frame()->GetTaskRunner(
blink::TaskType::kInternalTranslation);
}
TranslateAgent::~TranslateAgent() {}
void TranslateAgent::PrepareForUrl(const GURL& url) {
// Navigated to a new url, reset current page translation.
ResetPage();
}
void TranslateAgent::PageCaptured(const base::string16& contents) {
// Get the document language as set by WebKit from the http-equiv
// meta tag for "content-language". This may or may not also
// have a value derived from the actual Content-Language HTTP
// header. The two actually have different meanings (despite the
// original intent of http-equiv to be an equivalent) with the former
// being the language of the document and the latter being the
// language of the intended audience (a distinction really only
// relevant for things like langauge textbooks). This distinction
// shouldn't affect translation.
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame)
return;
WebDocument document = main_frame->GetDocument();
WebLanguageDetectionDetails web_detection_details =
WebLanguageDetectionDetails::CollectLanguageDetectionDetails(document);
std::string content_language = web_detection_details.content_language.Utf8();
std::string html_lang = web_detection_details.html_language.Utf8();
std::string cld_language;
bool is_cld_reliable;
std::string language = DeterminePageLanguage(
content_language, html_lang, contents, &cld_language, &is_cld_reliable);
if (language.empty())
return;
language_determined_time_ = base::TimeTicks::Now();
LanguageDetectionDetails details;
details.time = base::Time::Now();
details.url = web_detection_details.url;
details.content_language = content_language;
details.cld_language = cld_language;
details.is_cld_reliable = is_cld_reliable;
details.has_notranslate = web_detection_details.has_no_translate_meta;
details.html_root_language = html_lang;
details.adopted_language = language;
// TODO(hajimehoshi): If this affects performance, it should be set only if
// translate-internals tab exists.
details.contents = contents;
// For the same render frame with the same url, each time when its texts are
// captured, it should be treated as a new page to do translation.
ResetPage();
GetTranslateHandler()->RegisterPage(
receiver_.BindNewPipeAndPassRemote(
main_frame->GetTaskRunner(blink::TaskType::kInternalTranslation)),
details, !details.has_notranslate && !language.empty());
}
void TranslateAgent::CancelPendingTranslation() {
weak_method_factory_.InvalidateWeakPtrs();
// Make sure to send the cancelled response back.
if (translate_callback_pending_) {
std::move(translate_callback_pending_)
.Run(true, source_lang_, target_lang_, TranslateErrors::NONE);
}
source_lang_.clear();
target_lang_.clear();
}
////////////////////////////////////////////////////////////////////////////////
// TranslateAgent, protected:
bool TranslateAgent::IsTranslateLibAvailable() {
return ExecuteScriptAndGetBoolResult(
"typeof cr != 'undefined' && typeof cr.googleTranslate != 'undefined' && "
"typeof cr.googleTranslate.translate == 'function'",
false);
}
bool TranslateAgent::IsTranslateLibReady() {
return ExecuteScriptAndGetBoolResult("cr.googleTranslate.libReady", false);
}
bool TranslateAgent::HasTranslationFinished() {
return ExecuteScriptAndGetBoolResult("cr.googleTranslate.finished", true);
}
bool TranslateAgent::HasTranslationFailed() {
return ExecuteScriptAndGetBoolResult("cr.googleTranslate.error", true);
}
int64_t TranslateAgent::GetErrorCode() {
int64_t error_code =
ExecuteScriptAndGetIntegerResult("cr.googleTranslate.errorCode");
DCHECK_LT(error_code, static_cast<int>(TranslateErrors::TRANSLATE_ERROR_MAX));
return error_code;
}
bool TranslateAgent::StartTranslation() {
return ExecuteScriptAndGetBoolResult(
BuildTranslationScript(source_lang_, target_lang_), false);
}
std::string TranslateAgent::GetOriginalPageLanguage() {
return ExecuteScriptAndGetStringResult("cr.googleTranslate.sourceLang");
}
base::TimeDelta TranslateAgent::AdjustDelay(int delay_in_milliseconds) {
// Just converts |delay_in_milliseconds| without any modification in practical
// cases. Tests will override this function to return modified value.
return base::TimeDelta::FromMilliseconds(delay_in_milliseconds);
}
void TranslateAgent::ExecuteScript(const std::string& script) {
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame)
return;
WebScriptSource source = WebScriptSource(WebString::FromASCII(script));
main_frame->ExecuteScriptInIsolatedWorld(world_id_, source);
}
bool TranslateAgent::ExecuteScriptAndGetBoolResult(const std::string& script,
bool fallback) {
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame)
return fallback;
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
WebScriptSource source = WebScriptSource(WebString::FromASCII(script));
v8::Local<v8::Value> result =
main_frame->ExecuteScriptInIsolatedWorldAndReturnValue(world_id_, source);
if (result.IsEmpty() || !result->IsBoolean()) {
NOTREACHED();
return fallback;
}
return result.As<v8::Boolean>()->Value();
}
std::string TranslateAgent::ExecuteScriptAndGetStringResult(
const std::string& script) {
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame)
return std::string();
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
WebScriptSource source = WebScriptSource(WebString::FromASCII(script));
v8::Local<v8::Value> result =
main_frame->ExecuteScriptInIsolatedWorldAndReturnValue(world_id_, source);
if (result.IsEmpty() || !result->IsString()) {
NOTREACHED();
return std::string();
}
v8::Local<v8::String> v8_str = result.As<v8::String>();
int length = v8_str->Utf8Length(isolate) + 1;
std::unique_ptr<char[]> str(new char[length]);
v8_str->WriteUtf8(isolate, str.get(), length);
return std::string(str.get());
}
double TranslateAgent::ExecuteScriptAndGetDoubleResult(
const std::string& script) {
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame)
return 0.0;
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
WebScriptSource source = WebScriptSource(WebString::FromASCII(script));
v8::Local<v8::Value> result =
main_frame->ExecuteScriptInIsolatedWorldAndReturnValue(world_id_, source);
if (result.IsEmpty() || !result->IsNumber()) {
NOTREACHED();
return 0.0;
}
return result.As<v8::Number>()->Value();
}
int64_t TranslateAgent::ExecuteScriptAndGetIntegerResult(
const std::string& script) {
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame)
return 0;
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
WebScriptSource source = WebScriptSource(WebString::FromASCII(script));
v8::Local<v8::Value> result =
main_frame->ExecuteScriptInIsolatedWorldAndReturnValue(world_id_, source);
if (result.IsEmpty() || !result->IsNumber()) {
NOTREACHED();
return 0;
}
return result.As<v8::Integer>()->Value();
}
// mojom::TranslateAgent implementations.
void TranslateAgent::GetWebLanguageDetectionDetails(
GetWebLanguageDetectionDetailsCallback callback) {
NOTREACHED() << "This interface supported by PerFrameTranslateAgent";
}
void TranslateAgent::TranslateFrame(const std::string& translate_script,
const std::string& source_lang,
const std::string& target_lang,
TranslateFrameCallback callback) {
WebLocalFrame* main_frame = render_frame()->GetWebFrame();
if (!main_frame) {
// Cancelled.
std::move(callback).Run(true, source_lang, target_lang,
TranslateErrors::NONE);
return; // We navigated away, nothing to do.
}
// A similar translation is already under way, nothing to do.
if (translate_callback_pending_ && target_lang_ == target_lang) {
// This request is ignored.
std::move(callback).Run(true, source_lang, target_lang,
TranslateErrors::NONE);
return;
}
// Any pending translation is now irrelevant.
CancelPendingTranslation();
// Set our states.
translate_callback_pending_ = std::move(callback);
// If the source language is undetermined, we'll let the translate element
// detect it.
source_lang_ = (source_lang != kUnknownLanguageCode) ? source_lang
: kAutoDetectionLanguage;
target_lang_ = target_lang;
ReportUserActionDuration(language_determined_time_, base::TimeTicks::Now());
GURL url(main_frame->GetDocument().Url());
ReportPageScheme(url.scheme());
// Set up v8 isolated world with proper content-security-policy and
// security-origin.
blink::WebIsolatedWorldInfo info;
info.security_origin =
WebSecurityOrigin::Create(GetTranslateSecurityOrigin());
info.content_security_policy = WebString::FromUTF8(kContentSecurityPolicy);
main_frame->SetIsolatedWorldInfo(world_id_, info);
if (!IsTranslateLibAvailable()) {
// Evaluate the script to add the translation related method to the global
// context of the page.
ExecuteScript(translate_script);
DCHECK(IsTranslateLibAvailable());
}
TranslatePageImpl(0);
}
void TranslateAgent::RevertTranslation() {
if (!IsTranslateLibAvailable()) {
NOTREACHED();
return;
}
CancelPendingTranslation();
ExecuteScript("cr.googleTranslate.revert()");
}
////////////////////////////////////////////////////////////////////////////////
// TranslateAgent, private:
void TranslateAgent::CheckTranslateStatus() {
// First check if there was an error.
if (HasTranslationFailed()) {
NotifyBrowserTranslationFailed(
static_cast<translate::TranslateErrors::Type>(GetErrorCode()));
return; // There was an error.
}
if (HasTranslationFinished()) {
std::string actual_source_lang;
// Translation was successfull, if it was auto, retrieve the source
// language the Translate Element detected.
if (source_lang_ == kAutoDetectionLanguage) {
actual_source_lang = GetOriginalPageLanguage();
if (actual_source_lang.empty()) {
NotifyBrowserTranslationFailed(TranslateErrors::UNKNOWN_LANGUAGE);
return;
} else if (actual_source_lang == target_lang_) {
NotifyBrowserTranslationFailed(TranslateErrors::IDENTICAL_LANGUAGES);
return;
}
} else {
actual_source_lang = source_lang_;
}
if (!translate_callback_pending_) {
NOTREACHED();
return;
}
// Check JavaScript performance counters for UMA reports.
ReportTimeToTranslate(
ExecuteScriptAndGetDoubleResult("cr.googleTranslate.translationTime"));
// Notify the browser we are done.
std::move(translate_callback_pending_)
.Run(false, actual_source_lang, target_lang_, TranslateErrors::NONE);
return;
}
// The translation is still pending, check again later.
translate_task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&TranslateAgent::CheckTranslateStatus,
weak_method_factory_.GetWeakPtr()),
AdjustDelay(kTranslateStatusCheckDelayMs));
}
void TranslateAgent::TranslatePageImpl(int count) {
DCHECK_LT(count, kMaxTranslateInitCheckAttempts);
if (!IsTranslateLibReady()) {
// There was an error during initialization of library.
TranslateErrors::Type error =
static_cast<translate::TranslateErrors::Type>(GetErrorCode());
if (error != TranslateErrors::NONE) {
NotifyBrowserTranslationFailed(error);
return;
}
// The library is not ready, try again later, unless we have tried several
// times unsuccessfully already.
if (++count >= kMaxTranslateInitCheckAttempts) {
NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_TIMEOUT);
return;
}
translate_task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&TranslateAgent::TranslatePageImpl,
weak_method_factory_.GetWeakPtr(), count),
AdjustDelay(count * kTranslateInitCheckDelayMs));
return;
}
// The library is loaded, and ready for translation now.
// Check JavaScript performance counters for UMA reports.
ReportTimeToBeReady(
ExecuteScriptAndGetDoubleResult("cr.googleTranslate.readyTime"));
ReportTimeToLoad(
ExecuteScriptAndGetDoubleResult("cr.googleTranslate.loadTime"));
if (!StartTranslation()) {
CheckTranslateStatus();
return;
}
// Check the status of the translation.
translate_task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&TranslateAgent::CheckTranslateStatus,
weak_method_factory_.GetWeakPtr()),
AdjustDelay(kTranslateStatusCheckDelayMs));
}
void TranslateAgent::NotifyBrowserTranslationFailed(
TranslateErrors::Type error) {
DCHECK(translate_callback_pending_);
// Notify the browser there was an error.
std::move(translate_callback_pending_)
.Run(false, source_lang_, target_lang_, error);
}
const mojo::Remote<mojom::ContentTranslateDriver>&
TranslateAgent::GetTranslateHandler() {
if (!translate_handler_) {
render_frame()->GetBrowserInterfaceBroker()->GetInterface(
translate_handler_.BindNewPipeAndPassReceiver());
}
return translate_handler_;
}
void TranslateAgent::ResetPage() {
receiver_.reset();
translate_callback_pending_.Reset();
CancelPendingTranslation();
}
void TranslateAgent::OnDestruct() {
delete this;
}
/* static */
std::string TranslateAgent::BuildTranslationScript(
const std::string& source_lang,
const std::string& target_lang) {
return "cr.googleTranslate.translate(" +
base::GetQuotedJSONString(source_lang) + "," +
base::GetQuotedJSONString(target_lang) + ")";
}
} // namespace translate