Home | History | Annotate | Download | only in app_mode
      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 
     26 namespace chromeos {
     27 
     28 namespace {
     29 
     30 const char kExternalUpdateManifest[] = "external_update.json";
     31 const char kExternalCrx[] = "external_crx";
     32 const char kExternalVersion[] = "external_version";
     33 
     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;
     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|.
     65 void 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|.
     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())
     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 
     93 KioskExternalUpdater::ExternalUpdate::ExternalUpdate() {
     94 }
     95 
     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);
    107 }
    108 
    109 KioskExternalUpdater::~KioskExternalUpdater() {
    110   if (disks::DiskMountManager::GetInstance())
    111     disks::DiskMountManager::GetInstance()->RemoveObserver(this);
    112 }
    113 
    114 void KioskExternalUpdater::OnDiskEvent(
    115     disks::DiskMountManager::DiskEvent event,
    116     const disks::DiskMountManager::Disk* disk) {
    117 }
    118 
    119 void KioskExternalUpdater::OnDeviceEvent(
    120     disks::DiskMountManager::DeviceEvent event,
    121     const std::string& device_path) {
    122 }
    123 
    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) {
    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 
    172 void KioskExternalUpdater::OnFormatEvent(
    173     disks::DiskMountManager::FormatEvent event,
    174     FormatError error_code,
    175     const std::string& device_path) {
    176 }
    177 
    178 void 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 
    218 void 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 
    231 void 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 
    310 bool 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 
    322 void 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 
    339 bool 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 
    350 bool 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 
    361 bool 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 
    396 void 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 
    422 void 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 
    441 void KioskExternalUpdater::MaybeValidateNextExternalUpdate() {
    442   if (IsExternalUpdatePending())
    443     ValidateExternalUpdates();
    444   else
    445     MayBeNotifyKioskAppUpdate();
    446 }
    447 
    448 void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() {
    449   if (IsExternalUpdatePending())
    450     return;
    451 
    452   NotifyKioskUpdateProgress(GetUpdateReportMessage());
    453   NotifyKioskAppUpdateAvailable();
    454   KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(
    455       IsAllExternalUpdatesSucceeded());
    456 }
    457 
    458 void 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 
    469 void KioskExternalUpdater::NotifyKioskUpdateProgress(
    470     const base::string16& message) {
    471   if (!notification_)
    472     notification_.reset(new KioskExternalUpdateNotification(message));
    473   else
    474     notification_->ShowMessage(message);
    475 }
    476 
    477 void KioskExternalUpdater::DismissKioskUpdateNotification() {
    478   if (notification_.get()) {
    479     notification_.reset();
    480   }
    481 }
    482 
    483 base::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