blob: a034f503f3502a8ebc079082894e509866992d18 [file] [log] [blame]
// Copyright (c) 2012 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.
#include "chrome/browser/user_style_sheet_watcher.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/file_util.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/web_contents.h"
using ::base::FilePathWatcher;
using content::BrowserThread;
using content::WebContents;
namespace {
// The subdirectory of the profile that contains the style sheet.
const char kStyleSheetDir[] = "User StyleSheets";
// The filename of the stylesheet.
const char kUserStyleSheetFile[] = "Custom.css";
} // namespace
// UserStyleSheetLoader is responsible for loading the user style sheet on the
// file thread and sends a notification when the style sheet is loaded. It is
// a helper to UserStyleSheetWatcher. The reference graph is as follows:
//
// .-----------------------. owns .-----------------.
// | UserStyleSheetWatcher |----------->| FilePathWatcher |
// '-----------------------' '-----------------'
// | |
// V |
// .----------------------. |
// | UserStyleSheetLoader |<--------------------'
// '----------------------'
//
// FilePathWatcher's reference to UserStyleSheetLoader is used for delivering
// the change notifications. Since they happen asynchronously,
// UserStyleSheetWatcher and its FilePathWatcher may be destroyed while a
// callback to UserStyleSheetLoader is in progress, in which case the
// UserStyleSheetLoader object outlives the watchers.
class UserStyleSheetLoader
: public base::RefCountedThreadSafe<UserStyleSheetLoader> {
public:
UserStyleSheetLoader();
GURL user_style_sheet() const {
return user_style_sheet_;
}
// Load the user style sheet on the file thread and convert it to a
// base64 URL. Posts the base64 URL back to the UI thread.
void LoadStyleSheet(const base::FilePath& style_sheet_file);
// Register a callback to be called whenever the stylesheet gets updated.
scoped_ptr<base::CallbackRegistry<void>::Subscription>
RegisterOnStyleSheetUpdatedCallback(const base::Closure& callback);
// Send out a notification if the stylesheet has already been loaded.
void NotifyLoaded();
// FilePathWatcher::Callback method:
void NotifyPathChanged(const base::FilePath& path, bool error);
private:
friend class base::RefCountedThreadSafe<UserStyleSheetLoader>;
~UserStyleSheetLoader();
// Called on the UI thread after the stylesheet has loaded.
void SetStyleSheet(const GURL& url);
// The user style sheet as a base64 data:// URL.
GURL user_style_sheet_;
// Whether the stylesheet has been loaded.
bool has_loaded_;
base::CallbackRegistry<void> style_sheet_updated_callbacks_;
DISALLOW_COPY_AND_ASSIGN(UserStyleSheetLoader);
};
UserStyleSheetLoader::UserStyleSheetLoader()
: has_loaded_(false) {
}
UserStyleSheetLoader::~UserStyleSheetLoader() {
}
scoped_ptr<base::CallbackRegistry<void>::Subscription>
UserStyleSheetLoader::RegisterOnStyleSheetUpdatedCallback(
const base::Closure& callback) {
return style_sheet_updated_callbacks_.Add(callback);
}
void UserStyleSheetLoader::NotifyLoaded() {
if (has_loaded_) {
style_sheet_updated_callbacks_.Notify();
}
}
void UserStyleSheetLoader::NotifyPathChanged(const base::FilePath& path,
bool error) {
if (!error)
LoadStyleSheet(path);
}
void UserStyleSheetLoader::LoadStyleSheet(
const base::FilePath& style_sheet_file) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
// We keep the user style sheet in a subdir so we can watch for changes
// to the file.
base::FilePath style_sheet_dir = style_sheet_file.DirName();
if (!base::DirectoryExists(style_sheet_dir)) {
if (!file_util::CreateDirectory(style_sheet_dir))
return;
}
// Create the file if it doesn't exist.
if (!base::PathExists(style_sheet_file))
file_util::WriteFile(style_sheet_file, "", 0);
std::string css;
bool rv = base::ReadFileToString(style_sheet_file, &css);
GURL style_sheet_url;
if (rv && !css.empty()) {
std::string css_base64;
rv = base::Base64Encode(css, &css_base64);
if (rv) {
// WebKit knows about data urls, so convert the file to a data url.
const char kDataUrlPrefix[] = "data:text/css;charset=utf-8;base64,";
style_sheet_url = GURL(kDataUrlPrefix + css_base64);
}
}
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
base::Bind(&UserStyleSheetLoader::SetStyleSheet, this,
style_sheet_url));
}
void UserStyleSheetLoader::SetStyleSheet(const GURL& url) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
has_loaded_ = true;
user_style_sheet_ = url;
NotifyLoaded();
}
UserStyleSheetWatcher::UserStyleSheetWatcher(Profile* profile,
const base::FilePath& profile_path)
: RefcountedBrowserContextKeyedService(content::BrowserThread::UI),
profile_(profile),
profile_path_(profile_path),
loader_(new UserStyleSheetLoader) {
// Listen for when the first render view host is created. If we load
// too fast, the first tab won't hear the notification and won't get
// the user style sheet.
registrar_.Add(this,
content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED,
content::NotificationService::AllBrowserContextsAndSources());
}
UserStyleSheetWatcher::~UserStyleSheetWatcher() {
}
void UserStyleSheetWatcher::Init() {
// Make sure we run on the file thread.
if (!BrowserThread::CurrentlyOn(BrowserThread::FILE)) {
BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
base::Bind(&UserStyleSheetWatcher::Init, this));
return;
}
if (!file_watcher_.get()) {
file_watcher_.reset(new FilePathWatcher);
base::FilePath style_sheet_file = profile_path_.AppendASCII(kStyleSheetDir)
.AppendASCII(kUserStyleSheetFile);
if (!file_watcher_->Watch(
style_sheet_file,
false,
base::Bind(&UserStyleSheetLoader::NotifyPathChanged,
loader_.get()))) {
LOG(ERROR) << "Failed to setup watch for " << style_sheet_file.value();
}
loader_->LoadStyleSheet(style_sheet_file);
}
}
GURL UserStyleSheetWatcher::user_style_sheet() const {
return loader_->user_style_sheet();
}
scoped_ptr<base::CallbackRegistry<void>::Subscription>
UserStyleSheetWatcher::RegisterOnStyleSheetUpdatedCallback(
const base::Closure& callback) {
return loader_->RegisterOnStyleSheetUpdatedCallback(callback);
}
void UserStyleSheetWatcher::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(type == content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED);
if (profile_->IsSameProfile(Profile::FromBrowserContext(
content::Source<WebContents>(source)->GetBrowserContext()))) {
loader_->NotifyLoaded();
registrar_.RemoveAll();
}
}
void UserStyleSheetWatcher::ShutdownOnUIThread() {
registrar_.RemoveAll();
}