206c4fefe8851394b032dca379b2f86ae81e9df8
[WebKit-https.git] / WebCore / loader / icon / IconDatabase.cpp
1 /*
2  * Copyright (C) 2006 Apple Computer, 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "config.h"
27 #include "IconDatabase.h"
28
29 #include "IconDataCache.h"
30 #include "Image.h"
31 #include "Logging.h"
32 #include "PlatformString.h"
33 #include "SQLStatement.h"
34 #include "SQLTransaction.h"
35 #include "SystemTime.h"
36
37 // FIXME - One optimization to be made when this is no longer in flux is to make query construction smarter - that is queries that are created from
38 // multiple strings and numbers should be handled differently than with String + String + String + etc.
39
40 namespace WebCore {
41
42 IconDatabase* IconDatabase::m_sharedInstance = 0;
43
44 // This version number is in the DB and marks the current generation of the schema
45 // Theoretically once the switch is flipped this should never change
46 // Currently, an out-of-date schema causes the DB to be wiped and reset.  This isn't 
47 // so bad during development but in the future, we would need to write a conversion
48 // function to advance older released schemas to "current"
49 const int IconDatabase::currentDatabaseVersion = 5;
50
51 // Icons expire once a day
52 const int IconDatabase::iconExpirationTime = 60*60*24; 
53 // Absent icons are rechecked once a week
54 const int IconDatabase::missingIconExpirationTime = 60*60*24*7; 
55
56 const int IconDatabase::updateTimerDelay = 5; 
57
58 const String& IconDatabase::defaultDatabaseFilename()
59 {
60     static String defaultDatabaseFilename = "icon.db";
61     return defaultDatabaseFilename;
62 }
63
64 IconDatabase* IconDatabase::sharedIconDatabase()
65 {
66     if (!m_sharedInstance) {
67         m_sharedInstance = new IconDatabase();
68     }
69     return m_sharedInstance;
70 }
71
72 IconDatabase::IconDatabase()
73     : m_timeStampForIconURLStatement(0)
74     , m_iconURLForPageURLStatement(0)
75     , m_hasIconForIconURLStatement(0)
76     , m_forgetPageURLStatement(0)
77     , m_setIconIDForPageURLStatement(0)
78     , m_getIconIDForIconURLStatement(0)
79     , m_addIconForIconURLStatement(0)
80     , m_imageDataForIconURLStatement(0)
81     , m_currentDB(&m_mainDB)
82     , m_defaultIconDataCache(0)
83     , m_privateBrowsingEnabled(false)
84     , m_startupTimer(this, &IconDatabase::pruneUnretainedIconsOnStartup)
85     , m_updateTimer(this, &IconDatabase::updateDatabase)
86     , m_initialPruningComplete(false)
87     , m_initialPruningTransaction(0)
88     , m_preparedPageRetainInsertStatement(0)
89 {
90     
91 }
92
93 bool IconDatabase::open(const String& databasePath)
94 {
95     if (isOpen()) {
96         LOG_ERROR("Attempt to reopen the IconDatabase which is already open.  Must close it first.");
97         return false;
98     }
99     
100     // First we'll formulate the full path for the database file
101     String dbFilename;
102     if (databasePath[databasePath.length()] == '/')
103         dbFilename = databasePath + defaultDatabaseFilename();
104     else
105         dbFilename = databasePath + "/" + defaultDatabaseFilename();
106
107     // Now, we'll see if we can open the on-disk table
108     // If we can't, this ::open() failed and we should bail now
109     if (!m_mainDB.open(dbFilename)) {
110         LOG(IconDatabase, "Unable to open icon database at path %s", dbFilename.ascii().data());
111         return false;
112     }
113     
114     if (!isValidDatabase(m_mainDB)) {
115         LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", dbFilename.ascii().data());
116         clearDatabaseTables(m_mainDB);
117         createDatabaseTables(m_mainDB);
118     }
119
120     m_initialPruningTransaction = new SQLTransaction(m_mainDB);
121     // We're going to track an icon's retain count in a temp table in memory so we can cross reference it to to the on disk tables
122     bool result;
123     result = m_mainDB.executeCommand("CREATE TEMP TABLE PageRetain (url TEXT);");
124     // Creating an in-memory temp table should never, ever, ever fail
125     ASSERT(result);
126
127     // These are actually two different SQLite config options - not my fault they are named confusingly  ;)
128     m_mainDB.setSynchronous(SQLDatabase::SyncOff);
129     m_mainDB.setFullsync(false);
130
131     m_initialPruningTransaction->begin();
132     
133     // Open the in-memory table for private browsing
134     if (!m_privateBrowsingDB.open(":memory:"))
135         LOG_ERROR("Unabled to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
136
137     // Only if we successfully remained open will we start our "initial purge timer"
138     // rdar://4690949 - when we have deferred reads and writes all the way in, the prunetimer
139     // will become "deferredTimer" or something along those lines, and will be set only when
140     // a deferred read/write is queued
141     if (isOpen())
142         m_startupTimer.startOneShot(0);
143     
144     return isOpen();
145 }
146
147 void IconDatabase::close()
148 {
149     delete m_initialPruningTransaction;
150     if (m_preparedPageRetainInsertStatement)
151         m_preparedPageRetainInsertStatement->finalize();
152     delete m_preparedPageRetainInsertStatement;
153
154     syncDatabase();
155     delete m_timeStampForIconURLStatement;
156     delete m_iconURLForPageURLStatement;
157     delete m_hasIconForIconURLStatement;
158     delete m_forgetPageURLStatement;
159     delete m_setIconIDForPageURLStatement;
160     delete m_getIconIDForIconURLStatement;
161     delete m_addIconForIconURLStatement;
162     delete m_imageDataForIconURLStatement;
163     m_mainDB.close();
164     m_privateBrowsingDB.close();
165 }
166
167 bool IconDatabase::isEmpty()
168 {
169     if (m_privateBrowsingEnabled)
170         if (!pageURLTableIsEmptyQuery(m_privateBrowsingDB))
171             return false;
172             
173     return pageURLTableIsEmptyQuery(m_mainDB);
174 }
175
176 bool IconDatabase::isValidDatabase(SQLDatabase& db)
177 {
178     // These two tables should always exist in a valid db
179     if (!db.tableExists("Icon") || !db.tableExists("PageURL") || !db.tableExists("IconDatabaseInfo"))
180         return false;
181     
182     if (SQLStatement(db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0) < currentDatabaseVersion) {
183         LOG(IconDatabase, "DB version is not found or below expected valid version");
184         return false;
185     }
186     
187     return true;
188 }
189
190 void IconDatabase::clearDatabaseTables(SQLDatabase& db)
191 {
192     String query = "SELECT name FROM sqlite_master WHERE type='table';";
193     Vector<String> tables;
194     if (!SQLStatement(db, query).returnTextResults16(0, tables)) {
195         LOG(IconDatabase, "Unable to retrieve list of tables from database");
196         return;
197     }
198     
199     for (Vector<String>::iterator table = tables.begin(); table != tables.end(); ++table ) {
200         if (!db.executeCommand("DROP TABLE " + *table)) {
201             LOG(IconDatabase, "Unable to drop table %s", (*table).ascii().data());
202         }
203     }
204 }
205
206 void IconDatabase::createDatabaseTables(SQLDatabase& db)
207 {
208     if (!db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
209         LOG_ERROR("Could not create PageURL table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
210         db.close();
211         return;
212     }
213     if (!db.executeCommand("CREATE TABLE Icon (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE, stamp INTEGER, data BLOB);")) {
214         LOG_ERROR("Could not create Icon table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
215         db.close();
216         return;
217     }
218     if (!db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
219         LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
220         db.close();
221         return;
222     }
223     if (!db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
224         LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", db.lastError(), db.lastErrorMsg());
225         db.close();
226         return;
227     }
228 }    
229
230 void IconDatabase::imageDataForIconURL(const String& iconURL, Vector<unsigned char>& result)
231 {      
232     // If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
233     if (m_privateBrowsingEnabled) {    
234         imageDataForIconURLQuery(m_privateBrowsingDB, iconURL, result);
235         if (!result.isEmpty())
236             return;
237     } 
238     
239     // It wasn't found there, so lets check the main tables
240     imageDataForIconURLQuery(m_mainDB, iconURL, result);
241 }
242
243 void IconDatabase::setPrivateBrowsingEnabled(bool flag)
244 {
245     if (m_privateBrowsingEnabled == flag)
246         return;
247     
248     // Sync any deferred DB changes before we change the active DB
249     syncDatabase();
250     
251     m_privateBrowsingEnabled = flag;
252     
253     if (m_privateBrowsingEnabled) {
254         createDatabaseTables(m_privateBrowsingDB);
255         m_currentDB = &m_privateBrowsingDB;
256     } else {
257         clearDatabaseTables(m_privateBrowsingDB);
258         m_currentDB = &m_mainDB;
259     }
260 }
261
262 Image* IconDatabase::iconForPageURL(const String& pageURL, const IntSize& size, bool cache)
263 {   
264     // See if we even have an IconURL for this PageURL...
265     String iconURL = iconURLForPageURL(pageURL);
266     if (iconURL.isEmpty())
267         return 0;
268     
269     // If we do, maybe we have a IconDataCache for this IconURL
270     IconDataCache* icon = getOrCreateIconDataCache(iconURL);
271     
272     // If it's a new IconDataCache object that doesn't have its imageData set yet,
273     // we'll read in that image data now
274     if (icon->imageDataStatus() == ImageDataStatusUnknown) {
275         Vector<unsigned char> data;
276         imageDataForIconURL(iconURL, data);
277         icon->setImageData(data.data(), data.size());
278     }
279         
280     return icon->getImage(size);
281 }
282
283 // FIXME 4667425 - this check needs to see if the icon's data is empty or not and apply
284 // iconExpirationTime to present icons, and missingIconExpirationTime for missing icons
285 bool IconDatabase::isIconExpiredForIconURL(const String& iconURL)
286 {
287     if (iconURL.isEmpty()) 
288         return true;
289     
290     // If we have a IconDataCache, then it definitely has the Timestamp in it
291     IconDataCache* icon = m_iconURLToIconDataCacheMap.get(iconURL);
292     if (icon) 
293         return (int)currentTime() - icon->getTimestamp() > iconExpirationTime;
294             
295     // Otherwise, we'll get the timestamp from the DB and use it
296     int stamp;
297     if (m_privateBrowsingEnabled) {
298         stamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
299         if (stamp)
300             return ((int)currentTime() - stamp) > iconExpirationTime;
301     }
302     
303     stamp = timeStampForIconURLQuery(m_mainDB, iconURL);
304     if (stamp)
305         return ((int)currentTime() - stamp) > iconExpirationTime;
306     
307     return false;
308 }
309     
310 String IconDatabase::iconURLForPageURL(const String& pageURL)
311 {    
312     if (pageURL.isEmpty()) 
313         return String();
314         
315     if (m_pageURLToIconURLMap.contains(pageURL))
316         return m_pageURLToIconURLMap.get(pageURL);
317     
318     // Try the private browsing database because if any PageURL's IconURL was updated during privated browsing, 
319     // the most up-to-date iconURL would be there
320     if (m_privateBrowsingEnabled) {
321         String iconURL = iconURLForPageURLQuery(m_privateBrowsingDB, pageURL);
322         if (!iconURL.isEmpty()) {
323             m_pageURLToIconURLMap.set(pageURL, iconURL);
324             return iconURL;
325         }
326     }
327     
328     String iconURL = iconURLForPageURLQuery(m_mainDB, pageURL);
329     if (!iconURL.isEmpty())
330         m_pageURLToIconURLMap.set(pageURL, iconURL);
331     return iconURL;
332 }
333
334 Image* IconDatabase::defaultIcon(const IntSize& size)
335 {
336     if (!m_defaultIconDataCache) {
337         m_defaultIconDataCache = new IconDataCache("urlIcon");
338         m_defaultIconDataCache->loadImageFromResource("urlIcon");
339     }
340     
341     return m_defaultIconDataCache->getImage(size);
342 }
343
344 void IconDatabase::retainIconForPageURL(const String& pageURL)
345 {
346     if (pageURL.isEmpty())
347         return;
348     
349     // If we don't have the retain count for this page, we need to setup records of its retain
350     // Otherwise, get the count and increment it
351     int retainCount;
352     if (!(retainCount = m_pageURLToRetainCount.get(pageURL))) {
353         m_pageURLToRetainCount.set(pageURL, 1);   
354
355         // If we haven't done initial pruning, we store this retain record in the temporary in-memory table
356         // Note we only keep the URL in the temporary table, not the full retain count, because for pruning-considerations
357         // we only care *if* a pageURL is retained - not the full count.  This call to retainIconForPageURL incremented the PageURL's
358         // retain count from 0 to 1 therefore we may store it in the temporary table
359         // Also, if we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot, 
360         // so we bail here and skip those steps
361         if (!m_initialPruningComplete) {
362             String escapedPageURL = escapeSQLString(pageURL);
363             if (!m_preparedPageRetainInsertStatement) {
364                 m_preparedPageRetainInsertStatement = new SQLStatement(m_mainDB, "INSERT INTO PageRetain VALUES (?);");
365                 m_preparedPageRetainInsertStatement->prepare();
366             }
367             m_preparedPageRetainInsertStatement->reset();
368             m_preparedPageRetainInsertStatement->bindText16(1, pageURL);
369             if (m_preparedPageRetainInsertStatement->step() != SQLITE_DONE)
370                 LOG_ERROR("Failed to record icon retention in temporary table for IconURL %s", pageURL.ascii().data());
371             return;
372         }
373         
374         // If this pageURL is marked for deletion, bring it back from the brink
375         m_pageURLsPendingDeletion.remove(pageURL);
376         
377         // If we have an iconURL for this pageURL, we'll now retain the iconURL
378         String iconURL = iconURLForPageURL(pageURL);
379         if (!iconURL.isEmpty())
380             retainIconURL(iconURL);
381
382     } else
383         m_pageURLToRetainCount.set(pageURL, retainCount + 1);   
384 }
385
386 void IconDatabase::releaseIconForPageURL(const String& pageURL)
387 {
388     if (pageURL.isEmpty())
389         return;
390         
391     // Check if this pageURL is actually retained
392     if(!m_pageURLToRetainCount.contains(pageURL)) {
393         LOG_ERROR("Attempting to release icon for URL %s which is not retained", pageURL.ascii().data());
394         return;
395     }
396     
397     // Get its retain count
398     int retainCount = m_pageURLToRetainCount.get(pageURL);
399     ASSERT(retainCount > 0);
400     
401     // If it still has a positive retain count, store the new count and bail
402     if (--retainCount) {
403         m_pageURLToRetainCount.set(pageURL, retainCount);
404         return;
405     }
406     
407     LOG(IconDatabase, "No more retainers for PageURL %s", pageURL.ascii().data());
408     
409     // Otherwise, remove all record of the retain count
410     m_pageURLToRetainCount.remove(pageURL);   
411     
412     // If we haven't done initial pruning, we remove this retain record from the temporary in-memory table
413     // Note we only keep the URL in the temporary table, not the full retain count, because for pruning-considerations
414     // we only care *if* a pageURL is retained - not the full count.  This call to releaseIconForPageURL decremented the PageURL's
415     // retain count from 1 to 0 therefore we may remove it from the temporary table
416     // Also, if we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot, 
417     // so we bail here and skip those steps
418     if (!m_initialPruningComplete) {
419         String escapedPageURL = escapeSQLString(pageURL);
420         if (!m_mainDB.executeCommand("DELETE FROM PageRetain WHERE url='" + escapedPageURL + "';"))
421             LOG_ERROR("Failed to delete record of icon retention from temporary table for IconURL %s", pageURL.ascii().data());
422         return;
423     }
424
425     
426     // Then mark this pageURL for deletion
427     m_pageURLsPendingDeletion.add(pageURL);
428     
429     // Grab the iconURL and release it
430     String iconURL = iconURLForPageURL(pageURL);
431     if (!iconURL.isEmpty())
432         releaseIconURL(iconURL);
433 }
434
435 void IconDatabase::retainIconURL(const String& iconURL)
436 {
437     ASSERT(!iconURL.isEmpty());
438     
439     if (int retainCount = m_iconURLToRetainCount.get(iconURL)) {
440         ASSERT(retainCount > 0);
441         m_iconURLToRetainCount.set(iconURL, retainCount + 1);
442     } else {
443         m_iconURLToRetainCount.set(iconURL, 1);
444         if (m_iconURLsPendingDeletion.contains(iconURL))
445             m_iconURLsPendingDeletion.remove(iconURL);
446     }   
447 }
448
449 void IconDatabase::releaseIconURL(const String& iconURL)
450 {
451     ASSERT(!iconURL.isEmpty());
452     
453     // If the iconURL has no retain count, we can bail
454     if (!m_iconURLToRetainCount.contains(iconURL))
455         return;
456     
457     // Otherwise, decrement it
458     int retainCount = m_iconURLToRetainCount.get(iconURL) - 1;
459     ASSERT(retainCount > -1);
460     
461     // If the icon is still retained, store the count and bail
462     if (retainCount) {
463         m_iconURLToRetainCount.set(iconURL, retainCount);
464         return;
465     }
466     
467     LOG(IconDatabase, "No more retainers for IconURL %s", iconURL.ascii().data());
468     
469     // Otherwise, this icon is toast.  Remove all traces of its retain count...
470     m_iconURLToRetainCount.remove(iconURL);
471     
472     // And since we delay the actual deletion of icons, so lets add it to that queue
473     m_iconURLsPendingDeletion.add(iconURL);
474 }
475
476 void IconDatabase::forgetPageURL(const String& pageURL)
477 {
478     // Remove the PageURL->IconURL mapping
479     m_pageURLToIconURLMap.remove(pageURL);
480     
481     // And remove this pageURL from the DB
482     forgetPageURLQuery(*m_currentDB, pageURL);
483 }
484     
485 bool IconDatabase::isIconURLRetained(const String& iconURL)
486 {
487     if (iconURL.isEmpty())
488         return false;
489         
490     return m_iconURLToRetainCount.contains(iconURL);
491 }
492
493 void IconDatabase::forgetIconForIconURLFromDatabase(const String& iconURL)
494 {
495     if (iconURL.isEmpty())
496         return;
497
498     // For private browsing safety, since this alters the database, we only forget from the current database
499     // If we're in private browsing and the Icon also exists in the main database, it will be pruned on the next startup
500     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL, false);
501     
502     // If we didn't actually have an icon for this iconURL... well, thats a screwy condition we should track down, but also 
503     // something we could move on from
504     ASSERT(iconID);
505     if (!iconID) {
506         LOG_ERROR("Attempting to forget icon for IconURL %s, though we don't have it in the database", iconURL.ascii().data());
507         return;
508     }
509     
510     if (!m_currentDB->executeCommand(String::sprintf("DELETE FROM Icon WHERE Icon.iconID = %lli;", iconID)))
511         LOG_ERROR("Unable to drop Icon for IconURL", iconURL.ascii().data()); 
512     if (!m_currentDB->executeCommand(String::sprintf("DELETE FROM PageURL WHERE PageURL.iconID = %lli", iconID)))
513         LOG_ERROR("Unable to drop all PageURL for IconURL", iconURL.ascii().data()); 
514 }
515
516 IconDataCache* IconDatabase::getOrCreateIconDataCache(const String& iconURL)
517 {
518     IconDataCache* icon;
519     if ((icon = m_iconURLToIconDataCacheMap.get(iconURL)))
520         return icon;
521         
522     icon = new IconDataCache(iconURL);
523     m_iconURLToIconDataCacheMap.set(iconURL, icon);
524     
525     // Get the most current time stamp for this IconURL
526     int timestamp = 0;
527     if (m_privateBrowsingEnabled)
528         timestamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
529     if (!timestamp)
530         timestamp = timeStampForIconURLQuery(m_mainDB, iconURL);
531         
532     // If we can't get a timestamp for this URL, then it is a new icon and we initialize its timestamp now
533     if (!timestamp) {
534         icon->setTimestamp((int)currentTime());
535         m_iconDataCachesPendingUpdate.add(icon);
536     } else 
537         icon->setTimestamp(timestamp);
538         
539     return icon;
540 }
541
542 void IconDatabase::setIconDataForIconURL(const void* data, int size, const String& iconURL)
543 {
544     ASSERT(size > -1);
545     if (size)
546         ASSERT(data);
547     else
548         data = 0;
549         
550     if (iconURL.isEmpty())
551         return;
552     
553     // Get the IconDataCache for this IconURL (note, IconDataCacheForIconURL will create it if necessary)
554     IconDataCache* icon = getOrCreateIconDataCache(iconURL);
555     
556     // Set the data in the IconDataCache
557     icon->setImageData((unsigned char*)data, size);
558     
559     // Update the timestamp in the IconDataCache to NOW
560     icon->setTimestamp((int)currentTime());
561
562     // Mark the IconDataCache as requiring an update to the database
563     m_iconDataCachesPendingUpdate.add(icon);
564 }
565
566 void IconDatabase::setHaveNoIconForIconURL(const String& iconURL)
567 {   
568     setIconDataForIconURL(0, 0, iconURL);
569 }
570
571 bool IconDatabase::setIconURLForPageURL(const String& iconURL, const String& pageURL)
572 {
573     ASSERT(!iconURL.isEmpty());
574     ASSERT(!pageURL.isEmpty());
575     
576     // If the urls already map to each other, bail.
577     // This happens surprisingly often, and seems to cream iBench performance
578     if (m_pageURLToIconURLMap.get(pageURL) == iconURL)
579         return false;
580
581     // If this pageURL is retained, we have some work to do on the IconURL retain counts
582     if (m_pageURLToRetainCount.contains(pageURL)) {
583         String oldIconURL = m_pageURLToIconURLMap.get(pageURL);
584         if (!oldIconURL.isEmpty())
585             releaseIconURL(oldIconURL);
586         retainIconURL(iconURL);
587     } else {
588         // If this pageURL is *not* retained, then we may be marking it for deletion, as well!
589         // As counterintuitive as it seems to mark it for addition and for deletion at the same time,
590         // it's valid because when we do a new pageURL->iconURL mapping we *have* to mark it for addition,
591         // no matter what, as there is no efficient was to determine if the mapping is in the DB already.
592         // But, if the iconURL is marked for deletion, we'll also mark this pageURL for deletion - if a 
593         // client comes along and retains it before the timer fires, the "pendingDeletion" lists will
594         // be manipulated appopriately and new pageURL will be brought back from the brink
595         if (m_iconURLsPendingDeletion.contains(iconURL))
596             m_pageURLsPendingDeletion.add(pageURL);
597     }
598     
599     // Cache the pageURL->iconURL map
600     m_pageURLToIconURLMap.set(pageURL, iconURL);
601     
602     // And mark this mapping to be added to the database
603     m_pageURLsPendingAddition.add(pageURL);
604     
605     // Then start the timer to commit this change - or further delay the timer if it
606     // was already started
607     m_updateTimer.startOneShot(updateTimerDelay);
608     
609     return true;
610 }
611
612 void IconDatabase::setIconURLForPageURLInDatabase(const String& iconURL, const String& pageURL)
613 {
614     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL);
615     if (!iconID) {
616         LOG_ERROR("Failed to establish an ID for iconURL %s", iconURL.ascii().data());
617         return;
618     }
619     setIconIDForPageURLQuery(*m_currentDB, iconID, pageURL);
620 }
621
622 int64_t IconDatabase::establishIconIDForIconURL(SQLDatabase& db, const String& iconURL, bool createIfNecessary)
623 {
624     String escapedIconURL = escapeSQLString(iconURL);
625     
626     // Get the iconID thats already in this database and return it - or return 0 if we're read-only
627     int64_t iconID = getIconIDForIconURLQuery(db, iconURL);
628     if (iconID || !createIfNecessary)
629         return iconID;
630         
631     // Create the icon table entry for the iconURL
632     return addIconForIconURLQuery(db, iconURL);
633 }
634
635 void IconDatabase::pruneUnretainedIconsOnStartup(Timer<IconDatabase>*)
636 {
637     if (!isOpen())
638         return;
639         
640     // This function should only be called once per run, and ideally only via the timer
641     // on program startup
642     ASSERT(!m_initialPruningComplete);
643
644 #ifndef NDEBUG
645     double timestamp = currentTime();
646 #endif
647     
648     // rdar://4690949 - Need to prune unretained iconURLs here, then prune out all pageURLs that reference
649     // nonexistent icons
650     
651     // Finalize the PageRetain statement
652     delete m_preparedPageRetainInsertStatement;
653     m_preparedPageRetainInsertStatement = 0;
654     
655     // Commit all of the PageRetains and start a new transaction for the pruning dirty-work
656     m_initialPruningTransaction->commit();
657     m_initialPruningTransaction->begin();
658     
659     // Then wipe all PageURLs and Icons that aren't retained
660     if (!m_mainDB.executeCommand("DELETE FROM PageURL WHERE PageURL.url NOT IN (SELECT url FROM PageRetain);") ||
661         !m_mainDB.executeCommand("DELETE FROM Icon WHERE Icon.iconID NOT IN (SELECT iconID FROM PageURL);") ||
662         !m_mainDB.executeCommand("DROP TABLE PageRetain;"))
663         LOG_ERROR("Failed to execute SQL to prune unretained pages and icons from the on-disk tables");
664     
665     
666     // Since we lazily retained the pageURLs without getting the iconURLs or retaining the iconURLs, 
667     // we need to do that now
668     // We now should be in a spiffy situation where we know every single pageURL left in the DB is retained, so we are interested
669     // in the iconURLs for all remaining pageURLs
670     // So we can simply add all the remaining mappings, and retain each pageURL's icon once
671     
672     SQLStatement sql(m_mainDB, "SELECT PageURL.url, Icon.url FROM PageURL INNER JOIN Icon ON PageURL.iconID=Icon.iconID");
673     sql.prepare();
674     int result;
675     while((result = sql.step()) == SQLITE_ROW) {
676         String iconURL = sql.getColumnText16(1);
677         m_pageURLToIconURLMap.set(sql.getColumnText16(0), iconURL);
678         retainIconURL(iconURL);
679         LOG(IconDatabase, "Found a PageURL that mapped to %s", iconURL.ascii().data());
680     }
681     if (result != SQLITE_DONE)
682         LOG_ERROR("Error reading PageURL->IconURL mappings from on-disk DB");
683     sql.finalize();
684     
685     // Commit the transaction and do some cleanup
686     m_initialPruningTransaction->commit();
687     delete m_initialPruningTransaction;
688     m_initialPruningTransaction = 0;
689     
690     m_initialPruningComplete = true;
691     
692 #ifndef NDEBUG
693     timestamp = currentTime() - timestamp;
694     if (timestamp <= 1.0)
695         LOG(IconDatabase, "Pruning unretained icons took %.4f seconds", timestamp);
696     else
697         LOG(IconDatabase, "Pruning unretained icons took %.4f seconds - this is much too long!", timestamp);
698 #endif
699 }
700
701 void IconDatabase::updateDatabase(Timer<IconDatabase>*)
702 {
703     syncDatabase();
704 }
705
706 void IconDatabase::syncDatabase()
707 {
708 #ifndef NDEBUG
709     double timestamp = currentTime();
710 #endif
711
712     // First we'll do the pending additions
713     // Starting with the IconDataCaches that need updating/insertion
714     for (HashSet<IconDataCache*>::iterator i = m_iconDataCachesPendingUpdate.begin(), end = m_iconDataCachesPendingUpdate.end(); i != end; ++i) {
715         (*i)->writeToDatabase(*m_currentDB);
716         LOG(IconDatabase, "Wrote IconDataCache for IconURL %s with timestamp of %lli to the DB", (*i)->getIconURL().ascii().data(), (*i)->getTimestamp());
717     }
718     m_iconDataCachesPendingUpdate.clear();
719     
720     HashSet<String>::iterator i = m_pageURLsPendingAddition.begin(), end = m_pageURLsPendingAddition.end();
721     for (; i != end; ++i) {
722         setIconURLForPageURLInDatabase(m_pageURLToIconURLMap.get(*i), *i);
723         LOG(IconDatabase, "Commited IconURL for PageURL %s to database", (*i).ascii().data());
724     }
725     m_pageURLsPendingAddition.clear();
726     
727     // Then we'll do the pending deletions
728     // First lets wipe all the pageURLs
729     for (i = m_pageURLsPendingDeletion.begin(), end = m_pageURLsPendingDeletion.end(); i != end; ++i) {    
730         forgetPageURL(*i);
731         LOG(IconDatabase, "Deleted PageURL %s", (*i).ascii().data());
732     }
733     m_pageURLsPendingDeletion.clear();
734
735     // Then get rid of all traces of the icons and IconURLs
736     IconDataCache* icon;    
737     for (i = m_iconURLsPendingDeletion.begin(), end = m_iconURLsPendingDeletion.end(); i != end; ++i) {
738         // Forget the IconDataCache
739         icon = m_iconURLToIconDataCacheMap.get(*i);
740         if (icon)
741             m_iconURLToIconDataCacheMap.remove(*i);
742         delete icon;
743         
744         // Forget the IconURL from the database
745         forgetIconForIconURLFromDatabase(*i);
746         LOG(IconDatabase, "Deleted icon %s", (*i).ascii().data());   
747     }
748     m_iconURLsPendingDeletion.clear();
749     
750     // If the timer was running to cause this update, we can kill the timer as its firing would be redundant
751     m_updateTimer.stop();
752     
753 #ifndef NDEBUG
754     timestamp = currentTime() - timestamp;
755     if (timestamp <= 1.0)
756         LOG(IconDatabase, "Updating the database took %.4f seconds", timestamp);
757     else 
758         LOG(IconDatabase, "Updating the database took %.4f seconds - this is much too long!", timestamp);
759 #endif
760 }
761
762 bool IconDatabase::hasEntryForIconURL(const String& iconURL)
763 {
764     if (iconURL.isEmpty())
765         return false;
766         
767     // First check the in memory mapped icons...
768     if (m_iconURLToIconDataCacheMap.contains(iconURL))
769         return true;
770
771     // Then we'll check the main database
772     if (hasIconForIconURLQuery(m_mainDB, iconURL))
773         return true;
774         
775     // Finally, the last resort - check the private browsing database
776     if (m_privateBrowsingEnabled)  
777         if (hasIconForIconURLQuery(m_privateBrowsingDB, iconURL))
778             return true;    
779
780     // We must not have this iconURL!
781     return false;
782 }
783
784 IconDatabase::~IconDatabase()
785 {
786     close();
787 }
788
789 // readySQLStatement() handles two things
790 // 1 - If the SQLDatabase& argument is different, the statement must be destroyed and remade.  This happens when the user
791 //     switches to and from private browsing
792 // 2 - Lazy construction of the Statement in the first place, in case we've never made this query before
793 inline void readySQLStatement(SQLStatement*& statement, SQLDatabase& db, const String& str)
794 {
795     if (statement && statement->database() != &db) {
796         delete statement;
797         statement = 0;
798     }
799     if (!statement) {
800         statement = new SQLStatement(db, str);
801         statement->prepare();
802     }
803 }
804
805 // Any common IconDatabase query should be seperated into a fooQuery() and a *m_fooStatement.  
806 // This way we can lazily construct the SQLStatment for a query on its first use, then reuse the Statement binding
807 // the new parameter as needed
808 // The statement must be deleted in IconDatabase::close() before the actual SQLDatabase::close() call
809 // Also, m_fooStatement must be reset() before fooQuery() returns otherwise we will constantly get "database file locked" 
810 // errors in various combinations of queries
811
812 bool IconDatabase::pageURLTableIsEmptyQuery(SQLDatabase& db)
813 {  
814     // We won't make this use a m_fooStatement because its not really a "common" query
815     return !SQLStatement(db, "SELECT iconID FROM PageURL LIMIT 1;").returnsAtLeastOneResult();
816 }
817
818 void IconDatabase::imageDataForIconURLQuery(SQLDatabase& db, const String& iconURL, Vector<unsigned char>& result)
819 {
820     readySQLStatement(m_imageDataForIconURLStatement, db, "SELECT Icon.data FROM Icon WHERE Icon.url = (?);");
821     m_imageDataForIconURLStatement->bindText16(1, iconURL, false);
822     m_imageDataForIconURLStatement->step();
823     m_imageDataForIconURLStatement->getColumnBlobAsVector(0, result);
824     m_imageDataForIconURLStatement->reset();
825 }
826
827 int IconDatabase::timeStampForIconURLQuery(SQLDatabase& db, const String& iconURL)
828 {
829     readySQLStatement(m_timeStampForIconURLStatement, db, "SELECT Icon.stamp FROM Icon WHERE Icon.url = (?);");
830     m_timeStampForIconURLStatement->bindText16(1, iconURL, false);
831     m_timeStampForIconURLStatement->step();
832     int result = m_timeStampForIconURLStatement->getColumnInt(0);
833     m_timeStampForIconURLStatement->reset();
834     return result;
835 }
836
837 String IconDatabase::iconURLForPageURLQuery(SQLDatabase& db, const String& pageURL)
838 {
839     readySQLStatement(m_iconURLForPageURLStatement, db, "SELECT Icon.url FROM Icon, PageURL WHERE PageURL.url = (?) AND Icon.iconID = PageURL.iconID;");
840     m_iconURLForPageURLStatement->bindText16(1, pageURL, false);
841     m_iconURLForPageURLStatement->step();
842     String result = m_iconURLForPageURLStatement->getColumnText16(0);
843     m_iconURLForPageURLStatement->reset();
844     return result;
845 }
846
847 void IconDatabase::forgetPageURLQuery(SQLDatabase& db, const String& pageURL)
848 {
849     readySQLStatement(m_forgetPageURLStatement, db, "DELETE FROM PageURL WHERE url = (?);");
850     m_forgetPageURLStatement->bindText16(1, pageURL, false);
851     m_forgetPageURLStatement->step();
852     m_forgetPageURLStatement->reset();
853 }
854
855 void IconDatabase::setIconIDForPageURLQuery(SQLDatabase& db, int64_t iconID, const String& pageURL)
856 {
857     readySQLStatement(m_setIconIDForPageURLStatement, db, "INSERT INTO PageURL (url, iconID) VALUES ((?), ?);");
858     m_setIconIDForPageURLStatement->bindText16(1, pageURL, false);
859     m_setIconIDForPageURLStatement->bindInt64(2, iconID);
860     m_setIconIDForPageURLStatement->step();
861     m_setIconIDForPageURLStatement->reset();
862 }
863
864 int64_t IconDatabase::getIconIDForIconURLQuery(SQLDatabase& db, const String& iconURL)
865 {
866     readySQLStatement(m_getIconIDForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
867     m_getIconIDForIconURLStatement->bindText16(1, iconURL, false);
868     m_getIconIDForIconURLStatement->step();
869     int64_t result = m_getIconIDForIconURLStatement->getColumnInt64(0);
870     m_getIconIDForIconURLStatement->reset();
871     return result;
872 }
873
874 int64_t IconDatabase::addIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
875 {
876     readySQLStatement(m_addIconForIconURLStatement, db, "INSERT INTO Icon (url) VALUES ((?));");
877     m_addIconForIconURLStatement->bindText16(1, iconURL, false);
878     int64_t result = 0;
879     if (m_addIconForIconURLStatement->step() == SQLITE_OK)
880         result = db.lastInsertRowID();
881     m_addIconForIconURLStatement->reset();
882     return result;
883 }
884
885 bool IconDatabase::hasIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
886 {
887     readySQLStatement(m_hasIconForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
888     m_hasIconForIconURLStatement->bindText16(1, iconURL, false);
889     bool result = m_hasIconForIconURLStatement->step() == SQLITE_ROW;
890     m_hasIconForIconURLStatement->reset();
891     return result;
892 }
893
894 } //namespace WebCore