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