1// Copyright (c) 2011 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/user_style_sheet_watcher.h"
6
7#include "base/base64.h"
8#include "base/file_util.h"
9#include "content/common/notification_service.h"
10#include "content/common/notification_type.h"
11
12using ::base::files::FilePathWatcher;
13
14namespace {
15
16// The subdirectory of the profile that contains the style sheet.
17const char kStyleSheetDir[] = "User StyleSheets";
18// The filename of the stylesheet.
19const char kUserStyleSheetFile[] = "Custom.css";
20
21}  // namespace
22
23// UserStyleSheetLoader is responsible for loading  the user style sheet on the
24// file thread and sends a notification when the style sheet is loaded. It is
25// a helper to UserStyleSheetWatcher. The reference graph is as follows:
26//
27// .-----------------------.    owns    .-----------------.
28// | UserStyleSheetWatcher |----------->| FilePathWatcher |
29// '-----------------------'            '-----------------'
30//             |                                 |
31//             V                                 |
32//  .----------------------.                     |
33//  | UserStyleSheetLoader |<--------------------'
34//  '----------------------'
35//
36// FilePathWatcher's reference to UserStyleSheetLoader is used for delivering
37// the change notifications. Since they happen asynchronously,
38// UserStyleSheetWatcher and its FilePathWatcher may be destroyed while a
39// callback to UserStyleSheetLoader is in progress, in which case the
40// UserStyleSheetLoader object outlives the watchers.
41class UserStyleSheetLoader : public FilePathWatcher::Delegate {
42 public:
43  UserStyleSheetLoader();
44  virtual ~UserStyleSheetLoader() {}
45
46  GURL user_style_sheet() const {
47    return user_style_sheet_;
48  }
49
50  // Load the user style sheet on the file thread and convert it to a
51  // base64 URL.  Posts the base64 URL back to the UI thread.
52  void LoadStyleSheet(const FilePath& style_sheet_file);
53
54  // Send out a notification if the stylesheet has already been loaded.
55  void NotifyLoaded();
56
57  // FilePathWatcher::Delegate interface
58  virtual void OnFilePathChanged(const FilePath& path);
59
60 private:
61  // Called on the UI thread after the stylesheet has loaded.
62  void SetStyleSheet(const GURL& url);
63
64  // The user style sheet as a base64 data:// URL.
65  GURL user_style_sheet_;
66
67  // Whether the stylesheet has been loaded.
68  bool has_loaded_;
69
70  DISALLOW_COPY_AND_ASSIGN(UserStyleSheetLoader);
71};
72
73UserStyleSheetLoader::UserStyleSheetLoader()
74    : has_loaded_(false) {
75}
76
77void UserStyleSheetLoader::NotifyLoaded() {
78  if (has_loaded_) {
79    NotificationService::current()->Notify(
80        NotificationType::USER_STYLE_SHEET_UPDATED,
81        Source<UserStyleSheetLoader>(this),
82        NotificationService::NoDetails());
83  }
84}
85
86void UserStyleSheetLoader::OnFilePathChanged(const FilePath& path) {
87  LoadStyleSheet(path);
88}
89
90void UserStyleSheetLoader::LoadStyleSheet(const FilePath& style_sheet_file) {
91  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
92  // We keep the user style sheet in a subdir so we can watch for changes
93  // to the file.
94  FilePath style_sheet_dir = style_sheet_file.DirName();
95  if (!file_util::DirectoryExists(style_sheet_dir)) {
96    if (!file_util::CreateDirectory(style_sheet_dir))
97      return;
98  }
99  // Create the file if it doesn't exist.
100  if (!file_util::PathExists(style_sheet_file))
101    file_util::WriteFile(style_sheet_file, "", 0);
102
103  std::string css;
104  bool rv = file_util::ReadFileToString(style_sheet_file, &css);
105  GURL style_sheet_url;
106  if (rv && !css.empty()) {
107    std::string css_base64;
108    rv = base::Base64Encode(css, &css_base64);
109    if (rv) {
110      // WebKit knows about data urls, so convert the file to a data url.
111      const char kDataUrlPrefix[] = "data:text/css;charset=utf-8;base64,";
112      style_sheet_url = GURL(kDataUrlPrefix + css_base64);
113    }
114  }
115  BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
116      NewRunnableMethod(this, &UserStyleSheetLoader::SetStyleSheet,
117                        style_sheet_url));
118}
119
120void UserStyleSheetLoader::SetStyleSheet(const GURL& url) {
121  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
122
123  has_loaded_ = true;
124  user_style_sheet_ = url;
125  NotifyLoaded();
126}
127
128UserStyleSheetWatcher::UserStyleSheetWatcher(const FilePath& profile_path)
129    : profile_path_(profile_path),
130      loader_(new UserStyleSheetLoader) {
131  // Listen for when the first render view host is created.  If we load
132  // too fast, the first tab won't hear the notification and won't get
133  // the user style sheet.
134  registrar_.Add(this, NotificationType::RENDER_VIEW_HOST_CREATED_FOR_TAB,
135                 NotificationService::AllSources());
136}
137
138UserStyleSheetWatcher::~UserStyleSheetWatcher() {
139}
140
141void UserStyleSheetWatcher::Init() {
142  // Make sure we run on the file thread.
143  if (!BrowserThread::CurrentlyOn(BrowserThread::FILE)) {
144    BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
145        NewRunnableMethod(this, &UserStyleSheetWatcher::Init));
146    return;
147  }
148
149  if (!file_watcher_.get()) {
150    file_watcher_.reset(new FilePathWatcher);
151    FilePath style_sheet_file = profile_path_.AppendASCII(kStyleSheetDir)
152                                             .AppendASCII(kUserStyleSheetFile);
153    if (!file_watcher_->Watch(
154        style_sheet_file,
155        loader_.get())) {
156      LOG(ERROR) << "Failed to setup watch for " << style_sheet_file.value();
157    }
158    loader_->LoadStyleSheet(style_sheet_file);
159  }
160}
161
162GURL UserStyleSheetWatcher::user_style_sheet() const {
163  return loader_->user_style_sheet();
164}
165
166void UserStyleSheetWatcher::Observe(NotificationType type,
167    const NotificationSource& source, const NotificationDetails& details) {
168  DCHECK(type == NotificationType::RENDER_VIEW_HOST_CREATED_FOR_TAB);
169  loader_->NotifyLoaded();
170  registrar_.RemoveAll();
171}
172