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