1/*
2 * Copyright (C) 2007-2018 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. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "History.h"
28
29#include "BackForwardController.h"
30#include "Document.h"
31#include "Frame.h"
32#include "FrameLoader.h"
33#include "FrameLoaderClient.h"
34#include "HistoryController.h"
35#include "HistoryItem.h"
36#include "Logging.h"
37#include "NavigationScheduler.h"
38#include "Page.h"
39#include "ScriptController.h"
40#include "SecurityOrigin.h"
41#include <wtf/CheckedArithmetic.h>
42#include <wtf/IsoMallocInlines.h>
43#include <wtf/MainThread.h>
44#include <wtf/text/StringConcatenateNumbers.h>
45
46namespace WebCore {
47
48WTF_MAKE_ISO_ALLOCATED_IMPL(History);
49
50History::History(DOMWindow& window)
51 : DOMWindowProperty(&window)
52{
53}
54
55unsigned History::length() const
56{
57 auto* frame = this->frame();
58 if (!frame)
59 return 0;
60 auto* page = frame->page();
61 if (!page)
62 return 0;
63 return page->backForward().count();
64}
65
66ExceptionOr<History::ScrollRestoration> History::scrollRestoration() const
67{
68 auto* frame = this->frame();
69 if (!frame)
70 return Exception { SecurityError };
71
72 auto* historyItem = frame->loader().history().currentItem();
73 if (!historyItem)
74 return ScrollRestoration::Auto;
75
76 return historyItem->shouldRestoreScrollPosition() ? ScrollRestoration::Auto : ScrollRestoration::Manual;
77}
78
79ExceptionOr<void> History::setScrollRestoration(ScrollRestoration scrollRestoration)
80{
81 auto* frame = this->frame();
82 if (!frame)
83 return Exception { SecurityError };
84
85 auto* historyItem = frame->loader().history().currentItem();
86 if (historyItem)
87 historyItem->setShouldRestoreScrollPosition(scrollRestoration == ScrollRestoration::Auto);
88
89 return { };
90}
91
92SerializedScriptValue* History::state()
93{
94 m_lastStateObjectRequested = stateInternal();
95 return m_lastStateObjectRequested.get();
96}
97
98SerializedScriptValue* History::stateInternal() const
99{
100 auto* frame = this->frame();
101 if (!frame)
102 return nullptr;
103 auto* historyItem = frame->loader().history().currentItem();
104 if (!historyItem)
105 return nullptr;
106 return historyItem->stateObject();
107}
108
109bool History::stateChanged() const
110{
111 return m_lastStateObjectRequested != stateInternal();
112}
113
114JSValueInWrappedObject& History::cachedState()
115{
116 if (m_cachedState && stateChanged())
117 m_cachedState = { };
118 return m_cachedState;
119}
120
121bool History::isSameAsCurrentState(SerializedScriptValue* state) const
122{
123 return state == stateInternal();
124}
125
126void History::back()
127{
128 go(-1);
129}
130
131void History::back(Document& document)
132{
133 go(document, -1);
134}
135
136void History::forward()
137{
138 go(1);
139}
140
141void History::forward(Document& document)
142{
143 go(document, 1);
144}
145
146void History::go(int distance)
147{
148 auto* frame = this->frame();
149 LOG(History, "History %p go(%d) frame %p (main frame %d)", this, distance, frame, frame ? frame->isMainFrame() : false);
150
151 if (!frame)
152 return;
153
154 frame->navigationScheduler().scheduleHistoryNavigation(distance);
155}
156
157void History::go(Document& document, int distance)
158{
159 auto* frame = this->frame();
160 LOG(History, "History %p go(%d) in document %p frame %p (main frame %d)", this, distance, &document, frame, frame ? frame->isMainFrame() : false);
161
162 if (!frame)
163 return;
164
165 ASSERT(isMainThread());
166
167 if (!document.canNavigate(frame))
168 return;
169
170 frame->navigationScheduler().scheduleHistoryNavigation(distance);
171}
172
173URL History::urlForState(const String& urlString)
174{
175 auto* frame = this->frame();
176 if (urlString.isNull())
177 return frame->document()->url();
178 return frame->document()->completeURL(urlString);
179}
180
181ExceptionOr<void> History::stateObjectAdded(RefPtr<SerializedScriptValue>&& data, const String& title, const String& urlString, StateObjectType stateObjectType)
182{
183 m_cachedState = { };
184
185 // Each unique main-frame document is only allowed to send 64MB of state object payload to the UI client/process.
186 static uint32_t totalStateObjectPayloadLimit = 0x4000000;
187 static Seconds stateObjectTimeSpan { 30_s };
188 static unsigned perStateObjectTimeSpanLimit = 100;
189
190 auto* frame = this->frame();
191 if (!frame || !frame->page())
192 return { };
193
194 URL fullURL = urlForState(urlString);
195 if (!fullURL.isValid())
196 return Exception { SecurityError };
197
198 const URL& documentURL = frame->document()->url();
199
200 auto createBlockedURLSecurityErrorWithMessageSuffix = [&] (const char* suffix) {
201 const char* functionName = stateObjectType == StateObjectType::Replace ? "history.replaceState()" : "history.pushState()";
202 return Exception { SecurityError, makeString("Blocked attempt to use ", functionName, " to change session history URL from ", documentURL.stringCenterEllipsizedToLength(), " to ", fullURL.stringCenterEllipsizedToLength(), ". ", suffix) };
203 };
204 if (!protocolHostAndPortAreEqual(fullURL, documentURL) || fullURL.user() != documentURL.user() || fullURL.pass() != documentURL.pass())
205 return createBlockedURLSecurityErrorWithMessageSuffix("Protocols, domains, ports, usernames, and passwords must match.");
206
207 const auto& documentSecurityOrigin = frame->document()->securityOrigin();
208 // We allow sandboxed documents, 'data:'/'file:' URLs, etc. to use 'pushState'/'replaceState' to modify the URL query and fragments.
209 // See https://bugs.webkit.org/show_bug.cgi?id=183028 for the compatibility concerns.
210 bool allowSandboxException = (documentSecurityOrigin.isLocal() || documentSecurityOrigin.isUnique()) && equalIgnoringQueryAndFragment(documentURL, fullURL);
211
212 if (!allowSandboxException && !documentSecurityOrigin.canRequest(fullURL) && (fullURL.path() != documentURL.path() || fullURL.query() != documentURL.query()))
213 return createBlockedURLSecurityErrorWithMessageSuffix("Paths and fragments must match for a sandboxed document.");
214
215 auto* mainWindow = frame->page()->mainFrame().window();
216 if (!mainWindow)
217 return { };
218
219 auto& mainHistory = mainWindow->history();
220
221 WallTime currentTimestamp = WallTime::now();
222 if (currentTimestamp - mainHistory.m_currentStateObjectTimeSpanStart > stateObjectTimeSpan) {
223 mainHistory.m_currentStateObjectTimeSpanStart = currentTimestamp;
224 mainHistory.m_currentStateObjectTimeSpanObjectsAdded = 0;
225 }
226
227 if (mainHistory.m_currentStateObjectTimeSpanObjectsAdded >= perStateObjectTimeSpanLimit) {
228 if (stateObjectType == StateObjectType::Replace)
229 return Exception { SecurityError, makeString("Attempt to use history.replaceState() more than ", perStateObjectTimeSpanLimit, " times per ", stateObjectTimeSpan.seconds(), " seconds") };
230 return Exception { SecurityError, makeString("Attempt to use history.pushState() more than ", perStateObjectTimeSpanLimit, " times per ", stateObjectTimeSpan.seconds(), " seconds") };
231 }
232
233 Checked<unsigned> titleSize = title.length();
234 titleSize *= 2;
235
236 Checked<unsigned> urlSize = fullURL.string().length();
237 urlSize *= 2;
238
239 Checked<uint64_t> payloadSize = titleSize;
240 payloadSize += urlSize;
241 payloadSize += data ? data->data().size() : 0;
242
243 Checked<uint64_t> newTotalUsage = mainHistory.m_totalStateObjectUsage;
244
245 if (stateObjectType == StateObjectType::Replace)
246 newTotalUsage -= m_mostRecentStateObjectUsage;
247 newTotalUsage += payloadSize;
248
249 if (newTotalUsage > totalStateObjectPayloadLimit) {
250 if (stateObjectType == StateObjectType::Replace)
251 return Exception { QuotaExceededError, "Attempt to store more data than allowed using history.replaceState()"_s };
252 return Exception { QuotaExceededError, "Attempt to store more data than allowed using history.pushState()"_s };
253 }
254
255 m_mostRecentStateObjectUsage = payloadSize.unsafeGet();
256
257 mainHistory.m_totalStateObjectUsage = newTotalUsage.unsafeGet();
258 ++mainHistory.m_currentStateObjectTimeSpanObjectsAdded;
259
260 if (!urlString.isEmpty())
261 frame->document()->updateURLForPushOrReplaceState(fullURL);
262
263 if (stateObjectType == StateObjectType::Push) {
264 frame->loader().history().pushState(WTFMove(data), title, fullURL.string());
265 frame->loader().client().dispatchDidPushStateWithinPage();
266 } else if (stateObjectType == StateObjectType::Replace) {
267 frame->loader().history().replaceState(WTFMove(data), title, fullURL.string());
268 frame->loader().client().dispatchDidReplaceStateWithinPage();
269 }
270
271 return { };
272}
273
274} // namespace WebCore
275