WebKit:
[WebKit-https.git] / WebKit / History.subproj / WebHistoryItem.m
1 /*      
2     WebHistoryItem.m
3     Copyright 2001, 2002, Apple, Inc. All rights reserved.
4 */
5
6 #import <WebKit/WebHistoryItemPrivate.h>
7
8 #import <WebKit/WebFramePrivate.h>
9 #import <WebKit/WebFrameView.h>
10 #import <WebKit/WebHTMLViewPrivate.h>
11 #import <WebKit/WebIconDatabase.h>
12 #import <WebKit/WebIconLoader.h>
13 #import <WebKit/WebKitLogging.h>
14 #import <WebKit/WebNSURLExtras.h>
15 #import <WebKit/WebPluginController.h>
16
17 #import <WebKit/WebAssertions.h>
18 #import <Foundation/NSDictionary_NSURLExtras.h>
19
20 #import <CoreGraphics/CoreGraphicsPrivate.h>
21
22 // Private keys used in the WebHistoryItem's dictionary representation.
23 // see 3245793 for explanation of "lastVisitedDate"
24 static NSString *WebLastVisitedTimeIntervalKey = @"lastVisitedDate";
25 static NSString *WebVisitCountKey = @"visitCount";
26 static NSString *WebTitleKey = @"title";
27 static NSString *WebChildrenKey = @"children";
28 static NSString *WebDisplayTitleKey = @"displayTitle";
29
30 // Notification strings.
31 NSString *WebHistoryItemChangedNotification = @"WebHistoryItemChangedNotification";
32
33 @interface WebHistoryItemPrivate : NSObject
34 {
35 @public
36     NSString *URLString;
37     NSString *originalURLString;
38     NSString *target;
39     NSString *parent;
40     NSString *title;
41     NSString *displayTitle;
42     NSCalendarDate *lastVisitedDate;
43     NSTimeInterval lastVisitedTimeInterval;
44     NSPoint scrollPoint;
45     NSArray *documentState;
46     NSMutableArray *subItems;
47     NSMutableDictionary *pageCache;
48     BOOL isTargetItem;
49     BOOL alwaysAttemptToUsePageCache;
50     BOOL notificationsSuppressed;
51     int visitCount;
52     // info used to repost form data
53     NSData *formData;
54     NSString *formContentType;
55     NSString *formReferrer;
56     // info used to support RSS feeds
57     NSString *RSSFeedReferrer;
58 }
59 @end
60
61 @implementation WebHistoryItemPrivate
62 - (void)dealloc
63 {
64     [URLString release];
65     [originalURLString release];
66     [target release];
67     [parent release];
68     [title release];
69     [displayTitle release];
70     [lastVisitedDate release];
71     [documentState release];
72     [subItems release];
73     [pageCache release];
74     [formData release];
75     [formContentType release];
76     [formReferrer release];
77     [RSSFeedReferrer release];
78
79     [super dealloc];
80 }
81 @end
82
83 @implementation WebHistoryItem
84
85 - (id)init
86 {
87     return [self initWithURLString:nil title:nil lastVisitedTimeInterval:0];
88 }
89
90 - (id)initWithURLString:(NSString *)URLString title:(NSString *)title lastVisitedTimeInterval:(NSTimeInterval)time
91 {
92     self = [super init];
93     _private = [[WebHistoryItemPrivate alloc] init];
94     _private->lastVisitedTimeInterval = time;
95     _private->title = [title copy];
96     _private->URLString = [URLString copy];
97     _private->originalURLString = [_private->URLString retain];
98     [self _retainIconInDatabase:YES];
99     return self;
100 }
101
102 - (void)dealloc
103 {
104     [self _retainIconInDatabase:NO];
105
106     [_private release];
107     
108     [super dealloc];
109 }
110
111 - (id)copyWithZone:(NSZone *)zone
112 {
113     WebHistoryItem *copy = NSCopyObject(self, 0, zone);
114     copy->_private = [[WebHistoryItemPrivate alloc] init];
115     copy->_private->URLString = [_private->URLString copy];
116     [copy _retainIconInDatabase:YES];
117     copy->_private->originalURLString = [_private->originalURLString copy];
118     copy->_private->target = [_private->target copy];
119     copy->_private->parent = [_private->parent copy];
120     copy->_private->title = [_private->title copy];
121     copy->_private->displayTitle = [_private->displayTitle copy];
122     copy->_private->lastVisitedTimeInterval = _private->lastVisitedTimeInterval;
123     copy->_private->lastVisitedDate = [_private->lastVisitedDate copy];
124     copy->_private->scrollPoint = _private->scrollPoint;
125     copy->_private->documentState = [_private->documentState copy];
126     if (_private->subItems) {
127         copy->_private->subItems = [[NSMutableArray alloc] initWithArray:_private->subItems copyItems:YES];
128     }
129     copy->_private->isTargetItem = _private->isTargetItem;
130     copy->_private->formData = [_private->formData copy];
131     copy->_private->formContentType = [_private->formContentType copy];
132     copy->_private->formReferrer = [_private->formReferrer copy];
133     copy->_private->RSSFeedReferrer = [_private->RSSFeedReferrer copy];
134
135     return copy;
136 }
137
138 // FIXME: need to decide it this class ever returns URLs, and the name of this method
139 - (NSString *)URLString
140 {
141     return _private->URLString;
142 }
143
144 // The first URL we loaded to get to where this history item points.  Includes both client
145 // and server redirects.
146 - (NSString *)originalURLString
147 {
148     return _private->originalURLString;
149 }
150
151 - (NSString *)title
152 {
153     return _private->title;
154 }
155
156 - (void)setAlternateTitle:(NSString *)alternateTitle
157 {
158     NSString *newDisplayTitle;
159     if (alternateTitle && [alternateTitle isEqualToString:_private->title]) {
160         newDisplayTitle = [_private->title retain];
161     } else {
162         newDisplayTitle = [alternateTitle copy];
163     }
164     [_private->displayTitle release];
165     _private->displayTitle = newDisplayTitle;
166
167     if (!_private->notificationsSuppressed) {
168         [[NSNotificationCenter defaultCenter]
169         postNotificationName: WebHistoryItemChangedNotification object: self userInfo: nil];
170     }
171 }
172
173
174 - (NSString *)alternateTitle;
175 {
176     return _private->displayTitle;
177 }
178
179 - (NSImage *)icon
180 {
181     // Always get fresh icon from database. It's a client's responsibility to watch
182     // for updates to the database if desired.
183     return [[WebIconDatabase sharedIconDatabase] iconForURL:_private->URLString withSize:WebIconSmallSize];
184 }
185
186
187 - (NSTimeInterval)lastVisitedTimeInterval
188 {
189     return _private->lastVisitedTimeInterval;
190 }
191
192 - (unsigned)hash
193 {
194     return [_private->URLString hash];
195 }
196
197 - (BOOL)isEqual:(id)anObject
198 {
199     if (![anObject isMemberOfClass:[WebHistoryItem class]]) {
200         return NO;
201     }
202     
203     NSString *otherURL = ((WebHistoryItem *)anObject)->_private->URLString;
204     return _private->URLString == otherURL || [_private->URLString isEqualToString:otherURL];
205 }
206
207 - (NSString *)description
208 {
209     NSMutableString *result = [NSMutableString stringWithFormat:@"%@ %@", [super description], _private->URLString];
210     if (_private->target) {
211         [result appendFormat:@" in \"%@\"", _private->target];
212     }
213     if (_private->isTargetItem) {
214         [result appendString:@" *target*"];
215     }
216     if (_private->formData) {
217         [result appendString:@" *POST*"];
218     }
219     if (_private->subItems) {
220         int currPos = [result length];
221         int i;
222         for (i = 0; i < (int)[_private->subItems count]; i++) {
223             WebHistoryItem *child = [_private->subItems objectAtIndex:i];
224             [result appendString:@"\n"];
225             [result appendString:[child description]];
226         }
227         // shift all the contents over.  A bit slow, but hey, this is for debugging.
228         NSRange replRange = {currPos, [result length]-currPos};
229         [result replaceOccurrencesOfString:@"\n" withString:@"\n    " options:0 range:replRange];
230     }
231     return result;
232 }
233
234 @end
235
236 @interface WebWindowWatcher : NSObject
237 @end
238
239 @implementation WebHistoryItem (WebPrivate)
240
241 - (void)_retainIconInDatabase:(BOOL)retain
242 {
243     if (_private->URLString) {
244         WebIconDatabase *iconDB = [WebIconDatabase sharedIconDatabase];
245         if (retain) {
246             [iconDB retainIconForURL:_private->URLString];
247         } else {
248             [iconDB releaseIconForURL:_private->URLString];
249         }
250     }
251 }
252
253 + (WebHistoryItem *)entryWithURL:(NSURL *)URL
254 {
255     return [[[self alloc] initWithURL:URL title:nil] autorelease];
256 }
257
258 - (id)initWithURL:(NSURL *)URL title:(NSString *)title
259 {
260     return [self initWithURLString:[URL _web_originalDataAsString] title:title lastVisitedTimeInterval:0];
261 }
262
263 - (id)initWithURL:(NSURL *)URL target:(NSString *)target parent:(NSString *)parent title:(NSString *)title
264 {
265     self = [self initWithURLString:[URL _web_originalDataAsString] title:title lastVisitedTimeInterval:0];
266     if (self) {
267         _private->target = [target copy];
268         _private->parent = [parent copy];
269     }
270     return self;
271 }
272
273 - (NSURL *)URL
274 {
275     return _private->URLString ? [NSURL _web_URLWithDataAsString:_private->URLString] : nil;
276 }
277
278 - (NSString *)target
279 {
280     return _private->target;
281 }
282
283 - (NSString *)parent
284 {
285     return _private->parent;
286 }
287
288 - (void)setURLString:(NSString *)string
289 {
290     if (!(string == _private->URLString || [string isEqual:_private->URLString])) {
291         [self _retainIconInDatabase:NO];
292         [_private->URLString release];
293         _private->URLString = [string copy];
294         [self _retainIconInDatabase:YES];
295     }
296     
297     if (!_private->notificationsSuppressed) {
298         [[NSNotificationCenter defaultCenter]
299         postNotificationName: WebHistoryItemChangedNotification object: self userInfo: nil];
300     }
301 }
302
303 - (void)setURL:(NSURL *)URL
304 {
305     [self setURLString:[URL _web_originalDataAsString]];
306 }
307
308 // The first URL we loaded to get to where this history item points.  Includes both client
309 // and server redirects.
310 - (void)setOriginalURLString:(NSString *)URL
311 {
312     NSString *newURL = [URL copy];
313     [_private->originalURLString release];
314     _private->originalURLString = newURL;
315
316     if (!_private->notificationsSuppressed) {
317         [[NSNotificationCenter defaultCenter]
318         postNotificationName: WebHistoryItemChangedNotification object: self userInfo: nil];
319     }
320 }
321
322 - (void)setTitle:(NSString *)title
323 {
324     NSString *newTitle;
325     if (title && [title isEqualToString:_private->displayTitle]) {
326         newTitle = [_private->displayTitle retain];
327     } else {
328         newTitle = [title copy];
329     }
330     [_private->title release];
331     _private->title = newTitle;
332
333     if (!_private->notificationsSuppressed) {
334         [[NSNotificationCenter defaultCenter]
335         postNotificationName: WebHistoryItemChangedNotification object: self userInfo: nil];
336     }
337 }
338
339 - (void)setTarget:(NSString *)target
340 {
341     NSString *copy = [target copy];
342     [_private->target release];
343     _private->target = copy;
344 }
345
346 - (void)setParent:(NSString *)parent
347 {
348     NSString *copy = [parent copy];
349     [_private->parent release];
350     _private->parent = copy;
351 }
352
353 - (void)_setLastVisitedTimeInterval:(NSTimeInterval)time
354 {
355     if (_private->lastVisitedTimeInterval != time) {
356         _private->lastVisitedTimeInterval = time;
357         [_private->lastVisitedDate release];
358         _private->lastVisitedDate = nil;
359         _private->visitCount++;
360     }
361
362     if (!_private->notificationsSuppressed) {
363         [[NSNotificationCenter defaultCenter]
364         postNotificationName: WebHistoryItemChangedNotification object: self userInfo: nil];
365     }
366 }
367
368 // FIXME:  Remove this accessor and related ivar.
369 - (NSCalendarDate *)_lastVisitedDate
370 {
371     if (!_private->lastVisitedDate){
372         _private->lastVisitedDate = [[NSCalendarDate alloc]
373                     initWithTimeIntervalSinceReferenceDate:_private->lastVisitedTimeInterval];
374     }
375     return _private->lastVisitedDate;
376 }
377
378 - (int)visitCount
379 {
380     return _private->visitCount;
381 }
382
383 - (void)setVisitCount:(int)count
384 {
385     _private->visitCount = count;
386 }
387
388 - (void)setDocumentState:(NSArray *)state;
389 {
390     NSArray *copy = [state copy];
391     [_private->documentState release];
392     _private->documentState = copy;
393 }
394
395 - (NSArray *)documentState
396 {
397     return _private->documentState;
398 }
399
400 - (NSPoint)scrollPoint
401 {
402     return _private->scrollPoint;
403 }
404
405 - (void)setScrollPoint:(NSPoint)scrollPoint
406 {
407     _private->scrollPoint = scrollPoint;
408 }
409
410 - (BOOL)isTargetItem
411 {
412     return _private->isTargetItem;
413 }
414
415 - (void)setIsTargetItem:(BOOL)flag
416 {
417     _private->isTargetItem = flag;
418 }
419
420 // Main diff from the public method is that the public method will default to returning
421 // the top item if it can't find anything marked as target and has no kids
422 - (WebHistoryItem *)_recurseToFindTargetItem
423 {
424     if (_private->isTargetItem) {
425         return self;
426     } else if (!_private->subItems) {
427         return nil;
428     } else {
429         int i;
430         for (i = [_private->subItems count]-1; i >= 0; i--) {
431             WebHistoryItem *match = [[_private->subItems objectAtIndex:i] _recurseToFindTargetItem];
432             if (match) {
433                 return match;
434             }
435         }
436         return nil;
437     }
438 }
439
440 - (WebHistoryItem *)targetItem
441 {
442     if (_private->isTargetItem || !_private->subItems) {
443         return self;
444     } else {
445         return [self _recurseToFindTargetItem];
446     }
447 }
448
449 - (NSData *)formData
450 {
451     return _private->formData;
452 }
453
454 - (void)setFormData:(NSData *)data
455 {
456     NSData *copy = [data copy];
457     [_private->formData release];
458     _private->formData = copy;
459 }
460
461 - (NSString *)formContentType
462 {
463     return _private->formContentType;
464 }
465
466 - (void)setFormContentType:(NSString *)type
467 {
468     NSString *copy = [type copy];
469     [_private->formContentType release];
470     _private->formContentType = copy;
471 }
472
473 - (NSString *)formReferrer
474 {
475     return _private->formReferrer;
476 }
477
478 - (void)setFormReferrer:(NSString *)referrer
479 {
480     NSString *copy = [referrer copy];
481     [_private->formReferrer release];
482     _private->formReferrer = copy;
483 }
484
485 - (NSString *)RSSFeedReferrer
486 {
487     return _private->RSSFeedReferrer;
488 }
489
490 - (void)setRSSFeedReferrer:(NSString *)referrer
491 {
492     NSString *copy = [referrer copy];
493     [_private->RSSFeedReferrer release];
494     _private->RSSFeedReferrer = copy;
495 }
496
497 - (NSArray *)children
498 {
499     return _private->subItems;
500 }
501
502 - (void)_mergeAutoCompleteHints:(WebHistoryItem *)otherItem
503 {
504     if (otherItem != self) {
505         _private->visitCount += otherItem->_private->visitCount;
506     }
507 }
508
509 - (void)addChildItem:(WebHistoryItem *)item
510 {
511     if (!_private->subItems) {
512         _private->subItems = [[NSMutableArray arrayWithObject:item] retain];
513     } else {
514         [_private->subItems addObject:item];
515     }
516 }
517
518 - (WebHistoryItem *)childItemWithName:(NSString *)name
519 {
520     int i;
521     for (i = (_private->subItems ? [_private->subItems count] : 0)-1; i >= 0; i--) {
522         WebHistoryItem *child = [_private->subItems objectAtIndex:i];
523         if ([[child target] isEqualToString:name]) {
524             return child;
525         }
526     }
527     return nil;
528 }
529
530 - (NSDictionary *)dictionaryRepresentation
531 {
532     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:6];
533
534     if (_private->URLString) {
535         [dict setObject:_private->URLString forKey:@""];
536     }
537     if (_private->title) {
538         [dict setObject:_private->title forKey:WebTitleKey];
539     }
540     if (_private->displayTitle) {
541         [dict setObject:_private->displayTitle forKey:WebDisplayTitleKey];
542     }
543     if (_private->lastVisitedTimeInterval != 0.0) {
544         // store as a string to maintain backward compatibility (see 3245793)
545         [dict setObject:[NSString stringWithFormat:@"%.1lf", _private->lastVisitedTimeInterval]
546                  forKey:WebLastVisitedTimeIntervalKey];
547     }
548     if (_private->visitCount) {
549         [dict setObject:[NSNumber numberWithInt:_private->visitCount] forKey:WebVisitCountKey];
550     }
551     if (_private->subItems != nil) {
552         NSMutableArray *childDicts = [NSMutableArray arrayWithCapacity:[_private->subItems count]];
553         int i;
554         for (i = [_private->subItems count]; i >= 0; i--) {
555             [childDicts addObject: [[_private->subItems objectAtIndex:i] dictionaryRepresentation]];
556         }
557         [dict setObject: childDicts forKey:WebChildrenKey];
558     }
559
560     return dict;
561 }
562
563 - (id)initFromDictionaryRepresentation:(NSDictionary *)dict
564 {
565     NSString *URLString = [dict _web_stringForKey:@""];
566     NSString *title = [dict _web_stringForKey:WebTitleKey];
567
568     // Do an existence check to avoid calling doubleValue on a nil string. Leave
569     // time interval at 0 if there's no value in dict.
570     NSString *timeIntervalString = [dict _web_stringForKey:WebLastVisitedTimeIntervalKey];
571     NSTimeInterval lastVisited = timeIntervalString == nil ? 0 : [timeIntervalString doubleValue];
572
573     self = [self initWithURLString:URLString title:title lastVisitedTimeInterval:lastVisited];
574
575     [self setAlternateTitle:[dict _web_stringForKey:WebDisplayTitleKey]];
576
577     _private->visitCount = [dict _web_intForKey:WebVisitCountKey];
578
579     NSArray *childDicts = [dict objectForKey:WebChildrenKey];
580     if (childDicts) {
581         _private->subItems = [[NSMutableArray alloc] initWithCapacity:[childDicts count]];
582         int i;
583         for (i = [childDicts count]; i >= 0; i--) {
584             WebHistoryItem *child = [[WebHistoryItem alloc] initFromDictionaryRepresentation: [childDicts objectAtIndex:i]];
585             [_private->subItems addObject: child];
586         }
587     }
588
589     return self;
590 }
591
592 - (void)setAlwaysAttemptToUsePageCache: (BOOL)flag
593 {
594     _private->alwaysAttemptToUsePageCache = flag;
595 }
596
597 - (BOOL)alwaysAttemptToUsePageCache
598 {
599     return _private->alwaysAttemptToUsePageCache;
600 }
601
602 - (void)setNotificationsSuppressed:(BOOL)flag
603 {
604     _private->notificationsSuppressed = flag;
605 }
606
607 - (BOOL)notificationsSuppressed
608 {
609     return _private->notificationsSuppressed;
610 }
611
612
613
614 static WebWindowWatcher *_windowWatcher;
615 static NSMutableSet *_pendingPageCacheToRelease = nil;
616 static NSTimer *_pageCacheReleaseTimer = nil;
617
618 - (BOOL)hasPageCache;
619 {
620     return _private->pageCache != nil;
621 }
622
623 + (void)_invalidateReleaseTimer
624 {
625     if (_pageCacheReleaseTimer){
626         [_pageCacheReleaseTimer invalidate];
627         [_pageCacheReleaseTimer release];
628         _pageCacheReleaseTimer = nil;
629     }
630 }
631
632 + (void)_scheduleReleaseTimer
633 {
634     if (!_pageCacheReleaseTimer){
635         _pageCacheReleaseTimer = [[NSTimer scheduledTimerWithTimeInterval: 2.5 target:self selector:@selector(_releasePageCache:) userInfo:nil repeats:NO] retain];
636         if (_pendingPageCacheToRelease == nil){
637             _pendingPageCacheToRelease = [[NSMutableSet alloc] init];
638         }
639     }
640 }
641
642 - (void)_scheduleRelease
643 {
644     LOG (PageCache, "Scheduling release of %@", [self URLString]);
645     [WebHistoryItem _scheduleReleaseTimer];
646
647     if (_private->pageCache){
648         [_pendingPageCacheToRelease addObject: _private->pageCache];
649         [_private->pageCache release]; // Last reference held by _pendingPageCacheToRelease.
650         _private->pageCache = nil;
651     }
652     
653     if (!_windowWatcher){
654         _windowWatcher = [[WebWindowWatcher alloc] init];
655         [[NSNotificationCenter defaultCenter] addObserver:_windowWatcher selector:@selector(windowWillClose:)
656                         name:NSWindowWillCloseNotification object:nil];
657     }
658 }
659
660 + (void)_destroyAllPluginsInPendingPageCaches
661 {
662     NSEnumerator *pageCaches = [_pendingPageCacheToRelease objectEnumerator];
663     NSMutableDictionary *pageCache;
664     
665     while ((pageCache = [pageCaches nextObject]) != nil) {
666         WebHTMLView *HTMLView = [pageCache objectForKey:WebPageCacheDocumentViewKey];
667         if ([HTMLView isKindOfClass:[WebHTMLView class]]) {
668             // Don't destroy plug-ins that are currently being viewed.
669             if ([[[HTMLView _frame] frameView] documentView] != HTMLView) {
670                 [[HTMLView _pluginController] destroyAllPlugins];
671             }
672         }
673     }
674 }
675
676 + (void)_releaseAllPendingPageCaches
677 {
678     LOG (PageCache, "releasing %d items\n", [_pendingPageCacheToRelease count]);
679     [WebHistoryItem _invalidateReleaseTimer];
680     // Plug-ins could retain anything including the WebHTMLView or the window.
681     // To avoid any possible retain cycle, call destroyPlugin on all the plug-ins
682     // instead of completely relying on dealloc of WebHTMLView.
683     [self _destroyAllPluginsInPendingPageCaches];
684     [_pendingPageCacheToRelease removeAllObjects];
685 }
686
687 + (void)_releasePageCache: (NSTimer *)timer
688 {
689     CGSRealTimeDelta userDelta;
690     CFAbsoluteTime loadDelta;
691     
692     loadDelta = CFAbsoluteTimeGetCurrent()-[WebFrame _timeOfLastCompletedLoad];
693     userDelta = CGSSecondsSinceLastInputEvent(kCGSAnyInputEventType);
694
695     [_pageCacheReleaseTimer release];
696     _pageCacheReleaseTimer = nil;
697
698     if ((userDelta < 0.5 || loadDelta < 1.25) && [_pendingPageCacheToRelease count] < 42){
699         LOG (PageCache, "postponing again because not quiescent for more than a second (%f since last input, %f since last load).", userDelta, loadDelta);
700         [self _scheduleReleaseTimer];
701         return;
702     }
703     else
704         LOG (PageCache, "releasing, quiescent for more than a second (%f since last input, %f since last load).", userDelta, loadDelta);
705
706     [WebHistoryItem _releaseAllPendingPageCaches];
707 }
708
709 - (void)setHasPageCache: (BOOL)f
710 {
711     if (f && !_private->pageCache)
712         _private->pageCache = [[NSMutableDictionary alloc] init];
713     if (!f && _private->pageCache){
714         [self _scheduleRelease];
715     }
716 }
717
718 - pageCache
719 {
720     return _private->pageCache;
721 }
722
723
724 @end
725
726 @implementation WebWindowWatcher
727 -(void)windowWillClose:(NSNotification *)notification
728 {
729     [WebHistoryItem _releaseAllPendingPageCaches];
730 }
731 @end
732
733