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