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
17namespace extensions {
18namespace {
19
20// File name extension for CRX files (not case sensitive).
21const char kCRXFileExtension[] = ".crx";
22
23// Delay between checks for flag file presence when waiting for the cache to
24// become ready.
25const int64_t kCacheStatusPollingDelayMs = 1000;
26
27}  // namespace
28
29const char LocalExtensionCache::kCacheReadyFlagFileName[] = ".initialized";
30
31LocalExtensionCache::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
46LocalExtensionCache::~LocalExtensionCache() {
47  if (state_ == kReady)
48    CleanUp();
49}
50
51void 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
62void 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
71bool 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
98void 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
138bool 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
155bool 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
170void LocalExtensionCache::SetCacheStatusPollingDelayForTests(
171    const base::TimeDelta& delay) {
172  cache_status_polling_delay_ = delay;
173}
174
175void 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
190void 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
214void 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
234void 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
245void 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
261void 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
352void 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
361void LocalExtensionCache::BackendMarkFileUsed(const base::FilePath& file_path,
362                                              const base::Time& time) {
363  base::TouchFile(file_path, time, time);
364}
365
366// static
367void 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
410void 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
440void 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
456bool LocalExtensionCache::CompareCacheItemsAge(const CacheMap::iterator& lhs,
457                                               const CacheMap::iterator& rhs) {
458  return lhs->second.last_used < rhs->second.last_used;
459}
460
461void 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
485LocalExtensionCache::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