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 "Image.h"
30 #include "Logging.h"
31 #include "PlatformString.h"
32 #include <errno.h>
33 #include <sys/stat.h>
34 #include <sys/types.h>
35 #include <time.h>
36
37
38 // 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
39 // multiple strings and numbers should be handled differently than with String + String + String + etc.
40
41 namespace WebCore {
42
43 IconDatabase* IconDatabase::m_sharedInstance = 0;
44
45 // This version number is in the DB and marks the current generation of the schema
46 // Theoretically once the switch is flipped this should never change
47 // Currently, an out-of-date schema causes the DB to be wiped and reset.  This isn't 
48 // so bad during development but in the future, we would need to write a conversion
49 // function to advance older released schemas to "current"
50 const int IconDatabase::currentDatabaseVersion = 4;
51
52 // Icons expire once a day
53 const int IconDatabase::iconExpirationTime = 60*60*24; 
54 // Absent icons are rechecked once a week
55 const int IconDatabase::missingIconExpirationTime = 60*60*24*7; 
56
57 const int IconDatabase::updateTimerDelay = 5; 
58
59 const String& IconDatabase::defaultDatabaseFilename()
60 {
61     static String defaultDatabaseFilename = "icon.db";
62     return defaultDatabaseFilename;
63 }
64
65 // Query - Checks for at least 1 entry in the PageURL table
66 bool pageURLTableIsEmptyQuery(SQLDatabase&);
67 // Query - Returns the time stamp for an Icon entry
68 int timeStampForIconURLQuery(SQLDatabase&, const String& iconURL);    
69 // Query - Returns the IconURL for a PageURL
70 String iconURLForPageURLQuery(SQLDatabase&, const String& pageURL);    
71 // Query - Checks for the existence of the given IconURL in the Icon table
72 bool hasIconForIconURLQuery(SQLDatabase& db, const String& iconURL);
73 // Query - Deletes a PageURL from the PageURL table
74 void forgetPageURLQuery(SQLDatabase& db, const String& pageURL);
75 // Query - Sets the Icon.iconID for a PageURL in the PageURL table
76 void setIconIDForPageURLQuery(SQLDatabase& db, int64_t, const String&);
77 // Query - Returns the iconID for the given IconURL
78 int64_t getIconIDForIconURLQuery(SQLDatabase& db, const String& iconURL);
79 // Query - Creates the Icon entry for the given IconURL and returns the resulting iconID
80 int64_t addIconForIconURLQuery(SQLDatabase& db, const String& iconURL);
81 // Query - Returns the image data from the given database for the given IconURL
82 Vector<unsigned char> imageDataForIconURLQuery(SQLDatabase& db, const String& iconURL);   
83
84 IconDatabase* IconDatabase::sharedIconDatabase()
85 {
86     if (!m_sharedInstance) {
87         m_sharedInstance = new IconDatabase();
88     }
89     return m_sharedInstance;
90 }
91
92 IconDatabase::IconDatabase()
93     : m_currentDB(&m_mainDB)
94     , m_privateBrowsingEnabled(false)
95     , m_startupTimer(this, &IconDatabase::pruneUnretainedIconsOnStartup)
96     , m_updateTimer(this, &IconDatabase::updateDatabase)
97 {
98     
99 }
100
101 bool IconDatabase::open(const String& databasePath)
102 {
103     if (isOpen()) {
104         LOG_ERROR("Attempt to reopen the IconDatabase which is already open.  Must close it first.");
105         return false;
106     }
107     
108     // First we'll formulate the full path for the database file
109     String dbFilename;
110     if (databasePath[databasePath.length()] == '/')
111         dbFilename = databasePath + defaultDatabaseFilename();
112     else
113         dbFilename = databasePath + "/" + defaultDatabaseFilename();
114
115     // Now, we'll see if we can open the on-disk table
116     // If we can't, this ::open() failed and we should bail now
117     if (!m_mainDB.open(dbFilename)) {
118         LOG(IconDatabase, "Unable to open icon database at path %s", dbFilename.ascii().data());
119         return false;
120     }
121     
122     if (!isValidDatabase(m_mainDB)) {
123         LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", dbFilename.ascii().data());
124         clearDatabaseTables(m_mainDB);
125         createDatabaseTables(m_mainDB);
126     }
127
128     // 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
129     bool result;
130     result = m_mainDB.executeCommand("CREATE TEMP TABLE PageRetain (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,count INTEGER NOT NULL ON CONFLICT FAIL);");
131     // Creating an in-memory temp table should never, ever, ever fail
132     ASSERT(result);
133     
134     // These are actually two different SQLite config options - not my fault they are named confusingly  ;)
135     m_mainDB.setSynchronous(SQLDatabase::SyncOff);    
136     m_mainDB.setFullsync(false);
137     
138     // Open the in-memory table for private browsing
139     if (!m_privateBrowsingDB.open(":memory:"))
140         LOG_ERROR("Unabled to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
141
142     // Only if we successfully remained open will we start our "initial purge timer"
143     // rdar://4690949 - when we have deferred reads and writes all the way in, the prunetimer
144     // will become "deferredTimer" or something along those lines, and will be set only when
145     // a deferred read/write is queued
146     if (isOpen())
147         m_startupTimer.startOneShot(0);
148     
149     return isOpen();
150 }
151
152 void IconDatabase::close()
153 {
154     syncDatabase();
155     m_mainDB.close();
156     m_privateBrowsingDB.close();
157 }
158
159 bool IconDatabase::isEmpty()
160 {
161     if (m_privateBrowsingEnabled)
162         if (!pageURLTableIsEmptyQuery(m_privateBrowsingDB))
163             return false;
164             
165     return pageURLTableIsEmptyQuery(m_mainDB);
166 }
167
168 bool IconDatabase::isValidDatabase(SQLDatabase& db)
169 {
170     // These two tables should always exist in a valid db
171     if (!db.tableExists("Icon") || !db.tableExists("PageURL") || !db.tableExists("IconDatabaseInfo"))
172         return false;
173     
174     if (SQLStatement(db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0) < currentDatabaseVersion) {
175         LOG(IconDatabase, "DB version is not found or below expected valid version");
176         return false;
177     }
178     
179     return true;
180 }
181
182 void IconDatabase::clearDatabaseTables(SQLDatabase& db)
183 {
184     String query = "SELECT name FROM sqlite_master WHERE type='table';";
185     Vector<String> tables;
186     if (!SQLStatement(db, query).returnTextResults16(0, tables)) {
187         LOG(IconDatabase, "Unable to retrieve list of tables from database");
188         return;
189     }
190     
191     for (Vector<String>::iterator table = tables.begin(); table != tables.end(); ++table ) {
192         if (!db.executeCommand("DROP TABLE " + *table)) {
193             LOG(IconDatabase, "Unable to drop table %s", (*table).ascii().data());
194         }
195     }
196 }
197
198 void IconDatabase::createDatabaseTables(SQLDatabase& db)
199 {
200     if (!db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
201         LOG_ERROR("Could not create PageURL table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
202         db.close();
203         return;
204     }
205     if (!db.executeCommand("CREATE TABLE Icon (iconID INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL UNIQUE ON CONFLICT FAIL, stamp INTEGER, data BLOB);")) {
206         LOG_ERROR("Could not create Icon table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
207         db.close();
208         return;
209     }
210     if (!db.executeCommand("CREATE TRIGGER update_icon_timestamp AFTER UPDATE ON Icon BEGIN UPDATE Icon SET stamp = strftime('%s','now') WHERE iconID = new.iconID; END;")) {
211         LOG_ERROR("Could not create timestamp updater in database (%i) - %s", db.lastError(), db.lastErrorMsg());
212         db.close();
213         return;
214     }
215     if (!db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
216         LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
217         db.close();
218         return;
219     }
220     if (!db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
221         LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", db.lastError(), db.lastErrorMsg());
222         db.close();
223         return;
224     }
225 }    
226
227 Vector<unsigned char> IconDatabase::imageDataForIconURL(const String& iconURL)
228 {      
229     // If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
230     if (m_privateBrowsingEnabled) {    
231         Vector<unsigned char> blob = imageDataForIconURLQuery(m_privateBrowsingDB, iconURL);
232         if (!blob.isEmpty())
233             return blob;
234     } 
235     
236     // It wasn't found there, so lets check the main tables
237     return imageDataForIconURLQuery(m_mainDB, iconURL);
238 }
239
240 void IconDatabase::setPrivateBrowsingEnabled(bool flag)
241 {
242     if (m_privateBrowsingEnabled == flag)
243         return;
244     
245     // Sync any deferred DB changes before we change the active DB
246     syncDatabase();
247     
248     m_privateBrowsingEnabled = flag;
249     
250     if (m_privateBrowsingEnabled) {
251         createDatabaseTables(m_privateBrowsingDB);
252         m_currentDB = &m_privateBrowsingDB;
253     } else {
254         clearDatabaseTables(m_privateBrowsingDB);
255         m_currentDB = &m_mainDB;
256     }
257 }
258
259 Image* IconDatabase::iconForPageURL(const String& pageURL, const IntSize& size, bool cache)
260 {   
261     // See if we even have an IconURL for this PageURL...
262     String iconURL = iconURLForPageURL(pageURL);
263     if (iconURL.isEmpty())
264         return 0;
265     
266     // If we do, maybe we have a SiteIcon for this IconURL
267     if (m_iconURLToSiteIcons.contains(iconURL)) {
268         SiteIcon* icon = m_iconURLToSiteIcons.get(iconURL);
269         return icon->getImage(size);
270     }
271         
272     // If we don't have either, we have to create the SiteIcon
273     SiteIcon* icon = new SiteIcon(iconURL);
274     m_iconURLToSiteIcons.set(iconURL, icon);
275     return icon->getImage(size);
276 }
277
278 // FIXME 4667425 - this check needs to see if the icon's data is empty or not and apply
279 // iconExpirationTime to present icons, and missingIconExpirationTime for missing icons
280 bool IconDatabase::isIconExpiredForIconURL(const String& iconURL)
281 {
282     if (iconURL.isEmpty()) 
283         return true;
284     
285     int stamp;
286     if (m_privateBrowsingEnabled) {
287         stamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
288         if (stamp)
289             return (time(NULL) - stamp) > iconExpirationTime;
290     }
291     
292     stamp = timeStampForIconURLQuery(m_mainDB, iconURL);
293     if (stamp)
294         return (time(NULL) - stamp) > iconExpirationTime;
295     return false;
296 }
297     
298 String IconDatabase::iconURLForPageURL(const String& pageURL)
299 {    
300     if (pageURL.isEmpty()) 
301         return String();
302         
303     if (m_pageURLToIconURLMap.contains(pageURL))
304         return m_pageURLToIconURLMap.get(pageURL);
305     
306     // Try the private browsing database because if any PageURL's IconURL was updated during privated browsing, 
307     // the most up-to-date iconURL would be there
308     if (m_privateBrowsingEnabled) {
309         String iconURL = iconURLForPageURLQuery(m_privateBrowsingDB, pageURL);
310         if (!iconURL.isEmpty()) {
311             m_pageURLToIconURLMap.set(pageURL, iconURL);
312             return iconURL;
313         }
314     }
315     
316     String iconURL = iconURLForPageURLQuery(m_mainDB, pageURL);
317     if (!iconURL.isEmpty())
318         m_pageURLToIconURLMap.set(pageURL, iconURL);
319     return iconURL;
320 }
321
322 Image* IconDatabase::defaultIcon(const IntSize& size)
323 {
324     return 0;
325 }
326
327 void IconDatabase::retainIconForPageURL(const String& pageURL)
328 {
329     if (pageURL.isEmpty())
330         return;
331         
332     int retainCount;
333     
334     // If we don't have the retain count for this page, we need to setup records of its retain
335     // Otherwise, get the count and increment it
336     if (!m_pageURLToRetainCount.contains(pageURL)) {
337         // If this pageURL is marked for deletion, bring it back from the brink
338         m_pageURLsPendingDeletion.remove(pageURL);
339         
340         // The new retain count will be 1
341         retainCount = 1;
342         
343          // If we have an iconURL for this pageURL, we'll now retain the iconURL
344         String iconURL = iconURLForPageURL(pageURL);
345         if (!iconURL.isEmpty())
346             retainIconURL(iconURL);
347
348     } else
349         retainCount = m_pageURLToRetainCount.get(pageURL) + 1;
350         
351     m_pageURLToRetainCount.set(pageURL, retainCount);        
352 }
353
354 void IconDatabase::releaseIconForPageURL(const String& pageURL)
355 {
356     if (pageURL.isEmpty())
357         return;
358         
359     // Check if this pageURL is actually retained
360     if(!m_pageURLToRetainCount.contains(pageURL)) {
361         LOG_ERROR("Attempting to release icon for URL %s which is not retained", pageURL.ascii().data());
362         return;
363     }
364     
365     // Get its retain count
366     int retainCount = m_pageURLToRetainCount.get(pageURL);
367     ASSERT(retainCount > 0);
368     
369     // If it still has a positive retain count, store the new count and bail
370     if (--retainCount) {
371         m_pageURLToRetainCount.set(pageURL, retainCount);
372         return;
373     }
374     
375     LOG(IconDatabase, "No more retainers for PageURL %s", pageURL.ascii().data());
376     
377     // Otherwise, remove all record of the retain count
378     m_pageURLToRetainCount.remove(pageURL);   
379     
380     // Then mark this pageURL for deletion
381     m_pageURLsPendingDeletion.add(pageURL);
382             
383     // Grab the iconURL and release it
384     String iconURL = iconURLForPageURL(pageURL);
385     if (!iconURL.isEmpty())
386         releaseIconURL(iconURL);
387 }
388
389 void IconDatabase::retainIconURL(const String& iconURL)
390 {
391     ASSERT(!iconURL.isEmpty());
392     
393     if (m_iconURLToRetainCount.contains(iconURL)) {
394         ASSERT(m_iconURLToRetainCount.get(iconURL) > 0);
395         m_iconURLToRetainCount.set(iconURL, m_iconURLToRetainCount.get(iconURL) + 1);
396     } else {
397         m_iconURLToRetainCount.set(iconURL, 1);
398         if (m_iconURLsPendingDeletion.contains(iconURL))
399             m_iconURLsPendingDeletion.remove(iconURL);
400     }
401 }
402
403 void IconDatabase::releaseIconURL(const String& iconURL)
404 {
405     ASSERT(!iconURL.isEmpty());
406     
407     // If the iconURL has no retain count, we can bail
408     if (!m_iconURLToRetainCount.contains(iconURL))
409         return;
410     
411     // Otherwise, decrement it
412     int retainCount = m_iconURLToRetainCount.get(iconURL) - 1;
413     ASSERT(retainCount > -1);
414     
415     // If the icon is still retained, store the count and bail
416     if (retainCount) {
417         m_iconURLToRetainCount.set(iconURL, retainCount);
418         return;
419     }
420     
421     LOG(IconDatabase, "No more retainers for IconURL %s", iconURL.ascii().data());
422     
423     // Otherwise, this icon is toast.  Remove all traces of its retain count...
424     m_iconURLToRetainCount.remove(iconURL);
425     
426     // And since we delay the actual deletion of icons, so lets add it to that queue
427     m_iconURLsPendingDeletion.add(iconURL);
428 }
429
430 void IconDatabase::forgetPageURL(const String& pageURL)
431 {
432     // Remove the PageURL->IconURL mapping
433     m_pageURLToIconURLMap.remove(pageURL);
434     
435     // And remove this pageURL from the DB
436     forgetPageURLQuery(*m_currentDB, pageURL);
437 }
438     
439 bool IconDatabase::isIconURLRetained(const String& iconURL)
440 {
441     if (iconURL.isEmpty())
442         return false;
443         
444     return m_iconURLToRetainCount.contains(iconURL);
445 }
446
447 void IconDatabase::forgetIconForIconURLFromDatabase(const String& iconURL)
448 {
449     if (iconURL.isEmpty())
450         return;
451
452     // For private browsing safety, since this alters the database, we only forget from the current database
453     // If we're in private browsing and the Icon also exists in the main database, it will be pruned on the next startup
454     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL, false);
455     
456     // If we didn't actually have an icon for this iconURL... well, thats a screwy condition we should track down, but also 
457     // something we could move on from
458     ASSERT(iconID);
459     if (!iconID) {
460         LOG_ERROR("Attempting to forget icon for IconURL %s, though we don't have it in the database", iconURL.ascii().data());
461         return;
462     }
463         
464     String escapedIconURL = iconURL;
465     escapedIconURL.replace('\'', "''");
466     
467     if (!m_currentDB->executeCommand(String::sprintf("DELETE FROM Icon WHERE Icon.iconID = %lli;", iconID)))
468         LOG_ERROR("Unable to drop Icon for IconURL", iconURL.ascii().data()); 
469     if (!m_currentDB->executeCommand(String::sprintf("DELETE FROM PageURL WHERE PageURL.iconID = %lli", iconID)))
470         LOG_ERROR("Unable to drop all PageURL for IconURL", iconURL.ascii().data()); 
471 }
472
473 void IconDatabase::setIconDataForIconURL(const void* data, int size, const String& iconURL)
474 {
475     ASSERT(size > -1);
476     if (size)
477         ASSERT(data);
478     else
479         data = 0;
480         
481     if (iconURL.isEmpty())
482         return;
483     
484     // First, if we already have a SiteIcon in memory, let's update its image data
485     if (m_iconURLToSiteIcons.contains(iconURL))
486         m_iconURLToSiteIcons.get(iconURL)->manuallySetImageData((unsigned char*)data, size);
487
488     // Next, we actually commit the image data to the database
489     // Start by making sure there's an entry for this IconURL in the current database
490     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL, true);
491     ASSERT(iconID);
492     
493     // First we create and prepare the SQLStatement
494     // The following statement also works to set the icon data to NULL because sqlite defaults unbound ? parameters to NULL
495     SQLStatement sql(*m_currentDB, "UPDATE Icon SET data = ? WHERE iconID = ?;");
496     sql.prepare();
497         
498     // Then we bind the icondata and iconID to the SQLStatement
499     if (data)
500         sql.bindBlob(1, data, size);
501     sql.bindInt64(2, iconID);
502     
503     // Finally we step and make sure the step was successful
504     if (sql.step() != SQLITE_DONE)
505         LOG_ERROR("Unable to set icon data for iconURL %s", iconURL.ascii().data());
506         
507     return;
508 }
509
510 void IconDatabase::setHaveNoIconForIconURL(const String& iconURL)
511 {   
512     setIconDataForIconURL(0, 0, iconURL);
513 }
514
515 void IconDatabase::setIconURLForPageURL(const String& iconURL, const String& pageURL)
516 {
517     ASSERT(!iconURL.isEmpty());
518     ASSERT(!pageURL.isEmpty());
519     
520     // If the urls already map to each other, bail.
521     // This happens surprisingly often, and seems to cream iBench performance
522     if (m_pageURLToIconURLMap.get(pageURL) == iconURL)
523         return;
524
525     // If this pageURL is retained, we have some work to do on the IconURL retain counts
526     if (m_pageURLToRetainCount.contains(pageURL)) {
527         String oldIconURL = m_pageURLToIconURLMap.get(pageURL);
528         if (!oldIconURL.isEmpty())
529             releaseIconURL(oldIconURL);
530         retainIconURL(iconURL);
531     } else {
532         // If this pageURL is *not* retained, then we may be marking it for deletion, as well!
533         // As counterintuitive as it seems to mark it for addition and for deletion at the same time,
534         // it's valid because when we do a new pageURL->iconURL mapping we *have* to mark it for addition,
535         // no matter what, as there is no efficient was to determine if the mapping is in the DB already.
536         // But, if the iconURL is marked for deletion, we'll also mark this pageURL for deletion - if a 
537         // client comes along and retains it before the timer fires, the "pendingDeletion" lists will
538         // be manipulated appopriately and new pageURL will be brought back from the brink
539         if (m_iconURLsPendingDeletion.contains(iconURL))
540             m_pageURLsPendingDeletion.add(pageURL);
541     }
542     
543     // Cache the pageURL->iconURL map
544     m_pageURLToIconURLMap.set(pageURL, iconURL);
545     
546     // And mark this mapping to be added to the database
547     m_pageURLsPendingAddition.set(pageURL, iconURL);
548     
549     // Then start the timer to commit this change - or further delay the timer if it
550     // was already started
551     m_updateTimer.startOneShot(updateTimerDelay);
552 }
553
554 void IconDatabase::setIconURLForPageURLInDatabase(const String& iconURL, const String& pageURL)
555 {
556     int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL);
557     if (!iconID) {
558         LOG_ERROR("Failed to establish an ID for iconURL %s", iconURL.ascii().data());
559         return;
560     }
561     setIconIDForPageURLQuery(*m_currentDB, iconID, pageURL);
562 }
563
564 int64_t IconDatabase::establishIconIDForIconURL(SQLDatabase& db, const String& iconURL, bool createIfNecessary)
565 {
566     String escapedIconURL = iconURL;
567     escapedIconURL.replace('\'', "''");
568     
569     // Get the iconID thats already in this database and return it - or return 0 if we're read-only
570     int64_t iconID = getIconIDForIconURLQuery(db, iconURL);
571     if (iconID || !createIfNecessary)
572         return iconID;
573         
574     // Create the icon table entry for the iconURL
575     return addIconForIconURLQuery(db, iconURL);
576 }
577
578 void IconDatabase::pruneUnreferencedIcons(int numberToPrune)
579 {
580     if (!numberToPrune || !isOpen())
581         return;
582     
583     if (numberToPrune > 0) {
584         if (!m_mainDB.executeCommand(String::sprintf("DELETE FROM Icon WHERE Icon.iconID IN (SELECT Icon.iconID FROM Icon WHERE Icon.iconID NOT IN(SELECT PageURL.iconID FROM PageURL) LIMIT %i);", numberToPrune)))
585             LOG_ERROR("Failed to prune %i unreferenced icons from the DB - %s", numberToPrune, m_mainDB.lastErrorMsg());
586     } else {
587         if (!m_mainDB.executeCommand("DELETE FROM Icon WHERE Icon.iconID IN (SELECT Icon.iconID FROM Icon WHERE Icon.iconID NOT IN(SELECT PageURL.iconID FROM PageURL));"))
588             LOG_ERROR("Failed to prune all unreferenced icons from the DB - %s", m_mainDB.lastErrorMsg());
589     }
590 }
591
592 void IconDatabase::pruneUnretainedIconsOnStartup(Timer<IconDatabase>*)
593 {
594     if (!isOpen())
595         return;
596         
597 #ifndef NDEBUG
598     CFTimeInterval start = CFAbsoluteTimeGetCurrent();
599 #endif
600     
601     // FIXME - rdar://4690949 - Need to prune unretained pageURLs here
602     LOG_ERROR("pruneUnretainedIconsOnStartup is still unimplemented");
603     pruneUnreferencedIcons(-1);
604
605 #ifndef NDEBUG
606     CFTimeInterval duration = CFAbsoluteTimeGetCurrent() - start;
607     LOG(IconDatabase, "Pruning unretained icons took %.4f seconds", duration);
608     if (duration > 1.0) 
609         LOG_ERROR("Pruning unretained icons took %.4f seconds - this is much too long!", duration);
610 #endif
611 }
612
613 void IconDatabase::updateDatabase(Timer<IconDatabase>*)
614 {
615     syncDatabase();
616 }
617
618 void IconDatabase::syncDatabase()
619 {
620 #ifndef NDEBUG
621     CFTimeInterval start = CFAbsoluteTimeGetCurrent();
622 #endif
623
624     // First we'll do the pending additions
625     for (HashMap<String, String>::iterator i = m_pageURLsPendingAddition.begin(); i != m_pageURLsPendingAddition.end(); ++i) {
626         // The HashMap maps PageURL->IconURL, but the method takes IconURL then PageURL - so this ordering is correct
627         setIconURLForPageURLInDatabase(i->second, i->first);
628         LOG(IconDatabase, "Commited IconURL %s for PageURL %s to database", (i->second).ascii().data(), (i->first).ascii().data());
629     }
630     m_pageURLsPendingAddition.clear();
631     
632     // Then we'll do the pending deletions
633     // First lets wipe all the pageURLs
634     HashSet<String>::iterator i = m_pageURLsPendingDeletion.begin();
635     for (; i != m_pageURLsPendingDeletion.end(); ++i) {    
636         forgetPageURL(*i);
637         LOG(IconDatabase, "Deleted PageURL %s", (*i).ascii().data());
638     }
639     m_pageURLsPendingDeletion.clear();
640
641     // Then get rid of all traces of the icons and IconURLs
642     SiteIcon* icon;    
643     for (i = m_iconURLsPendingDeletion.begin(); i != m_iconURLsPendingDeletion.end(); ++i) {
644         // Forget the SiteIcon
645         icon = m_iconURLToSiteIcons.get(*i);
646         m_iconURLToSiteIcons.remove(*i);
647         delete icon;
648         
649         // Forget the IconURL from the database
650         forgetIconForIconURLFromDatabase(*i);
651         LOG(IconDatabase, "Deleted icon %s", (*i).ascii().data());   
652     }
653     m_iconURLsPendingDeletion.clear();
654     
655     // If the timer was running to cause this update, we can kill the timer as its firing would be redundant
656     m_updateTimer.stop();
657     
658 #ifndef NDEBUG
659     CFTimeInterval duration = CFAbsoluteTimeGetCurrent() - start;
660     LOG(IconDatabase, "Updating the database took %.4f seconds", duration);
661     if (duration > 1.0) 
662         LOG_ERROR("Updating the database took %.4f seconds - this is much too long!", duration);
663 #endif
664 }
665
666 bool IconDatabase::hasEntryForIconURL(const String& iconURL)
667 {
668     if (iconURL.isEmpty())
669         return false;
670         
671     // First check the in memory mapped icons...
672     if (m_iconURLToSiteIcons.contains(iconURL))
673         return true;
674
675     // Then we'll check the main database
676     if (hasIconForIconURLQuery(m_mainDB, iconURL))
677         return true;
678         
679     // Finally, the last resort - check the private browsing database
680     if (m_privateBrowsingEnabled)  
681         if (hasIconForIconURLQuery(m_privateBrowsingDB, iconURL))
682             return true;    
683
684     // We must not have this iconURL!
685     return false;
686 }
687
688 IconDatabase::~IconDatabase()
689 {
690     close();
691 }
692
693 // Query helper functions
694 bool pageURLTableIsEmptyQuery(SQLDatabase& db)
695 {
696     return !(SQLStatement(db, "SELECT iconID FROM PageURL LIMIT 1;").returnsAtLeastOneResult());
697 }
698
699 Vector<unsigned char> imageDataForIconURLQuery(SQLDatabase& db, const String& iconURL)
700 {
701     String escapedIconURL = iconURL;
702     escapedIconURL.replace('\'', "''");
703     return SQLStatement(db, "SELECT Icon.data FROM Icon WHERE Icon.url = '" + escapedIconURL + "';").getColumnBlobAsVector(0);
704 }
705
706 int timeStampForIconURLQuery(SQLDatabase& db, const String& iconURL)
707 {
708     String escapedIconURL = iconURL;
709     escapedIconURL.replace('\'', "''");
710     return SQLStatement(db, "SELECT Icon.stamp FROM Icon WHERE Icon.url = '" + escapedIconURL + "';").getColumnInt(0);
711 }
712
713
714 String iconURLForPageURLQuery(SQLDatabase& db, const String& pageURL)
715 {
716     String escapedPageURL = pageURL;
717     escapedPageURL.replace('\'', "''");
718     return SQLStatement(db, "SELECT Icon.url FROM Icon, PageURL WHERE PageURL.url = '" + escapedPageURL + "' AND Icon.iconID = PageURL.iconID").getColumnText16(0);
719 }
720
721 void forgetPageURLQuery(SQLDatabase& db, const String& pageURL)
722 {
723     String escapedPageURL = pageURL;
724     escapedPageURL.replace('\'', "''");
725     
726     db.executeCommand("DELETE FROM PageURL WHERE url = '" + escapedPageURL + "';");
727 }
728
729 void setIconIDForPageURLQuery(SQLDatabase& db, int64_t iconID, const String& pageURL)
730 {
731     String escapedPageURL = pageURL;
732     escapedPageURL.replace('\'', "''");
733     if (!db.executeCommand("INSERT INTO PageURL (url, iconID) VALUES ('" + escapedPageURL + "', " + String::number(iconID) + ");"))
734         LOG_ERROR("Failed to set iconid %lli for PageURL %s", iconID, pageURL.ascii().data());
735 }
736
737 int64_t getIconIDForIconURLQuery(SQLDatabase& db, const String& iconURL)
738 {
739     String escapedIconURL = iconURL;
740     escapedIconURL.replace('\'', "''");
741     return SQLStatement(db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = '" + escapedIconURL + "';").getColumnInt64(0);
742 }
743
744 int64_t addIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
745 {
746     String escapedIconURL = iconURL;
747     escapedIconURL.replace('\'', "''");
748     if (db.executeCommand("INSERT INTO Icon (url) VALUES ('" + escapedIconURL + "');"))
749         return db.lastInsertRowID();
750     return 0;
751 }
752
753 bool hasIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
754 {
755     String escapedIconURL = iconURL;
756     escapedIconURL.replace('\'', "''");
757     return SQLStatement(db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = '" + escapedIconURL + "';").returnsAtLeastOneResult();
758 }
759
760 } //namespace WebCore