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/chromeos/app_mode/kiosk_external_updater.h"
6
7 #include "base/bind.h"
8 #include "base/files/file_util.h"
9 #include "base/json/json_file_value_serializer.h"
10 #include "base/location.h"
11 #include "base/logging.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/task_runner_util.h"
14 #include "base/version.h"
15 #include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h"
16 #include "chrome/browser/chromeos/ui/kiosk_external_update_notification.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "components/version_info/version_info.h"
19 #include "content/public/browser/browser_thread.h"
20 #include "extensions/browser/sandboxed_unpacker.h"
21 #include "extensions/common/extension.h"
22 #include "extensions/common/verifier_formats.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/resource/resource_bundle.h"
25
26 namespace chromeos {
27
28 namespace {
29
30 constexpr base::FilePath::CharType kExternalUpdateManifest[] =
31 "external_update.json";
32 constexpr char kExternalCrx[] = "external_crx";
33 constexpr char kExternalVersion[] = "external_version";
34
35 std::pair<std::unique_ptr<base::DictionaryValue>,
36 KioskExternalUpdater::ExternalUpdateErrorCode>
ParseExternalUpdateManifest(const base::FilePath & external_update_dir)37 ParseExternalUpdateManifest(const base::FilePath& external_update_dir) {
38 base::FilePath manifest = external_update_dir.Append(kExternalUpdateManifest);
39 if (!base::PathExists(manifest)) {
40 return std::make_pair(nullptr, KioskExternalUpdater::ERROR_NO_MANIFEST);
41 }
42
43 JSONFileValueDeserializer deserializer(manifest);
44 std::unique_ptr<base::DictionaryValue> extensions =
45 base::DictionaryValue::From(deserializer.Deserialize(nullptr, nullptr));
46 if (!extensions) {
47 return std::make_pair(nullptr,
48 KioskExternalUpdater::ERROR_INVALID_MANIFEST);
49 }
50
51 return std::make_pair(std::move(extensions),
52 KioskExternalUpdater::ERROR_NONE);
53 }
54
55 // Copies |external_crx_file| to |temp_crx_file|, and removes |temp_dir|
56 // created for unpacking |external_crx_file|.
CopyExternalCrxAndDeleteTempDir(const base::FilePath & external_crx_file,const base::FilePath & temp_crx_file,const base::FilePath & temp_dir)57 bool CopyExternalCrxAndDeleteTempDir(const base::FilePath& external_crx_file,
58 const base::FilePath& temp_crx_file,
59 const base::FilePath& temp_dir) {
60 base::DeletePathRecursively(temp_dir);
61 return base::CopyFile(external_crx_file, temp_crx_file);
62 }
63
64 // Returns true if |version_1| < |version_2|, and
65 // if |update_for_same_version| is true and |version_1| = |version_2|.
ShouldUpdateForHigherVersion(const std::string & version_1,const std::string & version_2,bool update_for_same_version)66 bool ShouldUpdateForHigherVersion(const std::string& version_1,
67 const std::string& version_2,
68 bool update_for_same_version) {
69 const base::Version v1(version_1);
70 const base::Version v2(version_2);
71 if (!v1.IsValid() || !v2.IsValid())
72 return false;
73 int compare_result = v1.CompareTo(v2);
74 if (compare_result < 0)
75 return true;
76 return update_for_same_version && compare_result == 0;
77 }
78
79 } // namespace
80
ExternalUpdate()81 KioskExternalUpdater::ExternalUpdate::ExternalUpdate() {
82 }
83
84 KioskExternalUpdater::ExternalUpdate::ExternalUpdate(
85 const ExternalUpdate& other) = default;
86
~ExternalUpdate()87 KioskExternalUpdater::ExternalUpdate::~ExternalUpdate() {
88 }
89
KioskExternalUpdater(const scoped_refptr<base::SequencedTaskRunner> & backend_task_runner,const base::FilePath & crx_cache_dir,const base::FilePath & crx_unpack_dir)90 KioskExternalUpdater::KioskExternalUpdater(
91 const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner,
92 const base::FilePath& crx_cache_dir,
93 const base::FilePath& crx_unpack_dir)
94 : backend_task_runner_(backend_task_runner),
95 crx_cache_dir_(crx_cache_dir),
96 crx_unpack_dir_(crx_unpack_dir) {
97 // Subscribe to DiskMountManager.
98 DCHECK(disks::DiskMountManager::GetInstance());
99 disks::DiskMountManager::GetInstance()->AddObserver(this);
100 }
101
~KioskExternalUpdater()102 KioskExternalUpdater::~KioskExternalUpdater() {
103 if (disks::DiskMountManager::GetInstance())
104 disks::DiskMountManager::GetInstance()->RemoveObserver(this);
105 }
106
OnMountEvent(disks::DiskMountManager::MountEvent event,MountError error_code,const disks::DiskMountManager::MountPointInfo & mount_info)107 void KioskExternalUpdater::OnMountEvent(
108 disks::DiskMountManager::MountEvent event,
109 MountError error_code,
110 const disks::DiskMountManager::MountPointInfo& mount_info) {
111 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
112
113 if (mount_info.mount_type != MOUNT_TYPE_DEVICE ||
114 error_code != MOUNT_ERROR_NONE) {
115 return;
116 }
117
118 if (event == disks::DiskMountManager::MOUNTING) {
119 // If multiple disks have been mounted, skip the rest of them if kiosk
120 // update has already been found.
121 if (!external_update_path_.empty()) {
122 LOG(WARNING) << "External update path already found, skip "
123 << mount_info.mount_path;
124 return;
125 }
126
127 base::PostTaskAndReplyWithResult(
128 backend_task_runner_.get(), FROM_HERE,
129 base::BindOnce(&ParseExternalUpdateManifest,
130 base::FilePath(mount_info.mount_path)),
131 base::BindOnce(&KioskExternalUpdater::ProcessParsedManifest,
132 weak_factory_.GetWeakPtr(),
133 base::FilePath(mount_info.mount_path)));
134 return;
135 }
136
137 // unmounting a removable device case.
138 if (external_update_path_.value().empty()) {
139 // Clear any previously displayed message.
140 DismissKioskUpdateNotification();
141 } else if (external_update_path_.value() == mount_info.mount_path) {
142 DismissKioskUpdateNotification();
143 if (IsExternalUpdatePending()) {
144 LOG(ERROR) << "External kiosk update is not completed when the usb "
145 << "stick is unmoutned.";
146 }
147 external_updates_.clear();
148 external_update_path_.clear();
149 }
150 }
151
OnExternalUpdateUnpackSuccess(const std::string & app_id,const std::string & version,const std::string & min_browser_version,const base::FilePath & temp_dir)152 void KioskExternalUpdater::OnExternalUpdateUnpackSuccess(
153 const std::string& app_id,
154 const std::string& version,
155 const std::string& min_browser_version,
156 const base::FilePath& temp_dir) {
157 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
158
159 // User might pull out the usb stick before updating is completed.
160 if (CheckExternalUpdateInterrupted())
161 return;
162
163 if (!ShouldDoExternalUpdate(app_id, version, min_browser_version)) {
164 external_updates_[app_id].update_status = FAILED;
165 MaybeValidateNextExternalUpdate();
166 return;
167 }
168
169 // User might pull out the usb stick before updating is completed.
170 if (CheckExternalUpdateInterrupted())
171 return;
172
173 base::FilePath external_crx_path =
174 external_updates_[app_id].external_crx.path;
175 base::FilePath temp_crx_path =
176 crx_unpack_dir_.Append(external_crx_path.BaseName());
177 base::PostTaskAndReplyWithResult(
178 backend_task_runner_.get(), FROM_HERE,
179 base::BindOnce(&CopyExternalCrxAndDeleteTempDir, external_crx_path,
180 temp_crx_path, temp_dir),
181 base::BindOnce(&KioskExternalUpdater::PutValidatedExtension,
182 weak_factory_.GetWeakPtr(), app_id, temp_crx_path,
183 version));
184 }
185
OnExternalUpdateUnpackFailure(const std::string & app_id)186 void KioskExternalUpdater::OnExternalUpdateUnpackFailure(
187 const std::string& app_id) {
188 // User might pull out the usb stick before updating is completed.
189 if (CheckExternalUpdateInterrupted())
190 return;
191
192 external_updates_[app_id].update_status = FAILED;
193 external_updates_[app_id].error =
194 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
195 IDS_KIOSK_EXTERNAL_UPDATE_BAD_CRX);
196 MaybeValidateNextExternalUpdate();
197 }
198
ProcessParsedManifest(const base::FilePath & external_update_dir,const ParseManifestResult & result)199 void KioskExternalUpdater::ProcessParsedManifest(
200 const base::FilePath& external_update_dir,
201 const ParseManifestResult& result) {
202 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
203
204 const std::unique_ptr<base::DictionaryValue>& parsed_manifest = result.first;
205 ExternalUpdateErrorCode parsing_error = result.second;
206 if (parsing_error == ERROR_NO_MANIFEST) {
207 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
208 return;
209 }
210 if (parsing_error == ERROR_INVALID_MANIFEST) {
211 NotifyKioskUpdateProgress(
212 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
213 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST));
214 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
215 return;
216 }
217
218 NotifyKioskUpdateProgress(
219 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
220 IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS));
221
222 external_update_path_ = external_update_dir;
223 for (base::DictionaryValue::Iterator it(*parsed_manifest); !it.IsAtEnd();
224 it.Advance()) {
225 std::string app_id = it.key();
226 std::string cached_version_str;
227 base::FilePath cached_crx;
228 if (!KioskAppManager::Get()->GetCachedCrx(
229 app_id, &cached_crx, &cached_version_str)) {
230 LOG(WARNING) << "Can't find app in existing cache " << app_id;
231 continue;
232 }
233
234 const base::DictionaryValue* extension = nullptr;
235 if (!it.value().GetAsDictionary(&extension)) {
236 LOG(ERROR) << "Found bad entry in manifest type " << it.value().type();
237 continue;
238 }
239
240 std::string external_crx_str;
241 if (!extension->GetString(kExternalCrx, &external_crx_str)) {
242 LOG(ERROR) << "Can't find external crx in manifest " << app_id;
243 continue;
244 }
245
246 std::string external_version_str;
247 if (extension->GetString(kExternalVersion, &external_version_str)) {
248 if (!ShouldUpdateForHigherVersion(
249 cached_version_str, external_version_str, false)) {
250 LOG(WARNING) << "External app " << app_id
251 << " is at the same or lower version comparing to "
252 << " the existing one.";
253 continue;
254 }
255 }
256
257 ExternalUpdate update;
258 KioskAppManager::App app;
259 if (KioskAppManager::Get()->GetApp(app_id, &app)) {
260 update.app_name = app.name;
261 } else {
262 NOTREACHED();
263 }
264 update.external_crx = extensions::CRXFileInfo(
265 external_update_path_.AppendASCII(external_crx_str),
266 extensions::GetExternalVerifierFormat());
267 update.external_crx.extension_id = app_id;
268 update.update_status = PENDING;
269 external_updates_[app_id] = update;
270 }
271
272 if (external_updates_.empty()) {
273 NotifyKioskUpdateProgress(
274 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
275 IDS_KIOSK_EXTERNAL_UPDATE_NO_UPDATES));
276 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
277 return;
278 }
279
280 ValidateExternalUpdates();
281 }
282
CheckExternalUpdateInterrupted()283 bool KioskExternalUpdater::CheckExternalUpdateInterrupted() {
284 if (external_updates_.empty()) {
285 // This could happen if user pulls out the usb stick before the updating
286 // operation is completed.
287 LOG(ERROR) << "external_updates_ has been cleared before external "
288 << "updating completes.";
289 return true;
290 }
291
292 return false;
293 }
294
ValidateExternalUpdates()295 void KioskExternalUpdater::ValidateExternalUpdates() {
296 for (const auto& it : external_updates_) {
297 const ExternalUpdate& update = it.second;
298 if (update.update_status == PENDING) {
299 auto crx_validator = base::MakeRefCounted<KioskExternalUpdateValidator>(
300 backend_task_runner_, update.external_crx, crx_unpack_dir_,
301 weak_factory_.GetWeakPtr());
302 crx_validator->Start();
303 break;
304 }
305 }
306 }
307
IsExternalUpdatePending() const308 bool KioskExternalUpdater::IsExternalUpdatePending() const {
309 for (const auto& it : external_updates_) {
310 if (it.second.update_status == PENDING)
311 return true;
312 }
313 return false;
314 }
315
IsAllExternalUpdatesSucceeded() const316 bool KioskExternalUpdater::IsAllExternalUpdatesSucceeded() const {
317 for (const auto& it : external_updates_) {
318 if (it.second.update_status != SUCCESS)
319 return false;
320 }
321 return true;
322 }
323
ShouldDoExternalUpdate(const std::string & app_id,const std::string & version,const std::string & min_browser_version)324 bool KioskExternalUpdater::ShouldDoExternalUpdate(
325 const std::string& app_id,
326 const std::string& version,
327 const std::string& min_browser_version) {
328 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
329
330 std::string existing_version_str;
331 base::FilePath existing_path;
332 bool cached = KioskAppManager::Get()->GetCachedCrx(
333 app_id, &existing_path, &existing_version_str);
334 DCHECK(cached);
335
336 // Compare app version.
337 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
338 if (!ShouldUpdateForHigherVersion(existing_version_str, version, false)) {
339 external_updates_[app_id].error = rb->GetLocalizedString(
340 IDS_KIOSK_EXTERNAL_UPDATE_SAME_OR_LOWER_APP_VERSION);
341 return false;
342 }
343
344 // Check minimum browser version.
345 if (!min_browser_version.empty() &&
346 !ShouldUpdateForHigherVersion(min_browser_version,
347 version_info::GetVersionNumber(), true)) {
348 external_updates_[app_id].error = l10n_util::GetStringFUTF16(
349 IDS_KIOSK_EXTERNAL_UPDATE_REQUIRE_HIGHER_BROWSER_VERSION,
350 base::UTF8ToUTF16(min_browser_version));
351 return false;
352 }
353
354 return true;
355 }
356
PutValidatedExtension(const std::string & app_id,const base::FilePath & crx_file,const std::string & version,bool crx_copied)357 void KioskExternalUpdater::PutValidatedExtension(const std::string& app_id,
358 const base::FilePath& crx_file,
359 const std::string& version,
360 bool crx_copied) {
361 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
362
363 if (CheckExternalUpdateInterrupted())
364 return;
365
366 if (!crx_copied) {
367 LOG(ERROR) << "Cannot copy external crx file to " << crx_file.value();
368 external_updates_[app_id].update_status = FAILED;
369 external_updates_[app_id].error = l10n_util::GetStringFUTF16(
370 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_COPY_CRX_TO_TEMP,
371 base::UTF8ToUTF16(crx_file.value()));
372 MaybeValidateNextExternalUpdate();
373 return;
374 }
375
376 KioskAppManager::Get()->PutValidatedExternalExtension(
377 app_id, crx_file, version,
378 base::BindOnce(&KioskExternalUpdater::OnPutValidatedExtension,
379 weak_factory_.GetWeakPtr()));
380 }
381
OnPutValidatedExtension(const std::string & app_id,bool success)382 void KioskExternalUpdater::OnPutValidatedExtension(const std::string& app_id,
383 bool success) {
384 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
385
386 if (CheckExternalUpdateInterrupted())
387 return;
388
389 if (!success) {
390 external_updates_[app_id].update_status = FAILED;
391 external_updates_[app_id].error = l10n_util::GetStringFUTF16(
392 IDS_KIOSK_EXTERNAL_UPDATE_CANNOT_INSTALL_IN_LOCAL_CACHE,
393 base::UTF8ToUTF16(external_updates_[app_id].external_crx.path.value()));
394 } else {
395 external_updates_[app_id].update_status = SUCCESS;
396 }
397
398 // Validate the next pending external update.
399 MaybeValidateNextExternalUpdate();
400 }
401
MaybeValidateNextExternalUpdate()402 void KioskExternalUpdater::MaybeValidateNextExternalUpdate() {
403 if (IsExternalUpdatePending())
404 ValidateExternalUpdates();
405 else
406 MayBeNotifyKioskAppUpdate();
407 }
408
MayBeNotifyKioskAppUpdate()409 void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() {
410 if (IsExternalUpdatePending())
411 return;
412
413 NotifyKioskUpdateProgress(GetUpdateReportMessage());
414 NotifyKioskAppUpdateAvailable();
415 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(
416 IsAllExternalUpdatesSucceeded());
417 }
418
NotifyKioskAppUpdateAvailable()419 void KioskExternalUpdater::NotifyKioskAppUpdateAvailable() {
420 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
421
422 for (const auto& it : external_updates_) {
423 if (it.second.update_status == SUCCESS) {
424 KioskAppManager::Get()->OnKioskAppCacheUpdated(it.first);
425 }
426 }
427 }
428
NotifyKioskUpdateProgress(const base::string16 & message)429 void KioskExternalUpdater::NotifyKioskUpdateProgress(
430 const base::string16& message) {
431 if (!notification_)
432 notification_ = std::make_unique<KioskExternalUpdateNotification>(message);
433 else
434 notification_->ShowMessage(message);
435 }
436
DismissKioskUpdateNotification()437 void KioskExternalUpdater::DismissKioskUpdateNotification() {
438 if (notification_.get()) {
439 notification_.reset();
440 }
441 }
442
GetUpdateReportMessage() const443 base::string16 KioskExternalUpdater::GetUpdateReportMessage() const {
444 DCHECK(!IsExternalUpdatePending());
445 int updated = 0;
446 int failed = 0;
447 base::string16 updated_apps;
448 base::string16 failed_apps;
449 for (const auto& it : external_updates_) {
450 const ExternalUpdate& update = it.second;
451 base::string16 app_name = base::UTF8ToUTF16(update.app_name);
452 if (update.update_status == SUCCESS) {
453 ++updated;
454 if (updated_apps.empty())
455 updated_apps = app_name;
456 else
457 updated_apps += base::ASCIIToUTF16(", ") + app_name;
458 } else { // FAILED
459 ++failed;
460 if (failed_apps.empty()) {
461 failed_apps = app_name + base::ASCIIToUTF16(": ") + update.error;
462 } else {
463 failed_apps += base::ASCIIToUTF16("\n") + app_name +
464 base::ASCIIToUTF16(": ") + update.error;
465 }
466 }
467 }
468
469 base::string16 message =
470 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
471 IDS_KIOSK_EXTERNAL_UPDATE_COMPLETE);
472 if (updated) {
473 base::string16 success_app_msg = l10n_util::GetStringFUTF16(
474 IDS_KIOSK_EXTERNAL_UPDATE_SUCCESSFUL_UPDATED_APPS, updated_apps);
475 message += base::ASCIIToUTF16("\n") + success_app_msg;
476 }
477
478 if (failed) {
479 base::string16 failed_app_msg =
480 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
481 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_UPDATED_APPS) +
482 base::ASCIIToUTF16("\n") + failed_apps;
483 message += base::ASCIIToUTF16("\n") + failed_app_msg;
484 }
485 return message;
486 }
487
488 } // namespace chromeos
489