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