Home | History | Annotate | Download | only in translate
      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 //
     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 
     76 TranslateHelper::~TranslateHelper() {
     77   CancelPendingTranslation();
     78 }
     79 
     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 
    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 //
    143 bool TranslateHelper::IsTranslateLibAvailable() {
    144   return ExecuteScriptAndGetBoolResult(
    145       "typeof cr != 'undefined' && typeof cr.googleTranslate != 'undefined' && "
    146       "typeof cr.googleTranslate.translate == 'function'", false);
    147 }
    148 
    149 bool TranslateHelper::IsTranslateLibReady() {
    150   return ExecuteScriptAndGetBoolResult("cr.googleTranslate.libReady", false);
    151 }
    152 
    153 bool TranslateHelper::HasTranslationFinished() {
    154   return ExecuteScriptAndGetBoolResult("cr.googleTranslate.finished", true);
    155 }
    156 
    157 bool TranslateHelper::HasTranslationFailed() {
    158   return ExecuteScriptAndGetBoolResult("cr.googleTranslate.error", true);
    159 }
    160 
    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 
    170 std::string TranslateHelper::GetOriginalPageLanguage() {
    171   return ExecuteScriptAndGetStringResult("cr.googleTranslate.sourceLang");
    172 }
    173 
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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