Home | History | Annotate | Download | only in dom_storage
      1 // Copyright 2013 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 "content/browser/dom_storage/dom_storage_database.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/file_util.h"
      9 #include "base/logging.h"
     10 #include "sql/statement.h"
     11 #include "sql/transaction.h"
     12 #include "third_party/sqlite/sqlite3.h"
     13 
     14 namespace {
     15 
     16 const base::FilePath::CharType kJournal[] = FILE_PATH_LITERAL("-journal");
     17 
     18 }  // anon namespace
     19 
     20 namespace content {
     21 
     22 // static
     23 base::FilePath DOMStorageDatabase::GetJournalFilePath(
     24     const base::FilePath& database_path) {
     25   base::FilePath::StringType journal_file_name =
     26       database_path.BaseName().value() + kJournal;
     27   return database_path.DirName().Append(journal_file_name);
     28 }
     29 
     30 DOMStorageDatabase::DOMStorageDatabase(const base::FilePath& file_path)
     31     : file_path_(file_path) {
     32   // Note: in normal use we should never get an empty backing path here.
     33   // However, the unit test for this class can contruct an instance
     34   // with an empty path.
     35   Init();
     36 }
     37 
     38 DOMStorageDatabase::DOMStorageDatabase() {
     39   Init();
     40 }
     41 
     42 void DOMStorageDatabase::Init() {
     43   failed_to_open_ = false;
     44   tried_to_recreate_ = false;
     45   known_to_be_empty_ = false;
     46 }
     47 
     48 DOMStorageDatabase::~DOMStorageDatabase() {
     49   if (known_to_be_empty_ && !file_path_.empty()) {
     50     // Delete the db and any lingering journal file from disk.
     51     Close();
     52     sql::Connection::Delete(file_path_);
     53   }
     54 }
     55 
     56 void DOMStorageDatabase::ReadAllValues(DOMStorageValuesMap* result) {
     57   if (!LazyOpen(false))
     58     return;
     59 
     60   sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
     61                                                    "SELECT * from ItemTable"));
     62   DCHECK(statement.is_valid());
     63 
     64   while (statement.Step()) {
     65     base::string16 key = statement.ColumnString16(0);
     66     base::string16 value;
     67     statement.ColumnBlobAsString16(1, &value);
     68     (*result)[key] = base::NullableString16(value, false);
     69   }
     70   known_to_be_empty_ = result->empty();
     71 }
     72 
     73 bool DOMStorageDatabase::CommitChanges(bool clear_all_first,
     74                                        const DOMStorageValuesMap& changes) {
     75   if (!LazyOpen(!changes.empty())) {
     76     // If we're being asked to commit changes that will result in an
     77     // empty database, we return true if the database file doesn't exist.
     78     return clear_all_first && changes.empty() &&
     79            !base::PathExists(file_path_);
     80   }
     81 
     82   bool old_known_to_be_empty = known_to_be_empty_;
     83   sql::Transaction transaction(db_.get());
     84   if (!transaction.Begin())
     85     return false;
     86 
     87   if (clear_all_first) {
     88     if (!db_->Execute("DELETE FROM ItemTable"))
     89       return false;
     90     known_to_be_empty_ = true;
     91   }
     92 
     93   bool did_delete = false;
     94   bool did_insert = false;
     95   DOMStorageValuesMap::const_iterator it = changes.begin();
     96   for(; it != changes.end(); ++it) {
     97     sql::Statement statement;
     98     base::string16 key = it->first;
     99     base::NullableString16 value = it->second;
    100     if (value.is_null()) {
    101       statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE,
    102          "DELETE FROM ItemTable WHERE key=?"));
    103       statement.BindString16(0, key);
    104       did_delete = true;
    105     } else {
    106       statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE,
    107           "INSERT INTO ItemTable VALUES (?,?)"));
    108       statement.BindString16(0, key);
    109       statement.BindBlob(1, value.string().data(),
    110                          value.string().length() * sizeof(base::char16));
    111       known_to_be_empty_ = false;
    112       did_insert = true;
    113     }
    114     DCHECK(statement.is_valid());
    115     statement.Run();
    116   }
    117 
    118   if (!known_to_be_empty_ && did_delete && !did_insert) {
    119     sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
    120         "SELECT count(key) from ItemTable"));
    121     if (statement.Step())
    122       known_to_be_empty_ = statement.ColumnInt(0) == 0;
    123   }
    124 
    125   bool success = transaction.Commit();
    126   if (!success)
    127     known_to_be_empty_ = old_known_to_be_empty;
    128   return success;
    129 }
    130 
    131 bool DOMStorageDatabase::LazyOpen(bool create_if_needed) {
    132   if (failed_to_open_) {
    133     // Don't try to open a database that we know has failed
    134     // already.
    135     return false;
    136   }
    137 
    138   if (IsOpen())
    139     return true;
    140 
    141   bool database_exists = base::PathExists(file_path_);
    142 
    143   if (!database_exists && !create_if_needed) {
    144     // If the file doesn't exist already and we haven't been asked to create
    145     // a file on disk, then we don't bother opening the database. This means
    146     // we wait until we absolutely need to put something onto disk before we
    147     // do so.
    148     return false;
    149   }
    150 
    151   db_.reset(new sql::Connection());
    152   db_->set_histogram_tag("DOMStorageDatabase");
    153 
    154   if (file_path_.empty()) {
    155     // This code path should only be triggered by unit tests.
    156     if (!db_->OpenInMemory()) {
    157       NOTREACHED() << "Unable to open DOM storage database in memory.";
    158       failed_to_open_ = true;
    159       return false;
    160     }
    161   } else {
    162     if (!db_->Open(file_path_)) {
    163       LOG(ERROR) << "Unable to open DOM storage database at "
    164                  << file_path_.value()
    165                  << " error: " << db_->GetErrorMessage();
    166       if (database_exists && !tried_to_recreate_)
    167         return DeleteFileAndRecreate();
    168       failed_to_open_ = true;
    169       return false;
    170     }
    171   }
    172 
    173   // sql::Connection uses UTF-8 encoding, but WebCore style databases use
    174   // UTF-16, so ensure we match.
    175   ignore_result(db_->Execute("PRAGMA encoding=\"UTF-16\""));
    176 
    177   if (!database_exists) {
    178     // This is a new database, create the table and we're done!
    179     if (CreateTableV2())
    180       return true;
    181   } else {
    182     // The database exists already - check if we need to upgrade
    183     // and whether it's usable (i.e. not corrupted).
    184     SchemaVersion current_version = DetectSchemaVersion();
    185 
    186     if (current_version == V2) {
    187       return true;
    188     } else if (current_version == V1) {
    189       if (UpgradeVersion1To2())
    190         return true;
    191     }
    192   }
    193 
    194   // This is the exceptional case - to try and recover we'll attempt
    195   // to delete the file and start again.
    196   Close();
    197   return DeleteFileAndRecreate();
    198 }
    199 
    200 DOMStorageDatabase::SchemaVersion DOMStorageDatabase::DetectSchemaVersion() {
    201   DCHECK(IsOpen());
    202 
    203   // Connection::Open() may succeed even if the file we try and open is not a
    204   // database, however in the case that the database is corrupted to the point
    205   // that SQLite doesn't actually think it's a database,
    206   // sql::Connection::GetCachedStatement will DCHECK when we later try and
    207   // run statements. So we run a query here that will not DCHECK but fail
    208   // on an invalid database to verify that what we've opened is usable.
    209   if (db_->ExecuteAndReturnErrorCode("PRAGMA auto_vacuum") != SQLITE_OK)
    210     return INVALID;
    211 
    212   // Look at the current schema - if it doesn't look right, assume corrupt.
    213   if (!db_->DoesTableExist("ItemTable") ||
    214       !db_->DoesColumnExist("ItemTable", "key") ||
    215       !db_->DoesColumnExist("ItemTable", "value"))
    216     return INVALID;
    217 
    218   // We must use a unique statement here as we aren't going to step it.
    219   sql::Statement statement(
    220       db_->GetUniqueStatement("SELECT key,value from ItemTable LIMIT 1"));
    221   if (statement.DeclaredColumnType(0) != sql::COLUMN_TYPE_TEXT)
    222     return INVALID;
    223 
    224   switch (statement.DeclaredColumnType(1)) {
    225     case sql::COLUMN_TYPE_BLOB:
    226       return V2;
    227     case sql::COLUMN_TYPE_TEXT:
    228       return V1;
    229     default:
    230       return INVALID;
    231   }
    232 }
    233 
    234 bool DOMStorageDatabase::CreateTableV2() {
    235   DCHECK(IsOpen());
    236 
    237   return db_->Execute(
    238       "CREATE TABLE ItemTable ("
    239       "key TEXT UNIQUE ON CONFLICT REPLACE, "
    240       "value BLOB NOT NULL ON CONFLICT FAIL)");
    241 }
    242 
    243 bool DOMStorageDatabase::DeleteFileAndRecreate() {
    244   DCHECK(!IsOpen());
    245   DCHECK(base::PathExists(file_path_));
    246 
    247   // We should only try and do this once.
    248   if (tried_to_recreate_)
    249     return false;
    250 
    251   tried_to_recreate_ = true;
    252 
    253   // If it's not a directory and we can delete the file, try and open it again.
    254   if (!base::DirectoryExists(file_path_) &&
    255       sql::Connection::Delete(file_path_)) {
    256     return LazyOpen(true);
    257   }
    258 
    259   failed_to_open_ = true;
    260   return false;
    261 }
    262 
    263 bool DOMStorageDatabase::UpgradeVersion1To2() {
    264   DCHECK(IsOpen());
    265   DCHECK(DetectSchemaVersion() == V1);
    266 
    267   sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
    268       "SELECT * FROM ItemTable"));
    269   DCHECK(statement.is_valid());
    270 
    271   // Need to migrate from TEXT value column to BLOB.
    272   // Store the current database content so we can re-insert
    273   // the data into the new V2 table.
    274   DOMStorageValuesMap values;
    275   while (statement.Step()) {
    276     base::string16 key = statement.ColumnString16(0);
    277     base::NullableString16 value(statement.ColumnString16(1), false);
    278     values[key] = value;
    279   }
    280 
    281   sql::Transaction migration(db_.get());
    282   return migration.Begin() &&
    283       db_->Execute("DROP TABLE ItemTable") &&
    284       CreateTableV2() &&
    285       CommitChanges(false, values) &&
    286       migration.Commit();
    287 }
    288 
    289 void DOMStorageDatabase::Close() {
    290   db_.reset(NULL);
    291 }
    292 
    293 }  // namespace content
    294