1 // Copyright (c) 2011 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/net/sqlite_persistent_cookie_store.h"
6
7 #include <list>
8
9 #include "app/sql/meta_table.h"
10 #include "app/sql/statement.h"
11 #include "app/sql/transaction.h"
12 #include "base/basictypes.h"
13 #include "base/file_path.h"
14 #include "base/file_util.h"
15 #ifdef ANDROID
16 #include "base/lazy_instance.h"
17 #endif
18 #include "base/logging.h"
19 #include "base/memory/ref_counted.h"
20 #include "base/memory/scoped_ptr.h"
21 #include "base/metrics/histogram.h"
22 #include "base/string_util.h"
23 #include "base/threading/thread.h"
24 #include "base/threading/thread_restrictions.h"
25 #include "chrome/browser/diagnostics/sqlite_diagnostics.h"
26 #ifndef ANDROID
27 #include "content/browser/browser_thread.h"
28 #endif
29 #include "googleurl/src/gurl.h"
30
31 #ifdef ANDROID
32 namespace {
33
34 class DbThread : public base::Thread {
35 public:
DbThread()36 DbThread() : base::Thread("android-db") {
37 bool started = Start();
38 CHECK(started);
39 }
40 };
41
42 // This class is used by CookieMonster, which is threadsafe, so this class must
43 // be threadsafe too.
44 base::LazyInstance<DbThread> g_db_thread(base::LINKER_INITIALIZED);
45
46 } // namespace
47 #endif
48
49 using base::Time;
50
51 // This class is designed to be shared between any calling threads and the
52 // database thread. It batches operations and commits them on a timer.
53 class SQLitePersistentCookieStore::Backend
54 : public base::RefCountedThreadSafe<SQLitePersistentCookieStore::Backend> {
55 public:
Backend(const FilePath & path)56 explicit Backend(const FilePath& path)
57 : path_(path),
58 db_(NULL),
59 num_pending_(0),
60 clear_local_state_on_exit_(false)
61 #if defined(ANDROID)
62 , cookie_count_(0)
63 #endif
64 {
65 }
66
67 // Creates or load the SQLite database.
68 bool Load(std::vector<net::CookieMonster::CanonicalCookie*>* cookies);
69
70 // Batch a cookie addition.
71 void AddCookie(const net::CookieMonster::CanonicalCookie& cc);
72
73 // Batch a cookie access time update.
74 void UpdateCookieAccessTime(const net::CookieMonster::CanonicalCookie& cc);
75
76 // Batch a cookie deletion.
77 void DeleteCookie(const net::CookieMonster::CanonicalCookie& cc);
78
79 // Commit pending operations as soon as possible.
80 void Flush(Task* completion_task);
81
82 // Commit any pending operations and close the database. This must be called
83 // before the object is destructed.
84 void Close();
85
86 void SetClearLocalStateOnExit(bool clear_local_state);
87
88 #if defined(ANDROID)
get_cookie_count() const89 int get_cookie_count() const { return cookie_count_; }
set_cookie_count(int count)90 void set_cookie_count(int count) { cookie_count_ = count; }
91 #endif
92
93 private:
94 friend class base::RefCountedThreadSafe<SQLitePersistentCookieStore::Backend>;
95
96 // You should call Close() before destructing this object.
~Backend()97 ~Backend() {
98 DCHECK(!db_.get()) << "Close should have already been called.";
99 DCHECK(num_pending_ == 0 && pending_.empty());
100 }
101
102 // Database upgrade statements.
103 bool EnsureDatabaseVersion();
104
105 class PendingOperation {
106 public:
107 typedef enum {
108 COOKIE_ADD,
109 COOKIE_UPDATEACCESS,
110 COOKIE_DELETE,
111 } OperationType;
112
PendingOperation(OperationType op,const net::CookieMonster::CanonicalCookie & cc)113 PendingOperation(OperationType op,
114 const net::CookieMonster::CanonicalCookie& cc)
115 : op_(op), cc_(cc) { }
116
op() const117 OperationType op() const { return op_; }
cc() const118 const net::CookieMonster::CanonicalCookie& cc() const { return cc_; }
119
120 private:
121 OperationType op_;
122 net::CookieMonster::CanonicalCookie cc_;
123 };
124
125 private:
126 // Batch a cookie operation (add or delete)
127 void BatchOperation(PendingOperation::OperationType op,
128 const net::CookieMonster::CanonicalCookie& cc);
129 // Commit our pending operations to the database.
130 #if defined(ANDROID)
131 void Commit(Task* completion_task);
132 #else
133 void Commit();
134 #endif
135 // Close() executed on the background thread.
136 void InternalBackgroundClose();
137
138 FilePath path_;
139 scoped_ptr<sql::Connection> db_;
140 sql::MetaTable meta_table_;
141
142 typedef std::list<PendingOperation*> PendingOperationsList;
143 PendingOperationsList pending_;
144 PendingOperationsList::size_type num_pending_;
145 // True if the persistent store should be deleted upon destruction.
146 bool clear_local_state_on_exit_;
147 // Guard |pending_|, |num_pending_| and |clear_local_state_on_exit_|.
148 base::Lock lock_;
149
150 #if defined(ANDROID)
151 // Number of cookies that have actually been saved. Updated during Commit().
152 volatile int cookie_count_;
153 #endif
154
155 DISALLOW_COPY_AND_ASSIGN(Backend);
156 };
157
158 // Version number of the database. In version 4, we migrated the time epoch.
159 // If you open the DB with an older version on Mac or Linux, the times will
160 // look wonky, but the file will likely be usable. On Windows version 3 and 4
161 // are the same.
162 //
163 // Version 3 updated the database to include the last access time, so we can
164 // expire them in decreasing order of use when we've reached the maximum
165 // number of cookies.
166 static const int kCurrentVersionNumber = 4;
167 static const int kCompatibleVersionNumber = 3;
168
169 namespace {
170
171 // Initializes the cookies table, returning true on success.
InitTable(sql::Connection * db)172 bool InitTable(sql::Connection* db) {
173 if (!db->DoesTableExist("cookies")) {
174 if (!db->Execute("CREATE TABLE cookies ("
175 "creation_utc INTEGER NOT NULL UNIQUE PRIMARY KEY,"
176 "host_key TEXT NOT NULL,"
177 "name TEXT NOT NULL,"
178 "value TEXT NOT NULL,"
179 "path TEXT NOT NULL,"
180 #if defined(ANDROID)
181 // On some mobile platforms, we persist session cookies
182 // because the OS can kill the browser during a session.
183 // If so, expires_utc is set to 0. When the field is read
184 // into a Time object, Time::is_null() will return true.
185 #else
186 // We only store persistent, so we know it expires
187 #endif
188 "expires_utc INTEGER NOT NULL,"
189 "secure INTEGER NOT NULL,"
190 "httponly INTEGER NOT NULL,"
191 "last_access_utc INTEGER NOT NULL)"))
192 return false;
193 }
194
195 // Try to create the index every time. Older versions did not have this index,
196 // so we want those people to get it. Ignore errors, since it may exist.
197 db->Execute(
198 "CREATE INDEX IF NOT EXISTS cookie_times ON cookies (creation_utc)");
199 return true;
200 }
201
202 } // namespace
203
Load(std::vector<net::CookieMonster::CanonicalCookie * > * cookies)204 bool SQLitePersistentCookieStore::Backend::Load(
205 std::vector<net::CookieMonster::CanonicalCookie*>* cookies) {
206 // This function should be called only once per instance.
207 DCHECK(!db_.get());
208
209 // Ensure the parent directory for storing cookies is created before reading
210 // from it. We make an exception to allow IO on the UI thread here because
211 // we are going to disk anyway in db_->Open. (This code will be moved to the
212 // DB thread as part of http://crbug.com/52909.)
213 {
214 base::ThreadRestrictions::ScopedAllowIO allow_io;
215 const FilePath dir = path_.DirName();
216 if (!file_util::PathExists(dir) && !file_util::CreateDirectory(dir))
217 return false;
218 }
219
220 db_.reset(new sql::Connection);
221 if (!db_->Open(path_)) {
222 NOTREACHED() << "Unable to open cookie DB.";
223 db_.reset();
224 return false;
225 }
226
227 #ifndef ANDROID
228 // GetErrorHandlerForCookieDb is defined in sqlite_diagnostics.h
229 // which we do not currently include on Android
230 db_->set_error_delegate(GetErrorHandlerForCookieDb());
231 #endif
232
233 if (!EnsureDatabaseVersion() || !InitTable(db_.get())) {
234 NOTREACHED() << "Unable to open cookie DB.";
235 db_.reset();
236 return false;
237 }
238
239 db_->Preload();
240
241 // Slurp all the cookies into the out-vector.
242 sql::Statement smt(db_->GetUniqueStatement(
243 "SELECT creation_utc, host_key, name, value, path, expires_utc, secure, "
244 "httponly, last_access_utc FROM cookies"));
245 if (!smt) {
246 NOTREACHED() << "select statement prep failed";
247 db_.reset();
248 return false;
249 }
250
251 while (smt.Step()) {
252 #if defined(ANDROID)
253 base::Time expires = Time::FromInternalValue(smt.ColumnInt64(5));
254 #endif
255 scoped_ptr<net::CookieMonster::CanonicalCookie> cc(
256 new net::CookieMonster::CanonicalCookie(
257 // The "source" URL is not used with persisted cookies.
258 GURL(), // Source
259 smt.ColumnString(2), // name
260 smt.ColumnString(3), // value
261 smt.ColumnString(1), // domain
262 smt.ColumnString(4), // path
263 Time::FromInternalValue(smt.ColumnInt64(0)), // creation_utc
264 Time::FromInternalValue(smt.ColumnInt64(5)), // expires_utc
265 Time::FromInternalValue(smt.ColumnInt64(8)), // last_access_utc
266 smt.ColumnInt(6) != 0, // secure
267 smt.ColumnInt(7) != 0, // httponly
268 #if defined(ANDROID)
269 !expires.is_null())); // has_expires
270 #else
271 true)); // has_expires
272 #endif
273 DLOG_IF(WARNING,
274 cc->CreationDate() > Time::Now()) << L"CreationDate too recent";
275 cookies->push_back(cc.release());
276 }
277
278 #ifdef ANDROID
279 set_cookie_count(cookies->size());
280 #endif
281
282 return true;
283 }
284
EnsureDatabaseVersion()285 bool SQLitePersistentCookieStore::Backend::EnsureDatabaseVersion() {
286 // Version check.
287 if (!meta_table_.Init(
288 db_.get(), kCurrentVersionNumber, kCompatibleVersionNumber)) {
289 return false;
290 }
291
292 if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) {
293 LOG(WARNING) << "Cookie database is too new.";
294 return false;
295 }
296
297 int cur_version = meta_table_.GetVersionNumber();
298 if (cur_version == 2) {
299 sql::Transaction transaction(db_.get());
300 if (!transaction.Begin())
301 return false;
302 if (!db_->Execute("ALTER TABLE cookies ADD COLUMN last_access_utc "
303 "INTEGER DEFAULT 0") ||
304 !db_->Execute("UPDATE cookies SET last_access_utc = creation_utc")) {
305 LOG(WARNING) << "Unable to update cookie database to version 3.";
306 return false;
307 }
308 ++cur_version;
309 meta_table_.SetVersionNumber(cur_version);
310 meta_table_.SetCompatibleVersionNumber(
311 std::min(cur_version, kCompatibleVersionNumber));
312 transaction.Commit();
313 }
314
315 if (cur_version == 3) {
316 // The time epoch changed for Mac & Linux in this version to match Windows.
317 // This patch came after the main epoch change happened, so some
318 // developers have "good" times for cookies added by the more recent
319 // versions. So we have to be careful to only update times that are under
320 // the old system (which will appear to be from before 1970 in the new
321 // system). The magic number used below is 1970 in our time units.
322 sql::Transaction transaction(db_.get());
323 transaction.Begin();
324 #if !defined(OS_WIN)
325 db_->Execute(
326 "UPDATE cookies "
327 "SET creation_utc = creation_utc + 11644473600000000 "
328 "WHERE rowid IN "
329 "(SELECT rowid FROM cookies WHERE "
330 "creation_utc > 0 AND creation_utc < 11644473600000000)");
331 db_->Execute(
332 "UPDATE cookies "
333 "SET expires_utc = expires_utc + 11644473600000000 "
334 "WHERE rowid IN "
335 "(SELECT rowid FROM cookies WHERE "
336 "expires_utc > 0 AND expires_utc < 11644473600000000)");
337 db_->Execute(
338 "UPDATE cookies "
339 "SET last_access_utc = last_access_utc + 11644473600000000 "
340 "WHERE rowid IN "
341 "(SELECT rowid FROM cookies WHERE "
342 "last_access_utc > 0 AND last_access_utc < 11644473600000000)");
343 #endif
344 ++cur_version;
345 meta_table_.SetVersionNumber(cur_version);
346 transaction.Commit();
347 }
348
349 // Put future migration cases here.
350
351 // When the version is too old, we just try to continue anyway, there should
352 // not be a released product that makes a database too old for us to handle.
353 LOG_IF(WARNING, cur_version < kCurrentVersionNumber) <<
354 "Cookie database version " << cur_version << " is too old to handle.";
355
356 return true;
357 }
358
AddCookie(const net::CookieMonster::CanonicalCookie & cc)359 void SQLitePersistentCookieStore::Backend::AddCookie(
360 const net::CookieMonster::CanonicalCookie& cc) {
361 BatchOperation(PendingOperation::COOKIE_ADD, cc);
362 }
363
UpdateCookieAccessTime(const net::CookieMonster::CanonicalCookie & cc)364 void SQLitePersistentCookieStore::Backend::UpdateCookieAccessTime(
365 const net::CookieMonster::CanonicalCookie& cc) {
366 BatchOperation(PendingOperation::COOKIE_UPDATEACCESS, cc);
367 }
368
DeleteCookie(const net::CookieMonster::CanonicalCookie & cc)369 void SQLitePersistentCookieStore::Backend::DeleteCookie(
370 const net::CookieMonster::CanonicalCookie& cc) {
371 BatchOperation(PendingOperation::COOKIE_DELETE, cc);
372 }
373
BatchOperation(PendingOperation::OperationType op,const net::CookieMonster::CanonicalCookie & cc)374 void SQLitePersistentCookieStore::Backend::BatchOperation(
375 PendingOperation::OperationType op,
376 const net::CookieMonster::CanonicalCookie& cc) {
377 // Commit every 30 seconds.
378 static const int kCommitIntervalMs = 30 * 1000;
379 // Commit right away if we have more than 512 outstanding operations.
380 static const size_t kCommitAfterBatchSize = 512;
381 #ifndef ANDROID
382 DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::DB));
383 #endif
384
385 // We do a full copy of the cookie here, and hopefully just here.
386 scoped_ptr<PendingOperation> po(new PendingOperation(op, cc));
387
388 PendingOperationsList::size_type num_pending;
389 {
390 base::AutoLock locked(lock_);
391 pending_.push_back(po.release());
392 num_pending = ++num_pending_;
393 }
394
395 #ifdef ANDROID
396 MessageLoop* loop = g_db_thread.Get().message_loop();
397 #endif
398
399 if (num_pending == 1) {
400 // We've gotten our first entry for this batch, fire off the timer.
401 #ifdef ANDROID
402 loop->PostDelayedTask(FROM_HERE, NewRunnableMethod(
403 this, &Backend::Commit, static_cast<Task*>(NULL)), kCommitIntervalMs);
404 #else
405 BrowserThread::PostDelayedTask(
406 BrowserThread::DB, FROM_HERE,
407 NewRunnableMethod(this, &Backend::Commit), kCommitIntervalMs);
408 #endif
409 } else if (num_pending == kCommitAfterBatchSize) {
410 // We've reached a big enough batch, fire off a commit now.
411 #ifdef ANDROID
412 loop->PostTask(FROM_HERE, NewRunnableMethod(
413 this, &Backend::Commit, static_cast<Task*>(NULL)));
414 #else
415 BrowserThread::PostTask(
416 BrowserThread::DB, FROM_HERE,
417 NewRunnableMethod(this, &Backend::Commit));
418 #endif
419 }
420 }
421
422 #if defined(ANDROID)
Commit(Task * completion_task)423 void SQLitePersistentCookieStore::Backend::Commit(Task* completion_task) {
424 #else
425 void SQLitePersistentCookieStore::Backend::Commit() {
426 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
427 #endif
428
429 #if defined(ANDROID)
430 if (completion_task) {
431 // We post this task to the current thread, so it won't run until we exit.
432 MessageLoop::current()->PostTask(FROM_HERE, completion_task);
433 }
434 #endif
435
436 PendingOperationsList ops;
437 {
438 base::AutoLock locked(lock_);
439 pending_.swap(ops);
440 num_pending_ = 0;
441 }
442
443 // Maybe an old timer fired or we are already Close()'ed.
444 if (!db_.get() || ops.empty())
445 return;
446
447 sql::Statement add_smt(db_->GetCachedStatement(SQL_FROM_HERE,
448 "INSERT INTO cookies (creation_utc, host_key, name, value, path, "
449 "expires_utc, secure, httponly, last_access_utc) "
450 "VALUES (?,?,?,?,?,?,?,?,?)"));
451 if (!add_smt) {
452 NOTREACHED();
453 return;
454 }
455
456 sql::Statement update_access_smt(db_->GetCachedStatement(SQL_FROM_HERE,
457 "UPDATE cookies SET last_access_utc=? WHERE creation_utc=?"));
458 if (!update_access_smt) {
459 NOTREACHED();
460 return;
461 }
462
463 sql::Statement del_smt(db_->GetCachedStatement(SQL_FROM_HERE,
464 "DELETE FROM cookies WHERE creation_utc=?"));
465 if (!del_smt) {
466 NOTREACHED();
467 return;
468 }
469
470 sql::Transaction transaction(db_.get());
471 if (!transaction.Begin()) {
472 NOTREACHED();
473 return;
474 }
475 #if defined(ANDROID)
476 int cookie_delta = 0;
477 #endif
478 for (PendingOperationsList::iterator it = ops.begin();
479 it != ops.end(); ++it) {
480 // Free the cookies as we commit them to the database.
481 scoped_ptr<PendingOperation> po(*it);
482 switch (po->op()) {
483 case PendingOperation::COOKIE_ADD:
484 #if defined(ANDROID)
485 ++cookie_delta;
486 #endif
487 add_smt.Reset();
488 add_smt.BindInt64(0, po->cc().CreationDate().ToInternalValue());
489 add_smt.BindString(1, po->cc().Domain());
490 add_smt.BindString(2, po->cc().Name());
491 add_smt.BindString(3, po->cc().Value());
492 add_smt.BindString(4, po->cc().Path());
493 add_smt.BindInt64(5, po->cc().ExpiryDate().ToInternalValue());
494 add_smt.BindInt(6, po->cc().IsSecure());
495 add_smt.BindInt(7, po->cc().IsHttpOnly());
496 add_smt.BindInt64(8, po->cc().LastAccessDate().ToInternalValue());
497 if (!add_smt.Run())
498 NOTREACHED() << "Could not add a cookie to the DB.";
499 break;
500
501 case PendingOperation::COOKIE_UPDATEACCESS:
502 update_access_smt.Reset();
503 update_access_smt.BindInt64(0,
504 po->cc().LastAccessDate().ToInternalValue());
505 update_access_smt.BindInt64(1,
506 po->cc().CreationDate().ToInternalValue());
507 if (!update_access_smt.Run())
508 NOTREACHED() << "Could not update cookie last access time in the DB.";
509 break;
510
511 case PendingOperation::COOKIE_DELETE:
512 #if defined(ANDROID)
513 --cookie_delta;
514 #endif
515 del_smt.Reset();
516 del_smt.BindInt64(0, po->cc().CreationDate().ToInternalValue());
517 if (!del_smt.Run())
518 NOTREACHED() << "Could not delete a cookie from the DB.";
519 break;
520
521 default:
522 NOTREACHED();
523 break;
524 }
525 }
526 bool succeeded = transaction.Commit();
527 #if defined(ANDROID)
528 if (succeeded)
529 cookie_count_ += cookie_delta;
530 #endif
531 UMA_HISTOGRAM_ENUMERATION("Cookie.BackingStoreUpdateResults",
532 succeeded ? 0 : 1, 2);
533 }
534
535 void SQLitePersistentCookieStore::Backend::Flush(Task* completion_task) {
536 #if defined(ANDROID)
537 MessageLoop* loop = g_db_thread.Get().message_loop();
538 loop->PostTask(FROM_HERE, NewRunnableMethod(
539 this, &Backend::Commit, completion_task));
540 #else
541 DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::DB));
542 BrowserThread::PostTask(
543 BrowserThread::DB, FROM_HERE, NewRunnableMethod(this, &Backend::Commit));
544 if (completion_task) {
545 // We want the completion task to run immediately after Commit() returns.
546 // Posting it from here means there is less chance of another task getting
547 // onto the message queue first, than if we posted it from Commit() itself.
548 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, completion_task);
549 }
550 #endif
551 }
552
553 // Fire off a close message to the background thread. We could still have a
554 // pending commit timer that will be holding a reference on us, but if/when
555 // this fires we will already have been cleaned up and it will be ignored.
556 void SQLitePersistentCookieStore::Backend::Close() {
557 #ifndef ANDROID
558 DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::DB));
559 #endif
560
561 #ifdef ANDROID
562 MessageLoop* loop = g_db_thread.Get().message_loop();
563 loop->PostTask(FROM_HERE,
564 NewRunnableMethod(this, &Backend::InternalBackgroundClose));
565 #else
566 // Must close the backend on the background thread.
567 BrowserThread::PostTask(
568 BrowserThread::DB, FROM_HERE,
569 NewRunnableMethod(this, &Backend::InternalBackgroundClose));
570 #endif
571 }
572
573 void SQLitePersistentCookieStore::Backend::InternalBackgroundClose() {
574 #ifndef ANDROID
575 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
576 #endif
577 // Commit any pending operations
578 #if defined(ANDROID)
579 Commit(NULL);
580 #else
581 Commit();
582 #endif
583
584 db_.reset();
585
586 if (clear_local_state_on_exit_)
587 file_util::Delete(path_, false);
588 }
589
590 void SQLitePersistentCookieStore::Backend::SetClearLocalStateOnExit(
591 bool clear_local_state) {
592 base::AutoLock locked(lock_);
593 clear_local_state_on_exit_ = clear_local_state;
594 }
595 SQLitePersistentCookieStore::SQLitePersistentCookieStore(const FilePath& path)
596 : backend_(new Backend(path)) {
597 }
598
599 SQLitePersistentCookieStore::~SQLitePersistentCookieStore() {
600 if (backend_.get()) {
601 backend_->Close();
602 // Release our reference, it will probably still have a reference if the
603 // background thread has not run Close() yet.
604 backend_ = NULL;
605 }
606 }
607
608 bool SQLitePersistentCookieStore::Load(
609 std::vector<net::CookieMonster::CanonicalCookie*>* cookies) {
610 return backend_->Load(cookies);
611 }
612
613 void SQLitePersistentCookieStore::AddCookie(
614 const net::CookieMonster::CanonicalCookie& cc) {
615 if (backend_.get())
616 backend_->AddCookie(cc);
617 }
618
619 void SQLitePersistentCookieStore::UpdateCookieAccessTime(
620 const net::CookieMonster::CanonicalCookie& cc) {
621 if (backend_.get())
622 backend_->UpdateCookieAccessTime(cc);
623 }
624
625 void SQLitePersistentCookieStore::DeleteCookie(
626 const net::CookieMonster::CanonicalCookie& cc) {
627 if (backend_.get())
628 backend_->DeleteCookie(cc);
629 }
630
631 void SQLitePersistentCookieStore::SetClearLocalStateOnExit(
632 bool clear_local_state) {
633 if (backend_.get())
634 backend_->SetClearLocalStateOnExit(clear_local_state);
635 }
636
637 void SQLitePersistentCookieStore::Flush(Task* completion_task) {
638 if (backend_.get())
639 backend_->Flush(completion_task);
640 else if (completion_task)
641 MessageLoop::current()->PostTask(FROM_HERE, completion_task);
642 }
643
644 #if defined(ANDROID)
645 int SQLitePersistentCookieStore::GetCookieCount() {
646 int result = backend_ ? backend_->get_cookie_count() : 0;
647 return result;
648 }
649 #endif
650