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