blob: 0d7ea866e0a5234e35bc5ebd20f83661ee4ac386 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/reading_list/offline_page_tab_helper.h"
#include "base/base64.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/post_task.h"
#include "base/timer/timer.h"
#include "components/reading_list/core/offline_url_utils.h"
#include "components/reading_list/core/reading_list_entry.h"
#include "components/reading_list/core/reading_list_model.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/chrome_url_constants.h"
#include "ios/chrome/browser/reading_list/offline_url_utils.h"
#include "ios/chrome/browser/reading_list/reading_list_download_service.h"
#include "ios/chrome/browser/reading_list/reading_list_download_service_factory.h"
#import "ios/web/public/navigation_manager.h"
#import "ios/web/public/web_state/navigation_context.h"
#include "ios/web/public/web_task_traits.h"
#include "ios/web/public/web_thread.h"
#include "ui/base/page_transition_types.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Gets the offline data at |offline_path|. The result is a single std::string
// with all resources inlined.
// This method access file system and cannot be called on UI thread.
std::string GetOfflineData(base::FilePath offline_root,
base::FilePath offline_path) {
base::FilePath absolute_path =
reading_list::OfflineURLAbsolutePathFromRelativePath(offline_root,
offline_path);
std::string content;
if (!base::ReadFileToString(absolute_path, &content)) {
return std::string();
}
if (offline_path.Extension() != ".html") {
// If page is not html (in the current version, it means it is PDF), there
// are no resources to inline.
return content;
}
base::FileEnumerator images_enumerator(absolute_path.DirName(), false,
base::FileEnumerator::FILES);
for (base::FilePath image_path = images_enumerator.Next();
!image_path.empty(); image_path = images_enumerator.Next()) {
if (image_path == absolute_path) {
// Skip the root file.
continue;
}
std::string file_name = image_path.BaseName().value();
if (file_name.size() != 32) {
// Resource file names are hashes with 32 hexadecimal characters.
continue;
}
std::string image;
if (!base::ReadFileToString(image_path, &image)) {
continue;
}
base::Base64Encode(image, &image);
std::string src_with_file = base::StringPrintf("%s", file_name.c_str());
std::string src_with_data =
base::StringPrintf("data:image/png;base64,%s", image.c_str());
base::ReplaceSubstringsAfterOffset(&content, 0, src_with_file,
src_with_data);
}
return content;
}
}
// static
void OfflinePageTabHelper::CreateForWebState(web::WebState* web_state,
ReadingListModel* model) {
if (!FromWebState(web_state)) {
web_state->SetUserData(
UserDataKey(),
base::WrapUnique(new OfflinePageTabHelper(web_state, model)));
}
}
OfflinePageTabHelper::OfflinePageTabHelper(web::WebState* web_state,
ReadingListModel* model)
: web_state_(web_state), reading_list_model_(model) {
web_state_->AddObserver(this);
reading_list_model_->AddObserver(this);
}
OfflinePageTabHelper::~OfflinePageTabHelper() {
Detach();
}
void OfflinePageTabHelper::Detach() {
StopCheckingLoadingProgress();
if (reading_list_model_) {
reading_list_model_->RemoveObserver(this);
reading_list_model_ = nullptr;
}
if (web_state_) {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
}
void OfflinePageTabHelper::LoadData(int offline_navigation,
const GURL& url,
const std::string& extension,
const std::string& data) {
if (!web_state_ || !web_state_->GetNavigationManager() ||
!web_state_->GetNavigationManager()->GetLastCommittedItem()) {
// It is possible that the web_state_ has been detached during the page
// loading.
return;
}
if (last_navigation_started_ != offline_navigation) {
return;
}
NSString* mime = nil;
if (extension == ".html") {
mime = @"text/html";
} else if (extension == ".pdf") {
mime = @"application/pdf";
}
DCHECK(mime);
presenting_offline_page_ = true;
NSData* ns_data = [NSData dataWithBytes:data.c_str() length:data.size()];
offline_navigation_triggered_ = url;
web_state_->LoadData(ns_data, mime, url);
}
void OfflinePageTabHelper::DidStartNavigation(web::WebState* web_state,
web::NavigationContext* context) {
if (context->GetUrl() == offline_navigation_triggered_ ||
context->IsSameDocument()) {
// This is the navigation triggered by the loadData. Ignore it, to not reset
// the presenting_offline_page_ flag.
offline_navigation_triggered_ = GURL::EmptyGURL();
return;
}
offline_navigation_triggered_ = GURL::EmptyGURL();
initial_navigation_url_ = context->GetUrl();
loading_slow_or_failed_ = false;
navigation_committed_ = false;
last_navigation_started_++;
bool is_reload = ui::PageTransitionTypeIncludingQualifiersIs(
context->GetPageTransition(), ui::PAGE_TRANSITION_RELOAD);
is_offline_navigation_ = reading_list::IsOfflineURL(initial_navigation_url_);
navigation_transition_type_ = context->GetPageTransition();
navigation_is_renderer_initiated_ = context->IsRendererInitiated();
if (!is_reload || !presenting_offline_page_) {
StartCheckingLoadingProgress(initial_navigation_url_);
}
presenting_offline_page_ = false;
}
void OfflinePageTabHelper::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (navigation_context->IsSameDocument()) {
return;
}
navigation_committed_ = navigation_context->HasCommitted();
if (!presenting_offline_page_) {
PresentOfflinePageForOnlineUrl(initial_navigation_url_);
}
}
GURL OfflinePageTabHelper::GetOnlineURLFromNavigationURL(
const GURL& url) const {
if (url.host() == kChromeUIOfflineHost) {
return reading_list::EntryURLForOfflineURL(url);
}
return url;
}
void OfflinePageTabHelper::PageLoaded(
web::WebState* web_state,
web::PageLoadCompletionStatus load_completion_status) {
StopCheckingLoadingProgress();
// If the offline page was loaded directly, the initial_navigation_url_ is
// chrome://offline?... and triggers a load error. Extract the meaningful
// URL from the loading URL.
GURL url = GetOnlineURLFromNavigationURL(initial_navigation_url_);
if (load_completion_status == web::PageLoadCompletionStatus::FAILURE) {
loading_slow_or_failed_ = true;
PresentOfflinePageForOnlineUrl(url);
return;
}
if (!url.is_valid() || !reading_list_model_->loaded() ||
!reading_list_model_->GetEntryByURL(url)) {
return;
}
reading_list_model_->SetReadStatus(url, true);
UMA_HISTOGRAM_BOOLEAN("ReadingList.OfflineVersionDisplayed",
presenting_offline_page_);
}
void OfflinePageTabHelper::WebStateDestroyed(web::WebState* web_state) {
DCHECK(web_state_ == nullptr || web_state_ == web_state);
Detach();
}
void OfflinePageTabHelper::ReadingListModelLoaded(
const ReadingListModel* model) {
if (navigation_committed_ && loading_slow_or_failed_) {
PresentOfflinePageForOnlineUrl(initial_navigation_url_);
}
}
void OfflinePageTabHelper::ReadingListModelBeingDeleted(
const ReadingListModel* model) {
DCHECK(reading_list_model_ == nullptr || reading_list_model_ == model);
Detach();
// The call to RemoveUserData cause the destruction of the current instance,
// so nothing should be done after that point (this is like "delete this;").
// Unregistration as an observer happens in the destructor.
web_state_->RemoveUserData(UserDataKey());
}
void OfflinePageTabHelper::PresentOfflinePageForOnlineUrl(const GURL& url) {
// As presenting the offline version will replace the content of the committed
// page, the offline version can only be presented if the navigation currently
// tracked by the OfflinePageTabHelper is the last committed one.
// This is the case in three scenarios:
// - the navigation has been committed (here, navigation_committed_ is true),
// - the navigation is a reload, which means it has been committed on the
// previous load (here, is_reload is true),
// - if the navigation is a new navigation (here, is_new_navigation is true).
// In this case, it will be cancel and a new placeholder navigation will be
// triggerred that will always becommitted. This new navigation will be
// replaced by offline version.
bool is_reload = ui::PageTransitionTypeIncludingQualifiersIs(
navigation_transition_type_, ui::PAGE_TRANSITION_RELOAD);
bool is_new_navigation =
ui::PageTransitionIsNewNavigation(navigation_transition_type_);
bool can_work_on_not_committed_navigation = is_reload || is_new_navigation;
bool can_load_offline =
navigation_committed_ || can_work_on_not_committed_navigation;
if (!can_load_offline) {
return;
}
if (!loading_slow_or_failed_) {
return;
}
if (!HasDistilledVersionForOnlineUrl(url)) {
return;
}
GURL entry_url = GetOnlineURLFromNavigationURL(url);
const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(entry_url);
if (!is_offline_navigation_ && is_new_navigation && !navigation_committed_) {
// If the current navigation was not committed, but it was a new navigation,
// a new placeholder navigation with a chrome://offline URL can be created
// which will be replaced by the offline version on load failure.
GURL offlineURL = reading_list::OfflineURLForPath(
entry->DistilledPath(), entry_url, entry->DistilledURL());
web::NavigationManager::WebLoadParams params(offlineURL);
params.transition_type = navigation_transition_type_;
params.virtual_url = entry_url;
params.is_renderer_initiated = navigation_is_renderer_initiated_;
web_state_->GetNavigationManager()->LoadURLWithParams(params);
return;
}
ios::ChromeBrowserState* browser_state =
ios::ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState());
base::FilePath offline_root =
ReadingListDownloadServiceFactory::GetForBrowserState(browser_state)
->OfflineRoot()
.DirName();
base::FilePath offline_path = entry->DistilledPath();
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&GetOfflineData, offline_root, offline_path),
base::BindOnce(&OfflinePageTabHelper::LoadData, base::Unretained(this),
last_navigation_started_, entry_url,
offline_path.Extension()));
}
bool OfflinePageTabHelper::HasDistilledVersionForOnlineUrl(
const GURL& online_url) const {
if (!reading_list_model_ || !web_state_ || !reading_list_model_->loaded() ||
!online_url.is_valid()) {
return false;
}
GURL url = GetOnlineURLFromNavigationURL(online_url);
const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url);
if (!entry)
return false;
return entry->DistilledState() == ReadingListEntry::PROCESSED;
}
void OfflinePageTabHelper::StartCheckingLoadingProgress(const GURL& url) {
if (reading_list_model_->loaded() && !HasDistilledVersionForOnlineUrl(url)) {
// No need to launch the timer if there is no distilled version.
return;
}
try_number_ = 0;
timer_.reset(new base::RepeatingTimer());
timer_->Start(FROM_HERE, base::TimeDelta::FromMilliseconds(1500),
base::BindRepeating(&OfflinePageTabHelper::CheckLoadingProgress,
base::Unretained(this), url));
}
void OfflinePageTabHelper::StopCheckingLoadingProgress() {
timer_.reset();
}
void OfflinePageTabHelper::CheckLoadingProgress(const GURL& url) {
if (reading_list_model_->loaded() && !HasDistilledVersionForOnlineUrl(url)) {
StopCheckingLoadingProgress();
return;
}
try_number_++;
double progress = web_state_->GetLoadingProgress();
const double kMinimumExpectedProgressPerStep = 0.25;
if (progress < try_number_ * kMinimumExpectedProgressPerStep) {
loading_slow_or_failed_ = true;
PresentOfflinePageForOnlineUrl(url);
StopCheckingLoadingProgress();
} else if (try_number_ >= 3) {
// Loading reached 75%, let the page finish normal loading.
// Do not call StopCheckingLoadingProgress() as pending url is still needed
// to mark the entry read on success loading or to display offline version
// on error.
timer_->Stop();
}
}
WEB_STATE_USER_DATA_KEY_IMPL(OfflinePageTabHelper)