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/files/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