2006-04-28 Eric Seidel <eseidel@apple.com>
[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
29 #import <WebKit/WebIconDatabase.h>
30
31 #import <WebKit/WebIconDatabasePrivate.h>
32 #import <WebKit/WebFileDatabase.h>
33 #import <WebKit/WebKitLogging.h>
34 #import <WebKit/WebKitNSStringExtras.h>
35 #import <WebKit/WebNSURLExtras.h>
36 #import <WebKit/WebPreferences.h>
37
38 NSString * const WebIconDatabaseVersionKey =    @"WebIconDatabaseVersion";
39 NSString * const WebURLToIconURLKey =           @"WebSiteURLToIconURLKey";
40
41 NSString * const ObsoleteIconsOnDiskKey =       @"WebIconsOnDisk";
42 NSString * const ObsoleteIconURLToURLsKey =     @"WebIconURLToSiteURLs";
43
44 static const int WebIconDatabaseCurrentVersion = 2;
45
46 NSString *WebIconDatabaseDidAddIconNotification =          @"WebIconDatabaseDidAddIconNotification";
47 NSString *WebIconNotificationUserInfoURLKey =              @"WebIconNotificationUserInfoURLKey";
48 NSString *WebIconDatabaseDidRemoveAllIconsNotification =   @"WebIconDatabaseDidRemoveAllIconsNotification";
49
50 NSString *WebIconDatabaseDirectoryDefaultsKey = @"WebIconDatabaseDirectoryDefaultsKey";
51 NSString *WebIconDatabaseEnabledDefaultsKey =   @"WebIconDatabaseEnabled";
52
53 NSSize WebIconSmallSize = {16, 16};
54 NSSize WebIconMediumSize = {32, 32};
55 NSSize WebIconLargeSize = {128, 128};
56
57 @interface NSMutableDictionary (WebIconDatabase)
58 - (void)_web_setObjectUsingSetIfNecessary:(id)object forKey:(id)key;
59 @end
60
61 @interface WebIconDatabase (WebInternal)
62 - (void)_clearDictionaries;
63 - (void)_createFileDatabase;
64 - (void)_loadIconDictionaries;
65 - (void)_updateFileDatabase;
66 - (void)_forgetIconForIconURLString:(NSString *)iconURLString;
67 - (NSMutableDictionary *)_iconsForIconURLString:(NSString *)iconURL;
68 - (NSImage *)_iconForFileURL:(NSString *)fileURL withSize:(NSSize)size;
69 - (void)_retainIconForIconURLString:(NSString *)iconURL;
70 - (void)_releaseIconForIconURLString:(NSString *)iconURL;
71 - (void)_retainOriginalIconsOnDisk;
72 - (void)_releaseOriginalIconsOnDisk;
73 - (void)_resetCachedWebPreferences:(NSNotification *)notification;
74 - (void)_sendNotificationForURL:(NSString *)URL;
75 - (int)_totalRetainCountForIconURLString:(NSString *)iconURLString;
76 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons;
77 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon;
78 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache;
79 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size;
80 @end
81
82
83 @implementation WebIconDatabase
84
85 + (WebIconDatabase *)sharedIconDatabase
86 {
87     static WebIconDatabase *database = nil;
88     
89     if (!database) {
90 #if !LOG_DISABLED
91         double start = CFAbsoluteTimeGetCurrent();
92 #endif
93         database = [[WebIconDatabase alloc] init];
94 #if !LOG_DISABLED
95         LOG(Timing, "initializing icon database with %d sites and %d icons took %f", 
96             [database->_private->pageURLToIconURL count], [database->_private->iconURLToPageURLs count], (CFAbsoluteTimeGetCurrent() - start));
97 #endif
98     }
99     return database;
100 }
101
102 - init
103 {
104     [super init];
105     
106     _private = [[WebIconDatabasePrivate alloc] init];
107
108     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
109     NSDictionary *initialDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithBool:YES], WebIconDatabaseEnabledDefaultsKey, nil];
110     [defaults registerDefaults:initialDefaults];
111     [initialDefaults release];
112     if (![defaults boolForKey:WebIconDatabaseEnabledDefaultsKey]) {
113         return self;
114     }
115     
116     [self _createFileDatabase];
117     [self _loadIconDictionaries];
118
119     _private->iconURLToIcons = [[NSMutableDictionary alloc] init];
120     _private->iconURLToExtraRetainCount = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, NULL);
121     _private->pageURLToRetainCount = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, NULL);
122     _private->iconsToEraseWithURLs = [[NSMutableSet alloc] init];
123     _private->iconsToSaveWithURLs = [[NSMutableSet alloc] init];
124     _private->iconURLsWithNoIcons = [[NSMutableSet alloc] init];
125     _private->iconURLsBoundDuringPrivateBrowsing = [[NSMutableSet alloc] init];
126     _private->pageURLsBoundDuringPrivateBrowsing = [[NSMutableSet alloc] init];
127     _private->privateBrowsingEnabled = [[WebPreferences standardPreferences] privateBrowsingEnabled];
128     
129     [[NSNotificationCenter defaultCenter] addObserver:self
130                                              selector:@selector(_applicationWillTerminate:)
131                                                  name:NSApplicationWillTerminateNotification
132                                                object:NSApp];
133     [[NSNotificationCenter defaultCenter] 
134             addObserver:self selector:@selector(_resetCachedWebPreferences:) 
135                    name:WebPreferencesChangedNotification object:nil];
136     
137
138     // Retain icons on disk then release them once clean-up has begun.
139     // This gives the client the opportunity to retain them before they are erased.
140     [self _retainOriginalIconsOnDisk];
141     [self performSelector:@selector(_releaseOriginalIconsOnDisk) withObject:nil afterDelay:0];
142     
143     return self;
144 }
145
146 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size cache:(BOOL)cache
147 {
148     ASSERT(size.width);
149     ASSERT(size.height);
150     
151     if (!URL || ![self _isEnabled])
152         return [self defaultIconWithSize:size];
153
154     if ([URL _webkit_isFileURL])
155         return [self _iconForFileURL:URL withSize:size];
156     
157     NSString *iconURLString = [_private->pageURLToIconURL objectForKey:URL];
158     if (!iconURLString)
159         // Don't have it
160         return [self defaultIconWithSize:size];
161
162     NSMutableDictionary *icons = [self _iconsForIconURLString:iconURLString];
163     if (!icons) {
164         if (![_private->iconURLsWithNoIcons containsObject:iconURLString]) {
165            // We used to have this icon, but don't have it anymore for some reason. (Bug? Deleted from
166            // disk behind our back?). Forget that we ever had it so it will be re-fetched next time.
167             LOG_ERROR("WebIconDatabase used to contain %@, but the icon file is missing. Now forgetting that we ever knew about this icon.", iconURLString);
168             [self _forgetIconForIconURLString:iconURLString];
169         }
170         return [self defaultIconWithSize:size];
171     }        
172
173     return [self _iconFromDictionary:icons forSize:size cache:cache];
174 }
175
176 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size
177 {
178     return [self iconForURL:URL withSize:size cache:YES];
179 }
180
181 - (NSString *)iconURLForURL:(NSString *)URL
182 {
183     if (![self _isEnabled])
184         return nil;
185     return URL ? [_private->pageURLToIconURL objectForKey:URL] : nil;
186 }
187
188 - (NSImage *)defaultIconWithSize:(NSSize)size
189 {
190     ASSERT(size.width);
191     ASSERT(size.height);
192     
193     if (!_private->defaultIcons) {
194         NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"url_icon" ofType:@"tiff"];
195         if (path) {
196             NSImage *icon = [[NSImage alloc] initByReferencingFile:path];
197             _private->defaultIcons = [[NSMutableDictionary dictionaryWithObject:icon
198                                             forKey:[NSValue valueWithSize:[icon size]]] retain];
199             [icon release];
200         }
201     }
202
203     return [self _iconFromDictionary:_private->defaultIcons forSize:size cache:YES];
204 }
205
206 - (void)retainIconForURL:(NSString *)URL
207 {
208     ASSERT(URL);
209     
210     if (![self _isEnabled])
211         return;
212     
213     int retainCount = (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, URL);
214     CFDictionarySetValue(_private->pageURLToRetainCount, URL, (void *)(retainCount + 1));
215 }
216
217 - (void)releaseIconForURL:(NSString *)pageURL
218 {
219     ASSERT(pageURL);
220     
221     if (![self _isEnabled])
222         return;
223     
224     int retainCount = (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, pageURL);
225     
226     if (retainCount <= 0) {
227         LOG_ERROR("The icon for %@ was released more times than it was retained.", pageURL);
228         return;
229     }
230     
231     int newRetainCount = retainCount - 1;
232
233     if (newRetainCount == 0) {
234         // Forget association between this page URL and a retain count
235         CFDictionaryRemoveValue(_private->pageURLToRetainCount, pageURL);
236
237         // If there's a known iconURL for this page URL, we need to do more cleanup
238         NSString *iconURL = [_private->pageURLToIconURL objectForKey:pageURL];
239         if (iconURL != nil) {
240             // If there are no other retainers of this icon, forget it entirely
241             if ([self _totalRetainCountForIconURLString:iconURL] == 0) {
242                 [self _forgetIconForIconURLString:iconURL];
243             } else {
244                 // There's at least one other retainer of this icon, so we need to forget the
245                 // two-way links between this page URL and the icon URL, without blowing away
246                 // the icon entirely.                
247                 id pageURLs = [_private->iconURLToPageURLs objectForKey:iconURL];
248                 if ([pageURLs isKindOfClass:[NSMutableSet class]]) {
249                     ASSERT([pageURLs containsObject:pageURL]);
250                     [pageURLs removeObject:pageURL];
251                     
252                     // Maybe this was the last page URL mapped to this icon URL
253                     if ([(NSMutableSet *)pageURLs count] == 0) {
254                         [_private->iconURLToPageURLs removeObjectForKey:iconURL];
255                     }
256                 } else {
257                     // Only one page URL was associated with this icon URL; it must have been us
258                     ASSERT([pageURLs isKindOfClass:[NSString class]]);
259                     ASSERT([pageURLs isEqualToString:pageURL]);
260                     [_private->iconURLToPageURLs removeObjectForKey:pageURL];
261                 }
262                 
263                 // Remove iconURL from this dictionary last, since this might be the last
264                 // reference and we need to use it as a key for _private->iconURLToPageURLs above.
265                 [_private->pageURLToIconURL removeObjectForKey:pageURL];
266             }
267         }
268     } else {
269         CFDictionarySetValue(_private->pageURLToRetainCount, pageURL, (void *)newRetainCount);
270     }
271 }
272
273 - (void)delayDatabaseCleanup
274 {
275     if (![self _isEnabled]) {
276         return;
277     }
278     
279     if(_private->didCleanup){
280         LOG_ERROR("delayDatabaseCleanup cannot be called after cleanup has begun");
281         return;
282     }
283     
284     _private->cleanupCount++;
285 }
286
287 - (void)allowDatabaseCleanup
288 {
289     if (![self _isEnabled]) {
290         return;
291     }
292     
293     if(_private->didCleanup){
294         LOG_ERROR("allowDatabaseCleanup cannot be called after cleanup has begun");
295         return;
296     }
297     
298     _private->cleanupCount--;
299
300     if(_private->cleanupCount == 0 && _private->waitingToCleanup){
301         [self _releaseOriginalIconsOnDisk];
302     }
303 }
304
305 @end
306
307
308 @implementation WebIconDatabase (WebPendingPublic)
309
310 - (void)removeAllIcons
311 {
312     NSEnumerator *keyEnumerator = [(NSDictionary *)_private->iconURLToPageURLs keyEnumerator];
313     NSString *iconURLString;
314     while ((iconURLString = [keyEnumerator nextObject]) != nil)
315         [self _forgetIconForIconURLString:iconURLString];
316     
317     // Delete entire file database immediately. This has at least three advantages over waiting for
318     // _updateFileDatabase to execute:
319     // (1) _updateFileDatabase won't execute until an icon has been added
320     // (2) this is faster
321     // (3) this deletes all the on-disk hierarchy (especially useful if due to past problems there are
322     // some stale files in that hierarchy)
323     [_private->fileDatabase removeAllObjects];
324     [_private->iconsToEraseWithURLs removeAllObjects];
325     [_private->iconsToSaveWithURLs removeAllObjects];
326     [_private->iconURLsBoundDuringPrivateBrowsing removeAllObjects];
327     [_private->pageURLsBoundDuringPrivateBrowsing removeAllObjects];
328     [self _clearDictionaries];
329     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidRemoveAllIconsNotification
330                                                         object:self
331                                                       userInfo:nil];
332 }
333 @end
334
335 @implementation WebIconDatabase (WebPrivate)
336
337 - (BOOL)_isEnabled
338 {
339     return (_private->fileDatabase != nil);
340 }
341
342 - (void)_setIcon:(NSImage *)icon forIconURL:(NSString *)iconURL
343 {
344     ASSERT(icon);
345     ASSERT(iconURL);
346     ASSERT([self _isEnabled]);
347     
348     NSMutableDictionary *icons = [self _iconsBySplittingRepresentationsOfIcon:icon];
349     
350     if (!icons)
351         return;
352     
353     [_private->iconURLToIcons setObject:icons forKey:iconURL];
354     
355     // Don't update any icon information on disk during private browsing. Remember which icons have been
356     // affected during private browsing so we can forget this information when private browsing is turned off.
357     if (_private->privateBrowsingEnabled)
358         [_private->iconURLsBoundDuringPrivateBrowsing addObject:iconURL];
359
360     [self _retainIconForIconURLString:iconURL];
361     
362     // Release the newly created icon much like an autorelease.
363     // This gives the client enough time to retain it.
364     // FIXME: Should use an actual autorelease here using a proxy object instead.
365     [self performSelector:@selector(_releaseIconForIconURLString:) withObject:iconURL afterDelay:0];
366 }
367
368 - (void)_setHaveNoIconForIconURL:(NSString *)iconURL
369 {
370     ASSERT(iconURL);
371     ASSERT([self _isEnabled]);
372
373     [_private->iconURLsWithNoIcons addObject:iconURL];
374     
375     // Don't update any icon information on disk during private browsing. Remember which icons have been
376     // affected during private browsing so we can forget this information when private browsing is turned off.
377     if (_private->privateBrowsingEnabled)
378         [_private->iconURLsBoundDuringPrivateBrowsing addObject:iconURL];
379
380     [self _retainIconForIconURLString:iconURL];
381
382     // Release the newly created icon much like an autorelease.
383     // This gives the client enough time to retain it.
384     // FIXME: Should use an actual autorelease here using a proxy object instead.
385     [self performSelector:@selector(_releaseIconForIconURLString:) withObject:iconURL afterDelay:0];
386 }
387
388
389 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL
390 {
391     ASSERT(iconURL);
392     ASSERT(URL);
393     ASSERT([self _isEnabled]);
394     ASSERT([self _hasIconForIconURL:iconURL]);
395     ASSERT(_private->pageURLToIconURL);
396
397     if ([[_private->pageURLToIconURL objectForKey:URL] isEqualToString:iconURL]) {
398         // Don't do any work if the icon URL is already bound to the site URL
399         return;
400     }
401
402     if ([_private->iconURLsWithNoIcons containsObject:iconURL]) {
403         // Don't do any work if the icon URL is in the negative cache, it's not
404         // worth doing work just to record that this site is associated with
405         // a nonexistent icon
406         return;
407     }
408     
409     // Keep track of which entries in pageURLToIconURL were created during private browsing so that they can be skipped
410     // when saving to disk.
411     if (_private->privateBrowsingEnabled)
412         [_private->pageURLsBoundDuringPrivateBrowsing addObject:URL];
413
414     [_private->pageURLToIconURL setObject:iconURL forKey:URL];
415     [_private->iconURLToPageURLs _web_setObjectUsingSetIfNecessary:URL forKey:iconURL];
416         
417     [self _sendNotificationForURL:URL];
418     [self _updateFileDatabase];
419 }
420
421 - (BOOL)_hasIconForIconURL:(NSString *)iconURL;
422 {
423     ASSERT([self _isEnabled]);
424     
425     return (([_private->iconURLToIcons objectForKey:iconURL] ||
426              [_private->iconURLsWithNoIcons containsObject:iconURL] ||
427              [_private->iconsOnDiskWithURLs containsObject:iconURL]) &&
428              [self _totalRetainCountForIconURLString:iconURL] > 0);
429 }
430
431 @end
432
433 @implementation WebIconDatabase (WebInternal)
434
435 - (void)_createFileDatabase
436 {
437     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
438     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
439
440     if (!databaseDirectory) {
441         databaseDirectory = @"~/Library/Icons";
442         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
443     }
444     databaseDirectory = [databaseDirectory stringByExpandingTildeInPath];
445     
446     _private->fileDatabase = [[WebFileDatabase alloc] initWithPath:databaseDirectory];
447     [_private->fileDatabase setSizeLimit:20000000];
448     [_private->fileDatabase open];
449 }
450
451 - (void)_clearDictionaries
452 {
453     [_private->pageURLToIconURL release];
454     [_private->iconURLToPageURLs release];
455     [_private->iconsOnDiskWithURLs release];
456     [_private->originalIconsOnDiskWithURLs release];
457     [_private->iconURLsBoundDuringPrivateBrowsing release];
458     [_private->pageURLsBoundDuringPrivateBrowsing release];
459     _private->pageURLToIconURL = [[NSMutableDictionary alloc] init];
460     _private->iconURLToPageURLs = [[NSMutableDictionary alloc] init];
461     _private->iconsOnDiskWithURLs = [[NSMutableSet alloc] init];
462     _private->originalIconsOnDiskWithURLs = [[NSMutableSet alloc] init];
463     _private->iconURLsBoundDuringPrivateBrowsing = [[NSMutableSet alloc] init];
464     _private->pageURLsBoundDuringPrivateBrowsing = [[NSMutableSet alloc] init];
465 }
466
467 - (void)_loadIconDictionaries
468 {
469     WebFileDatabase *fileDB = _private->fileDatabase;
470     
471     // fileDB should be non-nil here because it should have been created by _createFileDatabase 
472     if (!fileDB) {
473         LOG_ERROR("Couldn't load icon dictionaries because file database didn't exist");
474         return;
475     }
476     
477     NSNumber *version = [fileDB objectForKey:WebIconDatabaseVersionKey];
478     int v = 0;
479     // no version means first version
480     if (version == nil) {
481         v = 1;
482     } else if ([version isKindOfClass:[NSNumber class]]) {
483         v = [version intValue];
484     }
485     
486     // Get the site URL to icon URL dictionary from the file DB.
487     NSMutableDictionary *pageURLToIconURL = nil;
488     if (v <= WebIconDatabaseCurrentVersion) {
489         pageURLToIconURL = [fileDB objectForKey:WebURLToIconURLKey];
490         // Remove the old unnecessary mapping files.
491         if (v < WebIconDatabaseCurrentVersion) {
492             [fileDB removeObjectForKey:ObsoleteIconsOnDiskKey];
493             [fileDB removeObjectForKey:ObsoleteIconURLToURLsKey];
494         }        
495     }
496     
497     // Must double-check all values read from disk. If any are bogus, we just throw out the whole icon cache.
498     // We expect this to be nil if the icon cache has been cleared, so we shouldn't whine in that case.
499     if (![pageURLToIconURL isKindOfClass:[NSMutableDictionary class]]) {
500         if (pageURLToIconURL)
501             LOG_ERROR("Clearing icon cache because bad value %@ was found on disk, expected an NSMutableDictionary", pageURLToIconURL);
502         [self _clearDictionaries];
503         return;
504     }
505
506     // Keep a set of icon URLs on disk so we know what we need to write out or remove.
507     NSMutableSet *iconsOnDiskWithURLs = [NSMutableSet setWithArray:[pageURLToIconURL allValues]];
508
509     // Reverse pageURLToIconURL so we have an icon URL to page URLs dictionary. 
510     NSMutableDictionary *iconURLToPageURLs = [NSMutableDictionary dictionaryWithCapacity:[_private->iconsOnDiskWithURLs count]];
511     NSEnumerator *enumerator = [pageURLToIconURL keyEnumerator];
512     NSString *URL;
513     while ((URL = [enumerator nextObject])) {
514         NSString *iconURL = (NSString *)[pageURLToIconURL objectForKey:URL];
515         // Must double-check all values read from disk. If any are bogus, we just throw out the whole icon cache.
516         if (![URL isKindOfClass:[NSString class]] || ![iconURL isKindOfClass:[NSString class]]) {
517             LOG_ERROR("Clearing icon cache because either %@ or %@ was a bad value on disk, expected both to be NSStrings", URL, iconURL);
518             [self _clearDictionaries];
519             return;
520         }
521         [iconURLToPageURLs _web_setObjectUsingSetIfNecessary:URL forKey:iconURL];
522     }
523
524     ASSERT(!_private->pageURLToIconURL);
525     ASSERT(!_private->iconURLToPageURLs);
526     ASSERT(!_private->iconsOnDiskWithURLs);
527     ASSERT(!_private->originalIconsOnDiskWithURLs);
528     
529     _private->pageURLToIconURL = [pageURLToIconURL retain];
530     _private->iconURLToPageURLs = [iconURLToPageURLs retain];
531     _private->iconsOnDiskWithURLs = [iconsOnDiskWithURLs retain];
532     _private->originalIconsOnDiskWithURLs = [iconsOnDiskWithURLs copy];
533 }
534
535 // Only called by _setIconURL:forKey:
536 - (void)_updateFileDatabase
537 {
538     if (_private->cleanupCount != 0)
539         return;
540
541     WebFileDatabase *fileDB = _private->fileDatabase;
542     if (!fileDB) {
543         LOG_ERROR("Couldn't update file database because it didn't exist");
544         return;
545     }
546
547     [fileDB setObject:[NSNumber numberWithInt:WebIconDatabaseCurrentVersion] forKey:WebIconDatabaseVersionKey];
548
549     // Erase icons that have been released that are on disk.
550     // Must remove icons before writing them to disk or else we could potentially remove the newly written ones.
551     NSEnumerator *enumerator = [_private->iconsToEraseWithURLs objectEnumerator];
552     NSString *iconURLString;
553     
554     while ((iconURLString = [enumerator nextObject]) != nil) {
555         [fileDB removeObjectForKey:iconURLString];
556         [_private->iconsOnDiskWithURLs removeObject:iconURLString];
557     }
558
559     // Save icons that have been retained that are not already on disk
560     enumerator = [_private->iconsToSaveWithURLs objectEnumerator];
561
562     while ((iconURLString = [enumerator nextObject]) != nil) {
563         NSMutableDictionary *icons = [_private->iconURLToIcons objectForKey:iconURLString];
564         if (icons) {
565             // Save the 16 x 16 size icons as this is the only size that Safari uses.
566             // If we ever use larger sizes, we should save the largest size so icons look better when scaling up.
567             // This also works around the problem with cnet's blank 32x32 icon (3105486).
568             NSImage *icon = [icons objectForKey:[NSValue valueWithSize:NSMakeSize(16,16)]];
569             if (!icon) {
570                 // In case there is no 16 x 16 size.
571                 icon = [self _largestIconFromDictionary:icons];
572             }
573             NSData *iconData = [icon TIFFRepresentation];
574             if (iconData) {
575                 //NSLog(@"Writing icon: %@", iconURLString);
576                 [fileDB setObject:iconData forKey:iconURLString];
577                 [_private->iconsOnDiskWithURLs addObject:iconURLString];
578             }
579         } else if ([_private->iconURLsWithNoIcons containsObject:iconURLString]) {
580             [fileDB setObject:[NSNull null] forKey:iconURLString];
581             [_private->iconsOnDiskWithURLs addObject:iconURLString];
582         }
583     }
584     
585     [_private->iconsToEraseWithURLs removeAllObjects];
586     [_private->iconsToSaveWithURLs removeAllObjects];
587
588     // Save the icon dictionaries to disk, after removing any values created during private browsing.
589     // Even if we weren't modifying the dictionary we'd still want to use a copy so that WebFileDatabase
590     // doesn't look at the original from a different thread. (We used to think this would fix 3566336
591     // but that bug's progeny are still alive and kicking.)
592     NSMutableDictionary *pageURLToIconURLCopy = [_private->pageURLToIconURL mutableCopy];
593     [pageURLToIconURLCopy removeObjectsForKeys:[_private->pageURLsBoundDuringPrivateBrowsing allObjects]];
594     [fileDB setObject:pageURLToIconURLCopy forKey:WebURLToIconURLKey];
595     [pageURLToIconURLCopy release];
596 }
597
598 - (void)_applicationWillTerminate:(NSNotification *)notification
599 {
600     // Should only cause a write if user quit before 3 seconds after the last _updateFileDatabase
601     [_private->fileDatabase sync];
602 }
603
604 - (int)_totalRetainCountForIconURLString:(NSString *)iconURLString
605 {
606     // Add up the retain counts for each associated page, plus the retain counts not associated
607     // with any page, which are stored in _private->iconURLToExtraRetainCount
608     int result = (int)(void *)CFDictionaryGetValue(_private->iconURLToExtraRetainCount, iconURLString);
609     
610     id URLStrings = [_private->iconURLToPageURLs objectForKey:iconURLString];
611     if (URLStrings != nil) {
612         if ([URLStrings isKindOfClass:[NSMutableSet class]]) {
613             NSEnumerator *e = [(NSMutableSet *)URLStrings objectEnumerator];
614             NSString *URLString;
615             while ((URLString = [e nextObject]) != nil) {
616                 ASSERT([URLString isKindOfClass:[NSString class]]);
617                 result += (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, URLString);
618             }
619         } else {
620             ASSERT([URLStrings isKindOfClass:[NSString class]]);
621             result += (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, URLStrings);
622         }
623     }
624
625     return result;
626 }
627
628 - (NSMutableDictionary *)_iconsForIconURLString:(NSString *)iconURLString
629 {
630     ASSERT(iconURLString);
631
632     if ([_private->iconURLsWithNoIcons containsObject:iconURLString])
633         return nil;
634     
635     NSMutableDictionary *icons = [_private->iconURLToIcons objectForKey:iconURLString];
636
637     if (icons)
638         return icons;
639         
640     // Not in memory, check disk
641     if(![_private->iconsOnDiskWithURLs containsObject:iconURLString])
642         return nil;
643     
644 #if !LOG_DISABLED         
645     double start = CFAbsoluteTimeGetCurrent();
646 #endif
647     NSData *iconData = [_private->fileDatabase objectForKey:iconURLString];
648
649     if ([iconData isKindOfClass:[NSNull class]]) {
650         [_private->iconURLsWithNoIcons addObject:iconURLString];
651         return nil;
652     }
653     
654     if (iconData) {
655         NS_DURING
656             NSImage *icon = [[NSImage alloc] initWithData:iconData];
657             icons = [self _iconsBySplittingRepresentationsOfIcon:icon];
658             if (icons) {
659 #if !LOG_DISABLED 
660                 double duration = CFAbsoluteTimeGetCurrent() - start;
661                 LOG(Timing, "loading and creating icon %@ took %f seconds", iconURLString, duration);
662 #endif
663                 [_private->iconURLToIcons setObject:icons forKey:iconURLString];
664             }
665         NS_HANDLER
666             icons = nil;
667         NS_ENDHANDLER
668     }
669     
670     return icons;
671 }
672
673 - (NSImage *)_iconForFileURL:(NSString *)file withSize:(NSSize)size
674 {
675     ASSERT(size.width);
676     ASSERT(size.height);
677
678     NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
679     NSString *path = [[NSURL _web_URLWithDataAsString:file] path];
680     NSString *suffix = [path pathExtension];
681     NSImage *icon = nil;
682     
683     if ([suffix _webkit_isCaseInsensitiveEqualToString:@"htm"] || [suffix _webkit_isCaseInsensitiveEqualToString:@"html"]) {
684         if (!_private->htmlIcons) {
685             icon = [workspace iconForFileType:@"html"];
686             _private->htmlIcons = [[self _iconsBySplittingRepresentationsOfIcon:icon] retain];
687         }
688         icon = [self _iconFromDictionary:_private->htmlIcons forSize:size cache:YES];
689     } else {
690         if (!path || ![path isAbsolutePath]) {
691             // Return the generic icon when there is no path.
692             icon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericDocumentIcon)];
693         } else {
694             icon = [workspace iconForFile:path];
695         }
696         [self _scaleIcon:icon toSize:size];
697     }
698
699     return icon;
700 }
701
702 - (void)_retainIconForIconURLString:(NSString *)iconURLString
703 {
704     ASSERT(iconURLString);
705     
706     int retainCount = (int)(void *)CFDictionaryGetValue(_private->iconURLToExtraRetainCount, iconURLString);
707     int newRetainCount = retainCount + 1;
708
709     CFDictionarySetValue(_private->iconURLToExtraRetainCount, iconURLString, (void *)newRetainCount);
710
711     if (newRetainCount == 1 && !_private->privateBrowsingEnabled) {
712
713         // Either we know nothing about this icon and need to save it to disk, or we were planning to remove it
714         // from disk (as set up in _forgetIconForIconURLString:) and should stop that process.
715         if ([_private->iconsOnDiskWithURLs containsObject:iconURLString]) {
716             ASSERT(![_private->iconsToSaveWithURLs containsObject:iconURLString]);
717             [_private->iconsToEraseWithURLs removeObject:iconURLString];
718         } else {
719             ASSERT(![_private->iconsToEraseWithURLs containsObject:iconURLString]);
720             [_private->iconsToSaveWithURLs addObject:iconURLString];
721         }
722     }
723 }
724
725 - (void)_forgetIconForIconURLString:(NSString *)iconURLString
726 {
727     ASSERT_ARG(iconURLString, iconURLString != nil);
728     if([_private->iconsOnDiskWithURLs containsObject:iconURLString]){
729         [_private->iconsToEraseWithURLs addObject:iconURLString];
730         [_private->iconsToSaveWithURLs removeObject:iconURLString];
731     }
732         
733     // Remove the icon's images
734     [_private->iconURLToIcons removeObjectForKey:iconURLString];
735     
736     // Remove negative cache item for icon, if any
737     [_private->iconURLsWithNoIcons removeObject:iconURLString];
738     
739     // Remove the icon's associated site URLs, if any
740     [iconURLString retain];
741     id URLs = [_private->iconURLToPageURLs objectForKey:iconURLString];
742     if (URLs != nil) {
743         if ([URLs isKindOfClass:[NSMutableSet class]])
744             [_private->pageURLToIconURL removeObjectsForKeys:[URLs allObjects]];
745         else {
746             ASSERT([URLs isKindOfClass:[NSString class]]);
747             [_private->pageURLToIconURL removeObjectForKey:URLs];
748         }
749     }
750     [_private->iconURLToPageURLs removeObjectForKey:iconURLString];
751     CFDictionaryRemoveValue(_private->iconURLToExtraRetainCount, iconURLString);
752     [iconURLString release];
753 }
754
755 - (void)_releaseIconForIconURLString:(NSString *)iconURLString
756 {
757     ASSERT(iconURLString);
758     
759     int retainCount = (int)(void *)CFDictionaryGetValue(_private->iconURLToExtraRetainCount, iconURLString);
760
761     ASSERT(retainCount > 0);
762     if (retainCount <= 0)
763         return;
764     
765     int newRetainCount = retainCount - 1;
766     if (newRetainCount == 0) {
767         CFDictionaryRemoveValue(_private->iconURLToExtraRetainCount, iconURLString);
768         if ([self _totalRetainCountForIconURLString:iconURLString] == 0) {
769             [self _forgetIconForIconURLString:iconURLString];
770         }
771     } else {
772         CFDictionarySetValue(_private->iconURLToExtraRetainCount, iconURLString, (void *)newRetainCount);
773     }
774 }
775
776 - (void)_retainOriginalIconsOnDisk
777 {
778     NSEnumerator *enumerator = [_private->originalIconsOnDiskWithURLs objectEnumerator];;
779     NSString *iconURLString;
780     while ((iconURLString = [enumerator nextObject]) != nil) {
781         [self _retainIconForIconURLString:iconURLString];
782     }
783 }
784
785 - (void)_releaseOriginalIconsOnDisk
786 {
787     if (_private->cleanupCount > 0) {
788         _private->waitingToCleanup = YES;
789         return;
790     }
791
792     NSEnumerator *enumerator = [_private->originalIconsOnDiskWithURLs objectEnumerator];
793     NSString *iconURLString;
794     while ((iconURLString = [enumerator nextObject]) != nil) {
795         [self _releaseIconForIconURLString:iconURLString];
796     }
797     
798     [_private->originalIconsOnDiskWithURLs release];
799     _private->originalIconsOnDiskWithURLs = nil;
800
801     _private->didCleanup = YES;
802 }
803
804 - (void)_resetCachedWebPreferences:(NSNotification *)notification
805 {
806     BOOL privateBrowsingEnabledNow = [[WebPreferences standardPreferences] privateBrowsingEnabled];
807     if (privateBrowsingEnabledNow == _private->privateBrowsingEnabled)
808         return;
809     
810     _private->privateBrowsingEnabled = privateBrowsingEnabledNow;
811     
812     // When private browsing is turned off, forget everything we learned while it was on 
813     if (!_private->privateBrowsingEnabled) {
814         // Forget all of the icons whose existence we learned of during private browsing.
815         NSEnumerator *iconEnumerator = [_private->iconURLsBoundDuringPrivateBrowsing objectEnumerator];
816         NSString *iconURLString;
817         while ((iconURLString = [iconEnumerator nextObject]) != nil)
818             [self _forgetIconForIconURLString:iconURLString];
819
820         // Forget the relationships between page and icon that we learned during private browsing.
821         NSEnumerator *pageEnumerator = [_private->pageURLsBoundDuringPrivateBrowsing objectEnumerator];
822         NSString *pageURLString;
823         while ((pageURLString = [pageEnumerator nextObject]) != nil) {
824             [_private->pageURLToIconURL removeObjectForKey:pageURLString];
825             // Tell clients that these pages' icons have changed (to generic). The notification is named
826             // WebIconDatabaseDidAddIconNotification but it really means just "did change icon".
827             [self _sendNotificationForURL:pageURLString];
828         }
829         
830         [_private->iconURLsBoundDuringPrivateBrowsing removeAllObjects];
831         [_private->pageURLsBoundDuringPrivateBrowsing removeAllObjects];
832     }
833 }
834
835 - (void)_sendNotificationForURL:(NSString *)URL
836 {
837     ASSERT(URL);
838     
839     NSDictionary *userInfo = [NSDictionary dictionaryWithObject:URL
840                                                          forKey:WebIconNotificationUserInfoURLKey];
841     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidAddIconNotification
842                                                         object:self
843                                                       userInfo:userInfo];
844 }
845
846 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons
847 {
848     ASSERT(icons);
849     
850     NSEnumerator *enumerator = [icons keyEnumerator];
851     NSValue *currentSize, *largestSize=nil;
852     float largestSizeArea=0;
853
854     while ((currentSize = [enumerator nextObject]) != nil) {
855         NSSize currentSizeSize = [currentSize sizeValue];
856         float currentSizeArea = currentSizeSize.width * currentSizeSize.height;
857         if(!largestSizeArea || (currentSizeArea > largestSizeArea)){
858             largestSize = currentSize;
859             largestSizeArea = currentSizeArea;
860         }
861     }
862
863     return [icons objectForKey:largestSize];
864 }
865
866 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon
867 {
868     ASSERT(icon);
869
870     NSMutableDictionary *icons = [NSMutableDictionary dictionary];
871     NSEnumerator *enumerator = [[icon representations] objectEnumerator];
872     NSImageRep *rep;
873
874     while ((rep = [enumerator nextObject]) != nil) {
875         NSSize size = [rep size];
876         NSImage *subIcon = [[NSImage alloc] initWithSize:size];
877         [subIcon addRepresentation:rep];
878         [icons setObject:subIcon forKey:[NSValue valueWithSize:size]];
879         [subIcon release];
880     }
881
882     if([icons count] > 0)
883         return icons;
884
885     LOG_ERROR("icon has no representations");
886     
887     return nil;
888 }
889
890 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache
891 {
892     ASSERT(size.width);
893     ASSERT(size.height);
894
895     NSImage *icon = [icons objectForKey:[NSValue valueWithSize:size]];
896
897     if(!icon){
898         icon = [[[self _largestIconFromDictionary:icons] copy] autorelease];
899         [self _scaleIcon:icon toSize:size];
900
901         if(cache){
902             [icons setObject:icon forKey:[NSValue valueWithSize:size]];
903         }
904     }
905
906     return icon;
907 }
908
909 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size
910 {
911     ASSERT(size.width);
912     ASSERT(size.height);
913     
914 #if !LOG_DISABLED        
915     double start = CFAbsoluteTimeGetCurrent();
916 #endif
917     
918     [icon setScalesWhenResized:YES];
919     [icon setSize:size];
920     
921 #if !LOG_DISABLED
922     double duration = CFAbsoluteTimeGetCurrent() - start;
923     LOG(Timing, "scaling icon took %f seconds.", duration);
924 #endif
925 }
926
927 @end
928
929 @implementation WebIconDatabasePrivate
930
931 @end
932
933 @implementation NSMutableDictionary (WebIconDatabase)
934
935 - (void)_web_setObjectUsingSetIfNecessary:(id)object forKey:(id)key
936 {
937     id previousObject = [self objectForKey:key];
938     if (previousObject == nil) {
939         [self setObject:object forKey:key];
940     } else if ([previousObject isKindOfClass:[NSMutableSet class]]) {
941         [previousObject addObject:object];
942     } else {
943         NSMutableSet *objects = [[NSMutableSet alloc] initWithObjects:previousObject, object, nil];
944         [self setObject:objects forKey:key];
945         [objects release];
946     }
947 }
948
949 @end