1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/renderer/translate/translate_helper.h"
8 #include "base/compiler_specific.h"
9 #include "base/logging.h"
10 #include "base/message_loop/message_loop.h"
11 #include "base/strings/string16.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/common/render_messages.h"
15 #include "chrome/renderer/extensions/extension_groups.h"
16 #include "chrome/renderer/isolated_world_ids.h"
17 #include "components/translate/core/common/translate_constants.h"
18 #include "components/translate/core/common/translate_metrics.h"
19 #include "components/translate/core/common/translate_util.h"
20 #include "components/translate/language_detection/language_detection_util.h"
21 #include "content/public/renderer/render_view.h"
22 #include "third_party/WebKit/public/web/WebDocument.h"
23 #include "third_party/WebKit/public/web/WebElement.h"
24 #include "third_party/WebKit/public/web/WebFrame.h"
25 #include "third_party/WebKit/public/web/WebNode.h"
26 #include "third_party/WebKit/public/web/WebNodeList.h"
27 #include "third_party/WebKit/public/web/WebScriptSource.h"
28 #include "third_party/WebKit/public/web/WebView.h"
29 #include "third_party/WebKit/public/web/WebWidget.h"
31 #include "v8/include/v8.h"
33 using base::ASCIIToUTF16
;
34 using blink::WebDocument
;
35 using blink::WebElement
;
36 using blink::WebFrame
;
38 using blink::WebNodeList
;
39 using blink::WebScriptSource
;
40 using blink::WebSecurityOrigin
;
41 using blink::WebString
;
42 using blink::WebVector
;
47 // The delay in milliseconds that we'll wait before checking to see if the
48 // translate library injected in the page is ready.
49 const int kTranslateInitCheckDelayMs
= 150;
51 // The maximum number of times we'll check to see if the translate library
52 // injected in the page is ready.
53 const int kMaxTranslateInitCheckAttempts
= 5;
55 // The delay we wait in milliseconds before checking whether the translation has
57 const int kTranslateStatusCheckDelayMs
= 400;
59 // Language name passed to the Translate element for it to detect the language.
60 const char kAutoDetectionLanguage
[] = "auto";
62 // Isolated world sets following content-security-policy.
63 const char kContentSecurityPolicy
[] = "script-src 'self' 'unsafe-eval'";
67 ////////////////////////////////////////////////////////////////////////////////
68 // TranslateHelper, public:
70 TranslateHelper::TranslateHelper(content::RenderView
* render_view
)
71 : content::RenderViewObserver(render_view
),
73 translation_pending_(false),
74 weak_method_factory_(this) {
77 TranslateHelper::~TranslateHelper() {
78 CancelPendingTranslation();
81 void TranslateHelper::PageCaptured(int page_id
,
82 const base::string16
& contents
) {
83 // Get the document language as set by WebKit from the http-equiv
84 // meta tag for "content-language". This may or may not also
85 // have a value derived from the actual Content-Language HTTP
86 // header. The two actually have different meanings (despite the
87 // original intent of http-equiv to be an equivalent) with the former
88 // being the language of the document and the latter being the
89 // language of the intended audience (a distinction really only
90 // relevant for things like langauge textbooks). This distinction
91 // shouldn't affect translation.
92 WebFrame
* main_frame
= GetMainFrame();
93 if (!main_frame
|| render_view()->GetPageId() != page_id
)
96 WebDocument document
= main_frame
->document();
97 std::string content_language
= document
.contentLanguage().utf8();
98 WebElement html_element
= document
.documentElement();
99 std::string html_lang
;
100 // |html_element| can be null element, e.g. in
101 // BrowserTest.WindowOpenClose.
102 if (!html_element
.isNull())
103 html_lang
= html_element
.getAttribute("lang").utf8();
104 std::string cld_language
;
105 bool is_cld_reliable
;
106 std::string language
= translate::DeterminePageLanguage(
107 content_language
, html_lang
, contents
, &cld_language
, &is_cld_reliable
);
109 if (language
.empty())
112 language_determined_time_
= base::TimeTicks::Now();
114 GURL
url(document
.url());
115 LanguageDetectionDetails details
;
116 details
.time
= base::Time::Now();
118 details
.content_language
= content_language
;
119 details
.cld_language
= cld_language
;
120 details
.is_cld_reliable
= is_cld_reliable
;
121 details
.html_root_language
= html_lang
;
122 details
.adopted_language
= language
;
124 // TODO(hajimehoshi): If this affects performance, it should be set only if
125 // translate-internals tab exists.
126 details
.contents
= contents
;
128 Send(new ChromeViewHostMsg_TranslateLanguageDetermined(
131 IsTranslationAllowed(&document
) && !language
.empty()));
134 void TranslateHelper::CancelPendingTranslation() {
135 weak_method_factory_
.InvalidateWeakPtrs();
136 translation_pending_
= false;
137 source_lang_
.clear();
138 target_lang_
.clear();
141 ////////////////////////////////////////////////////////////////////////////////
142 // TranslateHelper, protected:
144 bool TranslateHelper::IsTranslateLibAvailable() {
145 return ExecuteScriptAndGetBoolResult(
146 "typeof cr != 'undefined' && typeof cr.googleTranslate != 'undefined' && "
147 "typeof cr.googleTranslate.translate == 'function'", false);
150 bool TranslateHelper::IsTranslateLibReady() {
151 return ExecuteScriptAndGetBoolResult("cr.googleTranslate.libReady", false);
154 bool TranslateHelper::HasTranslationFinished() {
155 return ExecuteScriptAndGetBoolResult("cr.googleTranslate.finished", true);
158 bool TranslateHelper::HasTranslationFailed() {
159 return ExecuteScriptAndGetBoolResult("cr.googleTranslate.error", true);
162 bool TranslateHelper::StartTranslation() {
163 std::string script
= "cr.googleTranslate.translate('" +
168 return ExecuteScriptAndGetBoolResult(script
, false);
171 std::string
TranslateHelper::GetOriginalPageLanguage() {
172 return ExecuteScriptAndGetStringResult("cr.googleTranslate.sourceLang");
175 base::TimeDelta
TranslateHelper::AdjustDelay(int delayInMs
) {
176 // Just converts |delayInMs| without any modification in practical cases.
177 // Tests will override this function to return modified value.
178 return base::TimeDelta::FromMilliseconds(delayInMs
);
181 void TranslateHelper::ExecuteScript(const std::string
& script
) {
182 WebFrame
* main_frame
= GetMainFrame();
186 WebScriptSource source
= WebScriptSource(ASCIIToUTF16(script
));
187 main_frame
->executeScriptInIsolatedWorld(
188 chrome::ISOLATED_WORLD_ID_TRANSLATE
,
191 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS
);
194 bool TranslateHelper::ExecuteScriptAndGetBoolResult(const std::string
& script
,
196 WebFrame
* main_frame
= GetMainFrame();
200 v8::HandleScope
handle_scope(v8::Isolate::GetCurrent());
201 WebVector
<v8::Local
<v8::Value
> > results
;
202 WebScriptSource source
= WebScriptSource(ASCIIToUTF16(script
));
203 main_frame
->executeScriptInIsolatedWorld(
204 chrome::ISOLATED_WORLD_ID_TRANSLATE
,
207 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS
,
209 if (results
.size() != 1 || results
[0].IsEmpty() || !results
[0]->IsBoolean()) {
214 return results
[0]->BooleanValue();
217 std::string
TranslateHelper::ExecuteScriptAndGetStringResult(
218 const std::string
& script
) {
219 WebFrame
* main_frame
= GetMainFrame();
221 return std::string();
223 v8::HandleScope
handle_scope(v8::Isolate::GetCurrent());
224 WebVector
<v8::Local
<v8::Value
> > results
;
225 WebScriptSource source
= WebScriptSource(ASCIIToUTF16(script
));
226 main_frame
->executeScriptInIsolatedWorld(
227 chrome::ISOLATED_WORLD_ID_TRANSLATE
,
230 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS
,
232 if (results
.size() != 1 || results
[0].IsEmpty() || !results
[0]->IsString()) {
234 return std::string();
237 v8::Local
<v8::String
> v8_str
= results
[0]->ToString();
238 int length
= v8_str
->Utf8Length() + 1;
239 scoped_ptr
<char[]> str(new char[length
]);
240 v8_str
->WriteUtf8(str
.get(), length
);
241 return std::string(str
.get());
244 double TranslateHelper::ExecuteScriptAndGetDoubleResult(
245 const std::string
& script
) {
246 WebFrame
* main_frame
= GetMainFrame();
250 v8::HandleScope
handle_scope(v8::Isolate::GetCurrent());
251 WebVector
<v8::Local
<v8::Value
> > results
;
252 WebScriptSource source
= WebScriptSource(ASCIIToUTF16(script
));
253 main_frame
->executeScriptInIsolatedWorld(
254 chrome::ISOLATED_WORLD_ID_TRANSLATE
,
257 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS
,
259 if (results
.size() != 1 || results
[0].IsEmpty() || !results
[0]->IsNumber()) {
264 return results
[0]->NumberValue();
267 ////////////////////////////////////////////////////////////////////////////////
268 // TranslateHelper, private:
272 bool TranslateHelper::IsTranslationAllowed(WebDocument
* document
) {
273 WebElement head
= document
->head();
274 if (head
.isNull() || !head
.hasChildNodes())
277 const WebString
meta(ASCIIToUTF16("meta"));
278 const WebString
name(ASCIIToUTF16("name"));
279 const WebString
google(ASCIIToUTF16("google"));
280 const WebString
value(ASCIIToUTF16("value"));
281 const WebString
content(ASCIIToUTF16("content"));
283 WebNodeList children
= head
.childNodes();
284 for (size_t i
= 0; i
< children
.length(); ++i
) {
285 WebNode node
= children
.item(i
);
286 if (!node
.isElementNode())
288 WebElement element
= node
.to
<WebElement
>();
289 // Check if a tag is <meta>.
290 if (!element
.hasTagName(meta
))
292 // Check if the tag contains name="google".
293 WebString attribute
= element
.getAttribute(name
);
294 if (attribute
.isNull() || attribute
!= google
)
296 // Check if the tag contains value="notranslate", or content="notranslate".
297 attribute
= element
.getAttribute(value
);
298 if (attribute
.isNull())
299 attribute
= element
.getAttribute(content
);
300 if (attribute
.isNull())
302 if (LowerCaseEqualsASCII(attribute
, "notranslate"))
308 bool TranslateHelper::OnMessageReceived(const IPC::Message
& message
) {
310 IPC_BEGIN_MESSAGE_MAP(TranslateHelper
, message
)
311 IPC_MESSAGE_HANDLER(ChromeViewMsg_TranslatePage
, OnTranslatePage
)
312 IPC_MESSAGE_HANDLER(ChromeViewMsg_RevertTranslation
, OnRevertTranslation
)
313 IPC_MESSAGE_UNHANDLED(handled
= false)
314 IPC_END_MESSAGE_MAP()
318 void TranslateHelper::OnTranslatePage(int page_id
,
319 const std::string
& translate_script
,
320 const std::string
& source_lang
,
321 const std::string
& target_lang
) {
322 WebFrame
* main_frame
= GetMainFrame();
324 page_id_
!= page_id
||
325 render_view()->GetPageId() != page_id
)
326 return; // We navigated away, nothing to do.
328 // A similar translation is already under way, nothing to do.
329 if (translation_pending_
&& target_lang_
== target_lang
)
332 // Any pending translation is now irrelevant.
333 CancelPendingTranslation();
336 translation_pending_
= true;
338 // If the source language is undetermined, we'll let the translate element
340 source_lang_
= (source_lang
!= translate::kUnknownLanguageCode
) ?
341 source_lang
: kAutoDetectionLanguage
;
342 target_lang_
= target_lang
;
344 translate::ReportUserActionDuration(language_determined_time_
,
345 base::TimeTicks::Now());
347 GURL
url(main_frame
->document().url());
348 translate::ReportPageScheme(url
.scheme());
350 // Set up v8 isolated world with proper content-security-policy and
352 WebFrame
* frame
= GetMainFrame();
354 frame
->setIsolatedWorldContentSecurityPolicy(
355 chrome::ISOLATED_WORLD_ID_TRANSLATE
,
356 WebString::fromUTF8(kContentSecurityPolicy
));
358 GURL security_origin
= translate::GetTranslateSecurityOrigin();
359 frame
->setIsolatedWorldSecurityOrigin(
360 chrome::ISOLATED_WORLD_ID_TRANSLATE
,
361 WebSecurityOrigin::create(security_origin
));
364 if (!IsTranslateLibAvailable()) {
365 // Evaluate the script to add the translation related method to the global
366 // context of the page.
367 ExecuteScript(translate_script
);
368 DCHECK(IsTranslateLibAvailable());
371 TranslatePageImpl(0);
374 void TranslateHelper::OnRevertTranslation(int page_id
) {
375 if (page_id_
!= page_id
|| render_view()->GetPageId() != page_id
)
376 return; // We navigated away, nothing to do.
378 if (!IsTranslateLibAvailable()) {
383 CancelPendingTranslation();
385 ExecuteScript("cr.googleTranslate.revert()");
388 void TranslateHelper::CheckTranslateStatus() {
389 // If this is not the same page, the translation has been canceled. If the
390 // view is gone, the page is closing.
391 if (page_id_
!= render_view()->GetPageId() || !render_view()->GetWebView())
394 // First check if there was an error.
395 if (HasTranslationFailed()) {
396 // TODO(toyoshim): Check |errorCode| of translate.js and notify it here.
397 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR
);
398 return; // There was an error.
401 if (HasTranslationFinished()) {
402 std::string actual_source_lang
;
403 // Translation was successfull, if it was auto, retrieve the source
404 // language the Translate Element detected.
405 if (source_lang_
== kAutoDetectionLanguage
) {
406 actual_source_lang
= GetOriginalPageLanguage();
407 if (actual_source_lang
.empty()) {
408 NotifyBrowserTranslationFailed(TranslateErrors::UNKNOWN_LANGUAGE
);
410 } else if (actual_source_lang
== target_lang_
) {
411 NotifyBrowserTranslationFailed(TranslateErrors::IDENTICAL_LANGUAGES
);
415 actual_source_lang
= source_lang_
;
418 if (!translation_pending_
) {
423 translation_pending_
= false;
425 // Check JavaScript performance counters for UMA reports.
426 translate::ReportTimeToTranslate(
427 ExecuteScriptAndGetDoubleResult("cr.googleTranslate.translationTime"));
429 // Notify the browser we are done.
430 render_view()->Send(new ChromeViewHostMsg_PageTranslated(
431 render_view()->GetRoutingID(), render_view()->GetPageId(),
432 actual_source_lang
, target_lang_
, TranslateErrors::NONE
));
436 // The translation is still pending, check again later.
437 base::MessageLoop::current()->PostDelayedTask(
439 base::Bind(&TranslateHelper::CheckTranslateStatus
,
440 weak_method_factory_
.GetWeakPtr()),
441 AdjustDelay(kTranslateStatusCheckDelayMs
));
444 void TranslateHelper::TranslatePageImpl(int count
) {
445 DCHECK_LT(count
, kMaxTranslateInitCheckAttempts
);
446 if (page_id_
!= render_view()->GetPageId() || !render_view()->GetWebView())
449 if (!IsTranslateLibReady()) {
450 // The library is not ready, try again later, unless we have tried several
451 // times unsucessfully already.
452 if (++count
>= kMaxTranslateInitCheckAttempts
) {
453 NotifyBrowserTranslationFailed(TranslateErrors::INITIALIZATION_ERROR
);
456 base::MessageLoop::current()->PostDelayedTask(
458 base::Bind(&TranslateHelper::TranslatePageImpl
,
459 weak_method_factory_
.GetWeakPtr(),
461 AdjustDelay(count
* kTranslateInitCheckDelayMs
));
465 // The library is loaded, and ready for translation now.
466 // Check JavaScript performance counters for UMA reports.
467 translate::ReportTimeToBeReady(
468 ExecuteScriptAndGetDoubleResult("cr.googleTranslate.readyTime"));
469 translate::ReportTimeToLoad(
470 ExecuteScriptAndGetDoubleResult("cr.googleTranslate.loadTime"));
472 if (!StartTranslation()) {
473 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR
);
476 // Check the status of the translation.
477 base::MessageLoop::current()->PostDelayedTask(
479 base::Bind(&TranslateHelper::CheckTranslateStatus
,
480 weak_method_factory_
.GetWeakPtr()),
481 AdjustDelay(kTranslateStatusCheckDelayMs
));
484 void TranslateHelper::NotifyBrowserTranslationFailed(
485 TranslateErrors::Type error
) {
486 translation_pending_
= false;
487 // Notify the browser there was an error.
488 render_view()->Send(new ChromeViewHostMsg_PageTranslated(
489 render_view()->GetRoutingID(), page_id_
, source_lang_
,
490 target_lang_
, error
));
493 WebFrame
* TranslateHelper::GetMainFrame() {
494 WebView
* web_view
= render_view()->GetWebView();
496 // When the tab is going to be closed, the web_view can be NULL.
500 return web_view
->mainFrame();