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