/* * Copyright (C) 2015 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "SearchPopupMenuCocoa.h" #import namespace WebCore { static NSString * const dateKey = @"date"; static NSString * const itemsKey = @"items"; static NSString * const searchesKey = @"searches"; static NSString * const searchStringKey = @"searchString"; static NSString *searchFieldRecentSearchesStorageDirectory() { NSString *appName = [[NSBundle mainBundle] bundleIdentifier]; if (!appName) appName = [[NSProcessInfo processInfo] processName]; return [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/WebKit"] stringByAppendingPathComponent:appName]; } static NSString *searchFieldRecentSearchesPlistPath() { return [searchFieldRecentSearchesStorageDirectory() stringByAppendingPathComponent:@"RecentSearches.plist"]; } static RetainPtr readSearchFieldRecentSearchesPlist() { return adoptNS([[NSMutableDictionary alloc] initWithContentsOfFile:searchFieldRecentSearchesPlistPath()]); } static WallTime toSystemClockTime(NSDate *date) { ASSERT(date); return WallTime::fromRawSeconds(date.timeIntervalSince1970); } static NSDate *toNSDateFromSystemClock(WallTime time) { return [NSDate dateWithTimeIntervalSince1970:time.secondsSinceEpoch().seconds()]; } static NSMutableArray *typeCheckedRecentSearchesArray(NSMutableDictionary *itemsDictionary, NSString *name) { NSMutableDictionary *nameDictionary = [itemsDictionary objectForKey:name]; if (![nameDictionary isKindOfClass:[NSDictionary class]]) return nil; NSMutableArray *recentSearches = [nameDictionary objectForKey:searchesKey]; if (![recentSearches isKindOfClass:[NSArray class]]) return nil; return recentSearches; } static NSDate *typeCheckedDateInRecentSearch(NSDictionary *recentSearch) { if (![recentSearch isKindOfClass:[NSDictionary class]]) return nil; NSDate *date = [recentSearch objectForKey:dateKey]; if (![date isKindOfClass:[NSDate class]]) return nil; return date; } static RetainPtr typeCheckedRecentSearchesRemovingRecentSearchesAddedAfterDate(NSDate *date) { if ([date isEqualToDate:[NSDate distantPast]]) return nil; RetainPtr recentSearchesPlist = readSearchFieldRecentSearchesPlist(); NSMutableDictionary *itemsDictionary = [recentSearchesPlist objectForKey:itemsKey]; if (![itemsDictionary isKindOfClass:[NSDictionary class]]) return nil; RetainPtr keysToRemove = adoptNS([[NSMutableArray alloc] init]); for (NSString *key in itemsDictionary) { if (![key isKindOfClass:[NSString class]]) return nil; NSMutableArray *recentSearches = typeCheckedRecentSearchesArray(itemsDictionary, key); if (!recentSearches) return nil; RetainPtr entriesToRemove = adoptNS([[NSMutableArray alloc] init]); for (NSDictionary *recentSearch in recentSearches) { NSDate *dateAdded = typeCheckedDateInRecentSearch(recentSearch); if (!dateAdded) return nil; if ([dateAdded compare:date] == NSOrderedDescending) [entriesToRemove addObject:recentSearch]; } [recentSearches removeObjectsInArray:entriesToRemove.get()]; if (!recentSearches.count) [keysToRemove addObject:key]; } [itemsDictionary removeObjectsForKeys:keysToRemove.get()]; if (!itemsDictionary.count) return nil; return recentSearchesPlist; } static void writeEmptyRecentSearchesPlist() { auto emptyItemsDictionary = adoptNS([[NSDictionary alloc] init]); auto emptyRecentSearchesDictionary = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:emptyItemsDictionary.get(), itemsKey, nil]); [emptyRecentSearchesDictionary writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES]; } void saveRecentSearches(const String& name, const Vector& searchItems) { if (name.isEmpty()) return; RetainPtr recentSearchesPlist = readSearchFieldRecentSearchesPlist(); RetainPtr itemsDictionary = [recentSearchesPlist objectForKey:itemsKey]; // The NSMutableDictionary method we use to read the property list guarantees we get only // mutable containers, but it does not guarantee the file has a dictionary as expected. if (![itemsDictionary isKindOfClass:[NSDictionary class]]) { itemsDictionary = adoptNS([[NSMutableDictionary alloc] init]); recentSearchesPlist = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:itemsDictionary.get(), itemsKey, nil]); } if (searchItems.isEmpty()) [itemsDictionary removeObjectForKey:name]; else { auto items = createNSArray(searchItems, [] (auto& item) { return adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:item.string, searchStringKey, toNSDateFromSystemClock(item.time), dateKey, nil]); }); [itemsDictionary setObject:adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:items.get(), searchesKey, nil]).get() forKey:name]; } [recentSearchesPlist writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES]; } Vector loadRecentSearches(const String& name) { Vector searchItems; if (name.isEmpty()) return searchItems; RetainPtr recentSearchesPlist = readSearchFieldRecentSearchesPlist(); if (!recentSearchesPlist) return searchItems; NSMutableDictionary *items = [recentSearchesPlist objectForKey:itemsKey]; if (![items isKindOfClass:[NSDictionary class]]) return searchItems; NSArray *recentSearches = typeCheckedRecentSearchesArray(items, name); if (!recentSearches) return searchItems; for (NSDictionary *item in recentSearches) { NSDate *date = typeCheckedDateInRecentSearch(item); if (!date) continue; NSString *searchString = [item objectForKey:searchStringKey]; if (![searchString isKindOfClass:[NSString class]]) continue; searchItems.append({ String{ searchString }, toSystemClockTime(date) }); } return searchItems; } void removeRecentlyModifiedRecentSearches(WallTime oldestTimeToRemove) { NSDate *date = toNSDateFromSystemClock(oldestTimeToRemove); auto recentSearchesPlist = typeCheckedRecentSearchesRemovingRecentSearchesAddedAfterDate(date); if (recentSearchesPlist) [recentSearchesPlist writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES]; else writeEmptyRecentSearchesPlist(); } }