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