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_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" 25 26namespace chromeos { 27 28namespace { 29 30const char kExternalUpdateManifest[] = "external_update.json"; 31const char kExternalCrx[] = "external_crx"; 32const char kExternalVersion[] = "external_version"; 33 34void 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; 42 return; 43 } 44 45 JSONFileValueSerializer serializer(manifest); 46 std::string error_msg; 47 base::Value* extensions = serializer.Deserialize(NULL, &error_msg); 48 if (!extensions) { 49 *error_code = KioskExternalUpdater::ERROR_INVALID_MANIFEST; 50 return; 51 } 52 53 base::DictionaryValue* dict_value = NULL; 54 if (!extensions->GetAsDictionary(&dict_value)) { 55 *error_code = KioskExternalUpdater::ERROR_INVALID_MANIFEST; 56 return; 57 } 58 59 parsed_manifest->Swap(dict_value); 60 *error_code = KioskExternalUpdater::ERROR_NONE; 61} 62 63// Copies |external_crx_file| to |temp_crx_file|, and removes |temp_dir| 64// created for unpacking |external_crx_file|. 65void CopyExternalCrxAndDeleteTempDir(const base::FilePath& external_crx_file, 66 const base::FilePath& temp_crx_file, 67 const base::FilePath& temp_dir, 68 bool* success) { 69 base::DeleteFile(temp_dir, true); 70 *success = base::CopyFile(external_crx_file, temp_crx_file); 71} 72 73// Returns true if |version_1| < |version_2|, and 74// if |update_for_same_version| is true and |version_1| = |version_2|. 75bool 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()) 81 return false; 82 int compare_result = v1.CompareTo(v2); 83 if (compare_result < 0) 84 return true; 85 else if (update_for_same_version && compare_result == 0) 86 return true; 87 else 88 return false; 89} 90 91} // namespace 92 93KioskExternalUpdater::ExternalUpdate::ExternalUpdate() { 94} 95 96KioskExternalUpdater::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); 107} 108 109KioskExternalUpdater::~KioskExternalUpdater() { 110 if (disks::DiskMountManager::GetInstance()) 111 disks::DiskMountManager::GetInstance()->RemoveObserver(this); 112} 113 114void KioskExternalUpdater::OnDiskEvent( 115 disks::DiskMountManager::DiskEvent event, 116 const disks::DiskMountManager::Disk* disk) { 117} 118 119void KioskExternalUpdater::OnDeviceEvent( 120 disks::DiskMountManager::DeviceEvent event, 121 const std::string& device_path) { 122} 123 124void 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) { 131 return; 132 } 133 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; 140 return; 141 } 142 143 base::DictionaryValue* parsed_manifest = new base::DictionaryValue(); 144 ExternalUpdateErrorCode* parsing_error = new ExternalUpdateErrorCode; 145 backend_task_runner_->PostTaskAndReply( 146 FROM_HERE, 147 base::Bind(&ParseExternalUpdateManifest, 148 base::FilePath(mount_info.mount_path), 149 parsed_manifest, 150 parsing_error), 151 base::Bind(&KioskExternalUpdater::ProcessParsedManifest, 152 weak_factory_.GetWeakPtr(), 153 base::Owned(parsing_error), 154 base::FilePath(mount_info.mount_path), 155 base::Owned(parsed_manifest))); 156 } else { // unmounting a removable device. 157 if (external_update_path_.value().empty()) { 158 // Clear any previously displayed message. 159 DismissKioskUpdateNotification(); 160 } else if (external_update_path_.value() == mount_info.mount_path) { 161 DismissKioskUpdateNotification(); 162 if (IsExternalUpdatePending()) { 163 LOG(ERROR) << "External kiosk update is not completed when the usb " 164 "stick is unmoutned."; 165 } 166 external_updates_.clear(); 167 external_update_path_.clear(); 168 } 169 } 170} 171 172void KioskExternalUpdater::OnFormatEvent( 173 disks::DiskMountManager::FormatEvent event, 174 FormatError error_code, 175 const std::string& device_path) { 176} 177 178void KioskExternalUpdater::OnExtenalUpdateUnpackSuccess( 179 const std::string& app_id, 180 const std::string& version, 181 const std::string& min_browser_version, 182 const base::FilePath& temp_dir) { 183 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 184 185 // User might pull out the usb stick before updating is completed. 186 if (CheckExternalUpdateInterrupted()) 187 return; 188 189 if (!ShouldDoExternalUpdate(app_id, version, min_browser_version)) { 190 external_updates_[app_id].update_status = FAILED; 191 MaybeValidateNextExternalUpdate(); 192 return; 193 } 194 195 // User might pull out the usb stick before updating is completed. 196 if (CheckExternalUpdateInterrupted()) 197 return; 198 199 base::FilePath external_crx_path = external_updates_[app_id].external_crx; 200 base::FilePath temp_crx_path = 201 crx_unpack_dir_.Append(external_crx_path.BaseName()); 202 bool* success = new bool; 203 backend_task_runner_->PostTaskAndReply( 204 FROM_HERE, 205 base::Bind(&CopyExternalCrxAndDeleteTempDir, 206 external_crx_path, 207 temp_crx_path, 208 temp_dir, 209 success), 210 base::Bind(&KioskExternalUpdater::PutValidatedExtension, 211 weak_factory_.GetWeakPtr(), 212 base::Owned(success), 213 app_id, 214 temp_crx_path, 215 version)); 216} 217 218void KioskExternalUpdater::OnExternalUpdateUnpackFailure( 219 const std::string& app_id) { 220 // User might pull out the usb stick before updating is completed. 221 if (CheckExternalUpdateInterrupted()) 222 return; 223 224 external_updates_[app_id].update_status = FAILED; 225 external_updates_[app_id].error = 226 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 227 IDS_KIOSK_EXTERNAL_UPDATE_BAD_CRX); 228 MaybeValidateNextExternalUpdate(); 229} 230 231void KioskExternalUpdater::ProcessParsedManifest( 232 ExternalUpdateErrorCode* parsing_error, 233 const base::FilePath& external_update_dir, 234 base::DictionaryValue* parsed_manifest) { 235 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 236 237 if (*parsing_error == ERROR_NO_MANIFEST) { 238 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false); 239 return; 240 } else if (*parsing_error == ERROR_INVALID_MANIFEST) { 241 NotifyKioskUpdateProgress( 242 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 243 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST)); 244 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false); 245 return; 246 } 247 248 NotifyKioskUpdateProgress( 249 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 250 IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS)); 251 252 external_update_path_ = external_update_dir; 253 for (base::DictionaryValue::Iterator it(*parsed_manifest); !it.IsAtEnd(); 254 it.Advance()) { 255 std::string app_id = it.key(); 256 std::string cached_version_str; 257 base::FilePath cached_crx; 258 if (!KioskAppManager::Get()->GetCachedCrx( 259 app_id, &cached_crx, &cached_version_str)) { 260 LOG(WARNING) << "Can't find app in existing cache " << app_id; 261 continue; 262 } 263 264 const base::DictionaryValue* extension = NULL; 265 if (!it.value().GetAsDictionary(&extension)) { 266 LOG(ERROR) << "Found bad entry in manifest type " << it.value().GetType(); 267 continue; 268 } 269 270 std::string external_crx_str; 271 if (!extension->GetString(kExternalCrx, &external_crx_str)) { 272 LOG(ERROR) << "Can't find external crx in manifest " << app_id; 273 continue; 274 } 275 276 std::string external_version_str; 277 if (extension->GetString(kExternalVersion, &external_version_str)) { 278 if (!ShouldUpdateForHigherVersion( 279 cached_version_str, external_version_str, false)) { 280 LOG(WARNING) << "External app " << app_id 281 << " is at the same or lower version comparing to " 282 << " the existing one."; 283 continue; 284 } 285 } 286 287 ExternalUpdate update; 288 KioskAppManager::App app; 289 if (KioskAppManager::Get()->GetApp(app_id, &app)) { 290 update.app_name = app.name; 291 } else { 292 NOTREACHED(); 293 } 294 update.external_crx = external_update_path_.AppendASCII(external_crx_str); 295 update.update_status = PENDING; 296 external_updates_[app_id] = update; 297 } 298 299 if (external_updates_.empty()) { 300 NotifyKioskUpdateProgress( 301 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 302 IDS_KIOSK_EXTERNAL_UPDATE_NO_UPDATES)); 303 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false); 304 return; 305 } 306 307 ValidateExternalUpdates(); 308} 309 310bool KioskExternalUpdater::CheckExternalUpdateInterrupted() { 311 if (external_updates_.empty()) { 312 // This could happen if user pulls out the usb stick before the updating 313 // operation is completed. 314 LOG(ERROR) << "external_updates_ has been cleared before external " 315 << "updating completes."; 316 return true; 317 } 318 319 return false; 320} 321 322void KioskExternalUpdater::ValidateExternalUpdates() { 323 for (ExternalUpdateMap::iterator it = external_updates_.begin(); 324 it != external_updates_.end(); 325 ++it) { 326 if (it->second.update_status == PENDING) { 327 scoped_refptr<KioskExternalUpdateValidator> crx_validator = 328 new KioskExternalUpdateValidator(backend_task_runner_, 329 it->first, 330 it->second.external_crx, 331 crx_unpack_dir_, 332 weak_factory_.GetWeakPtr()); 333 crx_validator->Start(); 334 break; 335 } 336 } 337} 338 339bool KioskExternalUpdater::IsExternalUpdatePending() { 340 for (ExternalUpdateMap::iterator it = external_updates_.begin(); 341 it != external_updates_.end(); 342 ++it) { 343 if (it->second.update_status == PENDING) { 344 return true; 345 } 346 } 347 return false; 348} 349 350bool KioskExternalUpdater::IsAllExternalUpdatesSucceeded() { 351 for (ExternalUpdateMap::iterator it = external_updates_.begin(); 352 it != external_updates_.end(); 353 ++it) { 354 if (it->second.update_status != SUCCESS) { 355 return false; 356 } 357 } 358 return true; 359} 360 361bool KioskExternalUpdater::ShouldDoExternalUpdate( 362 const std::string& app_id, 363 const std::string& version, 364 const std::string& min_browser_version) { 365 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 366 367 std::string existing_version_str; 368 base::FilePath existing_path; 369 bool cached = KioskAppManager::Get()->GetCachedCrx( 370 app_id, &existing_path, &existing_version_str); 371 DCHECK(cached); 372 373 // Compare app version. 374 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 375 if (!ShouldUpdateForHigherVersion(existing_version_str, version, false)) { 376 external_updates_[app_id].error = rb.GetLocalizedString( 377 IDS_KIOSK_EXTERNAL_UPDATE_SAME_OR_LOWER_APP_VERSION); 378 return false; 379 } 380 381 // Check minimum browser version. 382 if (!min_browser_version.empty()) { 383 chrome::VersionInfo current_version_info; 384 if (!ShouldUpdateForHigherVersion( 385 min_browser_version, current_version_info.Version(), true)) { 386 external_updates_[app_id].error = l10n_util::GetStringFUTF16( 387 IDS_KIOSK_EXTERNAL_UPDATE_REQUIRE_HIGHER_BROWSER_VERSION, 388 base::UTF8ToUTF16(min_browser_version)); 389 return false; 390 } 391 } 392 393 return true; 394} 395 396void KioskExternalUpdater::PutValidatedExtension(bool* crx_copied, 397 const std::string& app_id, 398 const base::FilePath& crx_file, 399 const std::string& version) { 400 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 401 if (CheckExternalUpdateInterrupted()) 402 return; 403 404 if (!*crx_copied) { 405 LOG(ERROR) << "Cannot copy external crx file to " << crx_file.value(); 406 external_updates_[app_id].update_status = FAILED; 407 external_updates_[app_id].error = l10n_util::GetStringFUTF16( 408 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_COPY_CRX_TO_TEMP, 409 base::UTF8ToUTF16(crx_file.value())); 410 MaybeValidateNextExternalUpdate(); 411 return; 412 } 413 414 chromeos::KioskAppManager::Get()->PutValidatedExternalExtension( 415 app_id, 416 crx_file, 417 version, 418 base::Bind(&KioskExternalUpdater::OnPutValidatedExtension, 419 weak_factory_.GetWeakPtr())); 420} 421 422void KioskExternalUpdater::OnPutValidatedExtension(const std::string& app_id, 423 bool success) { 424 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 425 if (CheckExternalUpdateInterrupted()) 426 return; 427 428 if (!success) { 429 external_updates_[app_id].update_status = FAILED; 430 external_updates_[app_id].error = l10n_util::GetStringFUTF16( 431 IDS_KIOSK_EXTERNAL_UPDATE_CANNOT_INSTALL_IN_LOCAL_CACHE, 432 base::UTF8ToUTF16(external_updates_[app_id].external_crx.value())); 433 } else { 434 external_updates_[app_id].update_status = SUCCESS; 435 } 436 437 // Validate the next pending external update. 438 MaybeValidateNextExternalUpdate(); 439} 440 441void KioskExternalUpdater::MaybeValidateNextExternalUpdate() { 442 if (IsExternalUpdatePending()) 443 ValidateExternalUpdates(); 444 else 445 MayBeNotifyKioskAppUpdate(); 446} 447 448void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() { 449 if (IsExternalUpdatePending()) 450 return; 451 452 NotifyKioskUpdateProgress(GetUpdateReportMessage()); 453 NotifyKioskAppUpdateAvailable(); 454 KioskAppManager::Get()->OnKioskAppExternalUpdateComplete( 455 IsAllExternalUpdatesSucceeded()); 456} 457 458void KioskExternalUpdater::NotifyKioskAppUpdateAvailable() { 459 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 460 for (ExternalUpdateMap::iterator it = external_updates_.begin(); 461 it != external_updates_.end(); 462 ++it) { 463 if (it->second.update_status == SUCCESS) { 464 KioskAppManager::Get()->OnKioskAppCacheUpdated(it->first); 465 } 466 } 467} 468 469void KioskExternalUpdater::NotifyKioskUpdateProgress( 470 const base::string16& message) { 471 if (!notification_) 472 notification_.reset(new KioskExternalUpdateNotification(message)); 473 else 474 notification_->ShowMessage(message); 475} 476 477void KioskExternalUpdater::DismissKioskUpdateNotification() { 478 if (notification_.get()) { 479 notification_.reset(); 480 } 481} 482 483base::string16 KioskExternalUpdater::GetUpdateReportMessage() { 484 DCHECK(!IsExternalUpdatePending()); 485 int updated = 0; 486 int failed = 0; 487 base::string16 updated_apps; 488 base::string16 failed_apps; 489 for (ExternalUpdateMap::iterator it = external_updates_.begin(); 490 it != external_updates_.end(); 491 ++it) { 492 base::string16 app_name = base::UTF8ToUTF16(it->second.app_name); 493 if (it->second.update_status == SUCCESS) { 494 ++updated; 495 if (updated_apps.empty()) 496 updated_apps = app_name; 497 else 498 updated_apps = updated_apps + base::ASCIIToUTF16(", ") + app_name; 499 } else { // FAILED 500 ++failed; 501 if (failed_apps.empty()) { 502 failed_apps = app_name + base::ASCIIToUTF16(": ") + it->second.error; 503 } else { 504 failed_apps = failed_apps + base::ASCIIToUTF16("\n") + app_name + 505 base::ASCIIToUTF16(": ") + it->second.error; 506 } 507 } 508 } 509 510 base::string16 message; 511 message = ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 512 IDS_KIOSK_EXTERNAL_UPDATE_COMPLETE); 513 base::string16 success_app_msg; 514 if (updated) { 515 success_app_msg = l10n_util::GetStringFUTF16( 516 IDS_KIOSK_EXTERNAL_UPDATE_SUCCESSFUL_UPDATED_APPS, updated_apps); 517 message = message + base::ASCIIToUTF16("\n") + success_app_msg; 518 } 519 520 base::string16 failed_app_msg; 521 if (failed) { 522 failed_app_msg = ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 523 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_UPDATED_APPS) + 524 base::ASCIIToUTF16("\n") + failed_apps; 525 message = message + base::ASCIIToUTF16("\n") + failed_app_msg; 526 } 527 return message; 528} 529 530} // namespace chromeos 531