|
1 /* |
|
2 * Copyright (C) 2010 Google Inc. All rights reserved. |
|
3 * |
|
4 * Redistribution and use in source and binary forms, with or without |
|
5 * modification, are permitted provided that the following conditions |
|
6 * are met: |
|
7 * |
|
8 * 1. Redistributions of source code must retain the above copyright |
|
9 * notice, this list of conditions and the following disclaimer. |
|
10 * 2. Redistributions in binary form must reproduce the above copyright |
|
11 * notice, this list of conditions and the following disclaimer in the |
|
12 * documentation and/or other materials provided with the distribution. |
|
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
|
14 * its contributors may be used to endorse or promote products derived |
|
15 * from this software without specific prior written permission. |
|
16 * |
|
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
|
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
|
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
|
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
27 */ |
|
28 |
|
29 #include "config.h" |
|
30 #include "AbstractDatabase.h" |
|
31 |
|
32 #if ENABLE(DATABASE) |
|
33 #include "DatabaseAuthorizer.h" |
|
34 #include "DatabaseTracker.h" |
|
35 #include "Logging.h" |
|
36 #include "SQLiteStatement.h" |
|
37 #include "ScriptExecutionContext.h" |
|
38 #include "SecurityOrigin.h" |
|
39 #include "StringHash.h" |
|
40 #include <wtf/HashMap.h> |
|
41 #include <wtf/HashSet.h> |
|
42 #include <wtf/PassRefPtr.h> |
|
43 #include <wtf/RefPtr.h> |
|
44 #include <wtf/StdLibExtras.h> |
|
45 |
|
46 namespace WebCore { |
|
47 |
|
48 static bool retrieveTextResultFromDatabase(SQLiteDatabase& db, const String& query, String& resultString) |
|
49 { |
|
50 SQLiteStatement statement(db, query); |
|
51 int result = statement.prepare(); |
|
52 |
|
53 if (result != SQLResultOk) { |
|
54 LOG_ERROR("Error (%i) preparing statement to read text result from database (%s)", result, query.ascii().data()); |
|
55 return false; |
|
56 } |
|
57 |
|
58 result = statement.step(); |
|
59 if (result == SQLResultRow) { |
|
60 resultString = statement.getColumnText(0); |
|
61 return true; |
|
62 } |
|
63 if (result == SQLResultDone) { |
|
64 resultString = String(); |
|
65 return true; |
|
66 } |
|
67 |
|
68 LOG_ERROR("Error (%i) reading text result from database (%s)", result, query.ascii().data()); |
|
69 return false; |
|
70 } |
|
71 |
|
72 static bool setTextValueInDatabase(SQLiteDatabase& db, const String& query, const String& value) |
|
73 { |
|
74 SQLiteStatement statement(db, query); |
|
75 int result = statement.prepare(); |
|
76 |
|
77 if (result != SQLResultOk) { |
|
78 LOG_ERROR("Failed to prepare statement to set value in database (%s)", query.ascii().data()); |
|
79 return false; |
|
80 } |
|
81 |
|
82 statement.bindText(1, value); |
|
83 |
|
84 result = statement.step(); |
|
85 if (result != SQLResultDone) { |
|
86 LOG_ERROR("Failed to step statement to set value in database (%s)", query.ascii().data()); |
|
87 return false; |
|
88 } |
|
89 |
|
90 return true; |
|
91 } |
|
92 |
|
93 // FIXME: move all guid-related functions to a DatabaseVersionTracker class. |
|
94 static Mutex& guidMutex() |
|
95 { |
|
96 // Note: We don't have to use AtomicallyInitializedStatic here because |
|
97 // this function is called once in the constructor on the main thread |
|
98 // before any other threads that call this function are used. |
|
99 DEFINE_STATIC_LOCAL(Mutex, mutex, ()); |
|
100 return mutex; |
|
101 } |
|
102 |
|
103 typedef HashMap<int, String> GuidVersionMap; |
|
104 static GuidVersionMap& guidToVersionMap() |
|
105 { |
|
106 DEFINE_STATIC_LOCAL(GuidVersionMap, map, ()); |
|
107 return map; |
|
108 } |
|
109 |
|
110 // NOTE: Caller must lock guidMutex(). |
|
111 static inline void updateGuidVersionMap(int guid, String newVersion) |
|
112 { |
|
113 // Ensure the the mutex is locked. |
|
114 ASSERT(!guidMutex().tryLock()); |
|
115 |
|
116 // Note: It is not safe to put an empty string into the guidToVersionMap() map. |
|
117 // That's because the map is cross-thread, but empty strings are per-thread. |
|
118 // The copy() function makes a version of the string you can use on the current |
|
119 // thread, but we need a string we can keep in a cross-thread data structure. |
|
120 // FIXME: This is a quite-awkward restriction to have to program with. |
|
121 |
|
122 // Map null string to empty string (see comment above). |
|
123 guidToVersionMap().set(guid, newVersion.isEmpty() ? String() : newVersion.threadsafeCopy()); |
|
124 } |
|
125 |
|
126 typedef HashMap<int, HashSet<AbstractDatabase*>*> GuidDatabaseMap; |
|
127 static GuidDatabaseMap& guidToDatabaseMap() |
|
128 { |
|
129 DEFINE_STATIC_LOCAL(GuidDatabaseMap, map, ()); |
|
130 return map; |
|
131 } |
|
132 |
|
133 static int guidForOriginAndName(const String& origin, const String& name) |
|
134 { |
|
135 String stringID = origin + "/" + name; |
|
136 |
|
137 // Note: We don't have to use AtomicallyInitializedStatic here because |
|
138 // this function is called once in the constructor on the main thread |
|
139 // before any other threads that call this function are used. |
|
140 DEFINE_STATIC_LOCAL(Mutex, stringIdentifierMutex, ()); |
|
141 MutexLocker locker(stringIdentifierMutex); |
|
142 typedef HashMap<String, int> IDGuidMap; |
|
143 DEFINE_STATIC_LOCAL(IDGuidMap, stringIdentifierToGUIDMap, ()); |
|
144 int guid = stringIdentifierToGUIDMap.get(stringID); |
|
145 if (!guid) { |
|
146 static int currentNewGUID = 1; |
|
147 guid = currentNewGUID++; |
|
148 stringIdentifierToGUIDMap.set(stringID, guid); |
|
149 } |
|
150 |
|
151 return guid; |
|
152 } |
|
153 |
|
154 static bool isDatabaseAvailable = true; |
|
155 |
|
156 bool AbstractDatabase::isAvailable() |
|
157 { |
|
158 return isDatabaseAvailable; |
|
159 } |
|
160 |
|
161 void AbstractDatabase::setIsAvailable(bool available) |
|
162 { |
|
163 isDatabaseAvailable = available; |
|
164 } |
|
165 |
|
166 // static |
|
167 const String& AbstractDatabase::databaseInfoTableName() |
|
168 { |
|
169 DEFINE_STATIC_LOCAL(String, name, ("__WebKitDatabaseInfoTable__")); |
|
170 return name; |
|
171 } |
|
172 |
|
173 AbstractDatabase::AbstractDatabase(ScriptExecutionContext* context, const String& name, const String& expectedVersion, |
|
174 const String& displayName, unsigned long estimatedSize) |
|
175 : m_scriptExecutionContext(context) |
|
176 , m_name(name.crossThreadString()) |
|
177 , m_expectedVersion(expectedVersion.crossThreadString()) |
|
178 , m_displayName(displayName.crossThreadString()) |
|
179 , m_estimatedSize(estimatedSize) |
|
180 , m_guid(0) |
|
181 , m_opened(false) |
|
182 , m_new(false) |
|
183 { |
|
184 ASSERT(context->isContextThread()); |
|
185 m_contextThreadSecurityOrigin = m_scriptExecutionContext->securityOrigin(); |
|
186 |
|
187 m_databaseAuthorizer = DatabaseAuthorizer::create(databaseInfoTableName()); |
|
188 |
|
189 if (m_name.isNull()) |
|
190 m_name = ""; |
|
191 |
|
192 m_guid = guidForOriginAndName(securityOrigin()->toString(), name); |
|
193 { |
|
194 MutexLocker locker(guidMutex()); |
|
195 |
|
196 HashSet<AbstractDatabase*>* hashSet = guidToDatabaseMap().get(m_guid); |
|
197 if (!hashSet) { |
|
198 hashSet = new HashSet<AbstractDatabase*>; |
|
199 guidToDatabaseMap().set(m_guid, hashSet); |
|
200 } |
|
201 |
|
202 hashSet->add(this); |
|
203 } |
|
204 |
|
205 m_filename = DatabaseTracker::tracker().fullPathForDatabase(securityOrigin(), m_name); |
|
206 DatabaseTracker::tracker().addOpenDatabase(this); |
|
207 } |
|
208 |
|
209 AbstractDatabase::~AbstractDatabase() |
|
210 { |
|
211 } |
|
212 |
|
213 void AbstractDatabase::closeDatabase() |
|
214 { |
|
215 if (!m_opened) |
|
216 return; |
|
217 |
|
218 m_sqliteDatabase.close(); |
|
219 m_opened = false; |
|
220 { |
|
221 MutexLocker locker(guidMutex()); |
|
222 |
|
223 HashSet<AbstractDatabase*>* hashSet = guidToDatabaseMap().get(m_guid); |
|
224 ASSERT(hashSet); |
|
225 ASSERT(hashSet->contains(this)); |
|
226 hashSet->remove(this); |
|
227 if (hashSet->isEmpty()) { |
|
228 guidToDatabaseMap().remove(m_guid); |
|
229 delete hashSet; |
|
230 guidToVersionMap().remove(m_guid); |
|
231 } |
|
232 } |
|
233 } |
|
234 |
|
235 String AbstractDatabase::version() const |
|
236 { |
|
237 MutexLocker locker(guidMutex()); |
|
238 return guidToVersionMap().get(m_guid).threadsafeCopy(); |
|
239 } |
|
240 |
|
241 static const int maxSqliteBusyWaitTime = 30000; |
|
242 bool AbstractDatabase::performOpenAndVerify(bool shouldSetVersionInNewDatabase, ExceptionCode& ec) |
|
243 { |
|
244 if (!m_sqliteDatabase.open(m_filename, true)) { |
|
245 LOG_ERROR("Unable to open database at path %s", m_filename.ascii().data()); |
|
246 ec = INVALID_STATE_ERR; |
|
247 return false; |
|
248 } |
|
249 if (!m_sqliteDatabase.turnOnIncrementalAutoVacuum()) |
|
250 LOG_ERROR("Unable to turn on incremental auto-vacuum for database %s", m_filename.ascii().data()); |
|
251 |
|
252 ASSERT(m_databaseAuthorizer); |
|
253 m_sqliteDatabase.setAuthorizer(m_databaseAuthorizer); |
|
254 m_sqliteDatabase.setBusyTimeout(maxSqliteBusyWaitTime); |
|
255 |
|
256 String currentVersion; |
|
257 { |
|
258 MutexLocker locker(guidMutex()); |
|
259 |
|
260 GuidVersionMap::iterator entry = guidToVersionMap().find(m_guid); |
|
261 if (entry != guidToVersionMap().end()) { |
|
262 // Map null string to empty string (see updateGuidVersionMap()). |
|
263 currentVersion = entry->second.isNull() ? String("") : entry->second; |
|
264 LOG(StorageAPI, "Current cached version for guid %i is %s", m_guid, currentVersion.ascii().data()); |
|
265 } else { |
|
266 LOG(StorageAPI, "No cached version for guid %i", m_guid); |
|
267 |
|
268 if (!m_sqliteDatabase.tableExists(databaseInfoTableName())) { |
|
269 m_new = true; |
|
270 |
|
271 if (!m_sqliteDatabase.executeCommand("CREATE TABLE " + databaseInfoTableName() + " (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) { |
|
272 LOG_ERROR("Unable to create table %s in database %s", databaseInfoTableName().ascii().data(), databaseDebugName().ascii().data()); |
|
273 ec = INVALID_STATE_ERR; |
|
274 // Close the handle to the database file. |
|
275 m_sqliteDatabase.close(); |
|
276 return false; |
|
277 } |
|
278 } |
|
279 |
|
280 if (!getVersionFromDatabase(currentVersion)) { |
|
281 LOG_ERROR("Failed to get current version from database %s", databaseDebugName().ascii().data()); |
|
282 ec = INVALID_STATE_ERR; |
|
283 // Close the handle to the database file. |
|
284 m_sqliteDatabase.close(); |
|
285 return false; |
|
286 } |
|
287 if (currentVersion.length()) { |
|
288 LOG(StorageAPI, "Retrieved current version %s from database %s", currentVersion.ascii().data(), databaseDebugName().ascii().data()); |
|
289 } else if (!m_new || shouldSetVersionInNewDatabase) { |
|
290 LOG(StorageAPI, "Setting version %s in database %s that was just created", m_expectedVersion.ascii().data(), databaseDebugName().ascii().data()); |
|
291 if (!setVersionInDatabase(m_expectedVersion)) { |
|
292 LOG_ERROR("Failed to set version %s in database %s", m_expectedVersion.ascii().data(), databaseDebugName().ascii().data()); |
|
293 ec = INVALID_STATE_ERR; |
|
294 // Close the handle to the database file. |
|
295 m_sqliteDatabase.close(); |
|
296 return false; |
|
297 } |
|
298 currentVersion = m_expectedVersion; |
|
299 } |
|
300 |
|
301 updateGuidVersionMap(m_guid, currentVersion); |
|
302 } |
|
303 } |
|
304 |
|
305 if (currentVersion.isNull()) { |
|
306 LOG(StorageAPI, "Database %s does not have its version set", databaseDebugName().ascii().data()); |
|
307 currentVersion = ""; |
|
308 } |
|
309 |
|
310 // If the expected version isn't the empty string, ensure that the current database version we have matches that version. Otherwise, set an exception. |
|
311 // If the expected version is the empty string, then we always return with whatever version of the database we have. |
|
312 if ((!m_new || shouldSetVersionInNewDatabase) && m_expectedVersion.length() && m_expectedVersion != currentVersion) { |
|
313 LOG(StorageAPI, "page expects version %s from database %s, which actually has version name %s - openDatabase() call will fail", m_expectedVersion.ascii().data(), |
|
314 databaseDebugName().ascii().data(), currentVersion.ascii().data()); |
|
315 ec = INVALID_STATE_ERR; |
|
316 // Close the handle to the database file. |
|
317 m_sqliteDatabase.close(); |
|
318 return false; |
|
319 } |
|
320 |
|
321 m_opened = true; |
|
322 |
|
323 return true; |
|
324 } |
|
325 |
|
326 ScriptExecutionContext* AbstractDatabase::scriptExecutionContext() const |
|
327 { |
|
328 return m_scriptExecutionContext.get(); |
|
329 } |
|
330 |
|
331 SecurityOrigin* AbstractDatabase::securityOrigin() const |
|
332 { |
|
333 return m_contextThreadSecurityOrigin.get(); |
|
334 } |
|
335 |
|
336 String AbstractDatabase::stringIdentifier() const |
|
337 { |
|
338 // Return a deep copy for ref counting thread safety |
|
339 return m_name.threadsafeCopy(); |
|
340 } |
|
341 |
|
342 String AbstractDatabase::displayName() const |
|
343 { |
|
344 // Return a deep copy for ref counting thread safety |
|
345 return m_displayName.threadsafeCopy(); |
|
346 } |
|
347 |
|
348 unsigned long AbstractDatabase::estimatedSize() const |
|
349 { |
|
350 return m_estimatedSize; |
|
351 } |
|
352 |
|
353 String AbstractDatabase::fileName() const |
|
354 { |
|
355 // Return a deep copy for ref counting thread safety |
|
356 return m_filename.threadsafeCopy(); |
|
357 } |
|
358 |
|
359 // static |
|
360 const String& AbstractDatabase::databaseVersionKey() |
|
361 { |
|
362 DEFINE_STATIC_LOCAL(String, key, ("WebKitDatabaseVersionKey")); |
|
363 return key; |
|
364 } |
|
365 |
|
366 bool AbstractDatabase::getVersionFromDatabase(String& version) |
|
367 { |
|
368 DEFINE_STATIC_LOCAL(String, getVersionQuery, ("SELECT value FROM " + databaseInfoTableName() + " WHERE key = '" + databaseVersionKey() + "';")); |
|
369 |
|
370 m_databaseAuthorizer->disable(); |
|
371 |
|
372 bool result = retrieveTextResultFromDatabase(m_sqliteDatabase, getVersionQuery.threadsafeCopy(), version); |
|
373 if (!result) |
|
374 LOG_ERROR("Failed to retrieve version from database %s", databaseDebugName().ascii().data()); |
|
375 |
|
376 m_databaseAuthorizer->enable(); |
|
377 |
|
378 return result; |
|
379 } |
|
380 |
|
381 bool AbstractDatabase::setVersionInDatabase(const String& version) |
|
382 { |
|
383 // The INSERT will replace an existing entry for the database with the new version number, due to the UNIQUE ON CONFLICT REPLACE |
|
384 // clause in the CREATE statement (see Database::performOpenAndVerify()). |
|
385 DEFINE_STATIC_LOCAL(String, setVersionQuery, ("INSERT INTO " + databaseInfoTableName() + " (key, value) VALUES ('" + databaseVersionKey() + "', ?);")); |
|
386 |
|
387 m_databaseAuthorizer->disable(); |
|
388 |
|
389 bool result = setTextValueInDatabase(m_sqliteDatabase, setVersionQuery.threadsafeCopy(), version); |
|
390 if (!result) |
|
391 LOG_ERROR("Failed to set version %s in database (%s)", version.ascii().data(), setVersionQuery.ascii().data()); |
|
392 |
|
393 m_databaseAuthorizer->enable(); |
|
394 |
|
395 return result; |
|
396 } |
|
397 |
|
398 bool AbstractDatabase::versionMatchesExpected() const |
|
399 { |
|
400 if (!m_expectedVersion.isEmpty()) { |
|
401 MutexLocker locker(guidMutex()); |
|
402 return m_expectedVersion == guidToVersionMap().get(m_guid); |
|
403 } |
|
404 |
|
405 return true; |
|
406 } |
|
407 |
|
408 void AbstractDatabase::setExpectedVersion(const String& version) |
|
409 { |
|
410 m_expectedVersion = version.threadsafeCopy(); |
|
411 // Update the in memory database version map. |
|
412 MutexLocker locker(guidMutex()); |
|
413 updateGuidVersionMap(m_guid, version); |
|
414 } |
|
415 |
|
416 void AbstractDatabase::disableAuthorizer() |
|
417 { |
|
418 ASSERT(m_databaseAuthorizer); |
|
419 m_databaseAuthorizer->disable(); |
|
420 } |
|
421 |
|
422 void AbstractDatabase::enableAuthorizer() |
|
423 { |
|
424 ASSERT(m_databaseAuthorizer); |
|
425 m_databaseAuthorizer->enable(); |
|
426 } |
|
427 |
|
428 void AbstractDatabase::setAuthorizerReadOnly() |
|
429 { |
|
430 ASSERT(m_databaseAuthorizer); |
|
431 m_databaseAuthorizer->setReadOnly(); |
|
432 } |
|
433 |
|
434 bool AbstractDatabase::lastActionChangedDatabase() |
|
435 { |
|
436 ASSERT(m_databaseAuthorizer); |
|
437 return m_databaseAuthorizer->lastActionChangedDatabase(); |
|
438 } |
|
439 |
|
440 bool AbstractDatabase::lastActionWasInsert() |
|
441 { |
|
442 ASSERT(m_databaseAuthorizer); |
|
443 return m_databaseAuthorizer->lastActionWasInsert(); |
|
444 } |
|
445 |
|
446 void AbstractDatabase::resetDeletes() |
|
447 { |
|
448 ASSERT(m_databaseAuthorizer); |
|
449 m_databaseAuthorizer->resetDeletes(); |
|
450 } |
|
451 |
|
452 bool AbstractDatabase::hadDeletes() |
|
453 { |
|
454 ASSERT(m_databaseAuthorizer); |
|
455 return m_databaseAuthorizer->hadDeletes(); |
|
456 } |
|
457 |
|
458 void AbstractDatabase::resetAuthorizer() |
|
459 { |
|
460 if (m_databaseAuthorizer) |
|
461 m_databaseAuthorizer->reset(); |
|
462 } |
|
463 |
|
464 unsigned long long AbstractDatabase::maximumSize() const |
|
465 { |
|
466 return DatabaseTracker::tracker().getMaxSizeForDatabase(this); |
|
467 } |
|
468 |
|
469 void AbstractDatabase::incrementalVacuumIfNeeded() |
|
470 { |
|
471 int64_t freeSpaceSize = m_sqliteDatabase.freeSpaceSize(); |
|
472 int64_t totalSize = m_sqliteDatabase.totalSize(); |
|
473 if (totalSize <= 10 * freeSpaceSize) |
|
474 m_sqliteDatabase.runIncrementalVacuumCommand(); |
|
475 } |
|
476 |
|
477 } // namespace WebCore |
|
478 |
|
479 #endif // ENABLE(DATABASE) |