blob: 4fb8f653a2e822bc00caecc03c288081a9fe9d01 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/translate/content/browser/content_translate_driver.h"
#include <memory>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/strings/utf_string_conversions.h"
#include "base/supports_user_data.h"
#include "base/task/single_thread_task_runner.h"
#include "components/google/core/common/google_util.h"
#include "components/language/core/browser/url_language_histogram.h"
#include "components/translate/content/browser/content_record_page_language.h"
#include "components/translate/core/browser/translate_download_manager.h"
#include "components/translate/core/browser/translate_manager.h"
#include "components/translate/core/browser/translate_metrics_logger.h"
#include "components/translate/core/common/translate_metrics.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/referrer.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "url/gurl.h"
namespace translate {
namespace {
// The maximum number of attempts we'll do to see if the page has finshed
// loading before giving up the translation
const int kMaxTranslateLoadCheckAttempts = 20;
} // namespace
ContentTranslateDriver::ContentTranslateDriver(
content::WebContents& web_contents,
language::UrlLanguageHistogram* url_language_histogram)
: content::WebContentsObserver(&web_contents),
translate_manager_(nullptr),
is_otr_context_(web_contents.GetBrowserContext()->IsOffTheRecord()),
max_reload_check_attempts_(kMaxTranslateLoadCheckAttempts),
next_page_seq_no_(0),
language_histogram_(url_language_histogram) {}
ContentTranslateDriver::~ContentTranslateDriver() = default;
void ContentTranslateDriver::AddTranslationObserver(
TranslationObserver* observer) {
translation_observers_.AddObserver(observer);
}
void ContentTranslateDriver::RemoveTranslationObserver(
TranslationObserver* observer) {
translation_observers_.RemoveObserver(observer);
}
void ContentTranslateDriver::InitiateTranslation(const std::string& page_lang,
int attempt) {
if (translate_manager_->GetLanguageState()->translation_pending())
return;
// During a reload we need web content to be available before the
// translate script is executed. Otherwise we will run the translate script on
// an empty DOM which will fail. Therefore we wait a bit to see if the page
// has finished.
if (web_contents()->IsLoading() && attempt < max_reload_check_attempts_) {
int backoff = attempt * kMaxTranslateLoadCheckAttempts;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ContentTranslateDriver::InitiateTranslation,
weak_pointer_factory_.GetWeakPtr(), page_lang,
attempt + 1),
base::Milliseconds(backoff));
return;
}
translate_manager_->InitiateTranslation(
translate::TranslateDownloadManager::GetLanguageCode(page_lang));
}
// TranslateDriver methods
bool ContentTranslateDriver::IsLinkNavigation() {
return ui::PageTransitionCoreTypeIs(web_contents()
->GetController()
.GetLastCommittedEntry()
->GetTransitionType(),
ui::PAGE_TRANSITION_LINK);
}
void ContentTranslateDriver::OnTranslateEnabledChanged() {
for (auto& observer : translation_observers_)
observer.OnTranslateEnabledChanged(web_contents());
}
void ContentTranslateDriver::OnIsPageTranslatedChanged() {
for (auto& observer : translation_observers_)
observer.OnIsPageTranslatedChanged(web_contents());
}
void ContentTranslateDriver::TranslatePage(int page_seq_no,
const std::string& translate_script,
const std::string& source_lang,
const std::string& target_lang) {
auto it = translate_agents_.find(page_seq_no);
if (it == translate_agents_.end())
return; // This page has navigated away.
it->second->TranslateFrame(
translate_script, source_lang, target_lang,
base::BindOnce(&ContentTranslateDriver::OnPageTranslated,
base::Unretained(this)));
}
void ContentTranslateDriver::RevertTranslation(int page_seq_no) {
auto it = translate_agents_.find(page_seq_no);
if (it == translate_agents_.end())
return; // This page has navigated away.
it->second->RevertTranslation();
}
bool ContentTranslateDriver::IsIncognito() const {
return is_otr_context_;
}
const std::string& ContentTranslateDriver::GetContentsMimeType() {
return web_contents()->GetContentsMimeType();
}
const GURL& ContentTranslateDriver::GetLastCommittedURL() const {
return last_committed_url_;
}
const GURL& ContentTranslateDriver::GetVisibleURL() {
return web_contents()->GetVisibleURL();
}
ukm::SourceId ContentTranslateDriver::GetUkmSourceId() {
return web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
}
bool ContentTranslateDriver::HasCurrentPage() const {
// TODO(crbug.com/40432764): This method previously checked for the existence
// of GetLastCommittedEntry(), which always exists now. Check if this is true
// for other implementations and consider removing this method.
return true;
}
void ContentTranslateDriver::InitiateTranslationIfReload(
content::NavigationHandle* navigation_handle) {
// Check whether this is a reload: When doing a page reload, the
// TranslateLanguageDetermined IPC is not sent so the translation needs to be
// explicitly initiated.
// If the navigation happened while offline don't show the translate
// bar since there will be nothing to translate.
int response_code =
navigation_handle->GetResponseHeaders()
? navigation_handle->GetResponseHeaders()->response_code()
: 0;
if (response_code == 0 || response_code == net::HTTP_INTERNAL_SERVER_ERROR)
return;
if (!navigation_handle->IsInMainFrame() &&
translate_manager_->GetLanguageState()->translation_declined()) {
// Some sites (such as Google map) may trigger sub-frame navigations
// when the user interacts with the page. We don't want to show a new
// infobar if the user already dismissed one in that case.
return;
}
// If not a reload, return.
if (navigation_handle->GetReloadType() == content::ReloadType::NONE)
return;
if (navigation_handle->GetPageTransition() &
ui::PAGE_TRANSITION_FORWARD_BACK) {
// Workaround for http://crbug.com/653051: back navigation sometimes have
// the reload core type. Once http://crbug.com/669008 got resolved, we
// could revisit here for a thorough solution.
//
// This means that the new translation won't be started when the page
// is restored from back-forward cache, which is the right thing to do.
// TODO(crbug.com/40097545): Ensure that it stays disabled for
// back-forward navigations even when bug above is fixed.
return;
}
if (!translate_manager_->GetLanguageState()
->page_level_translation_criteria_met()) {
return;
}
// Note that we delay it as the ordering of the processing of this callback
// by WebContentsObservers is undefined and might result in the current
// infobars being removed. Since the translation initiation process might add
// an infobar, it must be done after that.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ContentTranslateDriver::InitiateTranslation,
weak_pointer_factory_.GetWeakPtr(),
translate_manager_->GetLanguageState()->source_language(),
0));
}
// content::WebContentsObserver methods
void ContentTranslateDriver::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->HasCommitted()) {
return;
}
// Continue to process the navigation only if it is for the primary main
// frame. It is safe to do so because:
// - A non-primary page should not reset `this`'s language state since the
// state is set for the primary page. It will be allowed to update the state
// after it becomes the primary page (at that time, this function will be
// invoked again, and the page will update the state).
// - This class does not need to handle subframe navigations. Employing this
// class means the flag of kTranslateSubFrames is disabled, i.e., subframe
// translation is not supported. Besides it, subframes cannot change language
// state.
if (!navigation_handle->IsInPrimaryMainFrame()) {
return;
}
// Store the main frame committed URL.
last_committed_url_ = web_contents()->GetLastCommittedURL();
InitiateTranslationIfReload(navigation_handle);
if (navigation_handle->IsPrerenderedPageActivation()) {
// Set it to NULL time, and do not report the LanguageDeterminedDuration
// metric in this case.
// The browser defers the RegisterPage() message on a prerendering page, so
// this kind of data is noisy and should be filtered out.
finish_navigation_time_ = base::TimeTicks();
} else if (navigation_handle->IsInPrimaryMainFrame()) {
finish_navigation_time_ = base::TimeTicks::Now();
}
// Let the LanguageState clear its state.
const bool reload =
navigation_handle->GetReloadType() != content::ReloadType::NONE ||
navigation_handle->IsSameDocument();
const std::optional<url::Origin>& initiator_origin =
navigation_handle->GetInitiatorOrigin();
bool navigation_from_google =
initiator_origin.has_value() &&
(google_util::IsGoogleDomainUrl(initiator_origin->GetURL(),
google_util::DISALLOW_SUBDOMAIN,
google_util::ALLOW_NON_STANDARD_PORTS));
translate_manager_->GetLanguageState()->DidNavigate(
navigation_handle->IsSameDocument(),
navigation_handle->IsInPrimaryMainFrame(), reload,
navigation_handle->GetHrefTranslate(), navigation_from_google);
}
void ContentTranslateDriver::OnPageAway(int page_seq_no) {
translate_agents_.erase(page_seq_no);
}
void ContentTranslateDriver::AddReceiver(
mojo::PendingReceiver<translate::mojom::ContentTranslateDriver> receiver) {
receivers_.Add(this, std::move(receiver));
}
void ContentTranslateDriver::RegisterPage(
mojo::PendingRemote<translate::mojom::TranslateAgent> translate_agent,
const translate::LanguageDetectionDetails& details,
const bool page_level_translation_criteria_met) {
base::TimeTicks language_determined_time = base::TimeTicks::Now();
ReportLanguageDeterminedDuration(finish_navigation_time_,
language_determined_time);
// If we have a language histogram (i.e. we're not in incognito), update it
// with the detected language of every page visited.
if (language_histogram_ && details.is_model_reliable)
language_histogram_->OnPageVisited(details.model_detected_language);
translate_agents_[++next_page_seq_no_].Bind(std::move(translate_agent));
translate_agents_[next_page_seq_no_].set_disconnect_handler(
base::BindOnce(&ContentTranslateDriver::OnPageAway,
base::Unretained(this), next_page_seq_no_));
translate_manager_->set_current_seq_no(next_page_seq_no_);
translate_manager_->GetLanguageState()->LanguageDetermined(
details.adopted_language, page_level_translation_criteria_met);
if (web_contents()) {
translate_manager_->InitiateTranslation(details.adopted_language);
// Save the page language on the navigation entry so it can be synced.
// TODO(crbug.com/40779913): The mojo IPC coming from the renderer might
// race with a navigation, so the page that sent this message might already
// be in the pending delete state after being navigated away from.
// Rearchitect the renderer-browser Mojo connection to be able to explicitly
// determine the document/content::Page with which this language
// determination event is associated, thus avoiding the potential for corner
// cases where the detected language is attributed to the wrong page.
auto* const entry = web_contents()->GetController().GetLastCommittedEntry();
SetPageLanguageInNavigation(details.adopted_language, entry);
}
for (auto& observer : language_detection_observers())
observer.OnLanguageDetermined(details);
translate_manager_->GetActiveTranslateMetricsLogger()
->LogHTMLDocumentLanguage(details.html_root_language);
translate_manager_->GetActiveTranslateMetricsLogger()->LogHTMLContentLanguage(
details.content_language);
translate_manager_->GetActiveTranslateMetricsLogger()->LogDetectedLanguage(
details.model_detected_language);
translate_manager_->GetActiveTranslateMetricsLogger()
->LogDetectionReliabilityScore(details.model_reliability_score);
translate_manager_->GetActiveTranslateMetricsLogger()->LogWasContentEmpty(
details.contents.length() > 0);
}
void ContentTranslateDriver::OnPageTranslated(
bool cancelled,
const std::string& source_lang,
const std::string& translated_lang,
TranslateErrors error_type) {
if (cancelled) {
// Informs the |TranslateMetricsLogger| that the translation was cancelled.
translate_manager_->GetActiveTranslateMetricsLogger()
->LogTranslationFinished(false, error_type);
return;
}
translate_manager_->PageTranslated(source_lang, translated_lang, error_type);
for (auto& observer : translation_observers_)
observer.OnPageTranslated(source_lang, translated_lang, error_type);
}
} // namespace translate