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