WebCore:
[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     // <rdar://problem/4678414> - New IconDB needs to delete icons when asked
193
194     if (![self _isEnabled])
195         return;
196         
197     [_private->databaseBridge removeAllIcons];
198
199     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidRemoveAllIconsNotification
200                                                         object:self
201                                                       userInfo:nil];
202 }
203
204 - (BOOL)isIconExpiredForIconURL:(NSString *)iconURL
205 {
206     return [_private->databaseBridge isIconExpiredForIconURL:iconURL];
207 }
208
209 @end
210
211 @implementation WebIconDatabase (WebPrivate)
212
213 - (BOOL)_isEnabled
214 {
215     // If we weren't enabled on startup, we marked the databaseBridge as nil
216     return _private->databaseBridge != nil;
217 }
218
219 - (void)_setIconData:(NSData *)data forIconURL:(NSString *)iconURL
220 {
221     ASSERT(data);
222     ASSERT(iconURL);
223     ASSERT([self _isEnabled]);   
224
225     [_private->databaseBridge _setIconData:data forIconURL:iconURL];
226 }
227
228 - (void)_setHaveNoIconForIconURL:(NSString *)iconURL
229 {
230     ASSERT(iconURL);
231     ASSERT([self _isEnabled]);
232
233     [_private->databaseBridge _setHaveNoIconForIconURL:iconURL];
234 }
235
236 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL
237 {
238     ASSERT(iconURL);
239     ASSERT(URL);
240     ASSERT([self _isEnabled]);
241     
242     // If this iconURL already maps to this pageURL, don't bother sending the notification
243     // The WebCore::IconDatabase returns TRUE if we should send the notification, and false if we shouldn't.
244     // This is a measurable win on the iBench - about 1% worth on average
245     if ([_private->databaseBridge _setIconURL:iconURL forPageURL:URL])
246         [self _sendNotificationForURL:URL];
247 }
248
249 - (BOOL)_hasEntryForIconURL:(NSString *)iconURL;
250 {
251     ASSERT([self _isEnabled]);
252
253     return [_private->databaseBridge _hasEntryForIconURL:iconURL];
254 }
255
256 - (void)_sendNotificationForURL:(NSString *)URL
257 {
258     ASSERT(URL);
259     
260     NSDictionary *userInfo = [NSDictionary dictionaryWithObject:URL
261                                                          forKey:WebIconNotificationUserInfoURLKey];
262     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidAddIconNotification
263                                                         object:self
264                                                       userInfo:userInfo];
265 }
266
267 - (void)loadIconFromURL:(NSString *)iconURL
268 {
269     [_private->databaseBridge loadIconFromURL:iconURL];
270 }
271
272 @end
273
274 @implementation WebIconDatabase (WebInternal)
275
276 - (void)_applicationWillTerminate:(NSNotification *)notification
277 {
278     [_private->databaseBridge closeSharedDatabase];
279     [_private->databaseBridge release];
280     _private->databaseBridge = nil;
281 }
282
283
284 - (NSImage *)_iconForFileURL:(NSString *)file withSize:(NSSize)size
285 {
286     ASSERT(size.width);
287     ASSERT(size.height);
288
289     NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
290     NSString *path = [[NSURL _web_URLWithDataAsString:file] path];
291     NSString *suffix = [path pathExtension];
292     NSImage *icon = nil;
293     
294     if ([suffix _webkit_isCaseInsensitiveEqualToString:@"htm"] || [suffix _webkit_isCaseInsensitiveEqualToString:@"html"]) {
295         if (!_private->htmlIcons) {
296             icon = [workspace iconForFileType:@"html"];
297             _private->htmlIcons = [[self _iconsBySplittingRepresentationsOfIcon:icon] retain];
298         }
299         icon = [self _iconFromDictionary:_private->htmlIcons forSize:size cache:YES];
300     } else {
301         if (!path || ![path isAbsolutePath]) {
302             // Return the generic icon when there is no path.
303             icon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericDocumentIcon)];
304         } else {
305             icon = [workspace iconForFile:path];
306         }
307         [self _scaleIcon:icon toSize:size];
308     }
309
310     return icon;
311 }
312
313 - (void)_resetCachedWebPreferences:(NSNotification *)notification
314 {
315     BOOL privateBrowsingEnabledNow = [[WebPreferences standardPreferences] privateBrowsingEnabled];
316
317     [_private->databaseBridge setPrivateBrowsingEnabled:privateBrowsingEnabledNow];
318 }
319
320 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons
321 {
322     ASSERT(icons);
323     
324     NSEnumerator *enumerator = [icons keyEnumerator];
325     NSValue *currentSize, *largestSize=nil;
326     float largestSizeArea=0;
327
328     while ((currentSize = [enumerator nextObject]) != nil) {
329         NSSize currentSizeSize = [currentSize sizeValue];
330         float currentSizeArea = currentSizeSize.width * currentSizeSize.height;
331         if(!largestSizeArea || (currentSizeArea > largestSizeArea)){
332             largestSize = currentSize;
333             largestSizeArea = currentSizeArea;
334         }
335     }
336
337     return [icons objectForKey:largestSize];
338 }
339
340 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon
341 {
342     ASSERT(icon);
343
344     NSMutableDictionary *icons = [NSMutableDictionary dictionary];
345     NSEnumerator *enumerator = [[icon representations] objectEnumerator];
346     NSImageRep *rep;
347
348     while ((rep = [enumerator nextObject]) != nil) {
349         NSSize size = [rep size];
350         NSImage *subIcon = [[NSImage alloc] initWithSize:size];
351         [subIcon addRepresentation:rep];
352         [icons setObject:subIcon forKey:[NSValue valueWithSize:size]];
353         [subIcon release];
354     }
355
356     if([icons count] > 0)
357         return icons;
358
359     LOG_ERROR("icon has no representations");
360     
361     return nil;
362 }
363
364 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache
365 {
366     ASSERT(size.width);
367     ASSERT(size.height);
368
369     NSImage *icon = [icons objectForKey:[NSValue valueWithSize:size]];
370
371     if(!icon){
372         icon = [[[self _largestIconFromDictionary:icons] copy] autorelease];
373         [self _scaleIcon:icon toSize:size];
374
375         if(cache){
376             [icons setObject:icon forKey:[NSValue valueWithSize:size]];
377         }
378     }
379
380     return icon;
381 }
382
383 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size
384 {
385     ASSERT(size.width);
386     ASSERT(size.height);
387     
388 #if !LOG_DISABLED        
389     double start = CFAbsoluteTimeGetCurrent();
390 #endif
391     
392     [icon setScalesWhenResized:YES];
393     [icon setSize:size];
394     
395 #if !LOG_DISABLED
396     double duration = CFAbsoluteTimeGetCurrent() - start;
397     LOG(Timing, "scaling icon took %f seconds.", duration);
398 #endif
399 }
400
401 // This hashing String->filename algorithm came from WebFileDatabase.m and is what was used in the 
402 // WebKit Icon Database
403 static void legacyIconDatabaseFilePathForKey(id key, char *buffer)
404 {
405     const char *s;
406     UInt32 hash1;
407     UInt32 hash2;
408     CFIndex len;
409     CFIndex cnt;
410     
411     s = [[[[key description] lowercaseString] stringByStandardizingPath] UTF8String];
412     len = strlen(s);
413
414     // compute first hash    
415     hash1 = len;
416     for (cnt = 0; cnt < len; cnt++) {
417         hash1 += (hash1 << 8) + s[cnt];
418     }
419     hash1 += (hash1 << (len & 31));
420
421     // compute second hash    
422     hash2 = len;
423     for (cnt = 0; cnt < len; cnt++) {
424         hash2 = (37 * hash2) ^ s[cnt];
425     }
426
427 #ifdef __LP64__
428     snprintf(buffer, UniqueFilePathSize, "%.2u/%.2u/%.10u-%.10u.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
429 #else
430     snprintf(buffer, UniqueFilePathSize, "%.2lu/%.2lu/%.10lu-%.10lu.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
431 #endif
432 }
433
434 // This method of getting an object from the filesystem is taken from the old 
435 // WebKit Icon Database
436 static id objectFromPathForKey(NSString *databasePath, id key)
437 {
438     ASSERT(key);
439     id result = nil;
440
441     // Use the key->filename hashing the old WebKit IconDatabase used
442     char uniqueKey[UniqueFilePathSize];    
443     legacyIconDatabaseFilePathForKey(key, uniqueKey);
444     
445     // Get the data from this file and setup for the un-archiving
446     NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", databasePath, uniqueKey];
447     NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
448     NSUnarchiver *unarchiver = nil;
449     
450     NS_DURING
451         if (data) {
452             unarchiver = [[NSUnarchiver alloc] initForReadingWithData:data];
453             if (unarchiver) {
454                 id fileKey = [unarchiver decodeObject];
455                 if ([fileKey isEqual:key]) {
456                     id object = [unarchiver decodeObject];
457                     if (object) {
458                         // Decoded objects go away when the unarchiver does, so we need to
459                         // retain this so we can return it to our caller.
460                         result = [[object retain] autorelease];
461                         LOG(IconDatabase, "read disk cache file - %@", key);
462                     }
463                 }
464             }
465         }
466     NS_HANDLER
467         LOG(IconDatabase, "cannot unarchive cache file - %@", key);
468         result = nil;
469     NS_ENDHANDLER
470
471     [unarchiver release];
472     [data release];
473     [filePath release];
474     
475     return result;
476 }
477
478 static NSData* iconDataFromPathForIconURL(NSString *databasePath, NSString *iconURLString)
479 {
480     ASSERT(iconURLString);
481     ASSERT(databasePath);
482     
483     NSData *iconData = objectFromPathForKey(databasePath, iconURLString);
484     
485     if ((id)iconData == (id)[NSNull null]) 
486         return nil;
487         
488     return iconData;
489 }
490
491 - (void)_convertToWebCoreFormat
492 {
493     ASSERT(_private);
494     ASSERT(_private->databaseBridge);
495     
496     
497     // If the WebCore Icon Database is not empty, we assume that this conversion has already
498     // taken place and skip the rest of the steps 
499     if (![_private->databaseBridge _isEmpty]) {
500         return;
501     }
502
503     // Get the directory the old icon database *should* be in
504     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
505     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
506     if (!databaseDirectory) {
507         databaseDirectory = WebIconDatabasePath;
508         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
509     }
510     databaseDirectory = [databaseDirectory stringByExpandingTildeInPath];
511
512     // With this directory, get the PageURLToIconURL map that was saved to disk
513     NSMutableDictionary *pageURLToIconURL = objectFromPathForKey(databaseDirectory, WebURLToIconURLKey);
514
515     // If the retrieved object was not a valid NSMutableDictionary, then we have no valid
516     // icons to convert
517     if (![pageURLToIconURL isKindOfClass:[NSMutableDictionary class]])
518         pageURLToIconURL = nil;
519     
520     NSEnumerator *enumerator = [pageURLToIconURL keyEnumerator];
521     NSString *url, *iconURL;
522     
523     // First, we'll iterate through the PageURL->IconURL map
524     while ((url = [enumerator nextObject]) != nil) {
525         iconURL = [pageURLToIconURL objectForKey:url];
526         if (!iconURL)
527             continue;
528         [_private->databaseBridge _setIconURL:iconURL forPageURL:url];
529     }    
530
531     // Second, we'll get a list of the unique IconURLs we have
532     NSMutableSet *iconsOnDiskWithURLs = [NSMutableSet setWithArray:[pageURLToIconURL allValues]];
533     enumerator = [iconsOnDiskWithURLs objectEnumerator];
534     NSData *iconData;
535     
536     // And iterate through them, adding the icon data to the new icon database
537     while ((url = [enumerator nextObject]) != nil) {
538         iconData = iconDataFromPathForIconURL(databaseDirectory, url);
539         if (iconData)
540             [_private->databaseBridge _setIconData:iconData forIconURL:url];
541         else {
542             // This really *shouldn't* happen, so it'd be good to track down why it might happen in a debug build
543             // however, we do know how to handle it gracefully in release
544             LOG_ERROR("%@ is marked as having an icon on disk, but we couldn't get the data for it", url);
545             [_private->databaseBridge _setHaveNoIconForIconURL:url];
546         }
547     }
548
549     // After we're done converting old style icons over to webcore icons, we delete the entire directory hierarchy 
550     // for the old icon DB (skipping the new iconDB, which will likely be in the same directory)
551     NSFileManager *fileManager = [NSFileManager defaultManager];
552     enumerator = [[fileManager directoryContentsAtPath:databaseDirectory] objectEnumerator];
553     
554     NSString *databaseFilename = [_private->databaseBridge defaultDatabaseFilename];
555
556     NSString *file;
557     while ((file = [enumerator nextObject]) != nil) {
558         if ([file isEqualTo:databaseFilename])
559             continue;
560         NSString *filePath = [databaseDirectory stringByAppendingPathComponent:file];
561         if (![fileManager  removeFileAtPath:filePath handler:nil])
562             LOG_ERROR("Failed to delete %@ from old icon directory", filePath);
563     }
564 }
565
566 @end
567
568 // This empty implementation must exist 
569 @implementation WebIconDatabasePrivate
570 @end
571