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
LocalExtensionCache(const base::FilePath & cache_dir,uint64 max_cache_size,const base::TimeDelta & max_cache_age,const scoped_refptr<base::SequencedTaskRunner> & backend_task_runner)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
~LocalExtensionCache()46 LocalExtensionCache::~LocalExtensionCache() {
47 if (state_ == kReady)
48 CleanUp();
49 }
50
Init(bool wait_for_cache_initialization,const base::Closure & callback)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
Shutdown(const base::Closure & callback)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
GetExtension(const std::string & id,base::FilePath * file_path,std::string * version)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
PutExtension(const std::string & id,const base::FilePath & file_path,const std::string & version,const PutExtensionCallback & callback)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
RemoveExtension(const std::string & id)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
GetStatistics(uint64 * cache_size,size_t * extensions_count)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
SetCacheStatusPollingDelayForTests(const base::TimeDelta & delay)170 void LocalExtensionCache::SetCacheStatusPollingDelayForTests(
171 const base::TimeDelta& delay) {
172 cache_status_polling_delay_ = delay;
173 }
174
CheckCacheStatus(const base::Closure & callback)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
BackendCheckCacheStatus(base::WeakPtr<LocalExtensionCache> local_cache,const base::FilePath & cache_dir,const base::Closure & callback)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
OnCacheStatusChecked(bool ready,const base::Closure & callback)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
CheckCacheContents(const base::Closure & callback)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
BackendCheckCacheContents(base::WeakPtr<LocalExtensionCache> local_cache,const base::FilePath & cache_dir,const base::Closure & callback)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
BackendCheckCacheContentsInternal(const base::FilePath & cache_dir,CacheMap * cache_content)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
OnCacheContentsChecked(scoped_ptr<CacheMap> cache_content,const base::Closure & callback)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
BackendMarkFileUsed(const base::FilePath & file_path,const base::Time & time)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
BackendInstallCacheEntry(base::WeakPtr<LocalExtensionCache> local_cache,const base::FilePath & cache_dir,const std::string & id,const base::FilePath & file_path,const std::string & version,const PutExtensionCallback & callback)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
OnCacheEntryInstalled(const std::string & id,const CacheItemInfo & info,bool was_error,const PutExtensionCallback & callback)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
BackendRemoveCacheEntry(const base::FilePath & cache_dir,const std::string & id)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
CompareCacheItemsAge(const CacheMap::iterator & lhs,const CacheMap::iterator & rhs)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
CleanUp()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
CacheItemInfo(const std::string & version,const base::Time & last_used,uint64 size,const base::FilePath & file_path)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