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