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