1// Copyright 2014 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/browser/translate/chrome_translate_client.h"
6
7#include <vector>
8
9#include "base/logging.h"
10#include "base/prefs/pref_service.h"
11#include "base/strings/string_split.h"
12#include "chrome/browser/chrome_notification_types.h"
13#include "chrome/browser/infobars/infobar_service.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/translate/translate_accept_languages_factory.h"
16#include "chrome/browser/translate/translate_service.h"
17#include "chrome/browser/ui/browser.h"
18#include "chrome/browser/ui/browser_finder.h"
19#include "chrome/browser/ui/browser_tabstrip.h"
20#include "chrome/browser/ui/browser_window.h"
21#include "chrome/browser/ui/tabs/tab_strip_model.h"
22#include "chrome/browser/ui/translate/translate_bubble_factory.h"
23#include "chrome/common/pref_names.h"
24#include "components/infobars/core/infobar.h"
25#include "components/translate/content/common/translate_messages.h"
26#include "components/translate/core/browser/language_state.h"
27#include "components/translate/core/browser/page_translated_details.h"
28#include "components/translate/core/browser/translate_accept_languages.h"
29#include "components/translate/core/browser/translate_download_manager.h"
30#include "components/translate/core/browser/translate_infobar_delegate.h"
31#include "components/translate/core/browser/translate_manager.h"
32#include "components/translate/core/browser/translate_prefs.h"
33#include "components/translate/core/common/language_detection_details.h"
34#include "content/public/browser/navigation_details.h"
35#include "content/public/browser/navigation_entry.h"
36#include "content/public/browser/notification_service.h"
37#include "content/public/browser/render_view_host.h"
38#include "content/public/browser/web_contents.h"
39#include "grit/theme_resources.h"
40#include "net/http/http_status_code.h"
41#include "url/gurl.h"
42
43#if defined(CLD2_DYNAMIC_MODE)
44#include "base/files/file.h"
45#include "base/path_service.h"
46#include "chrome/common/chrome_constants.h"
47#include "chrome/common/chrome_paths.h"
48#include "content/public/browser/browser_thread.h"
49#include "content/public/browser/render_process_host.h"
50#endif
51
52#if defined(CLD2_IS_COMPONENT)
53#include "chrome/browser/component_updater/cld_component_installer.h"
54#endif
55
56namespace {
57
58// The maximum number of attempts we'll do to see if the page has finshed
59// loading before giving up the translation
60const int kMaxTranslateLoadCheckAttempts = 20;
61
62}  // namespace
63
64DEFINE_WEB_CONTENTS_USER_DATA_KEY(ChromeTranslateClient);
65
66#if defined(CLD2_DYNAMIC_MODE)
67// Statics defined in the .h file:
68base::File* ChromeTranslateClient::s_cached_file_ = NULL;
69uint64 ChromeTranslateClient::s_cached_data_offset_ = 0;
70uint64 ChromeTranslateClient::s_cached_data_length_ = 0;
71base::LazyInstance<base::Lock> ChromeTranslateClient::s_file_lock_ =
72    LAZY_INSTANCE_INITIALIZER;
73#endif
74
75ChromeTranslateClient::ChromeTranslateClient(content::WebContents* web_contents)
76    : content::WebContentsObserver(web_contents),
77      max_reload_check_attempts_(kMaxTranslateLoadCheckAttempts),
78      translate_driver_(&web_contents->GetController()),
79      translate_manager_(new TranslateManager(this, prefs::kAcceptLanguages)),
80      weak_pointer_factory_(this) {
81}
82
83ChromeTranslateClient::~ChromeTranslateClient() {
84}
85
86LanguageState& ChromeTranslateClient::GetLanguageState() {
87  return translate_manager_->GetLanguageState();
88}
89
90// static
91scoped_ptr<TranslatePrefs> ChromeTranslateClient::CreateTranslatePrefs(
92    PrefService* prefs) {
93#if defined(OS_CHROMEOS)
94  const char* preferred_languages_prefs = prefs::kLanguagePreferredLanguages;
95#else
96  const char* preferred_languages_prefs = NULL;
97#endif
98  return scoped_ptr<TranslatePrefs>(new TranslatePrefs(
99      prefs, prefs::kAcceptLanguages, preferred_languages_prefs));
100}
101
102// static
103TranslateAcceptLanguages* ChromeTranslateClient::GetTranslateAcceptLanguages(
104    content::BrowserContext* browser_context) {
105  return TranslateAcceptLanguagesFactory::GetForBrowserContext(browser_context);
106}
107
108// static
109TranslateManager* ChromeTranslateClient::GetManagerFromWebContents(
110    content::WebContents* web_contents) {
111  ChromeTranslateClient* chrome_translate_client =
112      FromWebContents(web_contents);
113  if (!chrome_translate_client)
114    return NULL;
115  return chrome_translate_client->GetTranslateManager();
116}
117
118// static
119void ChromeTranslateClient::GetTranslateLanguages(
120    content::WebContents* web_contents,
121    std::string* source,
122    std::string* target) {
123  DCHECK(source != NULL);
124  DCHECK(target != NULL);
125
126  ChromeTranslateClient* chrome_translate_client =
127      FromWebContents(web_contents);
128  if (!chrome_translate_client)
129    return;
130
131  *source = TranslateDownloadManager::GetLanguageCode(
132      chrome_translate_client->GetLanguageState().original_language());
133
134  Profile* profile =
135      Profile::FromBrowserContext(web_contents->GetBrowserContext());
136  Profile* original_profile = profile->GetOriginalProfile();
137  PrefService* prefs = original_profile->GetPrefs();
138  scoped_ptr<TranslatePrefs> translate_prefs = CreateTranslatePrefs(prefs);
139  if (!web_contents->GetBrowserContext()->IsOffTheRecord()) {
140    std::string auto_translate_language =
141        TranslateManager::GetAutoTargetLanguage(*source, translate_prefs.get());
142    if (!auto_translate_language.empty()) {
143      *target = auto_translate_language;
144      return;
145    }
146  }
147
148  std::string accept_languages_str = prefs->GetString(prefs::kAcceptLanguages);
149  std::vector<std::string> accept_languages_list;
150  base::SplitString(accept_languages_str, ',', &accept_languages_list);
151  *target = TranslateManager::GetTargetLanguage(accept_languages_list);
152}
153
154TranslateManager* ChromeTranslateClient::GetTranslateManager() {
155  return translate_manager_.get();
156}
157
158content::WebContents* ChromeTranslateClient::GetWebContents() {
159  return web_contents();
160}
161
162void ChromeTranslateClient::ShowTranslateUI(translate::TranslateStep step,
163                                            const std::string source_language,
164                                            const std::string target_language,
165                                            TranslateErrors::Type error_type,
166                                            bool triggered_from_menu) {
167  DCHECK(web_contents());
168  if (error_type != TranslateErrors::NONE)
169    step = translate::TRANSLATE_STEP_TRANSLATE_ERROR;
170
171  if (TranslateService::IsTranslateBubbleEnabled()) {
172    // Bubble UI.
173    if (step == translate::TRANSLATE_STEP_BEFORE_TRANSLATE) {
174      // TODO(droger): Move this logic out of UI code.
175      GetLanguageState().SetTranslateEnabled(true);
176      if (!GetLanguageState().HasLanguageChanged())
177        return;
178    }
179    ShowBubble(step, error_type);
180    return;
181  }
182
183  // Infobar UI.
184  TranslateInfoBarDelegate::Create(
185      step != translate::TRANSLATE_STEP_BEFORE_TRANSLATE,
186      translate_manager_->GetWeakPtr(),
187      InfoBarService::FromWebContents(web_contents()),
188      web_contents()->GetBrowserContext()->IsOffTheRecord(),
189      step,
190      source_language,
191      target_language,
192      error_type,
193      triggered_from_menu);
194}
195
196TranslateDriver* ChromeTranslateClient::GetTranslateDriver() {
197  return &translate_driver_;
198}
199
200PrefService* ChromeTranslateClient::GetPrefs() {
201  DCHECK(web_contents());
202  Profile* profile =
203      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
204  return profile->GetOriginalProfile()->GetPrefs();
205}
206
207scoped_ptr<TranslatePrefs> ChromeTranslateClient::GetTranslatePrefs() {
208  DCHECK(web_contents());
209  Profile* profile =
210      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
211  return CreateTranslatePrefs(profile->GetPrefs());
212}
213
214TranslateAcceptLanguages* ChromeTranslateClient::GetTranslateAcceptLanguages() {
215  DCHECK(web_contents());
216  return GetTranslateAcceptLanguages(web_contents()->GetBrowserContext());
217}
218
219int ChromeTranslateClient::GetInfobarIconID() const {
220  return IDR_INFOBAR_TRANSLATE;
221}
222
223// ChromeTranslateClient::CreateInfoBar() is implemented in platform-specific
224// files, except the TOOLKIT_VIEWS implementation, which has been removed.
225#if defined(TOOLKIT_VIEWS)
226scoped_ptr<infobars::InfoBar> ChromeTranslateClient::CreateInfoBar(
227    scoped_ptr<TranslateInfoBarDelegate> delegate) const {
228  return scoped_ptr<infobars::InfoBar>();
229}
230#endif
231
232bool ChromeTranslateClient::IsTranslatableURL(const GURL& url) {
233  return TranslateService::IsTranslatableURL(url);
234}
235
236void ChromeTranslateClient::ShowReportLanguageDetectionErrorUI(
237    const GURL& report_url) {
238#if defined(OS_ANDROID)
239  // Android does not support reporting language detection errors.
240  NOTREACHED();
241#else
242  // We'll open the URL in a new tab so that the user can tell us more.
243  Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
244  if (!browser) {
245    NOTREACHED();
246    return;
247  }
248
249  chrome::AddSelectedTabWithURL(
250      browser, report_url, content::PAGE_TRANSITION_AUTO_BOOKMARK);
251#endif  // defined(OS_ANDROID)
252}
253
254bool ChromeTranslateClient::OnMessageReceived(const IPC::Message& message) {
255  bool handled = true;
256  IPC_BEGIN_MESSAGE_MAP(ChromeTranslateClient, message)
257  IPC_MESSAGE_HANDLER(ChromeViewHostMsg_TranslateLanguageDetermined,
258                      OnLanguageDetermined)
259  IPC_MESSAGE_HANDLER(ChromeViewHostMsg_PageTranslated, OnPageTranslated)
260#if defined(CLD2_DYNAMIC_MODE)
261  IPC_MESSAGE_HANDLER(ChromeViewHostMsg_NeedCLDData, OnCLDDataRequested)
262#endif
263  IPC_MESSAGE_UNHANDLED(handled = false)
264  IPC_END_MESSAGE_MAP()
265
266  return handled;
267}
268
269void ChromeTranslateClient::NavigationEntryCommitted(
270    const content::LoadCommittedDetails& load_details) {
271  // Check whether this is a reload: When doing a page reload, the
272  // TranslateLanguageDetermined IPC is not sent so the translation needs to be
273  // explicitly initiated.
274
275  content::NavigationEntry* entry =
276      web_contents()->GetController().GetActiveEntry();
277  if (!entry) {
278    NOTREACHED();
279    return;
280  }
281
282  // If the navigation happened while offline don't show the translate
283  // bar since there will be nothing to translate.
284  if (load_details.http_status_code == 0 ||
285      load_details.http_status_code == net::HTTP_INTERNAL_SERVER_ERROR) {
286    return;
287  }
288
289  if (!load_details.is_main_frame &&
290      GetLanguageState().translation_declined()) {
291    // Some sites (such as Google map) may trigger sub-frame navigations
292    // when the user interacts with the page.  We don't want to show a new
293    // infobar if the user already dismissed one in that case.
294    return;
295  }
296
297  // If not a reload, return.
298  if (entry->GetTransitionType() != content::PAGE_TRANSITION_RELOAD &&
299      load_details.type != content::NAVIGATION_TYPE_SAME_PAGE) {
300    return;
301  }
302
303  if (!GetLanguageState().page_needs_translation())
304    return;
305
306  // Note that we delay it as the ordering of the processing of this callback
307  // by WebContentsObservers is undefined and might result in the current
308  // infobars being removed. Since the translation initiation process might add
309  // an infobar, it must be done after that.
310  base::MessageLoop::current()->PostTask(
311      FROM_HERE,
312      base::Bind(&ChromeTranslateClient::InitiateTranslation,
313                 weak_pointer_factory_.GetWeakPtr(),
314                 GetLanguageState().original_language(),
315                 0));
316}
317
318void ChromeTranslateClient::DidNavigateAnyFrame(
319    const content::LoadCommittedDetails& details,
320    const content::FrameNavigateParams& params) {
321  // Let the LanguageState clear its state.
322  const bool reload =
323      details.entry->GetTransitionType() == content::PAGE_TRANSITION_RELOAD ||
324      details.type == content::NAVIGATION_TYPE_SAME_PAGE;
325  GetLanguageState().DidNavigate(
326      details.is_in_page, details.is_main_frame, reload);
327}
328
329void ChromeTranslateClient::WebContentsDestroyed() {
330  // Translation process can be interrupted.
331  // Destroying the TranslateManager now guarantees that it never has to deal
332  // with NULL WebContents.
333  translate_manager_.reset();
334}
335
336#if defined(CLD2_DYNAMIC_MODE)
337void ChromeTranslateClient::OnCLDDataRequested() {
338  // Quickly try to read s_cached_file_. If valid, the file handle is
339  // cached and can be used immediately. Else, queue the caching task to the
340  // blocking pool.
341  base::File* handle = NULL;
342  uint64 data_offset = 0;
343  uint64 data_length = 0;
344  {
345    base::AutoLock lock(s_file_lock_.Get());
346    handle = s_cached_file_;
347    data_offset = s_cached_data_offset_;
348    data_length = s_cached_data_length_;
349  }
350
351  if (handle && handle->IsValid()) {
352    // Cached data available. Respond to the request.
353    SendCLDDataAvailable(handle, data_offset, data_length);
354    return;
355  }
356
357  // Else, we don't have the data file yet. Queue a caching attempt.
358  // The caching attempt happens in the blocking pool because it may involve
359  // arbitrary filesystem access.
360  // After the caching attempt is made, we call MaybeSendCLDDataAvailable
361  // to pass the file handle to the renderer. This only results in an IPC
362  // message if the caching attempt was successful.
363  content::BrowserThread::PostBlockingPoolTaskAndReply(
364      FROM_HERE,
365      base::Bind(&ChromeTranslateClient::HandleCLDDataRequest),
366      base::Bind(&ChromeTranslateClient::MaybeSendCLDDataAvailable,
367                 weak_pointer_factory_.GetWeakPtr()));
368}
369
370void ChromeTranslateClient::MaybeSendCLDDataAvailable() {
371  base::File* handle = NULL;
372  uint64 data_offset = 0;
373  uint64 data_length = 0;
374  {
375    base::AutoLock lock(s_file_lock_.Get());
376    handle = s_cached_file_;
377    data_offset = s_cached_data_offset_;
378    data_length = s_cached_data_length_;
379  }
380
381  if (handle && handle->IsValid())
382    SendCLDDataAvailable(handle, data_offset, data_length);
383}
384
385void ChromeTranslateClient::SendCLDDataAvailable(const base::File* handle,
386                                                 const uint64 data_offset,
387                                                 const uint64 data_length) {
388  // Data available, respond to the request.
389  IPC::PlatformFileForTransit ipc_platform_file = IPC::GetFileHandleForProcess(
390      handle->GetPlatformFile(),
391      GetWebContents()->GetRenderViewHost()->GetProcess()->GetHandle(),
392      false);
393  // In general, sending a response from within the code path that is processing
394  // a request is discouraged because there is potential for deadlock (if the
395  // methods are sent synchronously) or loops (if the response can trigger a
396  // new request). Neither of these concerns is relevant in this code, so
397  // sending the response from within the code path of the request handler is
398  // safe.
399  Send(new ChromeViewMsg_CLDDataAvailable(
400      GetWebContents()->GetRenderViewHost()->GetRoutingID(),
401      ipc_platform_file,
402      data_offset,
403      data_length));
404}
405
406void ChromeTranslateClient::HandleCLDDataRequest() {
407  // Because this function involves arbitrary file system access, it must run
408  // on the blocking pool.
409  DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
410  DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
411
412  {
413    base::AutoLock lock(s_file_lock_.Get());
414    if (s_cached_file_)
415      return;  // Already done, duplicate request
416  }
417
418#if defined(CLD2_IS_COMPONENT)
419  base::FilePath path = component_updater::GetLatestCldDataFile();
420  if (path.empty())
421    return;
422#else  // CLD2 data is at a well-known file path
423  base::FilePath path;
424  if (!PathService::Get(chrome::DIR_USER_DATA, &path)) {
425    LOG(WARNING) << "Unable to locate user data directory";
426    return;  // Chrome isn't properly installed.
427  }
428  path = path.Append(chrome::kCLDDataFilename);
429#endif
430
431  // If the file exists, we can send an IPC-safe construct back to the
432  // renderer process immediately; otherwise, nothing to do here.
433  if (!base::PathExists(path))
434    return;
435
436  // Attempt to open the file for reading.
437  scoped_ptr<base::File> file(
438      new base::File(path, base::File::FLAG_OPEN | base::File::FLAG_READ));
439  if (!file->IsValid()) {
440    LOG(WARNING) << "CLD data file exists but cannot be opened";
441    return;
442  }
443
444  base::File::Info file_info;
445  if (!file->GetInfo(&file_info)) {
446    LOG(WARNING) << "CLD data file exists but cannot be inspected";
447    return;
448  }
449
450  // For now, our offset and length are simply 0 and the length of the file,
451  // respectively. If we later decide to include the CLD2 data file inside of
452  // a larger binary context, these params can be twiddled appropriately.
453  const uint64 data_offset = 0;
454  const uint64 data_length = file_info.size;
455
456  {
457    base::AutoLock lock(s_file_lock_.Get());
458    if (s_cached_file_) {
459      // Idempotence: Racing another request on the blocking pool, abort.
460    } else {
461      // Else, this request has taken care of it all. Cache all info.
462      s_cached_file_ = file.release();
463      s_cached_data_offset_ = data_offset;
464      s_cached_data_length_ = data_length;
465    }
466  }
467}
468
469#endif  // defined(CLD2_DYNAMIC_MODE)
470
471void ChromeTranslateClient::InitiateTranslation(const std::string& page_lang,
472                                                int attempt) {
473  if (GetLanguageState().translation_pending())
474    return;
475
476  // During a reload we need web content to be available before the
477  // translate script is executed. Otherwise we will run the translate script on
478  // an empty DOM which will fail. Therefore we wait a bit to see if the page
479  // has finished.
480  if (web_contents()->IsLoading() && attempt < max_reload_check_attempts_) {
481    int backoff = attempt * kMaxTranslateLoadCheckAttempts;
482    base::MessageLoop::current()->PostDelayedTask(
483        FROM_HERE,
484        base::Bind(&ChromeTranslateClient::InitiateTranslation,
485                   weak_pointer_factory_.GetWeakPtr(),
486                   page_lang,
487                   ++attempt),
488        base::TimeDelta::FromMilliseconds(backoff));
489    return;
490  }
491
492  translate_manager_->InitiateTranslation(
493      TranslateDownloadManager::GetLanguageCode(page_lang));
494}
495
496void ChromeTranslateClient::OnLanguageDetermined(
497    const LanguageDetectionDetails& details,
498    bool page_needs_translation) {
499  GetLanguageState().LanguageDetermined(details.adopted_language,
500                                        page_needs_translation);
501
502  if (web_contents())
503    translate_manager_->InitiateTranslation(details.adopted_language);
504
505  content::NotificationService::current()->Notify(
506      chrome::NOTIFICATION_TAB_LANGUAGE_DETERMINED,
507      content::Source<content::WebContents>(web_contents()),
508      content::Details<const LanguageDetectionDetails>(&details));
509}
510
511void ChromeTranslateClient::OnPageTranslated(int32 page_id,
512                                             const std::string& original_lang,
513                                             const std::string& translated_lang,
514                                             TranslateErrors::Type error_type) {
515  DCHECK(web_contents());
516  translate_manager_->PageTranslated(
517      original_lang, translated_lang, error_type);
518
519  PageTranslatedDetails details;
520  details.source_language = original_lang;
521  details.target_language = translated_lang;
522  details.error_type = error_type;
523  content::NotificationService::current()->Notify(
524      chrome::NOTIFICATION_PAGE_TRANSLATED,
525      content::Source<content::WebContents>(web_contents()),
526      content::Details<PageTranslatedDetails>(&details));
527}
528
529void ChromeTranslateClient::ShowBubble(translate::TranslateStep step,
530                                       TranslateErrors::Type error_type) {
531// The bubble is implemented only on the desktop platforms.
532#if !defined(OS_ANDROID) && !defined(OS_IOS)
533  Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
534
535  // |browser| might be NULL when testing. In this case, Show(...) should be
536  // called because the implementation for testing is used.
537  if (!browser) {
538    TranslateBubbleFactory::Show(NULL, web_contents(), step, error_type);
539    return;
540  }
541
542  if (web_contents() != browser->tab_strip_model()->GetActiveWebContents())
543    return;
544
545  // This ShowBubble function is also used for upating the existing bubble.
546  // However, with the bubble shown, any browser windows are NOT activated
547  // because the bubble takes the focus from the other widgets including the
548  // browser windows. So it is checked that |browser| is the last activated
549  // browser, not is now activated.
550  if (browser !=
551      chrome::FindLastActiveWithHostDesktopType(browser->host_desktop_type())) {
552    return;
553  }
554
555  // During auto-translating, the bubble should not be shown.
556  if (step == translate::TRANSLATE_STEP_TRANSLATING ||
557      step == translate::TRANSLATE_STEP_AFTER_TRANSLATE) {
558    if (GetLanguageState().InTranslateNavigation())
559      return;
560  }
561
562  TranslateBubbleFactory::Show(
563      browser->window(), web_contents(), step, error_type);
564#else
565  NOTREACHED();
566#endif
567}
568