1// Copyright 2014 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/ui/webui/extensions/extension_loader_handler.h"
6
7#include "base/bind.h"
8#include "base/files/file_util.h"
9#include "base/logging.h"
10#include "base/memory/ref_counted.h"
11#include "base/strings/string16.h"
12#include "base/strings/string_util.h"
13#include "base/strings/stringprintf.h"
14#include "base/strings/utf_string_conversions.h"
15#include "chrome/browser/extensions/path_util.h"
16#include "chrome/browser/extensions/unpacked_installer.h"
17#include "chrome/browser/extensions/zipfile_installer.h"
18#include "chrome/browser/profiles/profile.h"
19#include "chrome/browser/ui/chrome_select_file_policy.h"
20#include "chrome/grit/generated_resources.h"
21#include "content/public/browser/browser_thread.h"
22#include "content/public/browser/user_metrics.h"
23#include "content/public/browser/web_contents.h"
24#include "content/public/browser/web_ui.h"
25#include "content/public/browser/web_ui_data_source.h"
26#include "extensions/browser/extension_system.h"
27#include "extensions/browser/file_highlighter.h"
28#include "extensions/common/constants.h"
29#include "extensions/common/extension.h"
30#include "extensions/common/manifest_constants.h"
31#include "third_party/re2/re2/re2.h"
32#include "ui/base/l10n/l10n_util.h"
33#include "ui/shell_dialogs/select_file_dialog.h"
34
35namespace extensions {
36
37namespace {
38
39// Read a file to a string and return.
40std::string ReadFileToString(const base::FilePath& path) {
41  std::string data;
42  // This call can fail, but it doesn't matter for our purposes. If it fails,
43  // we simply return an empty string for the manifest, and ignore it.
44  base::ReadFileToString(path, &data);
45  return data;
46}
47
48}  // namespace
49
50class ExtensionLoaderHandler::FileHelper
51    : public ui::SelectFileDialog::Listener {
52 public:
53  explicit FileHelper(ExtensionLoaderHandler* loader_handler);
54  virtual ~FileHelper();
55
56  // Create a FileDialog for the user to select the unpacked extension
57  // directory.
58  void ChooseFile();
59
60 private:
61  // ui::SelectFileDialog::Listener implementation.
62  virtual void FileSelected(const base::FilePath& path,
63                            int index,
64                            void* params) OVERRIDE;
65  virtual void MultiFilesSelected(
66      const std::vector<base::FilePath>& files, void* params) OVERRIDE;
67
68  // The associated ExtensionLoaderHandler. Weak, but guaranteed to be alive,
69  // as it owns this object.
70  ExtensionLoaderHandler* loader_handler_;
71
72  // The dialog used to pick a directory when loading an unpacked extension.
73  scoped_refptr<ui::SelectFileDialog> load_extension_dialog_;
74
75  // The last selected directory, so we can start in the same spot.
76  base::FilePath last_unpacked_directory_;
77
78  // The title of the dialog.
79  base::string16 title_;
80
81  DISALLOW_COPY_AND_ASSIGN(FileHelper);
82};
83
84ExtensionLoaderHandler::FileHelper::FileHelper(
85    ExtensionLoaderHandler* loader_handler)
86    : loader_handler_(loader_handler),
87      title_(l10n_util::GetStringUTF16(IDS_EXTENSION_LOAD_FROM_DIRECTORY)) {
88}
89
90ExtensionLoaderHandler::FileHelper::~FileHelper() {
91  // There may be a pending file dialog; inform it the listener is destroyed so
92  // it doesn't try and call back.
93  if (load_extension_dialog_.get())
94    load_extension_dialog_->ListenerDestroyed();
95}
96
97void ExtensionLoaderHandler::FileHelper::ChooseFile() {
98  static const int kFileTypeIndex = 0;  // No file type information to index.
99  static const ui::SelectFileDialog::Type kSelectType =
100      ui::SelectFileDialog::SELECT_FOLDER;
101
102  if (!load_extension_dialog_.get()) {
103    load_extension_dialog_ = ui::SelectFileDialog::Create(
104        this,
105        new ChromeSelectFilePolicy(
106            loader_handler_->web_ui()->GetWebContents()));
107  }
108
109  load_extension_dialog_->SelectFile(
110      kSelectType,
111      title_,
112      last_unpacked_directory_,
113      NULL,
114      kFileTypeIndex,
115      base::FilePath::StringType(),
116      loader_handler_->web_ui()->GetWebContents()->GetTopLevelNativeWindow(),
117      NULL);
118
119  content::RecordComputedAction("Options_LoadUnpackedExtension");
120}
121
122void ExtensionLoaderHandler::FileHelper::FileSelected(
123    const base::FilePath& path, int index, void* params) {
124  loader_handler_->LoadUnpackedExtensionImpl(path);
125}
126
127void ExtensionLoaderHandler::FileHelper::MultiFilesSelected(
128      const std::vector<base::FilePath>& files, void* params) {
129  NOTREACHED();
130}
131
132ExtensionLoaderHandler::ExtensionLoaderHandler(Profile* profile)
133    : profile_(profile),
134      file_helper_(new FileHelper(this)),
135      extension_error_reporter_observer_(this),
136      ui_ready_(false),
137      weak_ptr_factory_(this) {
138  DCHECK(profile_);
139  extension_error_reporter_observer_.Add(ExtensionErrorReporter::GetInstance());
140}
141
142ExtensionLoaderHandler::~ExtensionLoaderHandler() {
143}
144
145void ExtensionLoaderHandler::GetLocalizedValues(
146    content::WebUIDataSource* source) {
147  source->AddString(
148      "extensionLoadErrorHeading",
149      l10n_util::GetStringUTF16(IDS_EXTENSIONS_LOAD_ERROR_HEADING));
150  source->AddString(
151      "extensionLoadErrorMessage",
152      l10n_util::GetStringUTF16(IDS_EXTENSIONS_LOAD_ERROR_MESSAGE));
153  source->AddString(
154      "extensionLoadErrorRetry",
155      l10n_util::GetStringUTF16(IDS_EXTENSIONS_LOAD_ERROR_RETRY));
156  source->AddString(
157      "extensionLoadErrorGiveUp",
158      l10n_util::GetStringUTF16(IDS_EXTENSIONS_LOAD_ERROR_GIVE_UP));
159  source->AddString(
160      "extensionLoadCouldNotLoadManifest",
161      l10n_util::GetStringUTF16(IDS_EXTENSIONS_LOAD_COULD_NOT_LOAD_MANIFEST));
162  source->AddString(
163      "extensionLoadAdditionalFailures",
164      l10n_util::GetStringUTF16(IDS_EXTENSIONS_LOAD_ADDITIONAL_FAILURES));
165}
166
167void ExtensionLoaderHandler::RegisterMessages() {
168  // We observe WebContents in order to detect page refreshes, since notifying
169  // the frontend of load failures must be delayed until the page finishes
170  // loading. We never call Observe(NULL) because this object is constructed
171  // on page load and persists between refreshes.
172  content::WebContentsObserver::Observe(web_ui()->GetWebContents());
173
174  web_ui()->RegisterMessageCallback(
175      "extensionLoaderLoadUnpacked",
176      base::Bind(&ExtensionLoaderHandler::HandleLoadUnpacked,
177                 weak_ptr_factory_.GetWeakPtr()));
178  web_ui()->RegisterMessageCallback(
179      "extensionLoaderRetry",
180      base::Bind(&ExtensionLoaderHandler::HandleRetry,
181                 weak_ptr_factory_.GetWeakPtr()));
182  web_ui()->RegisterMessageCallback(
183      "extensionLoaderIgnoreFailure",
184      base::Bind(&ExtensionLoaderHandler::HandleIgnoreFailure,
185                 weak_ptr_factory_.GetWeakPtr()));
186  web_ui()->RegisterMessageCallback(
187      "extensionLoaderDisplayFailures",
188      base::Bind(&ExtensionLoaderHandler::HandleDisplayFailures,
189                 weak_ptr_factory_.GetWeakPtr()));
190}
191
192void ExtensionLoaderHandler::HandleLoadUnpacked(const base::ListValue* args) {
193  DCHECK(args->empty());
194  file_helper_->ChooseFile();
195}
196
197void ExtensionLoaderHandler::HandleRetry(const base::ListValue* args) {
198  DCHECK(args->empty());
199  const base::FilePath file_path = failed_paths_.back();
200  failed_paths_.pop_back();
201  LoadUnpackedExtensionImpl(file_path);
202}
203
204void ExtensionLoaderHandler::HandleIgnoreFailure(const base::ListValue* args) {
205  DCHECK(args->empty());
206  failed_paths_.pop_back();
207}
208
209void ExtensionLoaderHandler::HandleDisplayFailures(
210    const base::ListValue* args) {
211  DCHECK(args->empty());
212  ui_ready_ = true;
213
214  // Notify the frontend of any load failures that were triggered while the
215  // chrome://extensions page was loading.
216  if (!failures_.empty())
217    NotifyFrontendOfFailure();
218}
219
220void ExtensionLoaderHandler::LoadUnpackedExtensionImpl(
221    const base::FilePath& file_path) {
222  scoped_refptr<UnpackedInstaller> installer = UnpackedInstaller::Create(
223      ExtensionSystem::Get(profile_)->extension_service());
224
225  // We do our own error handling, so we don't want a load failure to trigger
226  // a dialog.
227  installer->set_be_noisy_on_failure(false);
228
229  installer->Load(file_path);
230}
231
232void ExtensionLoaderHandler::OnLoadFailure(
233    content::BrowserContext* browser_context,
234    const base::FilePath& file_path,
235    const std::string& error) {
236  // Only show errors from our browser context.
237  if (web_ui()->GetWebContents()->GetBrowserContext() != browser_context)
238    return;
239
240  size_t line = 0u;
241  size_t column = 0u;
242  std::string regex =
243      base::StringPrintf("%s  Line: (\\d+), column: (\\d+), .*",
244                         manifest_errors::kManifestParseError);
245  // If this was a JSON parse error, we can highlight the exact line with the
246  // error. Otherwise, we should still display the manifest (for consistency,
247  // reference, and so that if we ever make this really fancy and add an editor,
248  // it's ready).
249  //
250  // This regex call can fail, but if it does, we just don't highlight anything.
251  re2::RE2::FullMatch(error, regex, &line, &column);
252
253  // This will read the manifest and call AddFailure with the read manifest
254  // contents.
255  base::PostTaskAndReplyWithResult(
256      content::BrowserThread::GetBlockingPool(),
257      FROM_HERE,
258      base::Bind(&ReadFileToString, file_path.Append(kManifestFilename)),
259      base::Bind(&ExtensionLoaderHandler::AddFailure,
260                 weak_ptr_factory_.GetWeakPtr(),
261                 file_path,
262                 error,
263                 line));
264}
265
266void ExtensionLoaderHandler::DidStartNavigationToPendingEntry(
267    const GURL& url,
268    content::NavigationController::ReloadType reload_type) {
269  // In the event of a page reload, we ensure that the frontend is not notified
270  // until the UI finishes loading, so we set |ui_ready_| to false. This is
271  // balanced in HandleDisplayFailures, which is called when the frontend is
272  // ready to receive failure notifications.
273  if (reload_type != content::NavigationController::NO_RELOAD)
274    ui_ready_ = false;
275}
276
277void ExtensionLoaderHandler::AddFailure(
278    const base::FilePath& file_path,
279    const std::string& error,
280    size_t line_number,
281    const std::string& manifest) {
282  failed_paths_.push_back(file_path);
283  base::FilePath prettified_path = path_util::PrettifyPath(file_path);
284
285  scoped_ptr<base::DictionaryValue> manifest_value(new base::DictionaryValue());
286  SourceHighlighter highlighter(manifest, line_number);
287  // If the line number is 0, this highlights no regions, but still adds the
288  // full manifest.
289  highlighter.SetHighlightedRegions(manifest_value.get());
290
291  scoped_ptr<base::DictionaryValue> failure(new base::DictionaryValue());
292  failure->Set("path",
293               new base::StringValue(prettified_path.LossyDisplayName()));
294  failure->Set("error", new base::StringValue(base::UTF8ToUTF16(error)));
295  failure->Set("manifest", manifest_value.release());
296  failures_.Append(failure.release());
297
298  // Only notify the frontend if the frontend UI is ready.
299  if (ui_ready_)
300    NotifyFrontendOfFailure();
301}
302
303void ExtensionLoaderHandler::NotifyFrontendOfFailure() {
304  web_ui()->CallJavascriptFunction(
305      "extensions.ExtensionLoader.notifyLoadFailed",
306      failures_);
307  failures_.Clear();
308}
309
310}  // namespace extensions
311