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/extensions/updater/local_extension_cache.h" 6 7 #include "base/bind.h" 8 #include "base/files/file_enumerator.h" 9 #include "base/files/file_util.h" 10 #include "base/sequenced_task_runner.h" 11 #include "base/strings/string_util.h" 12 #include "base/sys_info.h" 13 #include "base/version.h" 14 #include "components/crx_file/id_util.h" 15 #include "content/public/browser/browser_thread.h" 16 17 namespace extensions { 18 namespace { 19 20 // File name extension for CRX files (not case sensitive). 21 const char kCRXFileExtension[] = ".crx"; 22 23 // Delay between checks for flag file presence when waiting for the cache to 24 // become ready. 25 const int64_t kCacheStatusPollingDelayMs = 1000; 26 27 } // namespace 28 29 const char LocalExtensionCache::kCacheReadyFlagFileName[] = ".initialized"; 30 31 LocalExtensionCache::LocalExtensionCache( 32 const base::FilePath& cache_dir, 33 uint64 max_cache_size, 34 const base::TimeDelta& max_cache_age, 35 const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner) 36 : cache_dir_(cache_dir), 37 max_cache_size_(max_cache_size), 38 min_cache_age_(base::Time::Now() - max_cache_age), 39 backend_task_runner_(backend_task_runner), 40 state_(kUninitialized), 41 weak_ptr_factory_(this), 42 cache_status_polling_delay_( 43 base::TimeDelta::FromMilliseconds(kCacheStatusPollingDelayMs)) { 44 } 45 46 LocalExtensionCache::~LocalExtensionCache() { 47 if (state_ == kReady) 48 CleanUp(); 49 } 50 51 void LocalExtensionCache::Init(bool wait_for_cache_initialization, 52 const base::Closure& callback) { 53 DCHECK_EQ(state_, kUninitialized); 54 55 state_ = kWaitInitialization; 56 if (wait_for_cache_initialization) 57 CheckCacheStatus(callback); 58 else 59 CheckCacheContents(callback); 60 } 61 62 void LocalExtensionCache::Shutdown(const base::Closure& callback) { 63 DCHECK_NE(state_, kShutdown); 64 if (state_ == kReady) 65 CleanUp(); 66 state_ = kShutdown; 67 backend_task_runner_->PostTaskAndReply(FROM_HERE, 68 base::Bind(&base::DoNothing), callback); 69 } 70 71 bool LocalExtensionCache::GetExtension(const std::string& id, 72 base::FilePath* file_path, 73 std::string* version) { 74 if (state_ != kReady) 75 return false; 76 77 CacheMap::iterator it = cached_extensions_.find(id); 78 if (it == cached_extensions_.end()) 79 return false; 80 81 if (file_path) { 82 *file_path = it->second.file_path; 83 84 // If caller is not interesting in file_path, extension is not used. 85 base::Time now = base::Time::Now(); 86 backend_task_runner_->PostTask(FROM_HERE, 87 base::Bind(&LocalExtensionCache::BackendMarkFileUsed, 88 it->second.file_path, now)); 89 it->second.last_used = now; 90 } 91 92 if (version) 93 *version = it->second.version; 94 95 return true; 96 } 97 98 void LocalExtensionCache::PutExtension(const std::string& id, 99 const base::FilePath& file_path, 100 const std::string& version, 101 const PutExtensionCallback& callback) { 102 if (state_ != kReady) { 103 callback.Run(file_path, true); 104 return; 105 } 106 107 Version version_validator(version); 108 if (!version_validator.IsValid()) { 109 LOG(ERROR) << "Extension " << id << " has bad version " << version; 110 callback.Run(file_path, true); 111 return; 112 } 113 114 CacheMap::iterator it = cached_extensions_.find(id); 115 if (it != cached_extensions_.end()) { 116 Version new_version(version); 117 Version prev_version(it->second.version); 118 if (new_version.CompareTo(prev_version) <= 0) { 119 LOG(WARNING) << "Cache contains newer or the same version " 120 << prev_version.GetString() << " for extension " 121 << id << " version " << version; 122 callback.Run(file_path, true); 123 return; 124 } 125 } 126 127 backend_task_runner_->PostTask( 128 FROM_HERE, 129 base::Bind(&LocalExtensionCache::BackendInstallCacheEntry, 130 weak_ptr_factory_.GetWeakPtr(), 131 cache_dir_, 132 id, 133 file_path, 134 version, 135 callback)); 136 } 137 138 bool LocalExtensionCache::RemoveExtension(const std::string& id) { 139 if (state_ != kReady) 140 return false; 141 142 CacheMap::iterator it = cached_extensions_.find(id); 143 if (it == cached_extensions_.end()) 144 return false; 145 146 backend_task_runner_->PostTask( 147 FROM_HERE, 148 base::Bind( 149 &LocalExtensionCache::BackendRemoveCacheEntry, cache_dir_, id)); 150 151 cached_extensions_.erase(it); 152 return true; 153 } 154 155 bool LocalExtensionCache::GetStatistics(uint64* cache_size, 156 size_t* extensions_count) { 157 if (state_ != kReady) 158 return false; 159 160 *cache_size = 0; 161 for (CacheMap::iterator it = cached_extensions_.begin(); 162 it != cached_extensions_.end(); ++it) { 163 *cache_size += it->second.size; 164 } 165 *extensions_count = cached_extensions_.size(); 166 167 return true; 168 } 169 170 void LocalExtensionCache::SetCacheStatusPollingDelayForTests( 171 const base::TimeDelta& delay) { 172 cache_status_polling_delay_ = delay; 173 } 174 175 void LocalExtensionCache::CheckCacheStatus(const base::Closure& callback) { 176 if (state_ == kShutdown) { 177 callback.Run(); 178 return; 179 } 180 181 backend_task_runner_->PostTask( 182 FROM_HERE, 183 base::Bind(&LocalExtensionCache::BackendCheckCacheStatus, 184 weak_ptr_factory_.GetWeakPtr(), 185 cache_dir_, 186 callback)); 187 } 188 189 // static 190 void LocalExtensionCache::BackendCheckCacheStatus( 191 base::WeakPtr<LocalExtensionCache> local_cache, 192 const base::FilePath& cache_dir, 193 const base::Closure& callback) { 194 const bool exists = 195 base::PathExists(cache_dir.AppendASCII(kCacheReadyFlagFileName)); 196 197 static bool first_check = true; 198 if (first_check && !exists && !base::SysInfo::IsRunningOnChromeOS()) { 199 LOG(WARNING) << "Extensions will not be installed from update URLs until " 200 << cache_dir.AppendASCII(kCacheReadyFlagFileName).value() 201 << " exists."; 202 } 203 first_check = false; 204 205 content::BrowserThread::PostTask( 206 content::BrowserThread::UI, 207 FROM_HERE, 208 base::Bind(&LocalExtensionCache::OnCacheStatusChecked, 209 local_cache, 210 exists, 211 callback)); 212 } 213 214 void LocalExtensionCache::OnCacheStatusChecked(bool ready, 215 const base::Closure& callback) { 216 if (state_ == kShutdown) { 217 callback.Run(); 218 return; 219 } 220 221 if (ready) { 222 CheckCacheContents(callback); 223 } else { 224 content::BrowserThread::PostDelayedTask( 225 content::BrowserThread::UI, 226 FROM_HERE, 227 base::Bind(&LocalExtensionCache::CheckCacheStatus, 228 weak_ptr_factory_.GetWeakPtr(), 229 callback), 230 cache_status_polling_delay_); 231 } 232 } 233 234 void LocalExtensionCache::CheckCacheContents(const base::Closure& callback) { 235 DCHECK_EQ(state_, kWaitInitialization); 236 backend_task_runner_->PostTask( 237 FROM_HERE, 238 base::Bind(&LocalExtensionCache::BackendCheckCacheContents, 239 weak_ptr_factory_.GetWeakPtr(), 240 cache_dir_, 241 callback)); 242 } 243 244 // static 245 void LocalExtensionCache::BackendCheckCacheContents( 246 base::WeakPtr<LocalExtensionCache> local_cache, 247 const base::FilePath& cache_dir, 248 const base::Closure& callback) { 249 scoped_ptr<CacheMap> cache_content(new CacheMap); 250 BackendCheckCacheContentsInternal(cache_dir, cache_content.get()); 251 content::BrowserThread::PostTask( 252 content::BrowserThread::UI, 253 FROM_HERE, 254 base::Bind(&LocalExtensionCache::OnCacheContentsChecked, 255 local_cache, 256 base::Passed(&cache_content), 257 callback)); 258 } 259 260 // static 261 void LocalExtensionCache::BackendCheckCacheContentsInternal( 262 const base::FilePath& cache_dir, 263 CacheMap* cache_content) { 264 // Start by verifying that the cache_dir exists. 265 if (!base::DirectoryExists(cache_dir)) { 266 // Create it now. 267 if (!base::CreateDirectory(cache_dir)) { 268 LOG(ERROR) << "Failed to create cache directory at " 269 << cache_dir.value(); 270 } 271 272 // Nothing else to do. Cache is empty. 273 return; 274 } 275 276 // Enumerate all the files in the cache |cache_dir|, including directories 277 // and symlinks. Each unrecognized file will be erased. 278 int types = base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES; 279 base::FileEnumerator enumerator(cache_dir, false /* recursive */, types); 280 for (base::FilePath path = enumerator.Next(); 281 !path.empty(); path = enumerator.Next()) { 282 base::FileEnumerator::FileInfo info = enumerator.GetInfo(); 283 std::string basename = path.BaseName().value(); 284 285 if (info.IsDirectory() || base::IsLink(info.GetName())) { 286 LOG(ERROR) << "Erasing bad file in cache directory: " << basename; 287 base::DeleteFile(path, true /* recursive */); 288 continue; 289 } 290 291 // Skip flag file that indicates that cache is ready. 292 if (basename == kCacheReadyFlagFileName) 293 continue; 294 295 // crx files in the cache are named <extension-id>-<version>.crx. 296 std::string id; 297 std::string version; 298 if (EndsWith(basename, kCRXFileExtension, false /* case-sensitive */)) { 299 size_t n = basename.find('-'); 300 if (n != std::string::npos && n + 1 < basename.size() - 4) { 301 id = basename.substr(0, n); 302 // Size of |version| = total size - "<id>" - "-" - ".crx" 303 version = basename.substr(n + 1, basename.size() - 5 - id.size()); 304 } 305 } 306 307 // Enforce a lower-case id. 308 id = base::StringToLowerASCII(id); 309 if (!crx_file::id_util::IdIsValid(id)) { 310 LOG(ERROR) << "Bad extension id in cache: " << id; 311 id.clear(); 312 } 313 314 if (!Version(version).IsValid()) { 315 LOG(ERROR) << "Bad extension version in cache: " << version; 316 version.clear(); 317 } 318 319 if (id.empty() || version.empty()) { 320 LOG(ERROR) << "Invalid file in cache, erasing: " << basename; 321 base::DeleteFile(path, true /* recursive */); 322 continue; 323 } 324 325 VLOG(1) << "Found cached version " << version 326 << " for extension id " << id; 327 328 CacheMap::iterator it = cache_content->find(id); 329 if (it != cache_content->end()) { 330 // |cache_content| already has version for this ID. Removed older one. 331 Version curr_version(version); 332 Version prev_version(it->second.version); 333 if (prev_version.CompareTo(curr_version) <= 0) { 334 base::DeleteFile(base::FilePath(it->second.file_path), 335 true /* recursive */); 336 cache_content->erase(id); 337 VLOG(1) << "Remove older version " << it->second.version 338 << " for extension id " << id; 339 } else { 340 base::DeleteFile(path, true /* recursive */); 341 VLOG(1) << "Remove older version " << version 342 << " for extension id " << id; 343 continue; 344 } 345 } 346 347 cache_content->insert(std::make_pair(id, CacheItemInfo( 348 version, info.GetLastModifiedTime(), info.GetSize(), path))); 349 } 350 } 351 352 void LocalExtensionCache::OnCacheContentsChecked( 353 scoped_ptr<CacheMap> cache_content, 354 const base::Closure& callback) { 355 cache_content->swap(cached_extensions_); 356 state_ = kReady; 357 callback.Run(); 358 } 359 360 // static 361 void LocalExtensionCache::BackendMarkFileUsed(const base::FilePath& file_path, 362 const base::Time& time) { 363 base::TouchFile(file_path, time, time); 364 } 365 366 // static 367 void LocalExtensionCache::BackendInstallCacheEntry( 368 base::WeakPtr<LocalExtensionCache> local_cache, 369 const base::FilePath& cache_dir, 370 const std::string& id, 371 const base::FilePath& file_path, 372 const std::string& version, 373 const PutExtensionCallback& callback) { 374 std::string basename = id + "-" + version + kCRXFileExtension; 375 base::FilePath cached_crx_path = cache_dir.AppendASCII(basename); 376 377 bool was_error = false; 378 if (base::PathExists(cached_crx_path)) { 379 LOG(ERROR) << "File already exists " << file_path.value(); 380 cached_crx_path = file_path; 381 was_error = true; 382 } 383 384 base::File::Info info; 385 if (!was_error) { 386 if (!base::Move(file_path, cached_crx_path)) { 387 LOG(ERROR) << "Failed to copy from " << file_path.value() 388 << " to " << cached_crx_path.value(); 389 cached_crx_path = file_path; 390 was_error = true; 391 } else { 392 was_error = !base::GetFileInfo(cached_crx_path, &info); 393 VLOG(1) << "Cache entry installed for extension id " << id 394 << " version " << version; 395 } 396 } 397 398 content::BrowserThread::PostTask( 399 content::BrowserThread::UI, 400 FROM_HERE, 401 base::Bind(&LocalExtensionCache::OnCacheEntryInstalled, 402 local_cache, 403 id, 404 CacheItemInfo(version, info.last_modified, 405 info.size, cached_crx_path), 406 was_error, 407 callback)); 408 } 409 410 void LocalExtensionCache::OnCacheEntryInstalled( 411 const std::string& id, 412 const CacheItemInfo& info, 413 bool was_error, 414 const PutExtensionCallback& callback) { 415 if (state_ == kShutdown || was_error) { 416 callback.Run(info.file_path, true); 417 return; 418 } 419 420 CacheMap::iterator it = cached_extensions_.find(id); 421 if (it != cached_extensions_.end()) { 422 Version new_version(info.version); 423 Version prev_version(it->second.version); 424 if (new_version.CompareTo(prev_version) <= 0) { 425 DCHECK(0) << "Cache contains newer or the same version"; 426 callback.Run(info.file_path, true); 427 return; 428 } 429 it->second = info; 430 } else { 431 it = cached_extensions_.insert(std::make_pair(id, info)).first; 432 } 433 // Time from file system can have lower precision so use precise "now". 434 it->second.last_used = base::Time::Now(); 435 436 callback.Run(info.file_path, false); 437 } 438 439 // static 440 void LocalExtensionCache::BackendRemoveCacheEntry( 441 const base::FilePath& cache_dir, 442 const std::string& id) { 443 std::string file_pattern = id + "-*" + kCRXFileExtension; 444 base::FileEnumerator enumerator(cache_dir, 445 false /* not recursive */, 446 base::FileEnumerator::FILES, 447 file_pattern); 448 for (base::FilePath path = enumerator.Next(); !path.empty(); 449 path = enumerator.Next()) { 450 base::DeleteFile(path, false); 451 VLOG(1) << "Removed cached file " << path.value(); 452 } 453 } 454 455 // static 456 bool LocalExtensionCache::CompareCacheItemsAge(const CacheMap::iterator& lhs, 457 const CacheMap::iterator& rhs) { 458 return lhs->second.last_used < rhs->second.last_used; 459 } 460 461 void LocalExtensionCache::CleanUp() { 462 DCHECK_EQ(state_, kReady); 463 464 std::vector<CacheMap::iterator> items; 465 items.reserve(cached_extensions_.size()); 466 uint64_t total_size = 0; 467 for (CacheMap::iterator it = cached_extensions_.begin(); 468 it != cached_extensions_.end(); ++it) { 469 items.push_back(it); 470 total_size += it->second.size; 471 } 472 std::sort(items.begin(), items.end(), CompareCacheItemsAge); 473 474 for (std::vector<CacheMap::iterator>::iterator it = items.begin(); 475 it != items.end(); ++it) { 476 if ((*it)->second.last_used < min_cache_age_ || 477 (max_cache_size_ && total_size > max_cache_size_)) { 478 total_size -= (*it)->second.size; 479 VLOG(1) << "Clean up cached extension id " << (*it)->first; 480 RemoveExtension((*it)->first); 481 } 482 } 483 } 484 485 LocalExtensionCache::CacheItemInfo::CacheItemInfo( 486 const std::string& version, 487 const base::Time& last_used, 488 uint64 size, 489 const base::FilePath& file_path) 490 : version(version), last_used(last_used), size(size), file_path(file_path) { 491 } 492 493 } // namespace extensions 494