bundle_installer.cc revision 424c4d7b64af9d0d8fd9624f381f469654d5e3d2
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/extensions/bundle_installer.h"
6
7#include <algorithm>
8#include <string>
9#include <vector>
10
11#include "base/command_line.h"
12#include "base/i18n/rtl.h"
13#include "base/strings/utf_string_conversions.h"
14#include "base/values.h"
15#include "chrome/browser/extensions/crx_installer.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/ui/browser.h"
18#include "chrome/browser/ui/browser_finder.h"
19#include "chrome/browser/ui/browser_list.h"
20#include "chrome/browser/ui/tabs/tab_strip_model.h"
21#include "chrome/common/chrome_switches.h"
22#include "chrome/common/extensions/extension.h"
23#include "chrome/common/extensions/extension_manifest_constants.h"
24#include "chrome/common/extensions/permissions/permission_set.h"
25#include "chrome/common/extensions/permissions/permissions_data.h"
26#include "content/public/browser/navigation_controller.h"
27#include "content/public/browser/web_contents.h"
28#include "grit/generated_resources.h"
29#include "ui/base/l10n/l10n_util.h"
30
31using content::NavigationController;
32
33namespace extensions {
34
35namespace {
36
37enum AutoApproveForTest {
38  DO_NOT_SKIP = 0,
39  PROCEED,
40  ABORT
41};
42
43AutoApproveForTest g_auto_approve_for_test = DO_NOT_SKIP;
44
45// Creates a dummy extension and sets the manifest's name to the item's
46// localized name.
47scoped_refptr<Extension> CreateDummyExtension(const BundleInstaller::Item& item,
48                                              DictionaryValue* manifest) {
49  // We require localized names so we can have nice error messages when we can't
50  // parse an extension manifest.
51  CHECK(!item.localized_name.empty());
52
53  std::string error;
54  return Extension::Create(base::FilePath(),
55                           Manifest::INTERNAL,
56                           *manifest,
57                           Extension::NO_FLAGS,
58                           item.id,
59                           &error);
60}
61
62bool IsAppPredicate(scoped_refptr<const Extension> extension) {
63  return extension->is_app();
64}
65
66struct MatchIdFunctor {
67  explicit MatchIdFunctor(const std::string& id) : id(id) {}
68  bool operator()(scoped_refptr<const Extension> extension) {
69    return extension->id() == id;
70  }
71  std::string id;
72};
73
74// Holds the message IDs for BundleInstaller::GetHeadingTextFor.
75const int kHeadingIds[3][4] = {
76  {
77    0,
78    IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_HEADING_EXTENSIONS,
79    IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_HEADING_APPS,
80    IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_HEADING_EXTENSION_APPS
81  },
82  {
83    0,
84    IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_EXTENSIONS,
85    IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_APPS,
86    IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_EXTENSION_APPS
87  }
88};
89
90}  // namespace
91
92// static
93void BundleInstaller::SetAutoApproveForTesting(bool auto_approve) {
94  CHECK(CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType));
95  g_auto_approve_for_test = auto_approve ? PROCEED : ABORT;
96}
97
98BundleInstaller::Item::Item() : state(STATE_PENDING) {}
99
100string16 BundleInstaller::Item::GetNameForDisplay() {
101  string16 name = UTF8ToUTF16(localized_name);
102  base::i18n::AdjustStringForLocaleDirection(&name);
103  return l10n_util::GetStringFUTF16(IDS_EXTENSION_PERMISSION_LINE, name);
104}
105
106BundleInstaller::BundleInstaller(Browser* browser,
107                                 const BundleInstaller::ItemList& items)
108    : approved_(false),
109      browser_(browser),
110      host_desktop_type_(browser->host_desktop_type()),
111      profile_(browser->profile()),
112      delegate_(NULL) {
113  BrowserList::AddObserver(this);
114  for (size_t i = 0; i < items.size(); ++i) {
115    items_[items[i].id] = items[i];
116    items_[items[i].id].state = Item::STATE_PENDING;
117  }
118}
119
120BundleInstaller::ItemList BundleInstaller::GetItemsWithState(
121    Item::State state) const {
122  ItemList list;
123
124  for (ItemMap::const_iterator i = items_.begin(); i != items_.end(); ++i) {
125    if (i->second.state == state)
126      list.push_back(i->second);
127  }
128
129  return list;
130}
131
132void BundleInstaller::PromptForApproval(Delegate* delegate) {
133  delegate_ = delegate;
134
135  AddRef();  // Balanced in ReportApproved() and ReportCanceled().
136
137  ParseManifests();
138}
139
140void BundleInstaller::CompleteInstall(NavigationController* controller,
141                                      Delegate* delegate) {
142  CHECK(approved_);
143
144  delegate_ = delegate;
145
146  AddRef();  // Balanced in ReportComplete();
147
148  if (GetItemsWithState(Item::STATE_PENDING).empty()) {
149    ReportComplete();
150    return;
151  }
152
153  // Start each WebstoreInstaller.
154  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i) {
155    if (i->second.state != Item::STATE_PENDING)
156      continue;
157
158    // Since we've already confirmed the permissions, create an approval that
159    // lets CrxInstaller bypass the prompt.
160    scoped_ptr<WebstoreInstaller::Approval> approval(
161        WebstoreInstaller::Approval::CreateWithNoInstallPrompt(
162            profile_,
163            i->first,
164            scoped_ptr<base::DictionaryValue>(
165                parsed_manifests_[i->first]->DeepCopy()), true));
166    approval->use_app_installed_bubble = false;
167    approval->skip_post_install_ui = true;
168
169    scoped_refptr<WebstoreInstaller> installer = new WebstoreInstaller(
170        profile_,
171        this,
172        controller,
173        i->first,
174        approval.Pass(),
175        WebstoreInstaller::FLAG_NONE);
176    installer->Start();
177  }
178}
179
180string16 BundleInstaller::GetHeadingTextFor(Item::State state) const {
181  // For STATE_FAILED, we can't tell if the items were apps or extensions
182  // so we always show the same message.
183  if (state == Item::STATE_FAILED) {
184    if (GetItemsWithState(state).size())
185      return l10n_util::GetStringUTF16(IDS_EXTENSION_BUNDLE_ERROR_HEADING);
186    return string16();
187  }
188
189  size_t total = GetItemsWithState(state).size();
190  size_t apps = std::count_if(
191      dummy_extensions_.begin(), dummy_extensions_.end(), &IsAppPredicate);
192
193  bool has_apps = apps > 0;
194  bool has_extensions = apps < total;
195  size_t index = (has_extensions << 0) + (has_apps << 1);
196
197  CHECK_LT(static_cast<size_t>(state), arraysize(kHeadingIds));
198  CHECK_LT(index, arraysize(kHeadingIds[state]));
199
200  int msg_id = kHeadingIds[state][index];
201  if (!msg_id)
202    return string16();
203
204  return l10n_util::GetStringUTF16(msg_id);
205}
206
207BundleInstaller::~BundleInstaller() {
208  BrowserList::RemoveObserver(this);
209}
210
211void BundleInstaller::ParseManifests() {
212  if (items_.empty()) {
213    ReportCanceled(false);
214    return;
215  }
216
217  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i) {
218    scoped_refptr<WebstoreInstallHelper> helper = new WebstoreInstallHelper(
219        this, i->first, i->second.manifest, std::string(), GURL(), NULL);
220    helper->Start();
221  }
222}
223
224void BundleInstaller::ReportApproved() {
225  if (delegate_)
226    delegate_->OnBundleInstallApproved();
227
228  Release();  // Balanced in PromptForApproval().
229}
230
231void BundleInstaller::ReportCanceled(bool user_initiated) {
232  if (delegate_)
233    delegate_->OnBundleInstallCanceled(user_initiated);
234
235  Release();  // Balanced in PromptForApproval().
236}
237
238void BundleInstaller::ReportComplete() {
239  if (delegate_)
240    delegate_->OnBundleInstallCompleted();
241
242  Release();  // Balanced in CompleteInstall().
243}
244
245void BundleInstaller::ShowPromptIfDoneParsing() {
246  // We don't prompt until all the manifests have been parsed.
247  ItemList pending_items = GetItemsWithState(Item::STATE_PENDING);
248  if (pending_items.size() != dummy_extensions_.size())
249    return;
250
251  ShowPrompt();
252}
253
254void BundleInstaller::ShowPrompt() {
255  // Abort if we couldn't create any Extensions out of the manifests.
256  if (dummy_extensions_.empty()) {
257    ReportCanceled(false);
258    return;
259  }
260
261  scoped_refptr<PermissionSet> permissions;
262  for (size_t i = 0; i < dummy_extensions_.size(); ++i) {
263    permissions = PermissionSet::CreateUnion(
264        permissions.get(),
265        PermissionsData::GetRequiredPermissions(dummy_extensions_[i].get()));
266  }
267
268  if (g_auto_approve_for_test == PROCEED) {
269    InstallUIProceed();
270  } else if (g_auto_approve_for_test == ABORT) {
271    InstallUIAbort(true);
272  } else {
273    Browser* browser = browser_;
274    if (!browser) {
275      // The browser that we got initially could have gone away during our
276      // thread hopping.
277      browser = chrome::FindLastActiveWithProfile(profile_, host_desktop_type_);
278    }
279    content::WebContents* web_contents = NULL;
280    if (browser)
281      web_contents = browser->tab_strip_model()->GetActiveWebContents();
282    install_ui_.reset(new ExtensionInstallPrompt(web_contents));
283    install_ui_->ConfirmBundleInstall(this, permissions.get());
284  }
285}
286
287void BundleInstaller::ShowInstalledBubbleIfDone() {
288  // We're ready to show the installed bubble when no items are pending.
289  if (!GetItemsWithState(Item::STATE_PENDING).empty())
290    return;
291
292  if (browser_)
293    ShowInstalledBubble(this, browser_);
294
295  ReportComplete();
296}
297
298void BundleInstaller::OnWebstoreParseSuccess(
299    const std::string& id,
300    const SkBitmap& icon,
301    DictionaryValue* manifest) {
302  dummy_extensions_.push_back(CreateDummyExtension(items_[id], manifest));
303  parsed_manifests_[id] = linked_ptr<DictionaryValue>(manifest);
304
305  ShowPromptIfDoneParsing();
306}
307
308void BundleInstaller::OnWebstoreParseFailure(
309    const std::string& id,
310    WebstoreInstallHelper::Delegate::InstallHelperResultCode result_code,
311    const std::string& error_message) {
312  items_[id].state = Item::STATE_FAILED;
313
314  ShowPromptIfDoneParsing();
315}
316
317void BundleInstaller::InstallUIProceed() {
318  approved_ = true;
319  ReportApproved();
320}
321
322void BundleInstaller::InstallUIAbort(bool user_initiated) {
323  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i)
324    i->second.state = Item::STATE_FAILED;
325
326  ReportCanceled(user_initiated);
327}
328
329void BundleInstaller::OnExtensionInstallSuccess(const std::string& id) {
330  items_[id].state = Item::STATE_INSTALLED;
331
332  ShowInstalledBubbleIfDone();
333}
334
335void BundleInstaller::OnExtensionInstallFailure(
336    const std::string& id,
337    const std::string& error,
338    WebstoreInstaller::FailureReason reason) {
339  items_[id].state = Item::STATE_FAILED;
340
341  ExtensionList::iterator i = std::find_if(
342      dummy_extensions_.begin(), dummy_extensions_.end(), MatchIdFunctor(id));
343  CHECK(dummy_extensions_.end() != i);
344  dummy_extensions_.erase(i);
345
346  ShowInstalledBubbleIfDone();
347}
348
349void BundleInstaller::OnBrowserAdded(Browser* browser) {}
350
351void BundleInstaller::OnBrowserRemoved(Browser* browser) {
352  if (browser_ == browser)
353    browser_ = NULL;
354}
355
356void BundleInstaller::OnBrowserSetLastActive(Browser* browser) {}
357
358}  // namespace extensions
359