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 sharedInstance];
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     if (![_private->databaseBridge openSharedDatabaseWithPath:databaseDirectory]) {
111         LOG_ERROR("Unable to open IconDatabaseBridge");
112         [_private->databaseBridge release];
113         _private->databaseBridge = nil;
114         return self;
115     }
116     [_private->databaseBridge setPrivateBrowsingEnabled:[[WebPreferences standardPreferences] privateBrowsingEnabled]];
117     
118     // <rdar://problem/4674552> New IconDB: New Safari icon database needs to convert from the old icons on first load 
119     // We call this method on each load - it determines, on its own, whether or not we actually need to do the conversion and 
120     // returns early if the conversion is already done
121     [self _convertToWebCoreFormat];
122     
123     // Register for important notifications
124     [[NSNotificationCenter defaultCenter] addObserver:self
125                                              selector:@selector(_applicationWillTerminate:)
126                                                  name:NSApplicationWillTerminateNotification
127                                                object:NSApp];
128     [[NSNotificationCenter defaultCenter] 
129             addObserver:self selector:@selector(_resetCachedWebPreferences:) 
130                    name:WebPreferencesChangedNotification object:nil];
131
132     return self;
133 }
134
135 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size cache:(BOOL)cache
136 {
137     ASSERT(size.width);
138     ASSERT(size.height);
139
140     if (!URL || ![self _isEnabled])
141         return [self defaultIconWithSize:size];
142
143     // FIXME - <rdar://problem/4697934> - Move the handling of FileURLs to WebCore and implement in ObjC++
144     if ([URL _webkit_isFileURL])
145         return [self _iconForFileURL:URL withSize:size];
146       
147     NSImage* image = [_private->databaseBridge iconForPageURL:URL withSize:size];
148     return image ? image : [self defaultIconWithSize:size];
149 }
150
151 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size
152 {
153     return [self iconForURL:URL withSize:size cache:YES];
154 }
155
156 - (NSString *)iconURLForURL:(NSString *)URL
157 {
158     if (![self _isEnabled])
159         return nil;
160         
161     NSString* iconurl = [_private->databaseBridge iconURLForPageURL:URL];
162     return iconurl;
163 }
164
165 - (NSImage *)defaultIconWithSize:(NSSize)size
166 {
167     ASSERT(size.width);
168     ASSERT(size.height);
169     
170     return [_private->databaseBridge defaultIconWithSize:size];
171 }
172
173 - (void)retainIconForURL:(NSString *)URL
174 {
175     ASSERT(URL);
176     if (![self _isEnabled])
177         return;
178
179     [_private->databaseBridge retainIconForURL:URL];
180 }
181
182 - (void)releaseIconForURL:(NSString *)pageURL
183 {
184     ASSERT(pageURL);
185     if (![self _isEnabled])
186         return;
187
188     [_private->databaseBridge releaseIconForURL:pageURL];
189 }
190 @end
191
192
193 @implementation WebIconDatabase (WebPendingPublic)
194
195 - (void)removeAllIcons
196 {
197     // <rdar://problem/4678414> - New IconDB needs to delete icons when asked
198
199     if (![self _isEnabled])
200         return;
201         
202     [_private->databaseBridge removeAllIcons];
203
204     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidRemoveAllIconsNotification
205                                                         object:self
206                                                       userInfo:nil];
207 }
208
209 - (BOOL)isIconExpiredForIconURL:(NSString *)iconURL
210 {
211     return [_private->databaseBridge isIconExpiredForIconURL:iconURL];
212 }
213
214 @end
215
216 @implementation WebIconDatabase (WebPrivate)
217
218 - (BOOL)_isEnabled
219 {
220     // If we weren't enabled on startup, we marked the databaseBridge as nil
221     return _private->databaseBridge != nil;
222 }
223
224 - (void)_setIconData:(NSData *)data forIconURL:(NSString *)iconURL
225 {
226     ASSERT(data);
227     ASSERT(iconURL);
228     ASSERT([self _isEnabled]);   
229
230     [_private->databaseBridge _setIconData:data forIconURL:iconURL];
231 }
232
233 - (void)_setHaveNoIconForIconURL:(NSString *)iconURL
234 {
235     ASSERT(iconURL);
236     ASSERT([self _isEnabled]);
237
238     [_private->databaseBridge _setHaveNoIconForIconURL:iconURL];
239 }
240
241 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL
242 {
243     ASSERT(iconURL);
244     ASSERT(URL);
245     ASSERT([self _isEnabled]);
246     
247     // If this iconURL already maps to this pageURL, don't bother sending the notification
248     // The WebCore::IconDatabase returns TRUE if we should send the notification, and false if we shouldn't.
249     // This is a measurable win on the iBench - about 1% worth on average
250     if ([_private->databaseBridge _setIconURL:iconURL forPageURL:URL])
251         [self _sendNotificationForURL:URL];
252 }
253
254 - (BOOL)_hasEntryForIconURL:(NSString *)iconURL;
255 {
256     ASSERT([self _isEnabled]);
257
258     return [_private->databaseBridge _hasEntryForIconURL:iconURL];
259 }
260
261 - (void)_sendNotificationForURL:(NSString *)URL
262 {
263     ASSERT(URL);
264     
265     NSDictionary *userInfo = [NSDictionary dictionaryWithObject:URL
266                                                          forKey:WebIconNotificationUserInfoURLKey];
267     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidAddIconNotification
268                                                         object:self
269                                                       userInfo:userInfo];
270 }
271
272 - (void)loadIconFromURL:(NSString *)iconURL
273 {
274     [_private->databaseBridge loadIconFromURL:iconURL];
275 }
276
277 @end
278
279 @implementation WebIconDatabase (WebInternal)
280
281 - (void)_applicationWillTerminate:(NSNotification *)notification
282 {
283     [_private->databaseBridge closeSharedDatabase];
284     [_private->databaseBridge release];
285     _private->databaseBridge = nil;
286 }
287
288
289 - (NSImage *)_iconForFileURL:(NSString *)file withSize:(NSSize)size
290 {
291     ASSERT(size.width);
292     ASSERT(size.height);
293
294     NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
295     NSString *path = [[NSURL _web_URLWithDataAsString:file] path];
296     NSString *suffix = [path pathExtension];
297     NSImage *icon = nil;
298     
299     if ([suffix _webkit_isCaseInsensitiveEqualToString:@"htm"] || [suffix _webkit_isCaseInsensitiveEqualToString:@"html"]) {
300         if (!_private->htmlIcons) {
301             icon = [workspace iconForFileType:@"html"];
302             _private->htmlIcons = [[self _iconsBySplittingRepresentationsOfIcon:icon] retain];
303         }
304         icon = [self _iconFromDictionary:_private->htmlIcons forSize:size cache:YES];
305     } else {
306         if (!path || ![path isAbsolutePath]) {
307             // Return the generic icon when there is no path.
308             icon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericDocumentIcon)];
309         } else {
310             icon = [workspace iconForFile:path];
311         }
312         [self _scaleIcon:icon toSize:size];
313     }
314
315     return icon;
316 }
317
318 - (void)_resetCachedWebPreferences:(NSNotification *)notification
319 {
320     BOOL privateBrowsingEnabledNow = [[WebPreferences standardPreferences] privateBrowsingEnabled];
321
322     [_private->databaseBridge setPrivateBrowsingEnabled:privateBrowsingEnabledNow];
323 }
324
325 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons
326 {
327     ASSERT(icons);
328     
329     NSEnumerator *enumerator = [icons keyEnumerator];
330     NSValue *currentSize, *largestSize=nil;
331     float largestSizeArea=0;
332
333     while ((currentSize = [enumerator nextObject]) != nil) {
334         NSSize currentSizeSize = [currentSize sizeValue];
335         float currentSizeArea = currentSizeSize.width * currentSizeSize.height;
336         if(!largestSizeArea || (currentSizeArea > largestSizeArea)){
337             largestSize = currentSize;
338             largestSizeArea = currentSizeArea;
339         }
340     }
341
342     return [icons objectForKey:largestSize];
343 }
344
345 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon
346 {
347     ASSERT(icon);
348
349     NSMutableDictionary *icons = [NSMutableDictionary dictionary];
350     NSEnumerator *enumerator = [[icon representations] objectEnumerator];
351     NSImageRep *rep;
352
353     while ((rep = [enumerator nextObject]) != nil) {
354         NSSize size = [rep size];
355         NSImage *subIcon = [[NSImage alloc] initWithSize:size];
356         [subIcon addRepresentation:rep];
357         [icons setObject:subIcon forKey:[NSValue valueWithSize:size]];
358         [subIcon release];
359     }
360
361     if([icons count] > 0)
362         return icons;
363
364     LOG_ERROR("icon has no representations");
365     
366     return nil;
367 }
368
369 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache
370 {
371     ASSERT(size.width);
372     ASSERT(size.height);
373
374     NSImage *icon = [icons objectForKey:[NSValue valueWithSize:size]];
375
376     if(!icon){
377         icon = [[[self _largestIconFromDictionary:icons] copy] autorelease];
378         [self _scaleIcon:icon toSize:size];
379
380         if(cache){
381             [icons setObject:icon forKey:[NSValue valueWithSize:size]];
382         }
383     }
384
385     return icon;
386 }
387
388 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size
389 {
390     ASSERT(size.width);
391     ASSERT(size.height);
392     
393 #if !LOG_DISABLED        
394     double start = CFAbsoluteTimeGetCurrent();
395 #endif
396     
397     [icon setScalesWhenResized:YES];
398     [icon setSize:size];
399     
400 #if !LOG_DISABLED
401     double duration = CFAbsoluteTimeGetCurrent() - start;
402     LOG(Timing, "scaling icon took %f seconds.", duration);
403 #endif
404 }
405
406 // This hashing String->filename algorithm came from WebFileDatabase.m and is what was used in the 
407 // WebKit Icon Database
408 static void legacyIconDatabaseFilePathForKey(id key, char *buffer)
409 {
410     const char *s;
411     UInt32 hash1;
412     UInt32 hash2;
413     CFIndex len;
414     CFIndex cnt;
415     
416     s = [[[[key description] lowercaseString] stringByStandardizingPath] UTF8String];
417     len = strlen(s);
418
419     // compute first hash    
420     hash1 = len;
421     for (cnt = 0; cnt < len; cnt++) {
422         hash1 += (hash1 << 8) + s[cnt];
423     }
424     hash1 += (hash1 << (len & 31));
425
426     // compute second hash    
427     hash2 = len;
428     for (cnt = 0; cnt < len; cnt++) {
429         hash2 = (37 * hash2) ^ s[cnt];
430     }
431
432 #ifdef __LP64__
433     snprintf(buffer, UniqueFilePathSize, "%.2u/%.2u/%.10u-%.10u.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
434 #else
435     snprintf(buffer, UniqueFilePathSize, "%.2lu/%.2lu/%.10lu-%.10lu.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
436 #endif
437 }
438
439 // This method of getting an object from the filesystem is taken from the old 
440 // WebKit Icon Database
441 static id objectFromPathForKey(NSString *databasePath, id key)
442 {
443     ASSERT(key);
444     id result = nil;
445
446     // Use the key->filename hashing the old WebKit IconDatabase used
447     char uniqueKey[UniqueFilePathSize];    
448     legacyIconDatabaseFilePathForKey(key, uniqueKey);
449     
450     // Get the data from this file and setup for the un-archiving
451     NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", databasePath, uniqueKey];
452     NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
453     NSUnarchiver *unarchiver = nil;
454     
455     NS_DURING
456         if (data) {
457             unarchiver = [[NSUnarchiver alloc] initForReadingWithData:data];
458             if (unarchiver) {
459                 id fileKey = [unarchiver decodeObject];
460                 if ([fileKey isEqual:key]) {
461                     id object = [unarchiver decodeObject];
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