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.
4
5 #include "chrome/renderer/translate/translate_helper.h"
6
7 #include "base/bind.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/common/translate_constants.h"
18 #include "components/translate/common/translate_metrics.h"
19 #include "components/translate/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"
30 #include "url/gurl.h"
31 #include "v8/include/v8.h"
32
33 using blink::WebDocument;
34 using blink::WebElement;
35 using blink::WebFrame;
36 using blink::WebNode;
37 using blink::WebNodeList;
38 using blink::WebScriptSource;
39 using blink::WebSecurityOrigin;
40 using blink::WebString;
41 using blink::WebVector;
42 using blink::WebView;
43
44 namespace {
45
46 // The delay in milliseconds that we'll wait before checking to see if the
47 // translate library injected in the page is ready.
48 const int kTranslateInitCheckDelayMs = 150;
49
50 // The maximum number of times we'll check to see if the translate library
51 // injected in the page is ready.
52 const int kMaxTranslateInitCheckAttempts = 5;
53
54 // The delay we wait in milliseconds before checking whether the translation has
55 // finished.
56 const int kTranslateStatusCheckDelayMs = 400;
57
58 // Language name passed to the Translate element for it to detect the language.
59 const char kAutoDetectionLanguage[] = "auto";
60
61 // Isolated world sets following content-security-policy.
62 const char kContentSecurityPolicy[] = "script-src 'self' 'unsafe-eval'";
63
64 } // namespace
65
66 ////////////////////////////////////////////////////////////////////////////////
67 // TranslateHelper, public:
68 //
TranslateHelper(content::RenderView * render_view)69 TranslateHelper::TranslateHelper(content::RenderView* render_view)
70 : content::RenderViewObserver(render_view),
71 page_id_(-1),
72 translation_pending_(false),
73 weak_method_factory_(this) {
74 }
75
~TranslateHelper()76 TranslateHelper::~TranslateHelper() {
77 CancelPendingTranslation();
78 }
79
PageCaptured(int page_id,const base::string16 & contents)80 void TranslateHelper::PageCaptured(int page_id,
81 const base::string16& contents) {
82 // Get the document language as set by WebKit from the http-equiv
83 // meta tag for "content-language". This may or may not also
84 // have a value derived from the actual Content-Language HTTP
85 // header. The two actually have different meanings (despite the
86 // original intent of http-equiv to be an equivalent) with the former
87 // being the language of the document and the latter being the
88 // language of the intended audience (a distinction really only
89 // relevant for things like langauge textbooks). This distinction
90 // shouldn't affect translation.
91 WebFrame* main_frame = GetMainFrame();
92 if (!main_frame || render_view()->GetPageId() != page_id)
93 return;
94 page_id_ = page_id;
95 WebDocument document = main_frame->document();
96 std::string content_language = document.contentLanguage().utf8();
97 WebElement html_element = document.documentElement();
98 std::string html_lang;
99 // |html_element| can be null element, e.g. in
100 // BrowserTest.WindowOpenClose.
101 if (!html_element.isNull())
102 html_lang = html_element.getAttribute("lang").utf8();
103 std::string cld_language;
104 bool is_cld_reliable;
105 std::string language = translate::DeterminePageLanguage(
106 content_language, html_lang, contents, &cld_language, &is_cld_reliable);
107
108 if (language.empty())
109 return;
110
111 language_determined_time_ = base::TimeTicks::Now();
112
113 GURL url(document.url());
114 LanguageDetectionDetails details;
115 details.time = base::Time::Now();
116 details.url = url;
117 details.content_language = content_language;
118 details.cld_language = cld_language;
119 details.is_cld_reliable = is_cld_reliable;
120 details.html_root_language = html_lang;
121 details.adopted_language = language;
122
123 // TODO(hajimehoshi): If this affects performance, it should be set only if
124 // translate-internals tab exists.
125 details.contents = contents;
126
127 Send(new ChromeViewHostMsg_TranslateLanguageDetermined(
128 routing_id(),
129 details,
130 IsTranslationAllowed(&document) && !language.empty()));
131 }
132
CancelPendingTranslation()133 void TranslateHelper::CancelPendingTranslation() {
134 weak_method_factory_.InvalidateWeakPtrs();
135 translation_pending_ = false;
136 source_lang_.clear();
137 target_lang_.clear();
138 }
139
140 ////////////////////////////////////////////////////////////////////////////////
141 // TranslateHelper, protected:
142 //
IsTranslateLibAvailable()143 bool TranslateHelper::IsTranslateLibAvailable() {
144 return ExecuteScriptAndGetBoolResult(
145 "typeof cr != 'undefined' && typeof cr.googleTranslate != 'undefined' && "
146 "typeof cr.googleTranslate.translate == 'function'", false);
147 }
148
IsTranslateLibReady()149 bool TranslateHelper::IsTranslateLibReady() {
150 return ExecuteScriptAndGetBoolResult("cr.googleTranslate.libReady", false);
151 }
152
HasTranslationFinished()153 bool TranslateHelper::HasTranslationFinished() {
154 return ExecuteScriptAndGetBoolResult("cr.googleTranslate.finished", true);
155 }
156
HasTranslationFailed()157 bool TranslateHelper::HasTranslationFailed() {
158 return ExecuteScriptAndGetBoolResult("cr.googleTranslate.error", true);
159 }
160
StartTranslation()161 bool TranslateHelper::StartTranslation() {
162 std::string script = "cr.googleTranslate.translate('" +
163 source_lang_ +
164 "','" +
165 target_lang_ +
166 "')";
167 return ExecuteScriptAndGetBoolResult(script, false);
168 }
169
GetOriginalPageLanguage()170 std::string TranslateHelper::GetOriginalPageLanguage() {
171 return ExecuteScriptAndGetStringResult("cr.googleTranslate.sourceLang");
172 }
173
AdjustDelay(int delayInMs)174 base::TimeDelta TranslateHelper::AdjustDelay(int delayInMs) {
175 // Just converts |delayInMs| without any modification in practical cases.
176 // Tests will override this function to return modified value.
177 return base::TimeDelta::FromMilliseconds(delayInMs);
178 }
179
ExecuteScript(const std::string & script)180 void TranslateHelper::ExecuteScript(const std::string& script) {
181 WebFrame* main_frame = GetMainFrame();
182 if (!main_frame)
183 return;
184
185 WebScriptSource source = WebScriptSource(ASCIIToUTF16(script));
186 main_frame->executeScriptInIsolatedWorld(
187 chrome::ISOLATED_WORLD_ID_TRANSLATE,
188 &source,
189 1,
190 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS);
191 }
192
ExecuteScriptAndGetBoolResult(const std::string & script,bool fallback)193 bool TranslateHelper::ExecuteScriptAndGetBoolResult(const std::string& script,
194 bool fallback) {
195 WebFrame* main_frame = GetMainFrame();
196 if (!main_frame)
197 return fallback;
198
199 v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
200 WebVector<v8::Local<v8::Value> > results;
201 WebScriptSource source = WebScriptSource(ASCIIToUTF16(script));
202 main_frame->executeScriptInIsolatedWorld(
203 chrome::ISOLATED_WORLD_ID_TRANSLATE,
204 &source,
205 1,
206 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS,
207 &results);
208 if (results.size() != 1 || results[0].IsEmpty() || !results[0]->IsBoolean()) {
209 NOTREACHED();
210 return fallback;
211 }
212
213 return results[0]->BooleanValue();
214 }
215
ExecuteScriptAndGetStringResult(const std::string & script)216 std::string TranslateHelper::ExecuteScriptAndGetStringResult(
217 const std::string& script) {
218 WebFrame* main_frame = GetMainFrame();
219 if (!main_frame)
220 return std::string();
221
222 v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
223 WebVector<v8::Local<v8::Value> > results;
224 WebScriptSource source = WebScriptSource(ASCIIToUTF16(script));
225 main_frame->executeScriptInIsolatedWorld(
226 chrome::ISOLATED_WORLD_ID_TRANSLATE,
227 &source,
228 1,
229 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS,
230 &results);
231 if (results.size() != 1 || results[0].IsEmpty() || !results[0]->IsString()) {
232 NOTREACHED();
233 return std::string();
234 }
235
236 v8::Local<v8::String> v8_str = results[0]->ToString();
237 int length = v8_str->Utf8Length() + 1;
238 scoped_ptr<char[]> str(new char[length]);
239 v8_str->WriteUtf8(str.get(), length);
240 return std::string(str.get());
241 }
242
ExecuteScriptAndGetDoubleResult(const std::string & script)243 double TranslateHelper::ExecuteScriptAndGetDoubleResult(
244 const std::string& script) {
245 WebFrame* main_frame = GetMainFrame();
246 if (!main_frame)
247 return 0.0;
248
249 v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
250 WebVector<v8::Local<v8::Value> > results;
251 WebScriptSource source = WebScriptSource(ASCIIToUTF16(script));
252 main_frame->executeScriptInIsolatedWorld(
253 chrome::ISOLATED_WORLD_ID_TRANSLATE,
254 &source,
255 1,
256 extensions::EXTENSION_GROUP_INTERNAL_TRANSLATE_SCRIPTS,
257 &results);
258 if (results.size() != 1 || results[0].IsEmpty() || !results[0]->IsNumber()) {
259 NOTREACHED();
260 return 0.0;
261 }
262
263 return results[0]->NumberValue();
264 }
265
266 ////////////////////////////////////////////////////////////////////////////////
267 // TranslateHelper, private:
268 //
269
270 // static
IsTranslationAllowed(WebDocument * document)271 bool TranslateHelper::IsTranslationAllowed(WebDocument* document) {
272 WebElement head = document->head();
273 if (head.isNull() || !head.hasChildNodes())
274 return true;
275
276 const WebString meta(ASCIIToUTF16("meta"));
277 const WebString name(ASCIIToUTF16("name"));
278 const WebString google(ASCIIToUTF16("google"));
279 const WebString value(ASCIIToUTF16("value"));
280 const WebString content(ASCIIToUTF16("content"));
281
282 WebNodeList children = head.childNodes();
283 for (size_t i = 0; i < children.length(); ++i) {
284 WebNode node = children.item(i);
285 if (!node.isElementNode())
286 continue;
287 WebElement element = node.to<WebElement>();
288 // Check if a tag is <meta>.
289 if (!element.hasTagName(meta))
290 continue;
291 // Check if the tag contains name="google".
292 WebString attribute = element.getAttribute(name);
293 if (attribute.isNull() || attribute != google)
294 continue;
295 // Check if the tag contains value="notranslate", or content="notranslate".
296 attribute = element.getAttribute(value);
297 if (attribute.isNull())
298 attribute = element.getAttribute(content);
299 if (attribute.isNull())
300 continue;
301 if (LowerCaseEqualsASCII(attribute, "notranslate"))
302 return false;
303 }
304 return true;
305 }
306
OnMessageReceived(const IPC::Message & message)307 bool TranslateHelper::OnMessageReceived(const IPC::Message& message) {
308 bool handled = true;
309 IPC_BEGIN_MESSAGE_MAP(TranslateHelper, message)
310 IPC_MESSAGE_HANDLER(ChromeViewMsg_TranslatePage, OnTranslatePage)
311 IPC_MESSAGE_HANDLER(ChromeViewMsg_RevertTranslation, OnRevertTranslation)
312 IPC_MESSAGE_UNHANDLED(handled = false)
313 IPC_END_MESSAGE_MAP()
314 return handled;
315 }
316
OnTranslatePage(int page_id,const std::string & translate_script,const std::string & source_lang,const std::string & target_lang)317 void TranslateHelper::OnTranslatePage(int page_id,
318 const std::string& translate_script,
319 const std::string& source_lang,
320 const std::string& target_lang) {
321 WebFrame* main_frame = GetMainFrame();
322 if (!main_frame ||
323 page_id_ != page_id ||
324 render_view()->GetPageId() != page_id)
325 return; // We navigated away, nothing to do.
326
327 // A similar translation is already under way, nothing to do.
328 if (translation_pending_ && target_lang_ == target_lang)
329 return;
330
331 // Any pending translation is now irrelevant.
332 CancelPendingTranslation();
333
334 // Set our states.
335 translation_pending_ = true;
336
337 // If the source language is undetermined, we'll let the translate element
338 // detect it.
339 source_lang_ = (source_lang != translate::kUnknownLanguageCode) ?
340 source_lang : kAutoDetectionLanguage;
341 target_lang_ = target_lang;
342
343 translate::ReportUserActionDuration(language_determined_time_,
344 base::TimeTicks::Now());
345
346 GURL url(main_frame->document().url());
347 translate::ReportPageScheme(url.scheme());
348
349 // Set up v8 isolated world with proper content-security-policy and
350 // security-origin.
351 WebFrame* frame = GetMainFrame();
352 if (frame) {
353 frame->setIsolatedWorldContentSecurityPolicy(
354 chrome::ISOLATED_WORLD_ID_TRANSLATE,
355 WebString::fromUTF8(kContentSecurityPolicy));
356
357 GURL security_origin = translate::GetTranslateSecurityOrigin();
358 frame->setIsolatedWorldSecurityOrigin(
359 chrome::ISOLATED_WORLD_ID_TRANSLATE,
360 WebSecurityOrigin::create(security_origin));
361 }
362
363 if (!IsTranslateLibAvailable()) {
364 // Evaluate the script to add the translation related method to the global
365 // context of the page.
366 ExecuteScript(translate_script);
367 DCHECK(IsTranslateLibAvailable());
368 }
369
370 TranslatePageImpl(0);
371 }
372
OnRevertTranslation(int page_id)373 void TranslateHelper::OnRevertTranslation(int page_id) {
374 if (page_id_ != page_id || render_view()->GetPageId() != page_id)
375 return; // We navigated away, nothing to do.
376
377 if (!IsTranslateLibAvailable()) {
378 NOTREACHED();
379 return;
380 }
381
382 CancelPendingTranslation();
383
384 ExecuteScript("cr.googleTranslate.revert()");
385 }
386
CheckTranslateStatus()387 void TranslateHelper::CheckTranslateStatus() {
388 // If this is not the same page, the translation has been canceled. If the
389 // view is gone, the page is closing.
390 if (page_id_ != render_view()->GetPageId() || !render_view()->GetWebView())
391 return;
392
393 // First check if there was an error.
394 if (HasTranslationFailed()) {
395 // TODO(toyoshim): Check |errorCode| of translate.js and notify it here.
396 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR);
397 return; // There was an error.
398 }
399
400 if (HasTranslationFinished()) {
401 std::string actual_source_lang;
402 // Translation was successfull, if it was auto, retrieve the source
403 // language the Translate Element detected.
404 if (source_lang_ == kAutoDetectionLanguage) {
405 actual_source_lang = GetOriginalPageLanguage();
406 if (actual_source_lang.empty()) {
407 NotifyBrowserTranslationFailed(TranslateErrors::UNKNOWN_LANGUAGE);
408 return;
409 } else if (actual_source_lang == target_lang_) {
410 NotifyBrowserTranslationFailed(TranslateErrors::IDENTICAL_LANGUAGES);
411 return;
412 }
413 } else {
414 actual_source_lang = source_lang_;
415 }
416
417 if (!translation_pending_) {
418 NOTREACHED();
419 return;
420 }
421
422 translation_pending_ = false;
423
424 // Check JavaScript performance counters for UMA reports.
425 translate::ReportTimeToTranslate(
426 ExecuteScriptAndGetDoubleResult("cr.googleTranslate.translationTime"));
427
428 // Notify the browser we are done.
429 render_view()->Send(new ChromeViewHostMsg_PageTranslated(
430 render_view()->GetRoutingID(), render_view()->GetPageId(),
431 actual_source_lang, target_lang_, TranslateErrors::NONE));
432 return;
433 }
434
435 // The translation is still pending, check again later.
436 base::MessageLoop::current()->PostDelayedTask(
437 FROM_HERE,
438 base::Bind(&TranslateHelper::CheckTranslateStatus,
439 weak_method_factory_.GetWeakPtr()),
440 AdjustDelay(kTranslateStatusCheckDelayMs));
441 }
442
TranslatePageImpl(int count)443 void TranslateHelper::TranslatePageImpl(int count) {
444 DCHECK_LT(count, kMaxTranslateInitCheckAttempts);
445 if (page_id_ != render_view()->GetPageId() || !render_view()->GetWebView())
446 return;
447
448 if (!IsTranslateLibReady()) {
449 // The library is not ready, try again later, unless we have tried several
450 // times unsucessfully already.
451 if (++count >= kMaxTranslateInitCheckAttempts) {
452 NotifyBrowserTranslationFailed(TranslateErrors::INITIALIZATION_ERROR);
453 return;
454 }
455 base::MessageLoop::current()->PostDelayedTask(
456 FROM_HERE,
457 base::Bind(&TranslateHelper::TranslatePageImpl,
458 weak_method_factory_.GetWeakPtr(),
459 count),
460 AdjustDelay(count * kTranslateInitCheckDelayMs));
461 return;
462 }
463
464 // The library is loaded, and ready for translation now.
465 // Check JavaScript performance counters for UMA reports.
466 translate::ReportTimeToBeReady(
467 ExecuteScriptAndGetDoubleResult("cr.googleTranslate.readyTime"));
468 translate::ReportTimeToLoad(
469 ExecuteScriptAndGetDoubleResult("cr.googleTranslate.loadTime"));
470
471 if (!StartTranslation()) {
472 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR);
473 return;
474 }
475 // Check the status of the translation.
476 base::MessageLoop::current()->PostDelayedTask(
477 FROM_HERE,
478 base::Bind(&TranslateHelper::CheckTranslateStatus,
479 weak_method_factory_.GetWeakPtr()),
480 AdjustDelay(kTranslateStatusCheckDelayMs));
481 }
482
NotifyBrowserTranslationFailed(TranslateErrors::Type error)483 void TranslateHelper::NotifyBrowserTranslationFailed(
484 TranslateErrors::Type error) {
485 translation_pending_ = false;
486 // Notify the browser there was an error.
487 render_view()->Send(new ChromeViewHostMsg_PageTranslated(
488 render_view()->GetRoutingID(), page_id_, source_lang_,
489 target_lang_, error));
490 }
491
GetMainFrame()492 WebFrame* TranslateHelper::GetMainFrame() {
493 WebView* web_view = render_view()->GetWebView();
494
495 // When the tab is going to be closed, the web_view can be NULL.
496 if (!web_view)
497 return NULL;
498
499 return web_view->mainFrame();
500 }
501