Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / content / browser / dom_storage / dom_storage_database.cc
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