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
14namespace {
15
16const base::FilePath::CharType kJournal[] = FILE_PATH_LITERAL("-journal");
17
18}  // anon namespace
19
20namespace content {
21
22// static
23base::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
30DOMStorageDatabase::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
38DOMStorageDatabase::DOMStorageDatabase() {
39  Init();
40}
41
42void DOMStorageDatabase::Init() {
43  failed_to_open_ = false;
44  tried_to_recreate_ = false;
45  known_to_be_empty_ = false;
46}
47
48DOMStorageDatabase::~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
56void 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
73bool 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
131bool 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
200DOMStorageDatabase::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
234bool 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
243bool 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
263bool 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
289void DOMStorageDatabase::Close() {
290  db_.reset(NULL);
291}
292
293}  // namespace content
294