• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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