|
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 "WebHistory.h" |
|
30 #import "WebHistoryPrivate.h" |
|
31 |
|
32 #import "WebHistoryItem.h" |
|
33 #import "WebHistoryItemInternal.h" |
|
34 #import "WebHistoryItemPrivate.h" |
|
35 #import "WebKitLogging.h" |
|
36 #import "WebNSURLExtras.h" |
|
37 #import <Foundation/NSError.h> |
|
38 #import <JavaScriptCore/Assertions.h> |
|
39 #import <WebCore/WebCoreHistory.h> |
|
40 #import <wtf/Vector.h> |
|
41 |
|
42 |
|
43 NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification"; |
|
44 NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification"; |
|
45 NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification"; |
|
46 NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification"; |
|
47 NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification"; |
|
48 NSString *WebHistorySavedNotification = @"WebHistorySavedNotification"; |
|
49 NSString *WebHistoryItemsKey = @"WebHistoryItems"; |
|
50 |
|
51 static WebHistory *_sharedHistory = nil; |
|
52 |
|
53 |
|
54 |
|
55 NSString *FileVersionKey = @"WebHistoryFileVersion"; |
|
56 NSString *DatesArrayKey = @"WebHistoryDates"; |
|
57 |
|
58 #define currentFileVersion 1 |
|
59 |
|
60 @implementation WebHistoryPrivate |
|
61 |
|
62 #pragma mark OBJECT FRAMEWORK |
|
63 |
|
64 + (void)initialize |
|
65 { |
|
66 [[NSUserDefaults standardUserDefaults] registerDefaults: |
|
67 [NSDictionary dictionaryWithObjectsAndKeys: |
|
68 @"1000", @"WebKitHistoryItemLimit", |
|
69 @"7", @"WebKitHistoryAgeInDaysLimit", |
|
70 nil]]; |
|
71 } |
|
72 |
|
73 - (id)init |
|
74 { |
|
75 if (![super init]) { |
|
76 return nil; |
|
77 } |
|
78 |
|
79 _entriesByURL = [[NSMutableDictionary alloc] init]; |
|
80 _entriesByDate = new DateToEntriesMap; |
|
81 |
|
82 return self; |
|
83 } |
|
84 |
|
85 - (void)dealloc |
|
86 { |
|
87 [_entriesByURL release]; |
|
88 [_orderedLastVisitedDays release]; |
|
89 delete _entriesByDate; |
|
90 |
|
91 [super dealloc]; |
|
92 } |
|
93 |
|
94 - (void)finalize |
|
95 { |
|
96 delete _entriesByDate; |
|
97 [super finalize]; |
|
98 } |
|
99 |
|
100 #pragma mark MODIFYING CONTENTS |
|
101 |
|
102 WebHistoryDateKey timeIntervalForBeginningOfDay(NSTimeInterval interval) |
|
103 { |
|
104 CFTimeZoneRef timeZone = CFTimeZoneCopyDefault(); |
|
105 CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(interval, timeZone); |
|
106 date.hour = 0; |
|
107 date.minute = 0; |
|
108 date.second = 0; |
|
109 NSTimeInterval result = CFGregorianDateGetAbsoluteTime(date, timeZone); |
|
110 CFRelease(timeZone); |
|
111 |
|
112 // Converting from double to int64_t is safe here as NSDate's useful range |
|
113 // is -2**48 .. 2**47 which will safely fit in an int64_t. |
|
114 return (WebHistoryDateKey)result; |
|
115 } |
|
116 |
|
117 // Returns whether the day is already in the list of days, |
|
118 // and fills in *key with the key used to access its location |
|
119 - (BOOL)findKey:(WebHistoryDateKey*)key forDay:(NSTimeInterval)date |
|
120 { |
|
121 ASSERT_ARG(key, key != nil); |
|
122 *key = timeIntervalForBeginningOfDay(date); |
|
123 return _entriesByDate->contains(*key); |
|
124 } |
|
125 |
|
126 - (void)insertItem:(WebHistoryItem *)entry forDateKey:(WebHistoryDateKey)dateKey |
|
127 { |
|
128 ASSERT_ARG(entry, entry != nil); |
|
129 ASSERT(_entriesByDate->contains(dateKey)); |
|
130 |
|
131 NSMutableArray *entriesForDate = _entriesByDate->get(dateKey).get(); |
|
132 NSTimeInterval entryDate = [entry lastVisitedTimeInterval]; |
|
133 |
|
134 unsigned count = [entriesForDate count]; |
|
135 |
|
136 // The entries for each day are stored in a sorted array with the most recent entry first |
|
137 // Check for the common cases of the entry being newer than all existing entries or the first entry of the day |
|
138 if (!count || [[entriesForDate objectAtIndex:0] lastVisitedTimeInterval] < entryDate) { |
|
139 [entriesForDate insertObject:entry atIndex:0]; |
|
140 return; |
|
141 } |
|
142 // .. or older than all existing entries |
|
143 if (count > 0 && [[entriesForDate objectAtIndex:count - 1] lastVisitedTimeInterval] >= entryDate) { |
|
144 [entriesForDate insertObject:entry atIndex:count]; |
|
145 return; |
|
146 } |
|
147 |
|
148 unsigned low = 0; |
|
149 unsigned high = count; |
|
150 while (low < high) { |
|
151 unsigned mid = low + (high - low) / 2; |
|
152 if ([[entriesForDate objectAtIndex:mid] lastVisitedTimeInterval] >= entryDate) |
|
153 low = mid + 1; |
|
154 else |
|
155 high = mid; |
|
156 } |
|
157 |
|
158 // low is now the index of the first entry that is older than entryDate |
|
159 [entriesForDate insertObject:entry atIndex:low]; |
|
160 } |
|
161 |
|
162 - (BOOL)_removeItemFromDateCaches:(WebHistoryItem *)entry |
|
163 { |
|
164 WebHistoryDateKey dateKey; |
|
165 BOOL foundDate = [self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]]; |
|
166 |
|
167 if (!foundDate) |
|
168 return NO; |
|
169 |
|
170 DateToEntriesMap::iterator it = _entriesByDate->find(dateKey); |
|
171 NSMutableArray *entriesForDate = it->second.get(); |
|
172 [entriesForDate removeObjectIdenticalTo:entry]; |
|
173 |
|
174 // remove this date entirely if there are no other entries on it |
|
175 if ([entriesForDate count] == 0) { |
|
176 _entriesByDate->remove(it); |
|
177 // Clear _orderedLastVisitedDays so it will be regenerated when next requested. |
|
178 [_orderedLastVisitedDays release]; |
|
179 _orderedLastVisitedDays = nil; |
|
180 } |
|
181 |
|
182 return YES; |
|
183 } |
|
184 |
|
185 - (BOOL)removeItemForURLString: (NSString *)URLString |
|
186 { |
|
187 WebHistoryItem *entry = [_entriesByURL objectForKey: URLString]; |
|
188 if (entry == nil) { |
|
189 return NO; |
|
190 } |
|
191 |
|
192 [_entriesByURL removeObjectForKey: URLString]; |
|
193 |
|
194 #if ASSERT_DISABLED |
|
195 [self _removeItemFromDateCaches:entry]; |
|
196 #else |
|
197 BOOL itemWasInDateCaches = [self _removeItemFromDateCaches:entry]; |
|
198 ASSERT(itemWasInDateCaches); |
|
199 #endif |
|
200 |
|
201 return YES; |
|
202 } |
|
203 |
|
204 - (void)_addItemToDateCaches:(WebHistoryItem *)entry |
|
205 { |
|
206 WebHistoryDateKey dateKey; |
|
207 if ([self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]]) |
|
208 // other entries already exist for this date |
|
209 [self insertItem:entry forDateKey:dateKey]; |
|
210 else { |
|
211 // no other entries exist for this date |
|
212 NSMutableArray *entries = [[NSMutableArray alloc] initWithObjects:&entry count:1]; |
|
213 _entriesByDate->set(dateKey, entries); |
|
214 [entries release]; |
|
215 // Clear _orderedLastVisitedDays so it will be regenerated when next requested. |
|
216 [_orderedLastVisitedDays release]; |
|
217 _orderedLastVisitedDays = nil; |
|
218 } |
|
219 } |
|
220 |
|
221 - (void)addItem:(WebHistoryItem *)entry |
|
222 { |
|
223 ASSERT_ARG(entry, entry); |
|
224 ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0); |
|
225 |
|
226 NSString *URLString = [entry URLString]; |
|
227 |
|
228 WebHistoryItem *oldEntry = [_entriesByURL objectForKey:URLString]; |
|
229 if (oldEntry) { |
|
230 // The last reference to oldEntry might be this dictionary, so we hold onto a reference |
|
231 // until we're done with oldEntry. |
|
232 [oldEntry retain]; |
|
233 [self removeItemForURLString:URLString]; |
|
234 |
|
235 // If we already have an item with this URL, we need to merge info that drives the |
|
236 // URL autocomplete heuristics from that item into the new one. |
|
237 [entry _mergeAutoCompleteHints:oldEntry]; |
|
238 [oldEntry release]; |
|
239 } |
|
240 |
|
241 [self _addItemToDateCaches:entry]; |
|
242 [_entriesByURL setObject:entry forKey:URLString]; |
|
243 } |
|
244 |
|
245 - (void)setLastVisitedTimeInterval:(NSTimeInterval)time forItem:(WebHistoryItem *)entry |
|
246 { |
|
247 #if ASSERT_DISABLED |
|
248 [self _removeItemFromDateCaches:entry]; |
|
249 #else |
|
250 BOOL entryWasPresent = [self _removeItemFromDateCaches:entry]; |
|
251 ASSERT(entryWasPresent); |
|
252 #endif |
|
253 |
|
254 [entry _setLastVisitedTimeInterval:time]; |
|
255 [self _addItemToDateCaches:entry]; |
|
256 |
|
257 // Don't send notification until entry is back in the right place in the date caches, |
|
258 // since observers might fetch history by date when they receive the notification. |
|
259 [[NSNotificationCenter defaultCenter] |
|
260 postNotificationName:WebHistoryItemChangedNotification object:entry userInfo:nil]; |
|
261 } |
|
262 |
|
263 - (BOOL)removeItem: (WebHistoryItem *)entry |
|
264 { |
|
265 WebHistoryItem *matchingEntry; |
|
266 NSString *URLString; |
|
267 |
|
268 URLString = [entry URLString]; |
|
269 |
|
270 // If this exact object isn't stored, then make no change. |
|
271 // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is? |
|
272 // Maybe need to change the API to make something like removeEntryForURLString public instead. |
|
273 matchingEntry = [_entriesByURL objectForKey: URLString]; |
|
274 if (matchingEntry != entry) { |
|
275 return NO; |
|
276 } |
|
277 |
|
278 [self removeItemForURLString: URLString]; |
|
279 |
|
280 return YES; |
|
281 } |
|
282 |
|
283 - (BOOL)removeItems: (NSArray *)entries |
|
284 { |
|
285 int index, count; |
|
286 |
|
287 count = [entries count]; |
|
288 if (count == 0) { |
|
289 return NO; |
|
290 } |
|
291 |
|
292 for (index = 0; index < count; ++index) { |
|
293 [self removeItem:[entries objectAtIndex:index]]; |
|
294 } |
|
295 |
|
296 return YES; |
|
297 } |
|
298 |
|
299 - (BOOL)removeAllItems |
|
300 { |
|
301 if ([_entriesByURL count] == 0) { |
|
302 return NO; |
|
303 } |
|
304 |
|
305 _entriesByDate->clear(); |
|
306 [_entriesByURL removeAllObjects]; |
|
307 |
|
308 return YES; |
|
309 } |
|
310 |
|
311 - (void)addItems:(NSArray *)newEntries |
|
312 { |
|
313 NSEnumerator *enumerator; |
|
314 WebHistoryItem *entry; |
|
315 |
|
316 // There is no guarantee that the incoming entries are in any particular |
|
317 // order, but if this is called with a set of entries that were created by |
|
318 // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy |
|
319 // then they will be ordered chronologically from newest to oldest. We can make adding them |
|
320 // faster (fewer compares) by inserting them from oldest to newest. |
|
321 enumerator = [newEntries reverseObjectEnumerator]; |
|
322 while ((entry = [enumerator nextObject]) != nil) { |
|
323 [self addItem:entry]; |
|
324 } |
|
325 } |
|
326 |
|
327 #pragma mark DATE-BASED RETRIEVAL |
|
328 |
|
329 - (NSArray *)orderedLastVisitedDays |
|
330 { |
|
331 if (!_orderedLastVisitedDays) { |
|
332 Vector<int> daysAsTimeIntervals; |
|
333 daysAsTimeIntervals.reserveCapacity(_entriesByDate->size()); |
|
334 DateToEntriesMap::const_iterator end = _entriesByDate->end(); |
|
335 for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it) |
|
336 daysAsTimeIntervals.append(it->first); |
|
337 |
|
338 std::sort(daysAsTimeIntervals.begin(), daysAsTimeIntervals.end()); |
|
339 size_t count = daysAsTimeIntervals.size(); |
|
340 _orderedLastVisitedDays = [[NSMutableArray alloc] initWithCapacity:count]; |
|
341 for (int i = count - 1; i >= 0; i--) { |
|
342 NSTimeInterval interval = daysAsTimeIntervals[i]; |
|
343 NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:interval]; |
|
344 [_orderedLastVisitedDays addObject:date]; |
|
345 [date release]; |
|
346 } |
|
347 } |
|
348 return _orderedLastVisitedDays; |
|
349 } |
|
350 |
|
351 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date |
|
352 { |
|
353 WebHistoryDateKey dateKey; |
|
354 if ([self findKey:&dateKey forDay:[date timeIntervalSinceReferenceDate]]) |
|
355 return _entriesByDate->get(dateKey).get(); |
|
356 |
|
357 return nil; |
|
358 } |
|
359 |
|
360 #pragma mark URL MATCHING |
|
361 |
|
362 - (WebHistoryItem *)itemForURLString:(NSString *)URLString |
|
363 { |
|
364 return [_entriesByURL objectForKey: URLString]; |
|
365 } |
|
366 |
|
367 - (BOOL)containsItemForURLString: (NSString *)URLString |
|
368 { |
|
369 return [self itemForURLString:URLString] != nil; |
|
370 } |
|
371 |
|
372 - (BOOL)containsURL: (NSURL *)URL |
|
373 { |
|
374 return [self itemForURLString:[URL _web_originalDataAsString]] != nil; |
|
375 } |
|
376 |
|
377 - (WebHistoryItem *)itemForURL:(NSURL *)URL |
|
378 { |
|
379 return [self itemForURLString:[URL _web_originalDataAsString]]; |
|
380 } |
|
381 |
|
382 #pragma mark ARCHIVING/UNARCHIVING |
|
383 |
|
384 - (void)setHistoryAgeInDaysLimit:(int)limit |
|
385 { |
|
386 ageInDaysLimitSet = YES; |
|
387 ageInDaysLimit = limit; |
|
388 } |
|
389 |
|
390 - (int)historyAgeInDaysLimit |
|
391 { |
|
392 if (ageInDaysLimitSet) |
|
393 return ageInDaysLimit; |
|
394 return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryAgeInDaysLimit"]; |
|
395 } |
|
396 |
|
397 - (void)setHistoryItemLimit:(int)limit |
|
398 { |
|
399 itemLimitSet = YES; |
|
400 itemLimit = limit; |
|
401 } |
|
402 |
|
403 - (int)historyItemLimit |
|
404 { |
|
405 if (itemLimitSet) |
|
406 return itemLimit; |
|
407 return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryItemLimit"]; |
|
408 } |
|
409 |
|
410 // Return a date that marks the age limit for history entries saved to or |
|
411 // loaded from disk. Any entry older than this item should be rejected. |
|
412 - (NSCalendarDate *)_ageLimitDate |
|
413 { |
|
414 return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit] |
|
415 hours:0 minutes:0 seconds:0]; |
|
416 } |
|
417 |
|
418 // Return a flat array of WebHistoryItems. Ignores the date and item count limits; these are |
|
419 // respected when loading instead of when saving, so that clients can learn of discarded items |
|
420 // by listening to WebHistoryItemsDiscardedWhileLoadingNotification. |
|
421 - (NSArray *)arrayRepresentation |
|
422 { |
|
423 NSMutableArray *arrayRep = [NSMutableArray array]; |
|
424 |
|
425 Vector<int> dateKeys; |
|
426 dateKeys.reserveCapacity(_entriesByDate->size()); |
|
427 DateToEntriesMap::const_iterator end = _entriesByDate->end(); |
|
428 for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it) |
|
429 dateKeys.append(it->first); |
|
430 |
|
431 std::sort(dateKeys.begin(), dateKeys.end()); |
|
432 for (int dateIndex = dateKeys.size() - 1; dateIndex >= 0; dateIndex--) { |
|
433 NSArray *entries = _entriesByDate->get(dateKeys[dateIndex]).get(); |
|
434 int entryCount = [entries count]; |
|
435 for (int entryIndex = 0; entryIndex < entryCount; ++entryIndex) |
|
436 [arrayRep addObject:[[entries objectAtIndex:entryIndex] dictionaryRepresentation]]; |
|
437 } |
|
438 |
|
439 return arrayRep; |
|
440 } |
|
441 |
|
442 - (BOOL)_loadHistoryGutsFromURL:(NSURL *)URL savedItemsCount:(int *)numberOfItemsLoaded collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error |
|
443 { |
|
444 *numberOfItemsLoaded = 0; |
|
445 NSDictionary *dictionary = nil; |
|
446 |
|
447 // Optimize loading from local file, which is faster than using the general URL loading mechanism |
|
448 if ([URL isFileURL]) { |
|
449 dictionary = [NSDictionary dictionaryWithContentsOfFile:[URL path]]; |
|
450 if (!dictionary) { |
|
451 #if !LOG_DISABLED |
|
452 if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) |
|
453 LOG_ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]); |
|
454 #endif |
|
455 // else file doesn't exist, which is normal the first time |
|
456 return NO; |
|
457 } |
|
458 } else { |
|
459 NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error]; |
|
460 if (data && [data length] > 0) { |
|
461 dictionary = [NSPropertyListSerialization propertyListFromData:data |
|
462 mutabilityOption:NSPropertyListImmutable |
|
463 format:nil |
|
464 errorDescription:nil]; |
|
465 } |
|
466 } |
|
467 |
|
468 // We used to support NSArrays here, but that was before Safari 1.0 shipped. We will no longer support |
|
469 // that ancient format, so anything that isn't an NSDictionary is bogus. |
|
470 if (![dictionary isKindOfClass:[NSDictionary class]]) |
|
471 return NO; |
|
472 |
|
473 NSNumber *fileVersionObject = [dictionary objectForKey:FileVersionKey]; |
|
474 int fileVersion; |
|
475 // we don't trust data obtained from elsewhere, so double-check |
|
476 if (fileVersionObject != nil && [fileVersionObject isKindOfClass:[NSNumber class]]) { |
|
477 fileVersion = [fileVersionObject intValue]; |
|
478 } else { |
|
479 LOG_ERROR("history file version can't be determined, therefore not loading"); |
|
480 return NO; |
|
481 } |
|
482 if (fileVersion > currentFileVersion) { |
|
483 LOG_ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion); |
|
484 return NO; |
|
485 } |
|
486 |
|
487 NSArray *array = [dictionary objectForKey:DatesArrayKey]; |
|
488 |
|
489 int itemCountLimit = [self historyItemLimit]; |
|
490 NSTimeInterval ageLimitDate = [[self _ageLimitDate] timeIntervalSinceReferenceDate]; |
|
491 NSEnumerator *enumerator = [array objectEnumerator]; |
|
492 BOOL ageLimitPassed = NO; |
|
493 BOOL itemLimitPassed = NO; |
|
494 ASSERT(*numberOfItemsLoaded == 0); |
|
495 |
|
496 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
|
497 NSDictionary *itemAsDictionary; |
|
498 while ((itemAsDictionary = [enumerator nextObject]) != nil) { |
|
499 WebHistoryItem *item = [[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary]; |
|
500 |
|
501 // item without URL is useless; data on disk must have been bad; ignore |
|
502 if ([item URLString]) { |
|
503 // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing |
|
504 // once we've found the first item that's too old. |
|
505 if (!ageLimitPassed && [item lastVisitedTimeInterval] <= ageLimitDate) |
|
506 ageLimitPassed = YES; |
|
507 |
|
508 if (ageLimitPassed || itemLimitPassed) |
|
509 [discardedItems addObject:item]; |
|
510 else { |
|
511 [self addItem:item]; |
|
512 ++(*numberOfItemsLoaded); |
|
513 if (*numberOfItemsLoaded == itemCountLimit) |
|
514 itemLimitPassed = YES; |
|
515 |
|
516 // Draining the autorelease pool every 50 iterations was found by experimentation to be optimal |
|
517 if (*numberOfItemsLoaded % 50 == 0) { |
|
518 [pool drain]; |
|
519 pool = [[NSAutoreleasePool alloc] init]; |
|
520 } |
|
521 } |
|
522 } |
|
523 [item release]; |
|
524 } |
|
525 [pool drain]; |
|
526 |
|
527 return YES; |
|
528 } |
|
529 |
|
530 - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error |
|
531 { |
|
532 int numberOfItems; |
|
533 double start, duration; |
|
534 BOOL result; |
|
535 |
|
536 start = CFAbsoluteTimeGetCurrent(); |
|
537 result = [self _loadHistoryGutsFromURL:URL savedItemsCount:&numberOfItems collectDiscardedItemsInto:discardedItems error:error]; |
|
538 |
|
539 if (result) { |
|
540 duration = CFAbsoluteTimeGetCurrent() - start; |
|
541 LOG(Timing, "loading %d history entries from %@ took %f seconds", |
|
542 numberOfItems, URL, duration); |
|
543 } |
|
544 |
|
545 return result; |
|
546 } |
|
547 |
|
548 - (BOOL)_saveHistoryGuts: (int *)numberOfItemsSaved URL:(NSURL *)URL error:(NSError **)error |
|
549 { |
|
550 *numberOfItemsSaved = 0; |
|
551 |
|
552 // FIXME: Correctly report error when new API is ready. |
|
553 if (error) |
|
554 *error = nil; |
|
555 |
|
556 NSArray *array = [self arrayRepresentation]; |
|
557 NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: |
|
558 array, DatesArrayKey, |
|
559 [NSNumber numberWithInt:currentFileVersion], FileVersionKey, |
|
560 nil]; |
|
561 NSData *data = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil]; |
|
562 if (![data writeToURL:URL atomically:YES]) { |
|
563 LOG_ERROR("attempt to save %@ to %@ failed", dictionary, URL); |
|
564 return NO; |
|
565 } |
|
566 |
|
567 *numberOfItemsSaved = [array count]; |
|
568 return YES; |
|
569 } |
|
570 |
|
571 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error |
|
572 { |
|
573 int numberOfItems; |
|
574 double start, duration; |
|
575 BOOL result; |
|
576 |
|
577 start = CFAbsoluteTimeGetCurrent(); |
|
578 result = [self _saveHistoryGuts: &numberOfItems URL:URL error:error]; |
|
579 |
|
580 if (result) { |
|
581 duration = CFAbsoluteTimeGetCurrent() - start; |
|
582 LOG(Timing, "saving %d history entries to %@ took %f seconds", |
|
583 numberOfItems, URL, duration); |
|
584 } |
|
585 |
|
586 return result; |
|
587 } |
|
588 |
|
589 @end |
|
590 |
|
591 @interface _WebCoreHistoryProvider : NSObject <WebCoreHistoryProvider> |
|
592 { |
|
593 WebHistory *history; |
|
594 } |
|
595 - initWithHistory: (WebHistory *)h; |
|
596 @end |
|
597 |
|
598 @implementation _WebCoreHistoryProvider |
|
599 - initWithHistory: (WebHistory *)h |
|
600 { |
|
601 history = [h retain]; |
|
602 return self; |
|
603 } |
|
604 |
|
605 static inline bool matchLetter(char c, char lowercaseLetter) |
|
606 { |
|
607 return (c | 0x20) == lowercaseLetter; |
|
608 } |
|
609 |
|
610 static inline bool matchUnicodeLetter(UniChar c, UniChar lowercaseLetter) |
|
611 { |
|
612 return (c | 0x20) == lowercaseLetter; |
|
613 } |
|
614 |
|
615 #define BUFFER_SIZE 2048 |
|
616 |
|
617 - (BOOL)containsItemForURLLatin1:(const char *)latin1 length:(unsigned)length |
|
618 { |
|
619 const char *latin1Str = latin1; |
|
620 char staticStrBuffer[BUFFER_SIZE]; |
|
621 char *strBuffer = NULL; |
|
622 BOOL needToAddSlash = FALSE; |
|
623 |
|
624 if (length >= 6 && |
|
625 matchLetter(latin1[0], 'h') && |
|
626 matchLetter(latin1[1], 't') && |
|
627 matchLetter(latin1[2], 't') && |
|
628 matchLetter(latin1[3], 'p') && |
|
629 (latin1[4] == ':' |
|
630 || (matchLetter(latin1[4], 's') && latin1[5] == ':'))) { |
|
631 int pos = latin1[4] == ':' ? 5 : 6; |
|
632 // skip possible initial two slashes |
|
633 if (latin1[pos] == '/' && latin1[pos + 1] == '/') { |
|
634 pos += 2; |
|
635 } |
|
636 |
|
637 char *nextSlash = strchr(latin1 + pos, '/'); |
|
638 if (nextSlash == NULL) { |
|
639 needToAddSlash = TRUE; |
|
640 } |
|
641 } |
|
642 |
|
643 if (needToAddSlash) { |
|
644 if (length + 1 <= BUFFER_SIZE) { |
|
645 strBuffer = staticStrBuffer; |
|
646 } else { |
|
647 strBuffer = (char*)malloc(length + 2); |
|
648 } |
|
649 memcpy(strBuffer, latin1, length + 1); |
|
650 strBuffer[length] = '/'; |
|
651 strBuffer[length+1] = '\0'; |
|
652 length++; |
|
653 |
|
654 latin1Str = strBuffer; |
|
655 } |
|
656 |
|
657 CFStringRef str = CFStringCreateWithCStringNoCopy(NULL, latin1Str, kCFStringEncodingWindowsLatin1, kCFAllocatorNull); |
|
658 BOOL result = [history containsItemForURLString:(id)str]; |
|
659 CFRelease(str); |
|
660 |
|
661 if (strBuffer != staticStrBuffer) { |
|
662 free(strBuffer); |
|
663 } |
|
664 |
|
665 return result; |
|
666 } |
|
667 |
|
668 #define UNICODE_BUFFER_SIZE 1024 |
|
669 |
|
670 - (BOOL)containsItemForURLUnicode:(const UniChar *)unicode length:(unsigned)length |
|
671 { |
|
672 const UniChar *unicodeStr = unicode; |
|
673 UniChar staticStrBuffer[UNICODE_BUFFER_SIZE]; |
|
674 UniChar *strBuffer = NULL; |
|
675 BOOL needToAddSlash = FALSE; |
|
676 |
|
677 if (length >= 6 && |
|
678 matchUnicodeLetter(unicode[0], 'h') && |
|
679 matchUnicodeLetter(unicode[1], 't') && |
|
680 matchUnicodeLetter(unicode[2], 't') && |
|
681 matchUnicodeLetter(unicode[3], 'p') && |
|
682 (unicode[4] == ':' |
|
683 || (matchUnicodeLetter(unicode[4], 's') && unicode[5] == ':'))) { |
|
684 |
|
685 unsigned pos = unicode[4] == ':' ? 5 : 6; |
|
686 |
|
687 // skip possible initial two slashes |
|
688 if (pos + 1 < length && unicode[pos] == '/' && unicode[pos + 1] == '/') { |
|
689 pos += 2; |
|
690 } |
|
691 |
|
692 while (pos < length && unicode[pos] != '/') { |
|
693 pos++; |
|
694 } |
|
695 |
|
696 if (pos == length) { |
|
697 needToAddSlash = TRUE; |
|
698 } |
|
699 } |
|
700 |
|
701 if (needToAddSlash) { |
|
702 if (length + 1 <= UNICODE_BUFFER_SIZE) { |
|
703 strBuffer = staticStrBuffer; |
|
704 } else { |
|
705 strBuffer = (UniChar*)malloc(sizeof(UniChar) * (length + 1)); |
|
706 } |
|
707 memcpy(strBuffer, unicode, 2 * length); |
|
708 strBuffer[length] = '/'; |
|
709 length++; |
|
710 |
|
711 unicodeStr = strBuffer; |
|
712 } |
|
713 |
|
714 CFStringRef str = CFStringCreateWithCharactersNoCopy(NULL, unicodeStr, length, kCFAllocatorNull); |
|
715 BOOL result = [history containsItemForURLString:(id)str]; |
|
716 CFRelease(str); |
|
717 |
|
718 if (strBuffer != staticStrBuffer) { |
|
719 free(strBuffer); |
|
720 } |
|
721 |
|
722 return result; |
|
723 } |
|
724 |
|
725 - (void)dealloc |
|
726 { |
|
727 [history release]; |
|
728 [super dealloc]; |
|
729 } |
|
730 |
|
731 @end |
|
732 |
|
733 @implementation WebHistory |
|
734 |
|
735 + (WebHistory *)optionalSharedHistory |
|
736 { |
|
737 return _sharedHistory; |
|
738 } |
|
739 |
|
740 |
|
741 + (void)setOptionalSharedHistory: (WebHistory *)history |
|
742 { |
|
743 // FIXME. Need to think about multiple instances of WebHistory per application |
|
744 // and correct synchronization of history file between applications. |
|
745 [WebCoreHistory setHistoryProvider: [[[_WebCoreHistoryProvider alloc] initWithHistory: history] autorelease]]; |
|
746 if (_sharedHistory != history){ |
|
747 [_sharedHistory release]; |
|
748 _sharedHistory = [history retain]; |
|
749 } |
|
750 } |
|
751 |
|
752 - (id)init |
|
753 { |
|
754 if ((self = [super init]) != nil) { |
|
755 _historyPrivate = [[WebHistoryPrivate alloc] init]; |
|
756 } |
|
757 |
|
758 return self; |
|
759 } |
|
760 |
|
761 - (void)dealloc |
|
762 { |
|
763 [_historyPrivate release]; |
|
764 [super dealloc]; |
|
765 } |
|
766 |
|
767 #pragma mark MODIFYING CONTENTS |
|
768 |
|
769 - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries |
|
770 { |
|
771 NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil]; |
|
772 [[NSNotificationCenter defaultCenter] |
|
773 postNotificationName: name object: self userInfo: userInfo]; |
|
774 } |
|
775 |
|
776 - (WebHistoryItem *)addItemForURL: (NSURL *)URL |
|
777 { |
|
778 WebHistoryItem *entry = [[WebHistoryItem alloc] initWithURL:URL title:nil]; |
|
779 [entry _setLastVisitedTimeInterval: [NSDate timeIntervalSinceReferenceDate]]; |
|
780 [self addItem: entry]; |
|
781 [entry release]; |
|
782 return entry; |
|
783 } |
|
784 |
|
785 |
|
786 - (void)addItem: (WebHistoryItem *)entry |
|
787 { |
|
788 LOG (History, "adding %@", entry); |
|
789 [_historyPrivate addItem: entry]; |
|
790 [self _sendNotification: WebHistoryItemsAddedNotification |
|
791 entries: [NSArray arrayWithObject:entry]]; |
|
792 } |
|
793 |
|
794 - (void)removeItem: (WebHistoryItem *)entry |
|
795 { |
|
796 if ([_historyPrivate removeItem: entry]) { |
|
797 [self _sendNotification: WebHistoryItemsRemovedNotification |
|
798 entries: [NSArray arrayWithObject:entry]]; |
|
799 } |
|
800 } |
|
801 |
|
802 - (void)removeItems: (NSArray *)entries |
|
803 { |
|
804 if ([_historyPrivate removeItems:entries]) { |
|
805 [self _sendNotification: WebHistoryItemsRemovedNotification |
|
806 entries: entries]; |
|
807 } |
|
808 } |
|
809 |
|
810 - (void)removeAllItems |
|
811 { |
|
812 if ([_historyPrivate removeAllItems]) { |
|
813 [[NSNotificationCenter defaultCenter] |
|
814 postNotificationName: WebHistoryAllItemsRemovedNotification |
|
815 object: self]; |
|
816 } |
|
817 } |
|
818 |
|
819 - (void)addItems:(NSArray *)newEntries |
|
820 { |
|
821 [_historyPrivate addItems:newEntries]; |
|
822 [self _sendNotification: WebHistoryItemsAddedNotification |
|
823 entries: newEntries]; |
|
824 } |
|
825 |
|
826 - (void)setLastVisitedTimeInterval:(NSTimeInterval)time forItem:(WebHistoryItem *)entry |
|
827 { |
|
828 [_historyPrivate setLastVisitedTimeInterval:time forItem:entry]; |
|
829 } |
|
830 |
|
831 #pragma mark DATE-BASED RETRIEVAL |
|
832 |
|
833 - (NSArray *)orderedLastVisitedDays |
|
834 { |
|
835 return [_historyPrivate orderedLastVisitedDays]; |
|
836 } |
|
837 |
|
838 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date |
|
839 { |
|
840 return [_historyPrivate orderedItemsLastVisitedOnDay: date]; |
|
841 } |
|
842 |
|
843 #pragma mark URL MATCHING |
|
844 |
|
845 - (BOOL)containsItemForURLString: (NSString *)URLString |
|
846 { |
|
847 return [_historyPrivate containsItemForURLString: URLString]; |
|
848 } |
|
849 |
|
850 - (BOOL)containsURL: (NSURL *)URL |
|
851 { |
|
852 return [_historyPrivate containsURL: URL]; |
|
853 } |
|
854 |
|
855 - (WebHistoryItem *)itemForURL:(NSURL *)URL |
|
856 { |
|
857 return [_historyPrivate itemForURL:URL]; |
|
858 } |
|
859 |
|
860 #pragma mark SAVING TO DISK |
|
861 |
|
862 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error |
|
863 { |
|
864 NSMutableArray *discardedItems = [NSMutableArray array]; |
|
865 |
|
866 if ([_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems error:error]) { |
|
867 [[NSNotificationCenter defaultCenter] |
|
868 postNotificationName:WebHistoryLoadedNotification |
|
869 object:self]; |
|
870 |
|
871 if ([discardedItems count] > 0) |
|
872 [self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems]; |
|
873 |
|
874 return YES; |
|
875 } |
|
876 return NO; |
|
877 } |
|
878 |
|
879 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error |
|
880 { |
|
881 // FIXME: Use new foundation API to get error when ready. |
|
882 if([_historyPrivate saveToURL:URL error:error]){ |
|
883 [[NSNotificationCenter defaultCenter] |
|
884 postNotificationName: WebHistorySavedNotification |
|
885 object: self]; |
|
886 return YES; |
|
887 } |
|
888 return NO; |
|
889 } |
|
890 |
|
891 - (WebHistoryItem *)_itemForURLString:(NSString *)URLString |
|
892 { |
|
893 return [_historyPrivate itemForURLString: URLString]; |
|
894 } |
|
895 |
|
896 - (NSCalendarDate*)ageLimitDate |
|
897 { |
|
898 return [_historyPrivate _ageLimitDate]; |
|
899 } |
|
900 |
|
901 - (void)setHistoryItemLimit:(int)limit |
|
902 { |
|
903 [_historyPrivate setHistoryItemLimit:limit]; |
|
904 } |
|
905 |
|
906 - (int)historyItemLimit |
|
907 { |
|
908 return [_historyPrivate historyItemLimit]; |
|
909 } |
|
910 |
|
911 - (void)setHistoryAgeInDaysLimit:(int)limit |
|
912 { |
|
913 [_historyPrivate setHistoryAgeInDaysLimit:limit]; |
|
914 } |
|
915 |
|
916 - (int)historyAgeInDaysLimit |
|
917 { |
|
918 return [_historyPrivate historyAgeInDaysLimit]; |
|
919 } |
|
920 |
|
921 @end |