1/*
2 * Copyright (C) 2010, 2011, 2012 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#include "config.h"
27#include "WebBackForwardList.h"
28
29#include "Logging.h"
30#include <wtf/RetainPtr.h>
31#include <CoreFoundation/CoreFoundation.h>
32
33using namespace WebCore;
34
35namespace WebKit {
36
37static uint64_t generateWebBackForwardItemID()
38{
39    // These IDs exist in the UIProcess for items created by the UIProcess.
40    // The IDs generated here need to never collide with the IDs created in WebBackForwardListProxy in the WebProcess.
41    // We accomplish this by starting from 2, and only ever using even ids.
42    static uint64_t uniqueHistoryItemID = 0;
43    uniqueHistoryItemID += 2;
44    return uniqueHistoryItemID;
45}
46
47static CFIndex currentVersion = 1;
48DEFINE_STATIC_GETTER(CFNumberRef, SessionHistoryCurrentVersion, (CFNumberCreate(0, kCFNumberCFIndexType, &currentVersion)));
49
50DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryVersionKey, (CFSTR("SessionHistoryVersion")));
51DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryCurrentIndexKey, (CFSTR("SessionHistoryCurrentIndex")));
52DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryEntriesKey, (CFSTR("SessionHistoryEntries")));
53DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryEntryTitleKey, (CFSTR("SessionHistoryEntryTitle")));
54DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryEntryURLKey, (CFSTR("SessionHistoryEntryURL")));
55DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryEntryOriginalURLKey, (CFSTR("SessionHistoryEntryOriginalURL")));
56DEFINE_STATIC_GETTER(CFStringRef, SessionHistoryEntryDataKey, (CFSTR("SessionHistoryEntryData")));
57
58static bool extractBackForwardListEntriesFromArray(CFArrayRef, BackForwardListItemVector&);
59
60static CFDictionaryRef createEmptySessionHistoryDictionary()
61{
62    static const void* keys[1] = { SessionHistoryVersionKey() };
63    static const void* values[1] = { SessionHistoryCurrentVersion() };
64
65    return CFDictionaryCreate(0, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
66}
67
68CFDictionaryRef WebBackForwardList::createCFDictionaryRepresentation(WebPageProxy::WebPageProxySessionStateFilterCallback filter, void* context) const
69{
70    ASSERT(!m_hasCurrentIndex || m_currentIndex < m_entries.size());
71
72    if (!m_hasCurrentIndex) {
73        // We represent having no current index by writing out an empty dictionary (besides the version).
74        return createEmptySessionHistoryDictionary();
75    }
76
77    RetainPtr<CFMutableArrayRef> entries = adoptCF(CFArrayCreateMutable(0, m_entries.size(), &kCFTypeArrayCallBacks));
78
79    // We may need to update the current index to account for entries that are filtered by the callback.
80    CFIndex currentIndex = m_currentIndex;
81    bool hasCurrentIndex = true;
82
83    for (size_t i = 0; i < m_entries.size(); ++i) {
84        // If we somehow ended up with a null entry then we should consider the data invalid and not save session history at all.
85        ASSERT(m_entries[i]);
86        if (!m_entries[i]) {
87            LOG(SessionState, "WebBackForwardList contained a null entry at index %lu", i);
88            return 0;
89        }
90
91        if (filter) {
92            if (!filter(toAPI(m_page), WKPageGetSessionBackForwardListItemValueType(), toAPI(m_entries[i].get()), context)
93                || !filter(toAPI(m_page), WKPageGetSessionHistoryURLValueType(), toURLRef(m_entries[i]->originalURL().impl()), context)) {
94                if (i <= m_currentIndex)
95                    currentIndex--;
96                continue;
97            }
98        }
99
100        RetainPtr<CFStringRef> url = m_entries[i]->url().createCFString();
101        RetainPtr<CFStringRef> title = m_entries[i]->title().createCFString();
102        RetainPtr<CFStringRef> originalURL = m_entries[i]->originalURL().createCFString();
103
104        // FIXME: This uses the CoreIPC data encoding format, which means that whenever we change the CoreIPC encoding we need to bump the CurrentSessionStateDataVersion
105        // constant in WebPageProxyCF.cpp. The CoreIPC data format is meant to be an implementation detail, and not something that should be written to disk.
106        RetainPtr<CFDataRef> entryData = adoptCF(CFDataCreate(kCFAllocatorDefault, m_entries[i]->backForwardData().data(), m_entries[i]->backForwardData().size()));
107
108        const void* keys[4] = { SessionHistoryEntryURLKey(), SessionHistoryEntryTitleKey(), SessionHistoryEntryOriginalURLKey(), SessionHistoryEntryDataKey() };
109        const void* values[4] = { url.get(), title.get(), originalURL.get(), entryData.get() };
110
111        RetainPtr<CFDictionaryRef> entryDictionary = adoptCF(CFDictionaryCreate(0, keys, values, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
112        CFArrayAppendValue(entries.get(), entryDictionary.get());
113    }
114
115    ASSERT(currentIndex == -1 || (currentIndex > -1 && currentIndex < CFArrayGetCount(entries.get())));
116    if (currentIndex < -1 || currentIndex >= CFArrayGetCount(entries.get())) {
117        LOG(SessionState, "Filtering entries to be saved resulted in an inconsistent state that we cannot represent");
118        return 0;
119    }
120
121    // If we have an index and all items before and including the current item were filtered then currentIndex will be -1.
122    // In this case the new current index should point at the first item.
123    // It's also possible that all items were filtered so we should represent not having a current index.
124    if (currentIndex == -1) {
125        if (CFArrayGetCount(entries.get()))
126            currentIndex = 0;
127        else
128            hasCurrentIndex = false;
129    }
130
131    if (hasCurrentIndex) {
132        RetainPtr<CFNumberRef> currentIndexNumber = adoptCF(CFNumberCreate(0, kCFNumberCFIndexType, &currentIndex));
133        const void* keys[3] = { SessionHistoryVersionKey(), SessionHistoryCurrentIndexKey(), SessionHistoryEntriesKey() };
134        const void* values[3] = { SessionHistoryCurrentVersion(), currentIndexNumber.get(), entries.get() };
135
136        return CFDictionaryCreate(0, keys, values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
137    }
138
139    // We represent having no current index by writing out an empty dictionary (besides the version).
140    return createEmptySessionHistoryDictionary();
141}
142
143bool WebBackForwardList::restoreFromCFDictionaryRepresentation(CFDictionaryRef dictionary)
144{
145    CFNumberRef cfVersion = (CFNumberRef)CFDictionaryGetValue(dictionary, SessionHistoryVersionKey());
146    if (!cfVersion) {
147        // v0 session history dictionaries did not contain versioning
148        return restoreFromV0CFDictionaryRepresentation(dictionary);
149    }
150
151    if (CFGetTypeID(cfVersion) != CFNumberGetTypeID()) {
152        LOG(SessionState, "WebBackForwardList dictionary representation contains a version that is not a number");
153        return false;
154    }
155
156    CFIndex version;
157    if (!CFNumberGetValue(cfVersion, kCFNumberCFIndexType, &version)) {
158        LOG(SessionState, "WebBackForwardList dictionary representation does not have a correctly typed current version");
159        return false;
160    }
161
162    if (version == 1)
163        return restoreFromV1CFDictionaryRepresentation(dictionary);
164
165    LOG(SessionState, "WebBackForwardList dictionary representation has an invalid current version (%ld)", version);
166    return false;
167}
168
169bool WebBackForwardList::restoreFromV0CFDictionaryRepresentation(CFDictionaryRef dictionary)
170{
171    CFNumberRef cfIndex = (CFNumberRef)CFDictionaryGetValue(dictionary, SessionHistoryCurrentIndexKey());
172    if (!cfIndex || CFGetTypeID(cfIndex) != CFNumberGetTypeID()) {
173        LOG(SessionState, "WebBackForwardList dictionary representation does not have a valid current index");
174        return false;
175    }
176
177    CFIndex currentCFIndex;
178    if (!CFNumberGetValue(cfIndex, kCFNumberCFIndexType, &currentCFIndex)) {
179        LOG(SessionState, "WebBackForwardList dictionary representation does not have a correctly typed current index");
180        return false;
181    }
182
183    if (currentCFIndex < -1) {
184        LOG(SessionState, "WebBackForwardList dictionary representation contains an unexpected negative current index (%ld)", currentCFIndex);
185        return false;
186    }
187
188    CFArrayRef cfEntries = (CFArrayRef)CFDictionaryGetValue(dictionary, SessionHistoryEntriesKey());
189    if (!cfEntries || CFGetTypeID(cfEntries) != CFArrayGetTypeID()) {
190        LOG(SessionState, "WebBackForwardList dictionary representation does not have a valid list of entries");
191        return false;
192    }
193
194    CFIndex size = CFArrayGetCount(cfEntries);
195    if (size < 0 || currentCFIndex >= size) {
196        LOG(SessionState, "WebBackForwardList dictionary representation contains an invalid current index (%ld) for the number of entries (%ld)", currentCFIndex, size);
197        return false;
198    }
199
200    // Version 0 session history relied on currentIndex == -1 to represent the same thing as not having a current index.
201    bool hasCurrentIndex = currentCFIndex != -1;
202
203    if (!hasCurrentIndex && size) {
204        LOG(SessionState, "WebBackForwardList dictionary representation says there is no current index, but there is a list of %ld entries", size);
205        return false;
206    }
207
208    BackForwardListItemVector entries;
209    if (!extractBackForwardListEntriesFromArray(cfEntries, entries)) {
210        // extractBackForwardListEntriesFromArray has already logged the appropriate error message.
211        return false;
212    }
213
214    ASSERT(entries.size() == static_cast<unsigned>(size));
215
216    m_hasCurrentIndex = hasCurrentIndex;
217    m_currentIndex = m_hasCurrentIndex ? static_cast<uint32_t>(currentCFIndex) : 0;
218    m_entries = entries;
219
220    return true;
221}
222
223bool WebBackForwardList::restoreFromV1CFDictionaryRepresentation(CFDictionaryRef dictionary)
224{
225    CFNumberRef cfIndex = (CFNumberRef)CFDictionaryGetValue(dictionary, SessionHistoryCurrentIndexKey());
226    if (!cfIndex) {
227        // No current index means the dictionary represents an empty session.
228        m_hasCurrentIndex = false;
229        m_currentIndex = 0;
230        m_entries.clear();
231
232        return true;
233    }
234
235    if (CFGetTypeID(cfIndex) != CFNumberGetTypeID()) {
236        LOG(SessionState, "WebBackForwardList dictionary representation does not have a valid current index");
237        return false;
238    }
239
240    CFIndex currentCFIndex;
241    if (!CFNumberGetValue(cfIndex, kCFNumberCFIndexType, &currentCFIndex)) {
242        LOG(SessionState, "WebBackForwardList dictionary representation does not have a correctly typed current index");
243        return false;
244    }
245
246    if (currentCFIndex < 0) {
247        LOG(SessionState, "WebBackForwardList dictionary representation contains an unexpected negative current index (%ld)", currentCFIndex);
248        return false;
249    }
250
251    CFArrayRef cfEntries = (CFArrayRef)CFDictionaryGetValue(dictionary, SessionHistoryEntriesKey());
252    if (!cfEntries || CFGetTypeID(cfEntries) != CFArrayGetTypeID()) {
253        LOG(SessionState, "WebBackForwardList dictionary representation does not have a valid list of entries");
254        return false;
255    }
256
257    CFIndex size = CFArrayGetCount(cfEntries);
258    if (currentCFIndex >= size) {
259        LOG(SessionState, "WebBackForwardList dictionary representation contains an invalid current index (%ld) for the number of entries (%ld)", currentCFIndex, size);
260        return false;
261    }
262
263    BackForwardListItemVector entries;
264    if (!extractBackForwardListEntriesFromArray(cfEntries, entries)) {
265        // extractBackForwardListEntriesFromArray has already logged the appropriate error message.
266        return false;
267    }
268
269    ASSERT(entries.size() == static_cast<unsigned>(size));
270
271    m_hasCurrentIndex = true;
272    m_currentIndex = static_cast<uint32_t>(currentCFIndex);
273    m_entries = entries;
274
275    return true;
276}
277
278static bool extractBackForwardListEntriesFromArray(CFArrayRef cfEntries, BackForwardListItemVector& entries)
279{
280    CFIndex size = CFArrayGetCount(cfEntries);
281
282    entries.reserveCapacity(size);
283    for (CFIndex i = 0; i < size; ++i) {
284        CFDictionaryRef entryDictionary = (CFDictionaryRef)CFArrayGetValueAtIndex(cfEntries, i);
285        if (!entryDictionary || CFGetTypeID(entryDictionary) != CFDictionaryGetTypeID()) {
286            LOG(SessionState, "WebBackForwardList entry array does not have a valid entry at index %i", (int)i);
287            return false;
288        }
289
290        CFStringRef entryURL = (CFStringRef)CFDictionaryGetValue(entryDictionary, SessionHistoryEntryURLKey());
291        if (!entryURL || CFGetTypeID(entryURL) != CFStringGetTypeID()) {
292            LOG(SessionState, "WebBackForwardList entry at index %i does not have a valid URL", (int)i);
293            return false;
294        }
295
296        CFStringRef entryTitle = (CFStringRef)CFDictionaryGetValue(entryDictionary, SessionHistoryEntryTitleKey());
297        if (!entryTitle || CFGetTypeID(entryTitle) != CFStringGetTypeID()) {
298            LOG(SessionState, "WebBackForwardList entry at index %i does not have a valid title", (int)i);
299            return false;
300        }
301
302        CFStringRef originalURL = (CFStringRef)CFDictionaryGetValue(entryDictionary, SessionHistoryEntryOriginalURLKey());
303        if (!originalURL || CFGetTypeID(originalURL) != CFStringGetTypeID()) {
304            LOG(SessionState, "WebBackForwardList entry at index %i does not have a valid original URL", (int)i);
305            return false;
306        }
307
308        CFDataRef backForwardData = (CFDataRef)CFDictionaryGetValue(entryDictionary, SessionHistoryEntryDataKey());
309        if (!backForwardData || CFGetTypeID(backForwardData) != CFDataGetTypeID()) {
310            LOG(SessionState, "WebBackForwardList entry at index %i does not have back/forward data", (int)i);
311            return false;
312        }
313
314        entries.append(WebBackForwardListItem::create(originalURL, entryURL, entryTitle, CFDataGetBytePtr(backForwardData), CFDataGetLength(backForwardData), generateWebBackForwardItemID()));
315    }
316
317    return true;
318}
319
320} // namespace WebKit
321