JavaScriptCore:
[WebKit-https.git] / WebKit / Misc / WebIconDatabase.m
1 /*
2  * Copyright (C) 2005, 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  *
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
29 #import "WebIconDatabaseInternal.h"
30
31 #import "WebIconDatabaseDelegate.h"
32 #import "WebKitLogging.h"
33 #import "WebKitNSStringExtras.h"
34 #import "WebNSURLExtras.h"
35 #import "WebPreferences.h"
36 #import "WebTypesInternal.h"
37 #import <WebCore/IconDatabase.h>
38 #import <WebCore/Image.h>
39 #import <WebCore/IntSize.h>
40
41 using namespace WebCore;
42
43 NSString * const WebIconDatabaseVersionKey =    @"WebIconDatabaseVersion";
44 NSString * const WebURLToIconURLKey =           @"WebSiteURLToIconURLKey";
45
46 NSString *WebIconDatabaseDidAddIconNotification =          @"WebIconDatabaseDidAddIconNotification";
47 NSString *WebIconNotificationUserInfoURLKey =              @"WebIconNotificationUserInfoURLKey";
48 NSString *WebIconDatabaseDidRemoveAllIconsNotification =   @"WebIconDatabaseDidRemoveAllIconsNotification";
49
50 NSString *WebIconDatabaseDirectoryDefaultsKey = @"WebIconDatabaseDirectoryDefaultsKey";
51 NSString *WebIconDatabaseEnabledDefaultsKey =   @"WebIconDatabaseEnabled";
52
53 NSString *WebIconDatabasePath = @"~/Library/Icons";
54
55 NSSize WebIconSmallSize = {16, 16};
56 NSSize WebIconMediumSize = {32, 32};
57 NSSize WebIconLargeSize = {128, 128};
58
59 #define UniqueFilePathSize (34)
60
61 @interface WebIconDatabase (WebInternal)
62 - (BOOL)_isEnabled;
63 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL;
64 - (BOOL)_hasEntryForIconURL:(NSString *)iconURL;
65 - (void)_sendNotificationForURL:(NSString *)URL;
66 - (NSImage *)_iconForFileURL:(NSString *)fileURL withSize:(NSSize)size;
67 - (void)_resetCachedWebPreferences:(NSNotification *)notification;
68 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons;
69 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon;
70 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache;
71 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size;
72 - (void)_convertToWebCoreFormat; 
73 @end
74
75 @implementation WebIconDatabase
76
77 + (WebIconDatabase *)sharedIconDatabase
78 {
79     static WebIconDatabase *database = nil;
80     if (!database)
81         database = [[WebIconDatabase alloc] init];
82     return database;
83 }
84
85 - init
86 {
87     [super init];
88     
89     _private = [[WebIconDatabasePrivate alloc] init];
90     
91     // Check the user defaults and see if the icon database should even be enabled.
92     // Inform the bridge and, if we're disabled, bail from init right here
93     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
94     // <rdar://problem/4741419> - IconDatabase should be disabled by default
95     NSDictionary *initialDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithBool:YES], WebIconDatabaseEnabledDefaultsKey, nil];
96     [defaults registerDefaults:initialDefaults];
97     [initialDefaults release];
98     BOOL enabled = [defaults boolForKey:WebIconDatabaseEnabledDefaultsKey];
99     IconDatabase::sharedIconDatabase()->setEnabled(enabled);
100     if (!enabled)
101         return self;
102     
103     // Figure out the directory we should be using for the icon.db
104     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
105     if (!databaseDirectory) {
106         databaseDirectory = WebIconDatabasePath;
107         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
108     }
109     databaseDirectory = [[databaseDirectory stringByExpandingTildeInPath] stringByStandardizingPath];
110     
111     // Open the WebCore icon database and convert the old WebKit icon database if we haven't done the initial conversion yet
112     if (!IconDatabase::sharedIconDatabase()->open(databaseDirectory))
113         LOG_ERROR("Unable to open icon database");
114     else
115         if ([self _isEnabled])
116             [self _convertToWebCoreFormat];
117
118     IconDatabase::sharedIconDatabase()->setPrivateBrowsingEnabled([[WebPreferences standardPreferences] privateBrowsingEnabled]);
119     
120     // Register for important notifications
121     [[NSNotificationCenter defaultCenter] addObserver:self
122                                              selector:@selector(_applicationWillTerminate:)
123                                                  name:NSApplicationWillTerminateNotification
124                                                object:NSApp];
125     [[NSNotificationCenter defaultCenter] 
126             addObserver:self selector:@selector(_resetCachedWebPreferences:) 
127                    name:WebPreferencesChangedNotification object:nil];
128
129     return self;
130 }
131
132 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size cache:(BOOL)cache
133 {
134     ASSERT(size.width);
135     ASSERT(size.height);
136
137     if (!URL || ![self _isEnabled])
138         return [self defaultIconForURL:URL withSize:size];
139
140     // FIXME - <rdar://problem/4697934> - Move the handling of FileURLs to WebCore and implement in ObjC++
141     if ([URL _webkit_isFileURL])
142         return [self _iconForFileURL:URL withSize:size];
143       
144     if (Image* image = IconDatabase::sharedIconDatabase()->iconForPageURL(URL, IntSize(size)))
145         if (NSImage *icon = webGetNSImage(image, size))
146             return icon;
147     return [self defaultIconForURL:URL withSize:size];
148 }
149
150 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size
151 {
152     return [self iconForURL:URL withSize:size cache:YES];
153 }
154
155 - (NSString *)iconURLForURL:(NSString *)URL
156 {
157     if (![self _isEnabled])
158         return nil;
159         
160     return IconDatabase::sharedIconDatabase()->iconURLForPageURL(URL);
161 }
162
163 - (NSImage *)defaultIconWithSize:(NSSize)size
164 {
165     ASSERT(size.width);
166     ASSERT(size.height);
167     
168     Image* image = IconDatabase::sharedIconDatabase()->defaultIcon(IntSize(size));
169     return image ? image->getNSImage() : nil;
170 }
171
172 - (NSImage *)defaultIconForURL:(NSString *)URL withSize:(NSSize)size
173 {
174     if (_private->delegateImplementsDefaultIconForURL)
175         return [_private->delegate webIconDatabase:self defaultIconForURL:URL withSize:size];
176     return [self defaultIconWithSize:size];
177 }
178
179 - (void)retainIconForURL:(NSString *)URL
180 {
181     ASSERT(URL);
182     if (![self _isEnabled])
183         return;
184
185     IconDatabase::sharedIconDatabase()->retainIconForPageURL(URL);
186 }
187
188 - (void)releaseIconForURL:(NSString *)pageURL
189 {
190     ASSERT(pageURL);
191     if (![self _isEnabled])
192         return;
193
194     IconDatabase::sharedIconDatabase()->releaseIconForPageURL(pageURL);
195 }
196
197 - (void)setDelegate:(id)delegate
198 {
199     _private->delegate = delegate;
200     _private->delegateImplementsDefaultIconForURL = [delegate respondsToSelector:@selector(webIconDatabase:defaultIconForURL:withSize:)];
201 }
202
203 - (id)delegate
204 {
205     return _private->delegate;
206 }
207
208 @end
209
210
211 @implementation WebIconDatabase (WebPendingPublic)
212
213 - (void)removeAllIcons
214 {
215     if (![self _isEnabled])
216         return;
217     IconDatabase::sharedIconDatabase()->removeAllIcons();
218     // FIXME: This notification won't get sent if WebCore calls removeAllIcons.
219     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidRemoveAllIconsNotification
220                                                         object:self
221                                                       userInfo:nil];
222 }
223
224 @end
225
226 @implementation WebIconDatabase (WebInternal)
227
228 - (BOOL)_isEnabled
229 {
230     return IconDatabase::sharedIconDatabase()->enabled();
231 }
232
233 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL
234 {
235     ASSERT(iconURL);
236     ASSERT(URL);
237     ASSERT([self _isEnabled]);
238     
239     // If this iconURL already maps to this pageURL, don't bother sending the notification
240     // The WebCore::IconDatabase returns TRUE if we should send the notification, and false if we shouldn't.
241     // This is a measurable win on the iBench - about 1% worth on average
242     if (IconDatabase::sharedIconDatabase()->setIconURLForPageURL(iconURL, URL))
243         // FIXME: This notification won't get set when WebCore sets an icon.
244         [self _sendNotificationForURL:URL];
245 }
246
247 - (BOOL)_hasEntryForIconURL:(NSString *)iconURL;
248 {
249     ASSERT([self _isEnabled]);
250
251     return IconDatabase::sharedIconDatabase()->hasEntryForIconURL(iconURL);
252 }
253
254 - (void)_sendNotificationForURL:(NSString *)URL
255 {
256     ASSERT(URL);
257     
258     NSDictionary *userInfo = [NSDictionary dictionaryWithObject:URL
259                                                          forKey:WebIconNotificationUserInfoURLKey];
260     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidAddIconNotification
261                                                         object:self
262                                                       userInfo:userInfo];
263 }
264
265 - (void)_applicationWillTerminate:(NSNotification *)notification
266 {
267     IconDatabase::sharedIconDatabase()->close();
268 }
269
270 - (NSImage *)_iconForFileURL:(NSString *)file withSize:(NSSize)size
271 {
272     ASSERT(size.width);
273     ASSERT(size.height);
274
275     NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
276     NSString *path = [[NSURL _web_URLWithDataAsString:file] path];
277     NSString *suffix = [path pathExtension];
278     NSImage *icon = nil;
279     
280     if ([suffix _webkit_isCaseInsensitiveEqualToString:@"htm"] || [suffix _webkit_isCaseInsensitiveEqualToString:@"html"]) {
281         if (!_private->htmlIcons) {
282             icon = [workspace iconForFileType:@"html"];
283             _private->htmlIcons = [[self _iconsBySplittingRepresentationsOfIcon:icon] retain];
284         }
285         icon = [self _iconFromDictionary:_private->htmlIcons forSize:size cache:YES];
286     } else {
287         if (!path || ![path isAbsolutePath]) {
288             // Return the generic icon when there is no path.
289             icon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericDocumentIcon)];
290         } else {
291             icon = [workspace iconForFile:path];
292         }
293         [self _scaleIcon:icon toSize:size];
294     }
295
296     return icon;
297 }
298
299 - (void)_resetCachedWebPreferences:(NSNotification *)notification
300 {
301     BOOL privateBrowsingEnabledNow = [[WebPreferences standardPreferences] privateBrowsingEnabled];
302     IconDatabase::sharedIconDatabase()->setPrivateBrowsingEnabled(privateBrowsingEnabledNow);
303 }
304
305 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons
306 {
307     ASSERT(icons);
308     
309     NSEnumerator *enumerator = [icons keyEnumerator];
310     NSValue *currentSize, *largestSize=nil;
311     float largestSizeArea=0;
312
313     while ((currentSize = [enumerator nextObject]) != nil) {
314         NSSize currentSizeSize = [currentSize sizeValue];
315         float currentSizeArea = currentSizeSize.width * currentSizeSize.height;
316         if(!largestSizeArea || (currentSizeArea > largestSizeArea)){
317             largestSize = currentSize;
318             largestSizeArea = currentSizeArea;
319         }
320     }
321
322     return [icons objectForKey:largestSize];
323 }
324
325 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon
326 {
327     ASSERT(icon);
328
329     NSMutableDictionary *icons = [NSMutableDictionary dictionary];
330     NSEnumerator *enumerator = [[icon representations] objectEnumerator];
331     NSImageRep *rep;
332
333     while ((rep = [enumerator nextObject]) != nil) {
334         NSSize size = [rep size];
335         NSImage *subIcon = [[NSImage alloc] initWithSize:size];
336         [subIcon addRepresentation:rep];
337         [icons setObject:subIcon forKey:[NSValue valueWithSize:size]];
338         [subIcon release];
339     }
340
341     if([icons count] > 0)
342         return icons;
343
344     LOG_ERROR("icon has no representations");
345     
346     return nil;
347 }
348
349 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache
350 {
351     ASSERT(size.width);
352     ASSERT(size.height);
353
354     NSImage *icon = [icons objectForKey:[NSValue valueWithSize:size]];
355
356     if(!icon){
357         icon = [[[self _largestIconFromDictionary:icons] copy] autorelease];
358         [self _scaleIcon:icon toSize:size];
359
360         if(cache){
361             [icons setObject:icon forKey:[NSValue valueWithSize:size]];
362         }
363     }
364
365     return icon;
366 }
367
368 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size
369 {
370     ASSERT(size.width);
371     ASSERT(size.height);
372     
373 #if !LOG_DISABLED        
374     double start = CFAbsoluteTimeGetCurrent();
375 #endif
376     
377     [icon setScalesWhenResized:YES];
378     [icon setSize:size];
379     
380 #if !LOG_DISABLED
381     double duration = CFAbsoluteTimeGetCurrent() - start;
382     LOG(Timing, "scaling icon took %f seconds.", duration);
383 #endif
384 }
385
386 // This hashing String->filename algorithm came from WebFileDatabase.m and is what was used in the 
387 // WebKit Icon Database
388 static void legacyIconDatabaseFilePathForKey(id key, char *buffer)
389 {
390     const char *s;
391     UInt32 hash1;
392     UInt32 hash2;
393     CFIndex len;
394     CFIndex cnt;
395     
396     s = [[[[key description] lowercaseString] stringByStandardizingPath] UTF8String];
397     len = strlen(s);
398
399     // compute first hash    
400     hash1 = len;
401     for (cnt = 0; cnt < len; cnt++) {
402         hash1 += (hash1 << 8) + s[cnt];
403     }
404     hash1 += (hash1 << (len & 31));
405
406     // compute second hash    
407     hash2 = len;
408     for (cnt = 0; cnt < len; cnt++) {
409         hash2 = (37 * hash2) ^ s[cnt];
410     }
411
412 #ifdef __LP64__
413     snprintf(buffer, UniqueFilePathSize, "%.2u/%.2u/%.10u-%.10u.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
414 #else
415     snprintf(buffer, UniqueFilePathSize, "%.2lu/%.2lu/%.10lu-%.10lu.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
416 #endif
417 }
418
419 // This method of getting an object from the filesystem is taken from the old 
420 // WebKit Icon Database
421 static id objectFromPathForKey(NSString *databasePath, id key)
422 {
423     ASSERT(key);
424     id result = nil;
425
426     // Use the key->filename hashing the old WebKit IconDatabase used
427     char uniqueKey[UniqueFilePathSize];    
428     legacyIconDatabaseFilePathForKey(key, uniqueKey);
429     
430     // Get the data from this file and setup for the un-archiving
431     NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", databasePath, uniqueKey];
432     NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
433     NSUnarchiver *unarchiver = nil;
434     
435     NS_DURING
436         if (data) {
437             unarchiver = [[NSUnarchiver alloc] initForReadingWithData:data];
438             if (unarchiver) {
439                 id fileKey = [unarchiver decodeObject];
440                 if ([fileKey isEqual:key]) {
441                     id object = [unarchiver decodeObject];
442                     if (object) {
443                         // Decoded objects go away when the unarchiver does, so we need to
444                         // retain this so we can return it to our caller.
445                         result = [[object retain] autorelease];
446                         LOG(IconDatabase, "read disk cache file - %@", key);
447                     }
448                 }
449             }
450         }
451     NS_HANDLER
452         LOG(IconDatabase, "cannot unarchive cache file - %@", key);
453         result = nil;
454     NS_ENDHANDLER
455
456     [unarchiver release];
457     [data release];
458     [filePath release];
459     
460     return result;
461 }
462
463 static NSData* iconDataFromPathForIconURL(NSString *databasePath, NSString *iconURLString)
464 {
465     ASSERT(iconURLString);
466     ASSERT(databasePath);
467     
468     NSData *iconData = objectFromPathForKey(databasePath, iconURLString);
469     
470     if ((id)iconData == (id)[NSNull null]) 
471         return nil;
472         
473     return iconData;
474 }
475
476 - (void)_convertToWebCoreFormat
477 {
478     ASSERT(_private);    
479     
480     // If the WebCore Icon Database is not empty, we assume that this conversion has already
481     // taken place and skip the rest of the steps 
482     if (!IconDatabase::sharedIconDatabase()->isEmpty())
483         return;
484
485     // Get the directory the old icon database *should* be in
486     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
487     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
488     if (!databaseDirectory) {
489         databaseDirectory = WebIconDatabasePath;
490         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
491     }
492     databaseDirectory = [databaseDirectory stringByExpandingTildeInPath];
493
494     // With this directory, get the PageURLToIconURL map that was saved to disk
495     NSMutableDictionary *pageURLToIconURL = objectFromPathForKey(databaseDirectory, WebURLToIconURLKey);
496
497     // If the retrieved object was not a valid NSMutableDictionary, then we have no valid
498     // icons to convert
499     if (![pageURLToIconURL isKindOfClass:[NSMutableDictionary class]])
500         pageURLToIconURL = nil;
501     
502     NSEnumerator *enumerator = [pageURLToIconURL keyEnumerator];
503     NSString *url, *iconURL;
504     
505     // First, we'll iterate through the PageURL->IconURL map
506     while ((url = [enumerator nextObject]) != nil) {
507         iconURL = [pageURLToIconURL objectForKey:url];
508         if (!iconURL)
509             continue;
510         IconDatabase::sharedIconDatabase()->setIconURLForPageURL(iconURL, url);
511     }    
512
513     // Second, we'll get a list of the unique IconURLs we have
514     NSMutableSet *iconsOnDiskWithURLs = [NSMutableSet setWithArray:[pageURLToIconURL allValues]];
515     enumerator = [iconsOnDiskWithURLs objectEnumerator];
516     NSData *iconData;
517     
518     // And iterate through them, adding the icon data to the new icon database
519     while ((url = [enumerator nextObject]) != nil) {
520         iconData = iconDataFromPathForIconURL(databaseDirectory, url);
521         if (iconData)
522             IconDatabase::sharedIconDatabase()->setIconDataForIconURL([iconData bytes], [iconData length], url);
523         else {
524             // This really *shouldn't* happen, so it'd be good to track down why it might happen in a debug build
525             // however, we do know how to handle it gracefully in release
526             LOG_ERROR("%@ is marked as having an icon on disk, but we couldn't get the data for it", url);
527             IconDatabase::sharedIconDatabase()->setHaveNoIconForIconURL(url);
528         }
529     }
530
531     // After we're done converting old style icons over to webcore icons, we delete the entire directory hierarchy 
532     // for the old icon DB (skipping the new iconDB, which will likely be in the same directory)
533     NSFileManager *fileManager = [NSFileManager defaultManager];
534     enumerator = [[fileManager directoryContentsAtPath:databaseDirectory] objectEnumerator];
535     
536     NSString *databaseFilename = IconDatabase::sharedIconDatabase()->defaultDatabaseFilename();
537
538     NSString *file;
539     while ((file = [enumerator nextObject]) != nil) {
540         if ([file isEqualTo:databaseFilename])
541             continue;
542         NSString *filePath = [databaseDirectory stringByAppendingPathComponent:file];
543         if (![fileManager  removeFileAtPath:filePath handler:nil])
544             LOG_ERROR("Failed to delete %@ from old icon directory", filePath);
545     }
546 }
547
548 @end
549
550 @implementation WebIconDatabasePrivate
551 @end
552
553 NSImage *webGetNSImage(Image* image, NSSize size)
554 {
555     ASSERT(size.width);
556     ASSERT(size.height);
557
558     // FIXME: We're doing the resize here for now because WebCore::Image doesn't yet support resizing/multiple representations
559     // This makes it so there's effectively only one size of a particular icon in the system at a time. We should move this
560     // to WebCore::Image at some point.
561     if (!image)
562         return nil;
563     NSImage* nsImage = image->getNSImage();
564     if (!nsImage)
565         return nil;
566     if (!NSEqualSizes([nsImage size], size)) {
567         [nsImage setScalesWhenResized:YES];
568         [nsImage setSize:size];
569     }
570     return nsImage;
571 }