b83408f0a3da42aa0b266aecbba6c9b02a4b9bff
[WebKit.git] / Source / WebCore / platform / cocoa / SearchPopupMenuCocoa.mm
1 /*
2  * Copyright (C) 2015 Apple 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "SearchPopupMenuCocoa.h"
28
29 using namespace std::chrono;
30
31 namespace WebCore {
32
33 static NSString * const dateKey = @"date";
34 static NSString * const itemsKey = @"items";
35 static NSString * const searchesKey = @"searches";
36 static NSString * const searchStringKey = @"searchString";
37
38 static NSString *searchFieldRecentSearchesStorageDirectory()
39 {
40     NSString *appName = [[NSBundle mainBundle] bundleIdentifier];
41     if (!appName)
42         appName = [[NSProcessInfo processInfo] processName];
43     
44     return [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/WebKit"] stringByAppendingPathComponent:appName];
45 }
46
47 static NSString *searchFieldRecentSearchesPlistPath()
48 {
49     return [searchFieldRecentSearchesStorageDirectory() stringByAppendingPathComponent:@"RecentSearches.plist"];
50 }
51
52 static RetainPtr<NSMutableDictionary> readSearchFieldRecentSearchesPlist()
53 {
54     return adoptNS([[NSMutableDictionary alloc] initWithContentsOfFile:searchFieldRecentSearchesPlistPath()]);
55 }
56
57 static system_clock::time_point toSystemClockTime(NSDate *date)
58 {
59     ASSERT(date);
60
61     return system_clock::time_point(duration_cast<system_clock::duration>(duration<double>(date.timeIntervalSince1970)));
62 }
63
64 static NSDate *toNSDateFromSystemClock(system_clock::time_point time)
65 {
66     return [NSDate dateWithTimeIntervalSince1970:duration_cast<duration<double>>(time.time_since_epoch()).count()];
67 }
68
69 static NSMutableArray *typeCheckedRecentSearchesArray(NSMutableDictionary *itemsDictionary, NSString *name)
70 {
71     NSMutableDictionary *nameDictionary = [itemsDictionary objectForKey:name];
72     if (![nameDictionary isKindOfClass:[NSDictionary class]])
73         return nil;
74
75     NSMutableArray *recentSearches = [nameDictionary objectForKey:searchesKey];
76     if (![recentSearches isKindOfClass:[NSArray class]])
77         return nil;
78
79     return recentSearches;
80 }
81
82 static NSDate *typeCheckedDateInRecentSearch(NSDictionary *recentSearch)
83 {
84     if (![recentSearch isKindOfClass:[NSDictionary class]])
85         return nil;
86
87     NSDate *date = [recentSearch objectForKey:dateKey];
88     if (![date isKindOfClass:[NSDate class]])
89         return nil;
90
91     return date;
92 }
93
94 static RetainPtr<NSDictionary> typeCheckedRecentSearchesRemovingRecentSearchesAddedAfterDate(NSDate *date)
95 {
96     if ([date isEqualToDate:[NSDate distantPast]])
97         return nil;
98
99     RetainPtr<NSMutableDictionary> recentSearchesPlist = readSearchFieldRecentSearchesPlist();
100     NSMutableDictionary *itemsDictionary = [recentSearchesPlist objectForKey:itemsKey];
101     if (![itemsDictionary isKindOfClass:[NSDictionary class]])
102         return nil;
103
104     RetainPtr<NSMutableArray> keysToRemove = adoptNS([[NSMutableArray alloc] init]);
105     for (NSString *key in itemsDictionary) {
106         if (![key isKindOfClass:[NSString class]])
107             return nil;
108
109         NSMutableArray *recentSearches = typeCheckedRecentSearchesArray(itemsDictionary, key);
110         if (!recentSearches)
111             return nil;
112
113         RetainPtr<NSMutableArray> entriesToRemove = adoptNS([[NSMutableArray alloc] init]);
114         for (NSDictionary *recentSearch in recentSearches) {
115             NSDate *dateAdded = typeCheckedDateInRecentSearch(recentSearch);
116             if (!dateAdded)
117                 return nil;
118
119             if ([dateAdded compare:date] == NSOrderedDescending)
120                 [entriesToRemove addObject:recentSearch];
121         }
122
123         [recentSearches removeObjectsInArray:entriesToRemove.get()];
124         if (!recentSearches.count)
125             [keysToRemove addObject:key];
126     }
127
128     [itemsDictionary removeObjectsForKeys:keysToRemove.get()];
129     if (!itemsDictionary.count)
130         return nil;
131
132     return recentSearchesPlist;
133 }
134
135 static void writeEmptyRecentSearchesPlist()
136 {
137     auto emptyItemsDictionary = adoptNS([[NSDictionary alloc] init]);
138     auto emptyRecentSearchesDictionary = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:emptyItemsDictionary.get(), itemsKey, nil]);
139     [emptyRecentSearchesDictionary writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES];
140 }
141
142 void saveRecentSearches(const String& name, const Vector<RecentSearch>& searchItems)
143 {
144     if (name.isEmpty())
145         return;
146
147     RetainPtr<NSDictionary> recentSearchesPlist = readSearchFieldRecentSearchesPlist();
148     RetainPtr<NSMutableDictionary> itemsDictionary = [recentSearchesPlist objectForKey:itemsKey];
149     // The NSMutableDictionary method we use to read the property list guarantees we get only
150     // mutable containers, but it does not guarantee the file has a dictionary as expected.
151     if (![itemsDictionary isKindOfClass:[NSDictionary class]]) {
152         itemsDictionary = adoptNS([[NSMutableDictionary alloc] init]);
153         recentSearchesPlist = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:itemsDictionary.get(), itemsKey, nil]);
154     }
155
156     if (searchItems.isEmpty())
157         [itemsDictionary removeObjectForKey:name];
158     else {
159         RetainPtr<NSMutableArray> items = adoptNS([[NSMutableArray alloc] initWithCapacity:searchItems.size()]);
160         for (auto& searchItem : searchItems)
161             [items addObject:adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:searchItem.string, searchStringKey, toNSDateFromSystemClock(searchItem.time), dateKey, nil]).get()];
162
163         [itemsDictionary setObject:adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:items.get(), searchesKey, nil]).get() forKey:name];
164     }
165
166     [recentSearchesPlist writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES];
167 }
168
169 Vector<RecentSearch> loadRecentSearches(const String& name)
170 {
171     Vector<RecentSearch> searchItems;
172
173     if (name.isEmpty())
174         return searchItems;
175
176     RetainPtr<NSMutableDictionary> recentSearchesPlist = readSearchFieldRecentSearchesPlist();
177     if (!recentSearchesPlist)
178         return searchItems;
179
180     NSMutableDictionary *items = [recentSearchesPlist objectForKey:itemsKey];
181     if (![items isKindOfClass:[NSDictionary class]])
182         return searchItems;
183
184     NSArray *recentSearches = typeCheckedRecentSearchesArray(items, name);
185     if (!recentSearches)
186         return searchItems;
187     
188     for (NSDictionary *item in recentSearches) {
189         NSDate *date = typeCheckedDateInRecentSearch(item);
190         if (!date)
191             continue;
192
193         NSString *searchString = [item objectForKey:searchStringKey];
194         if (![searchString isKindOfClass:[NSString class]])
195             continue;
196         
197         searchItems.append({ String{ searchString }, toSystemClockTime(date) });
198     }
199
200     return searchItems;
201 }
202
203 void removeRecentlyModifiedRecentSearches(std::chrono::system_clock::time_point oldestTimeToRemove)
204 {
205     NSDate *date = toNSDateFromSystemClock(oldestTimeToRemove);
206     auto recentSearchesPlist = typeCheckedRecentSearchesRemovingRecentSearchesAddedAfterDate(date);
207     if (recentSearchesPlist)
208         [recentSearchesPlist writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES];
209     else
210         writeEmptyRecentSearchesPlist();
211 }
212
213 }