crx_installer.cc revision 731df977c0511bca2206b5f333555b1205ff1f43
1// Copyright (c) 2010 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/extensions/crx_installer.h"
6
7#include "app/l10n_util.h"
8#include "app/resource_bundle.h"
9#include "base/file_util.h"
10#include "base/path_service.h"
11#include "base/scoped_temp_dir.h"
12#include "base/singleton.h"
13#include "base/stringprintf.h"
14#include "base/task.h"
15#include "base/utf_string_conversions.h"
16#include "base/version.h"
17#include "chrome/browser/browser_process.h"
18#include "chrome/browser/browser_thread.h"
19#include "chrome/browser/extensions/convert_user_script.h"
20#include "chrome/browser/extensions/extensions_service.h"
21#include "chrome/browser/extensions/extension_error_reporter.h"
22#include "chrome/browser/profile.h"
23#include "chrome/browser/shell_integration.h"
24#include "chrome/browser/web_applications/web_app.h"
25#include "chrome/common/chrome_paths.h"
26#include "chrome/common/extensions/extension_file_util.h"
27#include "chrome/common/extensions/extension_constants.h"
28#include "chrome/common/notification_service.h"
29#include "chrome/common/notification_type.h"
30#include "grit/chromium_strings.h"
31#include "grit/generated_resources.h"
32#include "grit/theme_resources.h"
33#include "third_party/skia/include/core/SkBitmap.h"
34
35namespace {
36
37// Helper function to delete files. This is used to avoid ugly casts which
38// would be necessary with PostMessage since file_util::Delete is overloaded.
39static void DeleteFileHelper(const FilePath& path, bool recursive) {
40  file_util::Delete(path, recursive);
41}
42
43struct WhitelistedInstallData {
44  WhitelistedInstallData() {}
45  std::list<std::string> ids;
46};
47
48}
49
50// static
51void CrxInstaller::SetWhitelistedInstallId(const std::string& id) {
52  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
53  Singleton<WhitelistedInstallData>::get()->ids.push_back(id);
54}
55
56// static
57bool CrxInstaller::ClearWhitelistedInstallId(const std::string& id) {
58  std::list<std::string>& ids = Singleton<WhitelistedInstallData>::get()->ids;
59  std::list<std::string>::iterator iter = ids.begin();
60  for (; iter != ids.end(); ++iter) {
61    if (*iter == id) {
62      break;
63    }
64  }
65
66  if (iter != ids.end()) {
67    ids.erase(iter);
68    return true;
69  }
70
71  return false;
72}
73
74CrxInstaller::CrxInstaller(const FilePath& install_directory,
75                           ExtensionsService* frontend,
76                           ExtensionInstallUI* client)
77    : install_directory_(install_directory),
78      install_source_(Extension::INTERNAL),
79      delete_source_(false),
80      allow_privilege_increase_(false),
81      is_gallery_install_(false),
82      create_app_shortcut_(false),
83      frontend_(frontend),
84      client_(client),
85      apps_require_extension_mime_type_(false),
86      allow_silent_install_(false) {
87  extensions_enabled_ = frontend_->extensions_enabled();
88}
89
90CrxInstaller::~CrxInstaller() {
91  // Delete the temp directory and crx file as necessary. Note that the
92  // destructor might be called on any thread, so we post a task to the file
93  // thread to make sure the delete happens there.
94  if (!temp_dir_.value().empty()) {
95    BrowserThread::PostTask(
96        BrowserThread::FILE, FROM_HERE,
97        NewRunnableFunction(&DeleteFileHelper, temp_dir_, true));
98  }
99
100  if (delete_source_) {
101    BrowserThread::PostTask(
102        BrowserThread::FILE, FROM_HERE,
103        NewRunnableFunction(&DeleteFileHelper, source_file_, false));
104  }
105
106  // Make sure the UI is deleted on the ui thread.
107  BrowserThread::DeleteSoon(BrowserThread::UI, FROM_HERE, client_);
108  client_ = NULL;
109}
110
111void CrxInstaller::InstallCrx(const FilePath& source_file) {
112  source_file_ = source_file;
113
114  FilePath user_data_temp_dir;
115  CHECK(PathService::Get(chrome::DIR_USER_DATA_TEMP, &user_data_temp_dir));
116
117  scoped_refptr<SandboxedExtensionUnpacker> unpacker(
118      new SandboxedExtensionUnpacker(
119          source_file,
120          user_data_temp_dir,
121          g_browser_process->resource_dispatcher_host(),
122          this));
123
124  BrowserThread::PostTask(
125      BrowserThread::FILE, FROM_HERE,
126      NewRunnableMethod(
127          unpacker.get(), &SandboxedExtensionUnpacker::Start));
128}
129
130void CrxInstaller::InstallUserScript(const FilePath& source_file,
131                                     const GURL& original_url) {
132  DCHECK(!original_url.is_empty());
133
134  source_file_ = source_file;
135  original_url_ = original_url;
136
137  BrowserThread::PostTask(
138      BrowserThread::FILE, FROM_HERE,
139      NewRunnableMethod(this, &CrxInstaller::ConvertUserScriptOnFileThread));
140}
141
142void CrxInstaller::ConvertUserScriptOnFileThread() {
143  std::string error;
144  Extension* extension = ConvertUserScriptToExtension(source_file_,
145                                                      original_url_, &error);
146  if (!extension) {
147    ReportFailureFromFileThread(error);
148    return;
149  }
150
151  OnUnpackSuccess(extension->path(), extension->path(), extension);
152}
153
154bool CrxInstaller::AllowInstall(Extension* extension, std::string* error) {
155  DCHECK(error);
156
157  // We always allow themes and external installs.
158  if (extension->is_theme() || Extension::IsExternalLocation(install_source_))
159    return true;
160
161  if (!extensions_enabled_) {
162    *error = "Extensions are not enabled.";
163    return false;
164  }
165
166  // Make sure the expected id matches.
167  // TODO(aa): Also support expected version?
168  if (!expected_id_.empty() && expected_id_ != extension->id()) {
169    *error = base::StringPrintf(
170        "ID in new extension manifest (%s) does not match expected id (%s)",
171        extension->id().c_str(),
172        expected_id_.c_str());
173    return false;
174  }
175
176  if (extension_->is_app()) {
177    // If the app was downloaded, apps_require_extension_mime_type_
178    // will be set.  In this case, check that it was served with the
179    // right mime type.  Make an exception for file URLs, which come
180    // from the users computer and have no headers.
181    if (!original_url_.SchemeIsFile() &&
182        apps_require_extension_mime_type_ &&
183        original_mime_type_ != Extension::kMimeType) {
184      *error = base::StringPrintf(
185          "Apps must be served with content type %s.",
186          Extension::kMimeType);
187      return false;
188    }
189
190    // If the client_ is NULL, then the app is either being installed via
191    // an internal mechanism like sync, external_extensions, or default apps.
192    // In that case, we don't want to enforce things like the install origin.
193    if (!is_gallery_install_ && client_) {
194      // For apps with a gallery update URL, require that they be installed
195      // from the gallery.
196      // TODO(erikkay) Apply this rule for paid extensions and themes as well.
197      if ((extension->update_url() ==
198           GURL(extension_urls::kGalleryUpdateHttpsUrl)) ||
199          (extension->update_url() ==
200           GURL(extension_urls::kGalleryUpdateHttpUrl))) {
201        *error = l10n_util::GetStringFUTF8(
202            IDS_EXTENSION_DISALLOW_NON_DOWNLOADED_GALLERY_INSTALLS,
203            l10n_util::GetStringUTF16(IDS_EXTENSION_WEB_STORE_TITLE));
204        return false;
205      }
206
207      // For self-hosted apps, verify that the entire extent is on the same
208      // host (or a subdomain of the host) the download happened from.  There's
209      // no way for us to verify that the app controls any other hosts.
210      URLPattern pattern(UserScript::kValidUserScriptSchemes);
211      pattern.set_host(original_url_.host());
212      pattern.set_match_subdomains(true);
213
214      ExtensionExtent::PatternList patterns =
215          extension_->web_extent().patterns();
216      for (size_t i = 0; i < patterns.size(); ++i) {
217        if (!pattern.MatchesHost(patterns[i].host())) {
218          *error = base::StringPrintf(
219              "Apps must be served from the host that they affect.");
220          return false;
221        }
222      }
223    }
224  }
225
226  return true;
227}
228
229void CrxInstaller::OnUnpackFailure(const std::string& error_message) {
230  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
231  ReportFailureFromFileThread(error_message);
232}
233
234void CrxInstaller::OnUnpackSuccess(const FilePath& temp_dir,
235                                   const FilePath& extension_dir,
236                                   Extension* extension) {
237  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
238
239  // Note: We take ownership of |extension| and |temp_dir|.
240  extension_.reset(extension);
241  temp_dir_ = temp_dir;
242
243  // We don't have to delete the unpack dir explicity since it is a child of
244  // the temp dir.
245  unpacked_extension_root_ = extension_dir;
246
247  std::string error;
248  if (!AllowInstall(extension, &error)) {
249    ReportFailureFromFileThread(error);
250    return;
251  }
252
253  if (client_ || extension_->GetFullLaunchURL().is_valid()) {
254    Extension::DecodeIcon(extension_.get(), Extension::EXTENSION_ICON_LARGE,
255                          &install_icon_);
256  }
257
258  BrowserThread::PostTask(
259      BrowserThread::UI, FROM_HERE,
260      NewRunnableMethod(this, &CrxInstaller::ConfirmInstall));
261}
262
263void CrxInstaller::ConfirmInstall() {
264  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
265  if (frontend_->extension_prefs()->IsExtensionBlacklisted(extension_->id())) {
266    LOG(INFO) << "This extension: " << extension_->id()
267      << " is blacklisted. Install failed.";
268    ReportFailureFromUIThread("This extension is blacklisted.");
269    return;
270  }
271
272  if (!frontend_->extension_prefs()->IsExtensionAllowedByPolicy(
273      extension_->id())) {
274    ReportFailureFromUIThread("This extension is blacklisted by admin policy.");
275    return;
276  }
277
278  GURL overlapping_url;
279  Extension* overlapping_extension =
280      frontend_->GetExtensionByOverlappingWebExtent(extension_->web_extent());
281  if (overlapping_extension) {
282    ReportFailureFromUIThread(l10n_util::GetStringFUTF8(
283        IDS_EXTENSION_OVERLAPPING_WEB_EXTENT,
284        UTF8ToUTF16(overlapping_extension->name())));
285    return;
286  }
287
288  current_version_ =
289      frontend_->extension_prefs()->GetVersionString(extension_->id());
290
291  if (client_ &&
292      (!allow_silent_install_ ||
293       !ClearWhitelistedInstallId(extension_->id()))) {
294    AddRef();  // Balanced in Proceed() and Abort().
295    client_->ConfirmInstall(this, extension_.get());
296  } else {
297    BrowserThread::PostTask(
298        BrowserThread::FILE, FROM_HERE,
299        NewRunnableMethod(this, &CrxInstaller::CompleteInstall));
300  }
301  return;
302}
303
304void CrxInstaller::InstallUIProceed() {
305  BrowserThread::PostTask(
306        BrowserThread::FILE, FROM_HERE,
307        NewRunnableMethod(this, &CrxInstaller::CompleteInstall));
308
309  Release();  // balanced in ConfirmInstall().
310}
311
312void CrxInstaller::InstallUIAbort() {
313  // Kill the theme loading bubble.
314  NotificationService* service = NotificationService::current();
315  service->Notify(NotificationType::NO_THEME_DETECTED,
316                  Source<CrxInstaller>(this),
317                  NotificationService::NoDetails());
318  Release();  // balanced in ConfirmInstall().
319
320  // We're done. Since we don't post any more tasks to ourself, our ref count
321  // should go to zero and we die. The destructor will clean up the temp dir.
322}
323
324void CrxInstaller::CompleteInstall() {
325  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
326
327  if (!current_version_.empty()) {
328    scoped_ptr<Version> current_version(
329        Version::GetVersionFromString(current_version_));
330    if (current_version->CompareTo(*(extension_->version())) > 0) {
331      ReportFailureFromFileThread("Attempted to downgrade extension.");
332      return;
333    }
334  }
335
336  FilePath version_dir = extension_file_util::InstallExtension(
337      unpacked_extension_root_,
338      extension_->id(),
339      extension_->VersionString(),
340      install_directory_);
341  if (version_dir.empty()) {
342    ReportFailureFromFileThread(
343        l10n_util::GetStringUTF8(
344            IDS_EXTENSION_MOVE_DIRECTORY_TO_PROFILE_FAILED));
345    return;
346  }
347
348  // This is lame, but we must reload the extension because absolute paths
349  // inside the content scripts are established inside InitFromValue() and we
350  // just moved the extension.
351  // TODO(aa): All paths to resources inside extensions should be created
352  // lazily and based on the Extension's root path at that moment.
353  std::string error;
354  extension_.reset(extension_file_util::LoadExtension(
355      version_dir, install_source_, true, &error));
356  DCHECK(error.empty());
357
358  ReportSuccessFromFileThread();
359}
360
361void CrxInstaller::ReportFailureFromFileThread(const std::string& error) {
362  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
363  BrowserThread::PostTask(
364      BrowserThread::UI, FROM_HERE,
365      NewRunnableMethod(this, &CrxInstaller::ReportFailureFromUIThread, error));
366}
367
368void CrxInstaller::ReportFailureFromUIThread(const std::string& error) {
369  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
370
371  NotificationService* service = NotificationService::current();
372  service->Notify(NotificationType::EXTENSION_INSTALL_ERROR,
373                  Source<CrxInstaller>(this),
374                  Details<const std::string>(&error));
375
376  // This isn't really necessary, it is only used because unit tests expect to
377  // see errors get reported via this interface.
378  //
379  // TODO(aa): Need to go through unit tests and clean them up too, probably get
380  // rid of this line.
381  ExtensionErrorReporter::GetInstance()->ReportError(error, false);  // quiet
382
383  if (client_)
384    client_->OnInstallFailure(error);
385}
386
387void CrxInstaller::ReportSuccessFromFileThread() {
388  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
389  BrowserThread::PostTask(
390      BrowserThread::UI, FROM_HERE,
391      NewRunnableMethod(this, &CrxInstaller::ReportSuccessFromUIThread));
392}
393
394void CrxInstaller::ReportSuccessFromUIThread() {
395  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
396
397  // If there is a client, tell the client about installation.
398  if (client_)
399    client_->OnInstallSuccess(extension_.get());
400
401  // Tell the frontend about the installation and hand off ownership of
402  // extension_ to it.
403  frontend_->OnExtensionInstalled(extension_.release(),
404                                  allow_privilege_increase_);
405
406  // We're done. We don't post any more tasks to ourselves so we are deleted
407  // soon.
408}
409