WebCore:
[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     // <rdar://problem/4707718> - If user's Icon directory is unwritable, Safari will crash at startup
108     // Now, we'll see if we can open the on-disk database.  If we can't, we'll log the error and open it as
109     // an in-memory database so the user will have icons during their current browsing session
110     
111     if (!m_mainDB.open(dbFilename)) {
112         LOG_ERROR("Unable to open icon database at path %s - %s", dbFilename.ascii().data(), m_mainDB.lastErrorMsg());
113         if (!m_mainDB.open(":memory:"))
114             LOG_ERROR("Unable to open in-memory database for browsing - %s", m_mainDB.lastErrorMsg());
115     }
116     
117     if (!isValidDatabase(m_mainDB)) {
118         LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", dbFilename.ascii().data());
119         m_mainDB.clearAllTables();
120         createDatabaseTables(m_mainDB);
121     }
122
123     m_initialPruningTransaction = new SQLTransaction(m_mainDB);
124     // 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
125     bool result;
126     result = m_mainDB.executeCommand("CREATE TEMP TABLE PageRetain (url TEXT);");
127     // Creating an in-memory temp table should never, ever, ever fail
128     ASSERT(result);
129
130     // These are actually two different SQLite config options - not my fault they are named confusingly  ;)
131     m_mainDB.setSynchronous(SQLDatabase::SyncOff);
132     m_mainDB.setFullsync(false);
133
134     m_initialPruningTransaction->begin();
135     
136     // Open the in-memory table for private browsing
137     if (!m_privateBrowsingDB.open(":memory:"))
138         LOG_ERROR("Unable to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
139
140     // Only if we successfully remained open will we start our "initial purge timer"
141     // rdar://4690949 - when we have deferred reads and writes all the way in, the prunetimer
142     // will become "deferredTimer" or something along those lines, and will be set only when
143     // a deferred read/write is queued
144     if (isOpen())
145         m_startupTimer.startOneShot(0);
146     
147     return isOpen();
148 }
149
150 void IconDatabase::close()
151 {
152     // This will close all the SQL statements and transactions we have open,
153     // syncing the DB at the appropriate point
154     deleteAllPreparedStatements(true);
155  
156     m_mainDB.close();
157     m_privateBrowsingDB.close();
158 }
159
160 void IconDatabase::removeAllIcons()
161 {
162     // We don't need to sync anything anymore since we're wiping everything.  
163     // So we can kill the update timer, and clear all the hashes of "items that need syncing"
164     m_updateTimer.stop();
165     m_iconDataCachesPendingUpdate.clear();
166     m_pageURLsPendingAddition.clear();
167     m_pageURLsPendingDeletion.clear();
168     m_iconURLsPendingDeletion.clear();
169     
170     //  Now clear all in-memory URLs and Icons
171     m_pageURLToIconURLMap.clear();
172     m_pageURLToRetainCount.clear();
173     m_iconURLToRetainCount.clear();
174     
175     HashMap<String, IconDataCache*>::iterator i = m_iconURLToIconDataCacheMap.begin();
176     HashMap<String, IconDataCache*>::iterator end = m_iconURLToIconDataCacheMap.end();
177     for (; i != end; ++i)
178         delete i->second;
179     m_iconURLToIconDataCacheMap.clear();
180         
181     // Wipe any pre-prepared statements, otherwise resetting the SQLDatabases themselves will fail
182     deleteAllPreparedStatements(false);
183     
184     // The easiest way to wipe the in-memory database is by closing and reopening it
185     m_privateBrowsingDB.close();
186     if (!m_privateBrowsingDB.open(":memory:"))
187         LOG_ERROR("Unable to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
188     createDatabaseTables(m_privateBrowsingDB);
189         
190     // To reset the on-disk database, we'll wipe all its tables then vacuum it
191     // This is easier and safer than closing it, deleting the file, and recreating from scratch
192     m_mainDB.clearAllTables();
193     m_mainDB.runVacuumCommand();
194     createDatabaseTables(m_mainDB);
195 }
196
197 // There are two instances where you'd want to deleteAllPreparedStatements - one with sync, and one without
198 // A - Closing down the database on application exit - in this case, you *do* want to save the icons out
199 // B - Resetting the DB via removeAllIcons() - in this case, you *don't* want to sync, because it would be a waste of time
200 void IconDatabase::deleteAllPreparedStatements(bool withSync)
201 {
202     // Must wipe the initial retain statement before the initial transaction
203     delete m_preparedPageRetainInsertStatement;
204     m_preparedPageRetainInsertStatement = 0;
205     delete m_initialPruningTransaction;
206     m_initialPruningTransaction = 0;
207
208     // Sync, if desired
209     if (withSync)
210         syncDatabase();
211         
212     // Order doesn't matter on these
213     delete m_timeStampForIconURLStatement;
214     m_timeStampForIconURLStatement = 0;
215     delete m_iconURLForPageURLStatement;
216     m_iconURLForPageURLStatement = 0;
217     delete m_hasIconForIconURLStatement;
218     m_hasIconForIconURLStatement = 0;
219     delete m_forgetPageURLStatement;
220     m_forgetPageURLStatement = 0;
221     delete m_setIconIDForPageURLStatement;
222     m_setIconIDForPageURLStatement = 0;
223     delete m_getIconIDForIconURLStatement;
224     m_getIconIDForIconURLStatement = 0;
225     delete m_addIconForIconURLStatement;
226     m_addIconForIconURLStatement = 0;
227     delete m_imageDataForIconURLStatement;
228     m_imageDataForIconURLStatement = 0;
229 }
230
231 bool IconDatabase::isEmpty()
232 {
233     if (m_privateBrowsingEnabled)
234         if (!pageURLTableIsEmptyQuery(m_privateBrowsingDB))
235             return false;
236             
237     return pageURLTableIsEmptyQuery(m_mainDB);
238 }
239
240 bool IconDatabase::isValidDatabase(SQLDatabase& db)
241 {
242     // These two tables should always exist in a valid db
243     if (!db.tableExists("Icon") || !db.tableExists("PageURL") || !db.tableExists("IconDatabaseInfo"))
244         return false;
245     
246     if (SQLStatement(db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0) < currentDatabaseVersion) {
247         LOG(IconDatabase, "DB version is not found or below expected valid version");
248         return false;
249     }
250     
251     return true;
252 }
253
254 void IconDatabase::createDatabaseTables(SQLDatabase& db)
255 {
256     if (!db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
257         LOG_ERROR("Could not create PageURL table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
258         db.close();
259         return;
260     }
261     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);")) {
262         LOG_ERROR("Could not create Icon table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
263         db.close();
264         return;
265     }
266     if (!db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
267         LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
268         db.close();
269         return;
270     }
271     if (!db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
272         LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", db.lastError(), db.lastErrorMsg());
273         db.close();
274         return;
275     }
276 }    
277
278 void IconDatabase::imageDataForIconURL(const String& iconURL, Vector<unsigned char>& result)
279 {      
280     // If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
281     if (m_privateBrowsingEnabled) {    
282         imageDataForIconURLQuery(m_privateBrowsingDB, iconURL, result);
283         if (!result.isEmpty())
284             return;
285     } 
286     
287     // It wasn't found there, so lets check the main tables
288     imageDataForIconURLQuery(m_mainDB, iconURL, result);
289 }
290
291 void IconDatabase::setPrivateBrowsingEnabled(bool flag)
292 {
293     if (m_privateBrowsingEnabled == flag)
294         return;
295     
296     // Sync any deferred DB changes before we change the active DB
297     syncDatabase();
298     
299     m_privateBrowsingEnabled = flag;
300     
301     if (m_privateBrowsingEnabled) {
302         createDatabaseTables(m_privateBrowsingDB);
303         m_currentDB = &m_privateBrowsingDB;
304     } else {
305         m_privateBrowsingDB.clearAllTables();
306         m_currentDB = &m_mainDB;
307     }
308 }
309
310 Image* IconDatabase::iconForPageURL(const String& pageURL, const IntSize& size, bool cache)
311 {   
312     // See if we even have an IconURL for this PageURL...
313     String iconURL = iconURLForPageURL(pageURL);
314     if (iconURL.isEmpty())
315         return 0;
316     
317     // If we do, maybe we have a IconDataCache for this IconURL
318     IconDataCache* icon = getOrCreateIconDataCache(iconURL);
319     
320     // If it's a new IconDataCache object that doesn't have its imageData set yet,
321     // we'll read in that image data now
322     if (icon->imageDataStatus() == ImageDataStatusUnknown) {
323         Vector<unsigned char> data;
324         imageDataForIconURL(iconURL, data);
325         icon->setImageData(data.data(), data.size());
326     }
327         
328     return icon->getImage(size);
329 }
330
331 // FIXME 4667425 - this check needs to see if the icon's data is empty or not and apply
332 // iconExpirationTime to present icons, and missingIconExpirationTime for missing icons
333 bool IconDatabase::isIconExpiredForIconURL(const String& iconURL)
334 {
335     if (iconURL.isEmpty()) 
336         return true;
337     
338     // If we have a IconDataCache, then it definitely has the Timestamp in it
339     IconDataCache* icon = m_iconURLToIconDataCacheMap.get(iconURL);
340     if (icon) 
341         return (int)currentTime() - icon->getTimestamp() > iconExpirationTime;
342             
343     // Otherwise, we'll get the timestamp from the DB and use it
344     int stamp;
345     if (m_privateBrowsingEnabled) {
346         stamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
347         if (stamp)
348             return ((int)currentTime() - stamp) > iconExpirationTime;
349     }
350     
351     stamp = timeStampForIconURLQuery(m_mainDB, iconURL);
352     if (stamp)
353         return ((int)currentTime() - stamp) > iconExpirationTime;
354     
355     return false;
356 }
357     
358 String IconDatabase::iconURLForPageURL(const String& pageURL)
359 {    
360     if (pageURL.isEmpty()) 
361         return String();
362         
363     if (m_pageURLToIconURLMap.contains(pageURL))
364         return m_pageURLToIconURLMap.get(pageURL);
365     
366     // Try the private browsing database because if any PageURL's IconURL was updated during privated browsing, 
367     // the most up-to-date iconURL would be there
368     if (m_privateBrowsingEnabled) {
369         String iconURL = iconURLForPageURLQuery(m_privateBrowsingDB, pageURL);
370         if (!iconURL.isEmpty()) {
371             m_pageURLToIconURLMap.set(pageURL, iconURL);
372             return iconURL;
373         }
374     }
375     
376     String iconURL = iconURLForPageURLQuery(m_mainDB, pageURL);
377     if (!iconURL.isEmpty())
378         m_pageURLToIconURLMap.set(pageURL, iconURL);
379     return iconURL;
380 }
381
382 Image* IconDatabase::defaultIcon(const IntSize& size)
383 {
384     if (!m_defaultIconDataCache) {
385         m_defaultIconDataCache = new IconDataCache("urlIcon");
386         m_defaultIconDataCache->loadImageFromResource("urlIcon");
387     }
388     
389     return m_defaultIconDataCache->getImage(size);
390 }
391
392 void IconDatabase::retainIconForPageURL(const String& pageURL)
393 {
394     if (pageURL.isEmpty())
395         return;
396     
397     // If we don't have the retain count for this page, we need to setup records of its retain
398     // Otherwise, get the count and increment it
399     int retainCount;
400     if (!(retainCount = m_pageURLToRetainCount.get(pageURL))) {
401         m_pageURLToRetainCount.set(pageURL, 1);   
402
403         // If we haven't done initial pruning, we store this retain record in the temporary in-memory table
404         // Note we only keep the URL in the temporary table, not the full retain count, because for pruning-considerations
405         // we only care *if* a pageURL is retained - not the full count.  This call to retainIconForPageURL incremented the PageURL's
406         // retain count from 0 to 1 therefore we may store it in the temporary table
407         // Also, if we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot, 
408         // so we bail here and skip those steps
409         if (!m_initialPruningComplete) {
410             String escapedPageURL = escapeSQLString(pageURL);
411             if (!m_preparedPageRetainInsertStatement) {
412                 m_preparedPageRetainInsertStatement = new SQLStatement(m_mainDB, "INSERT INTO PageRetain VALUES (?);");
413                 m_preparedPageRetainInsertStatement->prepare();
414             }
415             m_preparedPageRetainInsertStatement->reset();
416             m_preparedPageRetainInsertStatement->bindText16(1, pageURL);
417             if (m_preparedPageRetainInsertStatement->step() != SQLITE_DONE)
418                 LOG_ERROR("Failed to record icon retention in temporary table for IconURL %s", pageURL.ascii().data());
419             return;
420         }
421         
422         // If this pageURL is marked for deletion, bring it back from the brink
423         m_pageURLsPendingDeletion.remove(pageURL);
424         
425         // If we have an iconURL for this pageURL, we'll now retain the iconURL
426         String iconURL = iconURLForPageURL(pageURL);
427         if (!iconURL.isEmpty())
428             retainIconURL(iconURL);
429
430     } else
431         m_pageURLToRetainCount.set(pageURL, retainCount + 1);   
432 }
433
434 void IconDatabase::releaseIconForPageURL(const String& pageURL)
435 {
436     if (pageURL.isEmpty())
437         return;
438         
439     // Check if this pageURL is actually retained
440     if(!m_pageURLToRetainCount.contains(pageURL)) {
441         LOG_ERROR("Attempting to release icon for URL %s which is not retained", pageURL.ascii().data());
442         return;
443     }
444     
445     // Get its retain count
446     int retainCount = m_pageURLToRetainCount.get(pageURL);
447     ASSERT(retainCount > 0);
448     
449     // If it still has a positive retain count, store the new count and bail
450     if (--retainCount) {
451         m_pageURLToRetainCount.set(pageURL, retainCount);
452         return;
453     }
454     
455     LOG(IconDatabase, "No more retainers for PageURL %s", pageURL.ascii().data());
456     
457     // Otherwise, remove all record of the retain count
458     m_pageURLToRetainCount.remove(pageURL);   
459     
460     // If we haven't done initial pruning, we remove this retain record from the temporary in-memory table
461     // Note we only keep the URL in the temporary table, not the full retain count, because for pruning-considerations
462     // we only care *if* a pageURL is retained - not the full count.  This call to releaseIconForPageURL decremented the PageURL's
463     // retain count from 1 to 0 therefore we may remove it from the temporary table
464     // Also, if we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot, 
465     // so we bail here and skip those steps
466     if (!m_initialPruningComplete) {
467         String escapedPageURL = escapeSQLString(pageURL);
468         if (!m_mainDB.executeCommand("DELETE FROM PageRetain WHERE url='" + escapedPageURL + "';"))
469             LOG_ERROR("Failed to delete record of icon retention from temporary table for IconURL %s", pageURL.ascii().data());
470         return;
471     }
472
473     
474     // Then mark this pageURL for deletion
475     m_pageURLsPendingDeletion.add(pageURL);
476     
477     // Grab the iconURL and release it
478     String iconURL = iconURLForPageURL(pageURL);
479     if (!iconURL.isEmpty())
480         releaseIconURL(iconURL);
481 }
482
483 void IconDatabase::retainIconURL(const String& iconURL)
484 {
485     ASSERT(!iconURL.isEmpty());
486     
487     if (int retainCount = m_iconURLToRetainCount.get(iconURL)) {
488         ASSERT(retainCount > 0);
489         m_iconURLToRetainCount.set(iconURL, retainCount + 1);
490     } else {
491         m_iconURLToRetainCount.set(iconURL, 1);
492         if (m_iconURLsPendingDeletion.contains(iconURL))
493             m_iconURLsPendingDeletion.remove(iconURL);
494     }   
495 }
496
497 void IconDatabase::releaseIconURL(const String& iconURL)
498 {
499     ASSERT(!iconURL.isEmpty());
500     
501     // If the iconURL has no retain count, we can bail
502     if (!m_iconURLToRetainCount.contains(iconURL))
503         return;
504     
505     // Otherwise, decrement it
506     int retainCount = m_iconURLToRetainCount.get(iconURL) - 1;
507     ASSERT(retainCount > -1);
508     
509     // If the icon is still retained, store the count and bail
510     if (retainCount) {
511         m_iconURLToRetainCount.set(iconURL, retainCount);
512         return;
513     }
514     
515     LOG(IconDatabase, "No more retainers for IconURL %s", iconURL.ascii().data());
516     
517     // Otherwise, this icon is toast.  Remove all traces of its retain count...
518     m_iconURLToRetainCount.remove(iconURL);
519     
520     // And since we delay the actual deletion of icons, so lets add it to that queue
521     m_iconURLsPendingDeletion.add(iconURL);
522 }
523
524 void IconDatabase::forgetPageURL(const String& pageURL)
525 {
526     // Remove the PageURL->IconURL mapping
527     m_pageURLToIconURLMap.remove(pageURL);
528     
529     // And remove this pageURL from the DB
530     forgetPageURLQuery(*m_currentDB, pageURL);
531 }
532     
533 bool IconDatabase::isIconURLRetained(const String& iconURL)
534 {
535     if (iconURL.isEmpty())
536         return false;
537         
538     return m_iconURLToRetainCount.contains(iconURL);
539 }
540
541 void IconDatabase::forgetIconForIconURLFromDatabase(const String& iconURL)
542 {
543     if (iconURL.isEmpty())
544         return;
545
546     // For private browsing safety, since this alters the database, we only forget from the current database
547     // If we're in private browsing and the Icon also exists in the main database, it will be pruned on the next startup
548     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL, false);
549     
550     // If we didn't actually have an icon for this iconURL... well, thats a screwy condition we should track down, but also 
551     // something we could move on from
552     ASSERT(iconID);
553     if (!iconID) {
554         LOG_ERROR("Attempting to forget icon for IconURL %s, though we don't have it in the database", iconURL.ascii().data());
555         return;
556     }
557     
558     if (!m_currentDB->executeCommand(String::sprintf("DELETE FROM Icon WHERE Icon.iconID = %lli;", iconID)))
559         LOG_ERROR("Unable to drop Icon for IconURL", iconURL.ascii().data()); 
560     if (!m_currentDB->executeCommand(String::sprintf("DELETE FROM PageURL WHERE PageURL.iconID = %lli", iconID)))
561         LOG_ERROR("Unable to drop all PageURL for IconURL", iconURL.ascii().data()); 
562 }
563
564 IconDataCache* IconDatabase::getOrCreateIconDataCache(const String& iconURL)
565 {
566     IconDataCache* icon;
567     if ((icon = m_iconURLToIconDataCacheMap.get(iconURL)))
568         return icon;
569         
570     icon = new IconDataCache(iconURL);
571     m_iconURLToIconDataCacheMap.set(iconURL, icon);
572     
573     // Get the most current time stamp for this IconURL
574     int timestamp = 0;
575     if (m_privateBrowsingEnabled)
576         timestamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
577     if (!timestamp)
578         timestamp = timeStampForIconURLQuery(m_mainDB, iconURL);
579         
580     // If we can't get a timestamp for this URL, then it is a new icon and we initialize its timestamp now
581     if (!timestamp) {
582         icon->setTimestamp((int)currentTime());
583         m_iconDataCachesPendingUpdate.add(icon);
584     } else 
585         icon->setTimestamp(timestamp);
586         
587     return icon;
588 }
589
590 void IconDatabase::setIconDataForIconURL(const void* data, int size, const String& iconURL)
591 {
592     ASSERT(size > -1);
593     if (size)
594         ASSERT(data);
595     else
596         data = 0;
597         
598     if (iconURL.isEmpty())
599         return;
600     
601     // Get the IconDataCache for this IconURL (note, IconDataCacheForIconURL will create it if necessary)
602     IconDataCache* icon = getOrCreateIconDataCache(iconURL);
603     
604     // Set the data in the IconDataCache
605     icon->setImageData((unsigned char*)data, size);
606     
607     // Update the timestamp in the IconDataCache to NOW
608     icon->setTimestamp((int)currentTime());
609
610     // Mark the IconDataCache as requiring an update to the database
611     m_iconDataCachesPendingUpdate.add(icon);
612 }
613
614 void IconDatabase::setHaveNoIconForIconURL(const String& iconURL)
615 {   
616     setIconDataForIconURL(0, 0, iconURL);
617 }
618
619 bool IconDatabase::setIconURLForPageURL(const String& iconURL, const String& pageURL)
620 {
621     ASSERT(!iconURL.isEmpty());
622     ASSERT(!pageURL.isEmpty());
623     
624     // If the urls already map to each other, bail.
625     // This happens surprisingly often, and seems to cream iBench performance
626     if (m_pageURLToIconURLMap.get(pageURL) == iconURL)
627         return false;
628
629     // If this pageURL is retained, we have some work to do on the IconURL retain counts
630     if (m_pageURLToRetainCount.contains(pageURL)) {
631         String oldIconURL = m_pageURLToIconURLMap.get(pageURL);
632         if (!oldIconURL.isEmpty())
633             releaseIconURL(oldIconURL);
634         retainIconURL(iconURL);
635     } else {
636         // If this pageURL is *not* retained, then we may be marking it for deletion, as well!
637         // As counterintuitive as it seems to mark it for addition and for deletion at the same time,
638         // it's valid because when we do a new pageURL->iconURL mapping we *have* to mark it for addition,
639         // no matter what, as there is no efficient was to determine if the mapping is in the DB already.
640         // But, if the iconURL is marked for deletion, we'll also mark this pageURL for deletion - if a 
641         // client comes along and retains it before the timer fires, the "pendingDeletion" lists will
642         // be manipulated appopriately and new pageURL will be brought back from the brink
643         if (m_iconURLsPendingDeletion.contains(iconURL))
644             m_pageURLsPendingDeletion.add(pageURL);
645     }
646     
647     // Cache the pageURL->iconURL map
648     m_pageURLToIconURLMap.set(pageURL, iconURL);
649     
650     // And mark this mapping to be added to the database
651     m_pageURLsPendingAddition.add(pageURL);
652     
653     // Then start the timer to commit this change - or further delay the timer if it
654     // was already started
655     m_updateTimer.startOneShot(updateTimerDelay);
656     
657     return true;
658 }
659
660 void IconDatabase::setIconURLForPageURLInDatabase(const String& iconURL, const String& pageURL)
661 {
662     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL);
663     if (!iconID) {
664         LOG_ERROR("Failed to establish an ID for iconURL %s", iconURL.ascii().data());
665         return;
666     }
667     setIconIDForPageURLQuery(*m_currentDB, iconID, pageURL);
668 }
669
670 int64_t IconDatabase::establishIconIDForIconURL(SQLDatabase& db, const String& iconURL, bool createIfNecessary)
671 {
672     String escapedIconURL = escapeSQLString(iconURL);
673     
674     // Get the iconID thats already in this database and return it - or return 0 if we're read-only
675     int64_t iconID = getIconIDForIconURLQuery(db, iconURL);
676     if (iconID || !createIfNecessary)
677         return iconID;
678         
679     // Create the icon table entry for the iconURL
680     return addIconForIconURLQuery(db, iconURL);
681 }
682
683 void IconDatabase::pruneUnretainedIconsOnStartup(Timer<IconDatabase>*)
684 {
685     if (!isOpen())
686         return;
687         
688     // This function should only be called once per run, and ideally only via the timer
689     // on program startup
690     ASSERT(!m_initialPruningComplete);
691
692 #ifndef NDEBUG
693     double timestamp = currentTime();
694 #endif
695     
696     // rdar://4690949 - Need to prune unretained iconURLs here, then prune out all pageURLs that reference
697     // nonexistent icons
698     
699     // Finalize the PageRetain statement
700     delete m_preparedPageRetainInsertStatement;
701     m_preparedPageRetainInsertStatement = 0;
702     
703     // Commit all of the PageRetains and start a new transaction for the pruning dirty-work
704     m_initialPruningTransaction->commit();
705     m_initialPruningTransaction->begin();
706     
707     // Then wipe all PageURLs and Icons that aren't retained
708     if (!m_mainDB.executeCommand("DELETE FROM PageURL WHERE PageURL.url NOT IN (SELECT url FROM PageRetain);") ||
709         !m_mainDB.executeCommand("DELETE FROM Icon WHERE Icon.iconID NOT IN (SELECT iconID FROM PageURL);") ||
710         !m_mainDB.executeCommand("DROP TABLE PageRetain;"))
711         LOG_ERROR("Failed to execute SQL to prune unretained pages and icons from the on-disk tables");
712     
713     
714     // Since we lazily retained the pageURLs without getting the iconURLs or retaining the iconURLs, 
715     // we need to do that now
716     // We now should be in a spiffy situation where we know every single pageURL left in the DB is retained, so we are interested
717     // in the iconURLs for all remaining pageURLs
718     // So we can simply add all the remaining mappings, and retain each pageURL's icon once
719     
720     SQLStatement sql(m_mainDB, "SELECT PageURL.url, Icon.url FROM PageURL INNER JOIN Icon ON PageURL.iconID=Icon.iconID");
721     sql.prepare();
722     int result;
723     while((result = sql.step()) == SQLITE_ROW) {
724         String iconURL = sql.getColumnText16(1);
725         m_pageURLToIconURLMap.set(sql.getColumnText16(0), iconURL);
726         retainIconURL(iconURL);
727         LOG(IconDatabase, "Found a PageURL that mapped to %s", iconURL.ascii().data());
728     }
729     if (result != SQLITE_DONE)
730         LOG_ERROR("Error reading PageURL->IconURL mappings from on-disk DB");
731     sql.finalize();
732     
733     // Commit the transaction and do some cleanup
734     m_initialPruningTransaction->commit();
735     delete m_initialPruningTransaction;
736     m_initialPruningTransaction = 0;
737     
738     m_initialPruningComplete = true;
739     
740 #ifndef NDEBUG
741     timestamp = currentTime() - timestamp;
742     if (timestamp <= 1.0)
743         LOG(IconDatabase, "Pruning unretained icons took %.4f seconds", timestamp);
744     else
745         LOG(IconDatabase, "Pruning unretained icons took %.4f seconds - this is much too long!", timestamp);
746 #endif
747 }
748
749 void IconDatabase::updateDatabase(Timer<IconDatabase>*)
750 {
751     syncDatabase();
752 }
753
754 void IconDatabase::syncDatabase()
755 {
756 #ifndef NDEBUG
757     double timestamp = currentTime();
758 #endif
759
760     // First we'll do the pending additions
761     // Starting with the IconDataCaches that need updating/insertion
762     for (HashSet<IconDataCache*>::iterator i = m_iconDataCachesPendingUpdate.begin(), end = m_iconDataCachesPendingUpdate.end(); i != end; ++i) {
763         (*i)->writeToDatabase(*m_currentDB);
764         LOG(IconDatabase, "Wrote IconDataCache for IconURL %s with timestamp of %lli to the DB", (*i)->getIconURL().ascii().data(), (*i)->getTimestamp());
765     }
766     m_iconDataCachesPendingUpdate.clear();
767     
768     HashSet<String>::iterator i = m_pageURLsPendingAddition.begin(), end = m_pageURLsPendingAddition.end();
769     for (; i != end; ++i) {
770         setIconURLForPageURLInDatabase(m_pageURLToIconURLMap.get(*i), *i);
771         LOG(IconDatabase, "Commited IconURL for PageURL %s to database", (*i).ascii().data());
772     }
773     m_pageURLsPendingAddition.clear();
774     
775     // Then we'll do the pending deletions
776     // First lets wipe all the pageURLs
777     for (i = m_pageURLsPendingDeletion.begin(), end = m_pageURLsPendingDeletion.end(); i != end; ++i) {    
778         forgetPageURL(*i);
779         LOG(IconDatabase, "Deleted PageURL %s", (*i).ascii().data());
780     }
781     m_pageURLsPendingDeletion.clear();
782
783     // Then get rid of all traces of the icons and IconURLs
784     IconDataCache* icon;    
785     for (i = m_iconURLsPendingDeletion.begin(), end = m_iconURLsPendingDeletion.end(); i != end; ++i) {
786         // Forget the IconDataCache
787         icon = m_iconURLToIconDataCacheMap.get(*i);
788         if (icon)
789             m_iconURLToIconDataCacheMap.remove(*i);
790         delete icon;
791         
792         // Forget the IconURL from the database
793         forgetIconForIconURLFromDatabase(*i);
794         LOG(IconDatabase, "Deleted icon %s", (*i).ascii().data());   
795     }
796     m_iconURLsPendingDeletion.clear();
797     
798     // If the timer was running to cause this update, we can kill the timer as its firing would be redundant
799     m_updateTimer.stop();
800     
801 #ifndef NDEBUG
802     timestamp = currentTime() - timestamp;
803     if (timestamp <= 1.0)
804         LOG(IconDatabase, "Updating the database took %.4f seconds", timestamp);
805     else 
806         LOG(IconDatabase, "Updating the database took %.4f seconds - this is much too long!", timestamp);
807 #endif
808 }
809
810 bool IconDatabase::hasEntryForIconURL(const String& iconURL)
811 {
812     if (iconURL.isEmpty())
813         return false;
814         
815     // First check the in memory mapped icons...
816     if (m_iconURLToIconDataCacheMap.contains(iconURL))
817         return true;
818
819     // Then we'll check the main database
820     if (hasIconForIconURLQuery(m_mainDB, iconURL))
821         return true;
822         
823     // Finally, the last resort - check the private browsing database
824     if (m_privateBrowsingEnabled)  
825         if (hasIconForIconURLQuery(m_privateBrowsingDB, iconURL))
826             return true;    
827
828     // We must not have this iconURL!
829     return false;
830 }
831
832 IconDatabase::~IconDatabase()
833 {
834     close();
835 }
836
837 // readySQLStatement() handles two things
838 // 1 - If the SQLDatabase& argument is different, the statement must be destroyed and remade.  This happens when the user
839 //     switches to and from private browsing
840 // 2 - Lazy construction of the Statement in the first place, in case we've never made this query before
841 inline void readySQLStatement(SQLStatement*& statement, SQLDatabase& db, const String& str)
842 {
843     if (statement && statement->database() != &db) {
844         delete statement;
845         statement = 0;
846     }
847     if (!statement) {
848         statement = new SQLStatement(db, str);
849         statement->prepare();
850     }
851 }
852
853 // Any common IconDatabase query should be seperated into a fooQuery() and a *m_fooStatement.  
854 // This way we can lazily construct the SQLStatment for a query on its first use, then reuse the Statement binding
855 // the new parameter as needed
856 // The statement must be deleted in IconDatabase::close() before the actual SQLDatabase::close() call
857 // Also, m_fooStatement must be reset() before fooQuery() returns otherwise we will constantly get "database file locked" 
858 // errors in various combinations of queries
859
860 bool IconDatabase::pageURLTableIsEmptyQuery(SQLDatabase& db)
861 {  
862     // We won't make this use a m_fooStatement because its not really a "common" query
863     return !SQLStatement(db, "SELECT iconID FROM PageURL LIMIT 1;").returnsAtLeastOneResult();
864 }
865
866 void IconDatabase::imageDataForIconURLQuery(SQLDatabase& db, const String& iconURL, Vector<unsigned char>& result)
867 {
868     readySQLStatement(m_imageDataForIconURLStatement, db, "SELECT Icon.data FROM Icon WHERE Icon.url = (?);");
869     m_imageDataForIconURLStatement->bindText16(1, iconURL, false);
870     m_imageDataForIconURLStatement->step();
871     m_imageDataForIconURLStatement->getColumnBlobAsVector(0, result);
872     m_imageDataForIconURLStatement->reset();
873 }
874
875 int IconDatabase::timeStampForIconURLQuery(SQLDatabase& db, const String& iconURL)
876 {
877     readySQLStatement(m_timeStampForIconURLStatement, db, "SELECT Icon.stamp FROM Icon WHERE Icon.url = (?);");
878     m_timeStampForIconURLStatement->bindText16(1, iconURL, false);
879     m_timeStampForIconURLStatement->step();
880     int result = m_timeStampForIconURLStatement->getColumnInt(0);
881     m_timeStampForIconURLStatement->reset();
882     return result;
883 }
884
885 String IconDatabase::iconURLForPageURLQuery(SQLDatabase& db, const String& pageURL)
886 {
887     readySQLStatement(m_iconURLForPageURLStatement, db, "SELECT Icon.url FROM Icon, PageURL WHERE PageURL.url = (?) AND Icon.iconID = PageURL.iconID;");
888     m_iconURLForPageURLStatement->bindText16(1, pageURL, false);
889     m_iconURLForPageURLStatement->step();
890     String result = m_iconURLForPageURLStatement->getColumnText16(0);
891     m_iconURLForPageURLStatement->reset();
892     return result;
893 }
894
895 void IconDatabase::forgetPageURLQuery(SQLDatabase& db, const String& pageURL)
896 {
897     readySQLStatement(m_forgetPageURLStatement, db, "DELETE FROM PageURL WHERE url = (?);");
898     m_forgetPageURLStatement->bindText16(1, pageURL, false);
899     m_forgetPageURLStatement->step();
900     m_forgetPageURLStatement->reset();
901 }
902
903 void IconDatabase::setIconIDForPageURLQuery(SQLDatabase& db, int64_t iconID, const String& pageURL)
904 {
905     readySQLStatement(m_setIconIDForPageURLStatement, db, "INSERT INTO PageURL (url, iconID) VALUES ((?), ?);");
906     m_setIconIDForPageURLStatement->bindText16(1, pageURL, false);
907     m_setIconIDForPageURLStatement->bindInt64(2, iconID);
908     m_setIconIDForPageURLStatement->step();
909     m_setIconIDForPageURLStatement->reset();
910 }
911
912 int64_t IconDatabase::getIconIDForIconURLQuery(SQLDatabase& db, const String& iconURL)
913 {
914     readySQLStatement(m_getIconIDForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
915     m_getIconIDForIconURLStatement->bindText16(1, iconURL, false);
916     m_getIconIDForIconURLStatement->step();
917     int64_t result = m_getIconIDForIconURLStatement->getColumnInt64(0);
918     m_getIconIDForIconURLStatement->reset();
919     return result;
920 }
921
922 int64_t IconDatabase::addIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
923 {
924     readySQLStatement(m_addIconForIconURLStatement, db, "INSERT INTO Icon (url) VALUES ((?));");
925     m_addIconForIconURLStatement->bindText16(1, iconURL, false);
926     int64_t result = 0;
927     if (m_addIconForIconURLStatement->step() == SQLITE_OK)
928         result = db.lastInsertRowID();
929     m_addIconForIconURLStatement->reset();
930     return result;
931 }
932
933 bool IconDatabase::hasIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
934 {
935     readySQLStatement(m_hasIconForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
936     m_hasIconForIconURLStatement->bindText16(1, iconURL, false);
937     bool result = m_hasIconForIconURLStatement->step() == SQLITE_ROW;
938     m_hasIconForIconURLStatement->reset();
939     return result;
940 }
941
942 } //namespace WebCore