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.
5 #include "chrome/browser/chromeos/app_mode/kiosk_external_updater.h"
8 #include "base/files/file_enumerator.h"
9 #include "base/files/file_util.h"
10 #include "base/json/json_file_value_serializer.h"
11 #include "base/location.h"
12 #include "base/logging.h"
13 #include "base/strings/utf_string_conversions.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/browser/extensions/sandboxed_unpacker.h"
18 #include "chrome/common/chrome_version_info.h"
19 #include "content/public/browser/browser_thread.h"
20 #include "extensions/common/extension.h"
21 #include "grit/chromium_strings.h"
22 #include "grit/generated_resources.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/resource/resource_bundle.h"
30 const char kExternalUpdateManifest
[] = "external_update.json";
31 const char kExternalCrx
[] = "external_crx";
32 const char kExternalVersion
[] = "external_version";
34 void ParseExternalUpdateManifest(
35 const base::FilePath
& external_update_dir
,
36 base::DictionaryValue
* parsed_manifest
,
37 KioskExternalUpdater::ExternalUpdateErrorCode
* error_code
) {
38 base::FilePath manifest
=
39 external_update_dir
.AppendASCII(kExternalUpdateManifest
);
40 if (!base::PathExists(manifest
)) {
41 *error_code
= KioskExternalUpdater::ERROR_NO_MANIFEST
;
45 JSONFileValueSerializer
serializer(manifest
);
46 std::string error_msg
;
47 base::Value
* extensions
= serializer
.Deserialize(NULL
, &error_msg
);
49 *error_code
= KioskExternalUpdater::ERROR_INVALID_MANIFEST
;
53 base::DictionaryValue
* dict_value
= NULL
;
54 if (!extensions
->GetAsDictionary(&dict_value
)) {
55 *error_code
= KioskExternalUpdater::ERROR_INVALID_MANIFEST
;
59 parsed_manifest
->Swap(dict_value
);
60 *error_code
= KioskExternalUpdater::ERROR_NONE
;
63 // Copies |external_crx_file| to |temp_crx_file|, and removes |temp_dir|
64 // created for unpacking |external_crx_file|.
65 void CopyExternalCrxAndDeleteTempDir(const base::FilePath
& external_crx_file
,
66 const base::FilePath
& temp_crx_file
,
67 const base::FilePath
& temp_dir
,
69 base::DeleteFile(temp_dir
, true);
70 *success
= base::CopyFile(external_crx_file
, temp_crx_file
);
73 // Returns true if |version_1| < |version_2|, and
74 // if |update_for_same_version| is true and |version_1| = |version_2|.
75 bool ShouldUpdateForHigherVersion(const std::string
& version_1
,
76 const std::string
& version_2
,
77 bool update_for_same_version
) {
78 const base::Version
v1(version_1
);
79 const base::Version
v2(version_2
);
80 if (!v1
.IsValid() || !v2
.IsValid())
82 int compare_result
= v1
.CompareTo(v2
);
83 if (compare_result
< 0)
85 else if (update_for_same_version
&& compare_result
== 0)
93 KioskExternalUpdater::ExternalUpdate::ExternalUpdate() {
96 KioskExternalUpdater::KioskExternalUpdater(
97 const scoped_refptr
<base::SequencedTaskRunner
>& backend_task_runner
,
98 const base::FilePath
& crx_cache_dir
,
99 const base::FilePath
& crx_unpack_dir
)
100 : backend_task_runner_(backend_task_runner
),
101 crx_cache_dir_(crx_cache_dir
),
102 crx_unpack_dir_(crx_unpack_dir
),
103 weak_factory_(this) {
104 // Subscribe to DiskMountManager.
105 DCHECK(disks::DiskMountManager::GetInstance());
106 disks::DiskMountManager::GetInstance()->AddObserver(this);
109 KioskExternalUpdater::~KioskExternalUpdater() {
110 if (disks::DiskMountManager::GetInstance())
111 disks::DiskMountManager::GetInstance()->RemoveObserver(this);
114 void KioskExternalUpdater::OnDiskEvent(
115 disks::DiskMountManager::DiskEvent event
,
116 const disks::DiskMountManager::Disk
* disk
) {
119 void KioskExternalUpdater::OnDeviceEvent(
120 disks::DiskMountManager::DeviceEvent event
,
121 const std::string
& device_path
) {
124 void KioskExternalUpdater::OnMountEvent(
125 disks::DiskMountManager::MountEvent event
,
126 MountError error_code
,
127 const disks::DiskMountManager::MountPointInfo
& mount_info
) {
128 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
129 if (mount_info
.mount_type
!= MOUNT_TYPE_DEVICE
||
130 error_code
!= MOUNT_ERROR_NONE
) {
134 if (event
== disks::DiskMountManager::MOUNTING
) {
135 // If multiple disks have been mounted, skip the rest of them if kiosk
136 // update has already been found.
137 if (!external_update_path_
.empty()) {
138 LOG(WARNING
) << "External update path already found, skip "
139 << mount_info
.mount_path
;
143 NotifyKioskUpdateProgress(
144 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
145 IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS
));
147 base::DictionaryValue
* parsed_manifest
= new base::DictionaryValue();
148 ExternalUpdateErrorCode
* parsing_error
= new ExternalUpdateErrorCode
;
149 backend_task_runner_
->PostTaskAndReply(
151 base::Bind(&ParseExternalUpdateManifest
,
152 base::FilePath(mount_info
.mount_path
),
155 base::Bind(&KioskExternalUpdater::ProcessParsedManifest
,
156 weak_factory_
.GetWeakPtr(),
157 base::Owned(parsing_error
),
158 base::FilePath(mount_info
.mount_path
),
159 base::Owned(parsed_manifest
)));
160 } else { // unmounting a removable device.
161 if (external_update_path_
.value().empty()) {
162 // Clear any previously displayed message.
163 DismissKioskUpdateNotification();
164 } else if (external_update_path_
.value() == mount_info
.mount_path
) {
165 DismissKioskUpdateNotification();
166 if (IsExternalUpdatePending()) {
167 LOG(ERROR
) << "External kiosk update is not completed when the usb "
168 "stick is unmoutned.";
170 external_updates_
.clear();
171 external_update_path_
.clear();
176 void KioskExternalUpdater::OnFormatEvent(
177 disks::DiskMountManager::FormatEvent event
,
178 FormatError error_code
,
179 const std::string
& device_path
) {
182 void KioskExternalUpdater::OnExtenalUpdateUnpackSuccess(
183 const std::string
& app_id
,
184 const std::string
& version
,
185 const std::string
& min_browser_version
,
186 const base::FilePath
& temp_dir
) {
187 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
189 // User might pull out the usb stick before updating is completed.
190 if (CheckExternalUpdateInterrupted())
193 if (!ShouldDoExternalUpdate(app_id
, version
, min_browser_version
)) {
194 external_updates_
[app_id
].update_status
= FAILED
;
195 MaybeValidateNextExternalUpdate();
199 // User might pull out the usb stick before updating is completed.
200 if (CheckExternalUpdateInterrupted())
203 base::FilePath external_crx_path
= external_updates_
[app_id
].external_crx
;
204 base::FilePath temp_crx_path
=
205 crx_unpack_dir_
.Append(external_crx_path
.BaseName());
206 bool* success
= new bool;
207 backend_task_runner_
->PostTaskAndReply(
209 base::Bind(&CopyExternalCrxAndDeleteTempDir
,
214 base::Bind(&KioskExternalUpdater::PutValidatedExtension
,
215 weak_factory_
.GetWeakPtr(),
216 base::Owned(success
),
222 void KioskExternalUpdater::OnExternalUpdateUnpackFailure(
223 const std::string
& app_id
) {
224 // User might pull out the usb stick before updating is completed.
225 if (CheckExternalUpdateInterrupted())
228 external_updates_
[app_id
].update_status
= FAILED
;
229 external_updates_
[app_id
].error
=
230 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
231 IDS_KIOSK_EXTERNAL_UPDATE_BAD_CRX
);
232 MaybeValidateNextExternalUpdate();
235 void KioskExternalUpdater::ProcessParsedManifest(
236 ExternalUpdateErrorCode
* parsing_error
,
237 const base::FilePath
& external_update_dir
,
238 base::DictionaryValue
* parsed_manifest
) {
239 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
241 if (*parsing_error
== ERROR_NO_MANIFEST
) {
242 NotifyKioskUpdateProgress(
243 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
244 IDS_KIOSK_EXTERNAL_UPDATE_NO_MANIFEST
));
245 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
247 } else if (*parsing_error
== ERROR_INVALID_MANIFEST
) {
248 NotifyKioskUpdateProgress(
249 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
250 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST
));
251 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
255 external_update_path_
= external_update_dir
;
256 for (base::DictionaryValue::Iterator
it(*parsed_manifest
); !it
.IsAtEnd();
258 std::string app_id
= it
.key();
259 std::string cached_version_str
;
260 base::FilePath cached_crx
;
261 if (!KioskAppManager::Get()->GetCachedCrx(
262 app_id
, &cached_crx
, &cached_version_str
)) {
263 LOG(WARNING
) << "Can't find app in existing cache " << app_id
;
267 const base::DictionaryValue
* extension
= NULL
;
268 if (!it
.value().GetAsDictionary(&extension
)) {
269 LOG(ERROR
) << "Found bad entry in manifest type " << it
.value().GetType();
273 std::string external_crx_str
;
274 if (!extension
->GetString(kExternalCrx
, &external_crx_str
)) {
275 LOG(ERROR
) << "Can't find external crx in manifest " << app_id
;
279 std::string external_version_str
;
280 if (extension
->GetString(kExternalVersion
, &external_version_str
)) {
281 if (!ShouldUpdateForHigherVersion(
282 cached_version_str
, external_version_str
, false)) {
283 LOG(WARNING
) << "External app " << app_id
284 << " is at the same or lower version comparing to "
285 << " the existing one.";
290 ExternalUpdate update
;
291 KioskAppManager::App app
;
292 if (KioskAppManager::Get()->GetApp(app_id
, &app
)) {
293 update
.app_name
= app
.name
;
297 update
.external_crx
= external_update_path_
.AppendASCII(external_crx_str
);
298 update
.update_status
= PENDING
;
299 external_updates_
[app_id
] = update
;
302 if (external_updates_
.empty()) {
303 NotifyKioskUpdateProgress(
304 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
305 IDS_KIOSK_EXTERNAL_UPDATE_NO_UPDATES
));
306 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
310 ValidateExternalUpdates();
313 bool KioskExternalUpdater::CheckExternalUpdateInterrupted() {
314 if (external_updates_
.empty()) {
315 // This could happen if user pulls out the usb stick before the updating
316 // operation is completed.
317 LOG(ERROR
) << "external_updates_ has been cleared before external "
318 << "updating completes.";
325 void KioskExternalUpdater::ValidateExternalUpdates() {
326 for (ExternalUpdateMap::iterator it
= external_updates_
.begin();
327 it
!= external_updates_
.end();
329 if (it
->second
.update_status
== PENDING
) {
330 scoped_refptr
<KioskExternalUpdateValidator
> crx_validator
=
331 new KioskExternalUpdateValidator(backend_task_runner_
,
333 it
->second
.external_crx
,
335 weak_factory_
.GetWeakPtr());
336 crx_validator
->Start();
342 bool KioskExternalUpdater::IsExternalUpdatePending() {
343 for (ExternalUpdateMap::iterator it
= external_updates_
.begin();
344 it
!= external_updates_
.end();
346 if (it
->second
.update_status
== PENDING
) {
353 bool KioskExternalUpdater::IsAllExternalUpdatesSucceeded() {
354 for (ExternalUpdateMap::iterator it
= external_updates_
.begin();
355 it
!= external_updates_
.end();
357 if (it
->second
.update_status
!= SUCCESS
) {
364 bool KioskExternalUpdater::ShouldDoExternalUpdate(
365 const std::string
& app_id
,
366 const std::string
& version
,
367 const std::string
& min_browser_version
) {
368 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
370 std::string existing_version_str
;
371 base::FilePath existing_path
;
372 bool cached
= KioskAppManager::Get()->GetCachedCrx(
373 app_id
, &existing_path
, &existing_version_str
);
376 // Compare app version.
377 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
378 if (!ShouldUpdateForHigherVersion(existing_version_str
, version
, false)) {
379 external_updates_
[app_id
].error
= rb
.GetLocalizedString(
380 IDS_KIOSK_EXTERNAL_UPDATE_SAME_OR_LOWER_APP_VERSION
);
384 // Check minimum browser version.
385 if (!min_browser_version
.empty()) {
386 chrome::VersionInfo current_version_info
;
387 if (!ShouldUpdateForHigherVersion(
388 min_browser_version
, current_version_info
.Version(), true)) {
389 external_updates_
[app_id
].error
= l10n_util::GetStringFUTF16(
390 IDS_KIOSK_EXTERNAL_UPDATE_REQUIRE_HIGHER_BROWSER_VERSION
,
391 base::UTF8ToUTF16(min_browser_version
));
399 void KioskExternalUpdater::PutValidatedExtension(bool* crx_copied
,
400 const std::string
& app_id
,
401 const base::FilePath
& crx_file
,
402 const std::string
& version
) {
403 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
404 if (CheckExternalUpdateInterrupted())
408 LOG(ERROR
) << "Cannot copy external crx file to " << crx_file
.value();
409 external_updates_
[app_id
].update_status
= FAILED
;
410 external_updates_
[app_id
].error
= l10n_util::GetStringFUTF16(
411 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_COPY_CRX_TO_TEMP
,
412 base::UTF8ToUTF16(crx_file
.value()));
413 MaybeValidateNextExternalUpdate();
417 chromeos::KioskAppManager::Get()->PutValidatedExternalExtension(
421 base::Bind(&KioskExternalUpdater::OnPutValidatedExtension
,
422 weak_factory_
.GetWeakPtr()));
425 void KioskExternalUpdater::OnPutValidatedExtension(const std::string
& app_id
,
427 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
428 if (CheckExternalUpdateInterrupted())
432 external_updates_
[app_id
].update_status
= FAILED
;
433 external_updates_
[app_id
].error
= l10n_util::GetStringFUTF16(
434 IDS_KIOSK_EXTERNAL_UPDATE_CANNOT_INSTALL_IN_LOCAL_CACHE
,
435 base::UTF8ToUTF16(external_updates_
[app_id
].external_crx
.value()));
437 external_updates_
[app_id
].update_status
= SUCCESS
;
440 // Validate the next pending external update.
441 MaybeValidateNextExternalUpdate();
444 void KioskExternalUpdater::MaybeValidateNextExternalUpdate() {
445 if (IsExternalUpdatePending())
446 ValidateExternalUpdates();
448 MayBeNotifyKioskAppUpdate();
451 void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() {
452 if (IsExternalUpdatePending())
455 NotifyKioskUpdateProgress(GetUpdateReportMessage());
456 NotifyKioskAppUpdateAvailable();
457 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(
458 IsAllExternalUpdatesSucceeded());
461 void KioskExternalUpdater::NotifyKioskAppUpdateAvailable() {
462 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
463 for (ExternalUpdateMap::iterator it
= external_updates_
.begin();
464 it
!= external_updates_
.end();
466 if (it
->second
.update_status
== SUCCESS
) {
467 KioskAppManager::Get()->OnKioskAppCacheUpdated(it
->first
);
472 void KioskExternalUpdater::NotifyKioskUpdateProgress(
473 const base::string16
& message
) {
475 notification_
.reset(new KioskExternalUpdateNotification(message
));
477 notification_
->ShowMessage(message
);
480 void KioskExternalUpdater::DismissKioskUpdateNotification() {
481 if (notification_
.get()) {
482 notification_
.reset();
486 base::string16
KioskExternalUpdater::GetUpdateReportMessage() {
487 DCHECK(!IsExternalUpdatePending());
490 base::string16 updated_apps
;
491 base::string16 failed_apps
;
492 for (ExternalUpdateMap::iterator it
= external_updates_
.begin();
493 it
!= external_updates_
.end();
495 base::string16 app_name
= base::UTF8ToUTF16(it
->second
.app_name
);
496 if (it
->second
.update_status
== SUCCESS
) {
498 if (updated_apps
.empty())
499 updated_apps
= app_name
;
501 updated_apps
= updated_apps
+ base::ASCIIToUTF16(", ") + app_name
;
504 if (failed_apps
.empty()) {
505 failed_apps
= app_name
+ base::ASCIIToUTF16(": ") + it
->second
.error
;
507 failed_apps
= failed_apps
+ base::ASCIIToUTF16("\n") + app_name
+
508 base::ASCIIToUTF16(": ") + it
->second
.error
;
513 base::string16 message
;
514 message
= ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
515 IDS_KIOSK_EXTERNAL_UPDATE_COMPLETE
);
516 base::string16 success_app_msg
;
518 success_app_msg
= l10n_util::GetStringFUTF16(
519 IDS_KIOSK_EXTERNAL_UPDATE_SUCCESSFUL_UPDATED_APPS
, updated_apps
);
520 message
= message
+ base::ASCIIToUTF16("\n") + success_app_msg
;
523 base::string16 failed_app_msg
;
525 failed_app_msg
= ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
526 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_UPDATED_APPS
) +
527 base::ASCIIToUTF16("\n") + failed_apps
;
528 message
= message
+ base::ASCIIToUTF16("\n") + failed_app_msg
;
533 } // namespace chromeos