Reviewed by Darin
[WebKit-https.git] / WebKit / Misc / WebIconDatabase.m
1 /*
2  * Copyright (C) 2005 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 #import <WebKit/WebIconDatabase.h>
29
30 #import <WebKit/WebIconDatabasePrivate.h>
31 #import <WebKit/WebKitLogging.h>
32 #import <WebKit/WebKitNSStringExtras.h>
33 #import <WebKit/WebNSURLExtras.h>
34 #import <WebKit/WebPreferences.h>
35
36 #import <WebKit/WebIconDatabaseBridge.h>
37
38 #import "WebTypesInternal.h"
39
40 NSString * const WebIconDatabaseVersionKey =    @"WebIconDatabaseVersion";
41 NSString * const WebURLToIconURLKey =           @"WebSiteURLToIconURLKey";
42
43 NSString *WebIconDatabaseDidAddIconNotification =          @"WebIconDatabaseDidAddIconNotification";
44 NSString *WebIconNotificationUserInfoURLKey =              @"WebIconNotificationUserInfoURLKey";
45 NSString *WebIconDatabaseDidRemoveAllIconsNotification =   @"WebIconDatabaseDidRemoveAllIconsNotification";
46
47 NSString *WebIconDatabaseDirectoryDefaultsKey = @"WebIconDatabaseDirectoryDefaultsKey";
48 NSString *WebIconDatabaseEnabledDefaultsKey =   @"WebIconDatabaseEnabled";
49
50 NSString *WebIconDatabasePath = @"~/Library/Icons";
51
52 NSSize WebIconSmallSize = {16, 16};
53 NSSize WebIconMediumSize = {32, 32};
54 NSSize WebIconLargeSize = {128, 128};
55
56 #define UniqueFilePathSize (34)
57
58 @interface WebIconDatabase (WebInternal)
59 - (NSImage *)_iconForFileURL:(NSString *)fileURL withSize:(NSSize)size;
60 - (void)_resetCachedWebPreferences:(NSNotification *)notification;
61 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons;
62 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon;
63 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache;
64 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size;
65 - (void)_convertToWebCoreFormat; 
66 @end
67
68 @implementation WebIconDatabase
69
70 + (WebIconDatabase *)sharedIconDatabase
71 {
72     static WebIconDatabase *database = nil;
73     if (!database)
74         database = [[WebIconDatabase alloc] init];
75     return database;
76 }
77
78 - init
79 {
80     [super init];
81     
82     _private = [[WebIconDatabasePrivate alloc] init];
83
84     // Check the user defaults and see if the icon database should even be enabled if not, we can bail from init right here
85     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
86     NSDictionary *initialDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithBool:YES], WebIconDatabaseEnabledDefaultsKey, nil];
87     [defaults registerDefaults:initialDefaults];
88     [initialDefaults release];
89     if (![defaults boolForKey:WebIconDatabaseEnabledDefaultsKey]) {
90         _private->databaseBridge = nil;
91         return self;
92     }
93         
94     // Get/create the shared database bridge - bail if we fail
95     _private->databaseBridge = [WebIconDatabaseBridge sharedBridgeInstance];
96     if (!_private->databaseBridge) {
97         LOG_ERROR("Unable to create IconDatabaseBridge");
98         return self;
99     }
100     
101     // Figure out the directory we should be using for the icon.db
102     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
103     if (!databaseDirectory) {
104         databaseDirectory = WebIconDatabasePath;
105         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
106     }
107     databaseDirectory = [databaseDirectory stringByExpandingTildeInPath];
108     
109     // Open the WebCore icon database
110     [_private->databaseBridge openSharedDatabaseWithPath:databaseDirectory];
111     [_private->databaseBridge setPrivateBrowsingEnabled:[[WebPreferences standardPreferences] privateBrowsingEnabled]];
112     
113     // <rdar://problem/4674552> New IconDB: New Safari icon database needs to convert from the old icons on first load 
114     // We call this method on each load - it determines, on its own, whether or not we actually need to do the conversion and 
115     // returns early if the conversion is already done
116     [self _convertToWebCoreFormat];
117     
118     // Register for important notifications
119     [[NSNotificationCenter defaultCenter] addObserver:self
120                                              selector:@selector(_applicationWillTerminate:)
121                                                  name:NSApplicationWillTerminateNotification
122                                                object:NSApp];
123     [[NSNotificationCenter defaultCenter] 
124             addObserver:self selector:@selector(_resetCachedWebPreferences:) 
125                    name:WebPreferencesChangedNotification object:nil];
126
127     return self;
128 }
129
130 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size cache:(BOOL)cache
131 {
132     ASSERT(size.width);
133     ASSERT(size.height);
134
135     if (!URL || ![self _isEnabled])
136         return [self defaultIconWithSize:size];
137
138     // FIXME - <rdar://problem/4697934> - Move the handling of FileURLs to WebCore and implement in ObjC++
139     if ([URL _webkit_isFileURL])
140         return [self _iconForFileURL:URL withSize:size];
141       
142     NSImage* image = [_private->databaseBridge iconForPageURL:URL withSize:size];
143     return image ? image : [self defaultIconWithSize:size];
144 }
145
146 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size
147 {
148     return [self iconForURL:URL withSize:size cache:YES];
149 }
150
151 - (NSString *)iconURLForURL:(NSString *)URL
152 {
153     if (![self _isEnabled])
154         return nil;
155         
156     NSString* iconurl = [_private->databaseBridge iconURLForPageURL:URL];
157     return iconurl;
158 }
159
160 - (NSImage *)defaultIconWithSize:(NSSize)size
161 {
162     ASSERT(size.width);
163     ASSERT(size.height);
164     
165     return [_private->databaseBridge defaultIconWithSize:size];
166 }
167
168 - (void)retainIconForURL:(NSString *)URL
169 {
170     ASSERT(URL);
171     if (![self _isEnabled])
172         return;
173
174     [_private->databaseBridge retainIconForURL:URL];
175 }
176
177 - (void)releaseIconForURL:(NSString *)pageURL
178 {
179     ASSERT(pageURL);
180     if (![self _isEnabled])
181         return;
182
183     [_private->databaseBridge releaseIconForURL:pageURL];
184 }
185 @end
186
187
188 @implementation WebIconDatabase (WebPendingPublic)
189
190 - (void)removeAllIcons
191 {
192     // FIXME - <rdar://problem/4678414>
193     // Need to create a bridge method that calls through to WebCore and performs a wipe of the DB there
194 }
195
196 - (BOOL)isIconExpiredForIconURL:(NSString *)iconURL
197 {
198     return [_private->databaseBridge isIconExpiredForIconURL:iconURL];
199 }
200
201 @end
202
203 @implementation WebIconDatabase (WebPrivate)
204
205 - (BOOL)_isEnabled
206 {
207     // If we weren't enabled on startup, we marked the databaseBridge as nil
208     return _private->databaseBridge != nil;
209 }
210
211 - (void)_setIconData:(NSData *)data forIconURL:(NSString *)iconURL
212 {
213     ASSERT(data);
214     ASSERT(iconURL);
215     ASSERT([self _isEnabled]);   
216
217     [_private->databaseBridge _setIconData:data forIconURL:iconURL];
218 }
219
220 - (void)_setHaveNoIconForIconURL:(NSString *)iconURL
221 {
222     ASSERT(iconURL);
223     ASSERT([self _isEnabled]);
224
225     [_private->databaseBridge _setHaveNoIconForIconURL:iconURL];
226 }
227
228 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL
229 {
230     ASSERT(iconURL);
231     ASSERT(URL);
232     ASSERT([self _isEnabled]);
233     
234     // FIXME - The following comment is a holdover from the old icon DB, which handled missing icons
235     // differently than the new db.  It would return early if the icon is in the negative cache,
236     // avoiding the notification.  We should explore and see if a similar optimization can take place-
237         // If the icon is in the negative cache (ie, there is no icon), avoid the
238         // work of delivering a notification for it or saving it to disk. This is a significant
239         // win on the iBench HTML test.
240         
241     // FIXME - The following comment is also a holdover - if the iconURL->pageURL mapping was already the
242     // same, the notification would again be avoided - we should try to do this, too.
243         // Don't do any work if the icon URL is already bound to the site URL
244     
245     // A possible solution for both of these is to have the bridge method return a BOOL saying "Yes, notify" or
246     // "no, don't bother notifying"
247     
248     [_private->databaseBridge _setIconURL:iconURL forPageURL:URL];
249     [self _sendNotificationForURL:URL];
250 }
251
252 - (BOOL)_hasEntryForIconURL:(NSString *)iconURL;
253 {
254     ASSERT([self _isEnabled]);
255
256     return [_private->databaseBridge _hasEntryForIconURL:iconURL];
257 }
258
259 - (void)_sendNotificationForURL:(NSString *)URL
260 {
261     ASSERT(URL);
262     
263     NSDictionary *userInfo = [NSDictionary dictionaryWithObject:URL
264                                                          forKey:WebIconNotificationUserInfoURLKey];
265     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidAddIconNotification
266                                                         object:self
267                                                       userInfo:userInfo];
268 }
269
270 - (void)loadIconFromURL:(NSString *)iconURL
271 {
272     [_private->databaseBridge loadIconFromURL:iconURL];
273 }
274
275 @end
276
277 @implementation WebIconDatabase (WebInternal)
278
279 - (void)_applicationWillTerminate:(NSNotification *)notification
280 {
281     [_private->databaseBridge closeSharedDatabase];
282     [_private->databaseBridge release];
283     _private->databaseBridge = nil;
284 }
285
286
287 - (NSImage *)_iconForFileURL:(NSString *)file withSize:(NSSize)size
288 {
289     ASSERT(size.width);
290     ASSERT(size.height);
291
292     NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
293     NSString *path = [[NSURL _web_URLWithDataAsString:file] path];
294     NSString *suffix = [path pathExtension];
295     NSImage *icon = nil;
296     
297     if ([suffix _webkit_isCaseInsensitiveEqualToString:@"htm"] || [suffix _webkit_isCaseInsensitiveEqualToString:@"html"]) {
298         if (!_private->htmlIcons) {
299             icon = [workspace iconForFileType:@"html"];
300             _private->htmlIcons = [[self _iconsBySplittingRepresentationsOfIcon:icon] retain];
301         }
302         icon = [self _iconFromDictionary:_private->htmlIcons forSize:size cache:YES];
303     } else {
304         if (!path || ![path isAbsolutePath]) {
305             // Return the generic icon when there is no path.
306             icon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericDocumentIcon)];
307         } else {
308             icon = [workspace iconForFile:path];
309         }
310         [self _scaleIcon:icon toSize:size];
311     }
312
313     return icon;
314 }
315
316 - (void)_resetCachedWebPreferences:(NSNotification *)notification
317 {
318     BOOL privateBrowsingEnabledNow = [[WebPreferences standardPreferences] privateBrowsingEnabled];
319
320     [_private->databaseBridge setPrivateBrowsingEnabled:privateBrowsingEnabledNow];
321 }
322
323 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons
324 {
325     ASSERT(icons);
326     
327     NSEnumerator *enumerator = [icons keyEnumerator];
328     NSValue *currentSize, *largestSize=nil;
329     float largestSizeArea=0;
330
331     while ((currentSize = [enumerator nextObject]) != nil) {
332         NSSize currentSizeSize = [currentSize sizeValue];
333         float currentSizeArea = currentSizeSize.width * currentSizeSize.height;
334         if(!largestSizeArea || (currentSizeArea > largestSizeArea)){
335             largestSize = currentSize;
336             largestSizeArea = currentSizeArea;
337         }
338     }
339
340     return [icons objectForKey:largestSize];
341 }
342
343 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon
344 {
345     ASSERT(icon);
346
347     NSMutableDictionary *icons = [NSMutableDictionary dictionary];
348     NSEnumerator *enumerator = [[icon representations] objectEnumerator];
349     NSImageRep *rep;
350
351     while ((rep = [enumerator nextObject]) != nil) {
352         NSSize size = [rep size];
353         NSImage *subIcon = [[NSImage alloc] initWithSize:size];
354         [subIcon addRepresentation:rep];
355         [icons setObject:subIcon forKey:[NSValue valueWithSize:size]];
356         [subIcon release];
357     }
358
359     if([icons count] > 0)
360         return icons;
361
362     LOG_ERROR("icon has no representations");
363     
364     return nil;
365 }
366
367 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache
368 {
369     ASSERT(size.width);
370     ASSERT(size.height);
371
372     NSImage *icon = [icons objectForKey:[NSValue valueWithSize:size]];
373
374     if(!icon){
375         icon = [[[self _largestIconFromDictionary:icons] copy] autorelease];
376         [self _scaleIcon:icon toSize:size];
377
378         if(cache){
379             [icons setObject:icon forKey:[NSValue valueWithSize:size]];
380         }
381     }
382
383     return icon;
384 }
385
386 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size
387 {
388     ASSERT(size.width);
389     ASSERT(size.height);
390     
391 #if !LOG_DISABLED        
392     double start = CFAbsoluteTimeGetCurrent();
393 #endif
394     
395     [icon setScalesWhenResized:YES];
396     [icon setSize:size];
397     
398 #if !LOG_DISABLED
399     double duration = CFAbsoluteTimeGetCurrent() - start;
400     LOG(Timing, "scaling icon took %f seconds.", duration);
401 #endif
402 }
403
404 // This hashing String->filename algorithm came from WebFileDatabase.m and is what was used in the 
405 // WebKit Icon Database
406 static void legacyIconDatabaseFilePathForKey(id key, char *buffer)
407 {
408     const char *s;
409     UInt32 hash1;
410     UInt32 hash2;
411     CFIndex len;
412     CFIndex cnt;
413     
414     s = [[[[key description] lowercaseString] stringByStandardizingPath] UTF8String];
415     len = strlen(s);
416
417     // compute first hash    
418     hash1 = len;
419     for (cnt = 0; cnt < len; cnt++) {
420         hash1 += (hash1 << 8) + s[cnt];
421     }
422     hash1 += (hash1 << (len & 31));
423
424     // compute second hash    
425     hash2 = len;
426     for (cnt = 0; cnt < len; cnt++) {
427         hash2 = (37 * hash2) ^ s[cnt];
428     }
429
430 #ifdef __LP64__
431     snprintf(buffer, UniqueFilePathSize, "%.2u/%.2u/%.10u-%.10u.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
432 #else
433     snprintf(buffer, UniqueFilePathSize, "%.2lu/%.2lu/%.10lu-%.10lu.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
434 #endif
435 }
436
437 // This method of getting an object from the filesystem is taken from the old 
438 // WebKit Icon Database
439 static id objectFromPathForKey(NSString *databasePath, id key)
440 {
441     ASSERT(key);
442     id result = nil;
443
444     // Use the key->filename hashing the old WebKit IconDatabase used
445     char uniqueKey[UniqueFilePathSize];    
446     legacyIconDatabaseFilePathForKey(key, uniqueKey);
447     
448     // Get the data from this file and setup for the un-archiving
449     NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", databasePath, uniqueKey];
450     NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
451     NSUnarchiver *unarchiver = nil;
452     
453     NS_DURING
454         if (data) {
455             unarchiver = [[NSUnarchiver alloc] initForReadingWithData:data];
456             if (unarchiver) {
457                 id fileKey = [unarchiver decodeObject];
458                 NSLog(@"%@", fileKey);
459                 if ([fileKey isEqual:key]) {
460                     id object = [unarchiver decodeObject];
461                     NSLog(@"%@", object);
462                     if (object) {
463                         // Decoded objects go away when the unarchiver does, so we need to
464                         // retain this so we can return it to our caller.
465                         result = [[object retain] autorelease];
466                         LOG(IconDatabase, "read disk cache file - %@", key);
467                     }
468                 }
469             }
470         }
471     NS_HANDLER
472         LOG(IconDatabase, "cannot unarchive cache file - %@", key);
473         result = nil;
474     NS_ENDHANDLER
475
476     [unarchiver release];
477     [data release];
478     [filePath release];
479     
480     return result;
481 }
482
483 static NSData* iconDataFromPathForIconURL(NSString *databasePath, NSString *iconURLString)
484 {
485     ASSERT(iconURLString);
486     ASSERT(databasePath);
487     
488     NSData *iconData = objectFromPathForKey(databasePath, iconURLString);
489     
490     if ((id)iconData == (id)[NSNull null]) 
491         return nil;
492         
493     return iconData;
494 }
495
496 - (void)_convertToWebCoreFormat
497 {
498     ASSERT(_private);
499     ASSERT(_private->databaseBridge);
500     
501     
502     // If the WebCore Icon Database is not empty, we assume that this conversion has already
503     // taken place and skip the rest of the steps 
504     if (![_private->databaseBridge _isEmpty]) {
505         return;
506     }
507
508     // Get the directory the old icon database *should* be in
509     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
510     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
511     if (!databaseDirectory) {
512         databaseDirectory = WebIconDatabasePath;
513         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
514     }
515     databaseDirectory = [databaseDirectory stringByExpandingTildeInPath];
516
517     // With this directory, get the PageURLToIconURL map that was saved to disk
518     NSMutableDictionary *pageURLToIconURL = objectFromPathForKey(databaseDirectory, WebURLToIconURLKey);
519
520     // If the retrieved object was not a valid NSMutableDictionary, then we have no valid
521     // icons to convert
522     if (![pageURLToIconURL isKindOfClass:[NSMutableDictionary class]])
523         pageURLToIconURL = nil;
524     
525     NSEnumerator *enumerator = [pageURLToIconURL keyEnumerator];
526     NSString *url, *iconURL;
527     
528     // First, we'll iterate through the PageURL->IconURL map
529     while ((url = [enumerator nextObject]) != nil) {
530         iconURL = [pageURLToIconURL objectForKey:url];
531         if (!iconURL)
532             continue;
533         [_private->databaseBridge _setIconURL:iconURL forPageURL:url];
534     }    
535
536     // Second, we'll get a list of the unique IconURLs we have
537     NSMutableSet *iconsOnDiskWithURLs = [NSMutableSet setWithArray:[pageURLToIconURL allValues]];
538     enumerator = [iconsOnDiskWithURLs objectEnumerator];
539     NSData *iconData;
540     
541     // And iterate through them, adding the icon data to the new icon database
542     while ((url = [enumerator nextObject]) != nil) {
543         iconData = iconDataFromPathForIconURL(databaseDirectory, url);
544         if (iconData)
545             [_private->databaseBridge _setIconData:iconData forIconURL:url];
546         else {
547             // This really *shouldn't* happen, so it'd be good to track down why it might happen in a debug build
548             // however, we do know how to handle it gracefully in release
549             LOG_ERROR("%@ is marked as having an icon on disk, but we couldn't get the data for it", url);
550             [_private->databaseBridge _setHaveNoIconForIconURL:url];
551         }
552     }
553
554     // After we're done converting old style icons over to webcore icons, we delete the entire directory hierarchy 
555     // for the old icon DB (skipping the new iconDB, which will likely be in the same directory)
556     NSFileManager *fileManager = [NSFileManager defaultManager];
557     enumerator = [[fileManager directoryContentsAtPath:databaseDirectory] objectEnumerator];
558     
559     NSString *databaseFilename = [_private->databaseBridge defaultDatabaseFilename];
560
561     NSString *file;
562     while ((file = [enumerator nextObject]) != nil) {
563         if ([file isEqualTo:databaseFilename])
564             continue;
565         NSString *filePath = [databaseDirectory stringByAppendingPathComponent:file];
566         if (![fileManager  removeFileAtPath:filePath handler:nil])
567             LOG_ERROR("Failed to delete %@ from old icon directory", filePath);
568     }
569 }
570
571 @end
572
573 // This empty implementation must exist 
574 @implementation WebIconDatabasePrivate
575 @end
576