Reviewed by Anders and Tim Hatcher
[WebKit-https.git] / WebCore / 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 #include "IconDatabase.h"
26
27 #include "Image.h"
28 #include "Logging.h"
29 #include "PlatformString.h"
30
31 #include <errno.h>
32 #include <sys/types.h>
33 #include <sys/stat.h>
34 #include <time.h>
35
36
37
38 // FIXME - Make sure we put a private browsing consideration in that uses the temporary tables anytime private browsing would be an issue.
39
40 const char* DefaultIconDatabaseFilename = "/icon.db";
41
42 namespace WebCore {
43
44 IconDatabase* IconDatabase::m_sharedInstance = 0;
45 const int IconDatabase::currentDatabaseVersion = 3;
46
47 IconDatabase* IconDatabase::sharedIconDatabase()
48 {
49     if (!m_sharedInstance) {
50         m_sharedInstance = new IconDatabase();
51     }
52     return m_sharedInstance;
53 }
54
55 IconDatabase::IconDatabase()
56     : m_privateBrowsingEnabled(false)
57 {
58     close();
59 }
60
61 bool IconDatabase::open(const String& databasePath)
62 {
63     close();
64     String dbFilename = databasePath + DefaultIconDatabaseFilename;
65     if (!m_db.open(dbFilename)) {
66         LOG(IconDatabase, "Unable to open icon database at path %s", dbFilename.ascii().data());
67         return false;
68     }
69     
70     if (!isValidDatabase()) {
71         LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", dbFilename.ascii().data());
72         clearDatabase();
73         recreateDatabase();
74     }
75
76     // These are actually two different SQLite config options - not my fault they are named confusingly  ;)
77     m_db.setSynchronous(SQLDatabase::SyncOff);    
78     m_db.setFullsync(false);
79     
80     return isOpen();
81 }
82
83 void IconDatabase::close()
84 {
85     //TODO - sync any cached info before m_db.close();
86     m_db.close();
87 }
88
89
90 bool IconDatabase::isValidDatabase()
91 {
92     if (!m_db.tableExists("Icon") || !m_db.tableExists("PageURL") || !m_db.tableExists("IconResource") || !m_db.tableExists("IconDatabaseInfo")) {
93         return false;
94     }
95     
96     if (SQLStatement(m_db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0) < currentDatabaseVersion) {
97         LOG(IconDatabase, "DB version is not found or below expected valid version");
98         return false;
99     }
100     
101     return true;
102 }
103
104 void IconDatabase::clearDatabase()
105 {
106     String query = "SELECT name FROM sqlite_master WHERE type='table';";
107     Vector<String> tables;
108     if (!SQLStatement(m_db, query).returnTextResults16(0, tables)) {
109         LOG(IconDatabase, "Unable to retrieve list of tables from database");
110         return;
111     }
112     
113     for (Vector<String>::iterator table = tables.begin(); table != tables.end(); ++table ) {
114         if (!m_db.executeCommand("DROP TABLE " + *table)) {
115             LOG(IconDatabase, "Unable to drop table %s", (*table).ascii().data());
116         }
117     }
118     
119     deletePrivateTables();
120 }
121
122 void IconDatabase::recreateDatabase()
123 {
124     if (!m_db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
125         LOG_ERROR("Could not create IconDatabaseInfo table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
126         m_db.close();
127         return;
128     }
129     if (!m_db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
130         LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
131         m_db.close();
132         return;
133     }
134     if (!m_db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
135         LOG_ERROR("Could not create PageURL table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
136         m_db.close();
137         return;
138     }
139     if (!m_db.executeCommand("CREATE TABLE Icon (iconID INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL UNIQUE ON CONFLICT FAIL, expires INTEGER);")) {
140         LOG_ERROR("Could not create Icon table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
141         m_db.close();
142         return;
143     }
144     if (!m_db.executeCommand("CREATE TABLE IconResource (iconID integer NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,data BLOB, touch INTEGER);")) {
145         LOG_ERROR("Could not create IconResource table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
146         m_db.close();
147         return;
148     }
149     if (!m_db.executeCommand("CREATE TRIGGER create_icon_resource AFTER INSERT ON Icon BEGIN INSERT INTO IconResource (iconID, data) VALUES (new.iconID, NULL); END;")) {
150         LOG_ERROR("Unable to create create_icon_resource trigger in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
151         m_db.close();
152         return;
153     }
154 }    
155
156 void IconDatabase::createPrivateTables()
157 {
158     if (!m_db.executeCommand("CREATE TEMP TABLE TempPageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) 
159         LOG_ERROR("Could not create TempPageURL table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
160
161     if (!m_db.executeCommand("CREATE TEMP TABLE TempIcon (iconID INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL UNIQUE ON CONFLICT FAIL, expires INTEGER);")) 
162         LOG_ERROR("Could not create TempIcon table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
163
164     if (!m_db.executeCommand("CREATE TEMP TABLE TempIconResource (iconID INTERGER NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,data BLOB, touch INTEGER);")) 
165         LOG_ERROR("Could not create TempIconResource table in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
166
167     if (!m_db.executeCommand("CREATE TEMP TRIGGER temp_create_icon_resource AFTER INSERT ON TempIcon BEGIN INSERT INTO TempIconResource (iconID, data) VALUES (new.iconID, NULL); END;")) 
168         LOG_ERROR("Unable to create temp_create_icon_resource trigger in icon.db (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
169 }
170
171 void IconDatabase::deletePrivateTables()
172 {
173     if (!m_db.executeCommand("DROP TABLE TempPageURL;"))
174         LOG_ERROR("Could not drop TempPageURL table - %s", m_db.lastErrorMsg());
175     if (!m_db.executeCommand("DROP TABLE TempIcon;"))
176         LOG_ERROR("Could not drop TempIcon table - %s", m_db.lastErrorMsg());
177     if (!m_db.executeCommand("DROP TABLE TempIconResource;"))
178         LOG_ERROR("Could not drop TempIconResource table - %s", m_db.lastErrorMsg());
179 }
180
181 // FIXME - This is a DIRTY, dirty workaround for a problem that we're seeing where certain blobs are having a corrupt buffer
182 // returned when we get the result as a const void* blob.  Getting the blob as a textual representation is 100% accurate so this hack
183 // does an in place hex-to-character from the textual representation of the icon data.  After I manage to follow up with Adam Swift, the OSX sqlite maintainer,
184 // who is too busy to help me until after 7-4-06, this NEEEEEEEEEEEEEEDS to be changed. 
185 // *SIGH*
186 unsigned char hexToUnsignedChar(UChar h, UChar l)
187 {
188     unsigned char c;
189     if (h >= '0' && h <= '9')
190         c = h - '0';
191     else if (h >= 'A' && h <= 'F')
192         c = h - 'A' + 10;
193     else {
194         LOG_ERROR("Failed to parse TEXT result from SQL BLOB query");
195         return 0;
196     }
197     c *= 16;
198     if (l >= '0' && l <= '9')
199         c += l - '0';
200     else if (l >= 'A' && l <= 'F')
201         c += l - 'A' + 10;
202     else {
203         LOG_ERROR("Failed to parse TEXT result from SQL BLOB query");
204         return 0;
205     }    
206     return c;
207 }
208
209 Vector<unsigned char> hexStringToVector(const String& s)
210 {
211     LOG(IconDatabase, "hexStringToVector() - s.length is %i", s.length());
212     if (s[0] != 'X' || s[1] != '\'') {
213         LOG(IconDatabase, "hexStringToVector() - string is invalid SQL HEX-string result - %s", s.ascii().data());
214         return Vector<unsigned char>();
215     }
216
217     Vector<unsigned char> result;
218     result.reserveCapacity(s.length() / 2);
219     const UChar* data = s.characters() + 2;
220     int count = 0;
221     while (data[0] != '\'') {
222         if (data[1] == '\'') {
223             LOG_ERROR("Invalid HEX TEXT data for BLOB query");
224             return Vector<unsigned char>();
225         }
226         result.append(hexToUnsignedChar(data[0], data[1]));
227         data++;
228         data++;
229         count++;
230     }
231     
232     LOG(IconDatabase, "Finished atoi() - %i iterations, result size %i", count, result.size());
233     return result;
234 }
235
236 Vector<unsigned char> IconDatabase::imageDataForIconID(int id)
237 {
238     String blob = SQLStatement(m_db, String::sprintf("SELECT data FROM IconResource WHERE iconid = %i", id)).getColumnText(0);
239     if (blob.isEmpty())
240         return Vector<unsigned char>();
241     return hexStringToVector(blob);
242 }
243
244 Vector<unsigned char> IconDatabase::imageDataForIconURL(const String& _iconURL)
245 {
246     //Escape single quotes for SQL 
247     String iconURL = _iconURL;
248     iconURL.replace('\'', "''");
249     String blob;
250     
251     // If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
252     if (m_privateBrowsingEnabled) {
253         blob = SQLStatement(m_db, "SELECT quote(TempIconResource.data) FROM TempIconResource, TempIcon WHERE TempIcon.url = '" + iconURL + "' AND TempIconResource.iconID = TempIcon.iconID;").getColumnText(0);
254         if (!blob.isEmpty()) {
255             LOG(IconDatabase, "Icon data pulled from temp tables");
256             return hexStringToVector(blob);
257         }
258     } 
259     
260     // It wasn't found there, so lets check the main tables
261     blob = SQLStatement(m_db, "SELECT quote(IconResource.data) FROM IconResource, Icon WHERE Icon.url = '" + iconURL + "' AND IconResource.iconID = Icon.iconID;").getColumnText(0);
262     if (blob.isEmpty())
263         return Vector<unsigned char>();
264     
265     return hexStringToVector(blob);
266 }
267
268 Vector<unsigned char> IconDatabase::imageDataForPageURL(const String& _pageURL)
269 {
270     //Escape single quotes for SQL 
271     String pageURL = _pageURL;
272     pageURL.replace('\'', "''");
273     String blob;
274     
275     // If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
276     if (m_privateBrowsingEnabled) {
277         blob = SQLStatement(m_db, "SELECT TempIconResource.data FROM TempIconResource, TempPageURL WHERE TempPageURL.url = '" + pageURL + "' AND TempIconResource.iconID = TempPageURL.iconID;").getColumnText(0);
278         if (!blob.isEmpty()) {
279             LOG(IconDatabase, "Icon data pulled from temp tables");
280             return hexStringToVector(blob);
281         }
282     } 
283     
284     // It wasn't found there, so lets check the main tables
285     blob = SQLStatement(m_db, "SELECT quote(IconResource.data) FROM IconResource, PageURL WHERE PageURL.url = '" + pageURL + "' AND IconResource.iconID = PageURL.iconID;").getColumnText(0);
286     if (blob.isEmpty())
287         return Vector<unsigned char>();
288     
289     return hexStringToVector(blob);
290 }
291
292 void IconDatabase::setPrivateBrowsingEnabled(bool flag)
293 {
294     if (m_privateBrowsingEnabled == flag)
295         return;
296     
297     m_privateBrowsingEnabled = flag;
298     
299     if (!m_privateBrowsingEnabled)
300         deletePrivateTables();
301     else
302         createPrivateTables();
303 }
304
305 Image* IconDatabase::iconForPageURL(const String& url, const IntSize& size, bool cache)
306 {   
307     // We may have a SiteIcon for this specific PageURL...
308     if (m_pageURLToSiteIcons.contains(url))
309         return m_pageURLToSiteIcons.get(url)->getImage(size);
310     
311     // Otherwise see if we even have an IconURL for this PageURL
312     String iconURL = iconURLForPageURL(url);
313     if (iconURL.isEmpty())
314         return 0;
315     
316     // If we do, maybe we have an image for this IconURL
317     if (m_iconURLToSiteIcons.contains(iconURL))
318         return m_iconURLToSiteIcons.get(iconURL)->getImage(size);
319         
320     // If we don't have either, we have to create the SiteIcon
321     SiteIcon* icon = new SiteIcon(iconURL);
322     m_pageURLToSiteIcons.set(url, icon);
323     m_iconURLToSiteIcons.set(iconURL, icon);
324     return icon->getImage(size);
325 }
326
327 String IconDatabase::iconURLForPageURL(const String& _pageURL)
328 {
329     if (_pageURL.isEmpty()) 
330         return String();
331         
332     String pageURL = _pageURL;
333     pageURL.replace('\'', "''");
334     
335     // Try the private browsing tables because if any PageURL's IconURL was updated during privated browsing, it would be here
336     if (m_privateBrowsingEnabled) {
337         String iconURL = SQLStatement(m_db, "SELECT TempIcon.url FROM TempIcon, TempPageURL WHERE TempPageURL.url = '" + pageURL + "' AND TempIcon.iconID = TempPageURL.iconID").getColumnText16(0);
338         if (!iconURL.isEmpty())
339             return iconURL;
340     }
341     
342     return SQLStatement(m_db, "SELECT Icon.url FROM Icon, PageURL WHERE PageURL.url = '" + pageURL + "' AND Icon.iconID = PageURL.iconID").getColumnText16(0);
343 }
344
345 Image* IconDatabase::defaultIcon(const IntSize& size)
346 {
347     return 0;
348 }
349
350 void IconDatabase::retainIconForURL(const String& url)
351 {
352
353 }
354
355 void IconDatabase::releaseIconForURL(const String& url)
356 {
357
358 }
359
360 void IconDatabase::setIconDataForIconURL(const void* data, int size, const String& _iconURL)
361 {
362     ASSERT(size > -1);
363     if (size)
364         ASSERT(data);
365     else
366         data = 0;
367         
368     if (_iconURL.isEmpty()) {
369         LOG_ERROR("Attempt to set icon for blank url");
370         return;
371     }
372     
373     String iconURL = _iconURL;
374     iconURL.replace('\'', "''");
375
376     int64_t iconID;
377     String resourceTable;
378
379     // If we're in private browsing, we'll keep a record in the temporary tables instead of in the ondisk table
380     if (m_privateBrowsingEnabled) {
381         iconID = establishTemporaryIconIDForEscapedIconURL(iconURL);
382         if (!iconID) {
383             LOG(IconDatabase, "Failed to establish an iconID for URL %s in the private browsing table", _iconURL.ascii().data());
384             return;
385         }
386         resourceTable = "TempIconResource";
387     } else {
388         iconID = establishIconIDForEscapedIconURL(iconURL);
389         if (!iconID) {
390             LOG(IconDatabase, "Failed to establish an iconID for URL %s in the on-disk table", _iconURL.ascii().data());
391             return;
392         }
393         resourceTable = "IconResource";
394     }
395     
396     performSetIconDataForIconID(iconID, resourceTable, data, size);
397 }
398
399 void IconDatabase::performSetIconDataForIconID(int64_t iconID, const String& resourceTable, const void* data, int size)
400 {
401     ASSERT(iconID);
402     ASSERT(!resourceTable.isEmpty());
403     if (data)
404         ASSERT(size > 0);
405         
406     // First we create and prepare the SQLStatement
407     // The following statement also works to set the icon data to NULL because sqlite defaults unbound ? parameters to NULL
408     SQLStatement sql(m_db, "UPDATE " + resourceTable + " SET data = ? WHERE iconID = ?;");
409     sql.prepare();
410         
411     // Then we bind the icondata and the iconID to the SQLStatement
412     if (data)
413         sql.bindBlob(1, data, size);
414     sql.bindInt64(2, iconID);
415         
416     // Finally we step and make sure the step was successful
417     if (sql.step() != SQLITE_DONE)
418         LOG_ERROR("Unable to set icon resource data in table %s for iconID %lli", resourceTable.ascii().data(), iconID);
419     LOG(IconDatabase, "Icon data set in table %s for iconID %lli", resourceTable.ascii().data(), iconID);
420     return;
421 }
422
423 int IconDatabase::establishTemporaryIconIDForEscapedIconURL(const String& iconURL)
424 {
425     // We either lookup the iconURL and return its ID, or we create a new one for it
426     int64_t iconID = 0;
427     SQLStatement sql(m_db, "SELECT iconID FROM TempIcon WHERE url = '" + iconURL + "';");
428     sql.prepare();    
429     if (sql.step() == SQLITE_ROW) {
430         iconID = sql.getColumnInt64(0);
431     } else {
432         sql.finalize();
433         if (m_db.executeCommand("INSERT INTO TempIcon (url) VALUES ('" + iconURL + "');"))
434             iconID = m_db.lastInsertRowID();
435     }
436     return iconID;
437 }
438
439 int IconDatabase::establishIconIDForEscapedIconURL(const String& iconURL)
440 {
441     // We either lookup the iconURL and return its ID, or we create a new one for it
442     int64_t iconID = 0;
443     SQLStatement sql(m_db, "SELECT iconID FROM Icon WHERE url = '" + iconURL + "';");
444     sql.prepare();    
445     if (sql.step() == SQLITE_ROW) {
446         iconID = sql.getColumnInt64(0);
447     } else {
448         sql.finalize();
449         if (m_db.executeCommand("INSERT INTO Icon (url) VALUES ('" + iconURL + "');"))
450             iconID = m_db.lastInsertRowID();
451     }
452     return iconID;
453 }
454
455 void IconDatabase::setHaveNoIconForIconURL(const String& _iconURL)
456 {    
457     setIconDataForIconURL(0, 0, _iconURL);
458 }
459
460 void IconDatabase::setIconURLForPageURL(const String& _iconURL, const String& _pageURL)
461 {
462     ASSERT(!_iconURL.isEmpty());
463     ASSERT(!_pageURL.isEmpty());
464     
465     String iconURL = _iconURL;
466     iconURL.replace('\'',"''");
467     String pageURL = _pageURL;
468     pageURL.replace('\'',"''");
469
470     int64_t iconID;
471     String pageTable;
472     if (m_privateBrowsingEnabled) {
473         iconID = establishTemporaryIconIDForEscapedIconURL(iconURL);
474         pageTable = "TempPageURL";
475     } else {
476         iconID = establishIconIDForEscapedIconURL(iconURL);
477         pageTable = "PageURL";
478     }
479     
480     performSetIconURLForPageURL(iconID, pageTable, pageURL);
481 }
482
483 void IconDatabase::performSetIconURLForPageURL(int64_t iconID, const String& pageTable, const String& pageURL)
484 {
485     ASSERT(iconID);
486     if (m_db.returnsAtLeastOneResult("SELECT url FROM " + pageTable + " WHERE url = '" + pageURL + "';")) {
487         if (!m_db.executeCommand("UPDATE " + pageTable + " SET iconID = " + String::number(iconID) + " WHERE url = '" + pageURL + "';"))
488             LOG_ERROR("Failed to update record in %s - %s", pageTable.ascii().data(), m_db.lastErrorMsg());
489     } else
490         if (!m_db.executeCommand("INSERT INTO " + pageTable + " (url, iconID) VALUES ('" + pageURL + "', " + String::number(iconID) + ");"))
491             LOG_ERROR("Failed to insert record into %s - %s", pageTable.ascii().data(), m_db.lastErrorMsg());
492 }
493
494 bool IconDatabase::hasIconForIconURL(const String& _url)
495 {
496     // First check the in memory mapped icons...
497     if (m_iconURLToSiteIcons.contains(_url))
498         return true;
499
500     // Check the on-disk database as we're more likely to have more icons there
501     String url = _url;
502     url.replace('\'', "''");
503     
504     String query = "SELECT IconResource.data FROM IconResource, Icon WHERE Icon.url = '" + url + "' AND IconResource.iconID = Icon.iconID;";
505     int size = 0;
506     const void* data = SQLStatement(m_db, query).getColumnBlob(0, size);
507     if (data && size)
508         return true;
509     
510     // Finally, check the temporary tables for private browsing if enabled
511     if (m_privateBrowsingEnabled) {
512         query = "SELECT TempIconResource.data FROM TempIconResource, TempIcon WHERE TempIcon.url = '" + url + "' AND TempIconResource.iconID = TempIcon.iconID;";
513         size = 0;
514         data = SQLStatement(m_db, query).getColumnBlob(0, size);
515         LOG(IconDatabase, "Checking for icon for IconURL %s in temporary tables", _url.ascii().data());
516         return data && size;
517     }
518     return false;
519 }
520
521 IconDatabase::~IconDatabase()
522 {
523     m_db.close();
524 }
525
526 } //namespace WebCore