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