1 /* 2 * Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) 4 * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/) 5 * Copyright (C) 2009 Adam Barth. All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 17 * its contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 24 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 29 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 #include "config.h" 33 #include "NavigationScheduler.h" 34 35 #include "BackForwardController.h" 36 #include "DOMWindow.h" 37 #include "DocumentLoader.h" 38 #include "Event.h" 39 #include "FormState.h" 40 #include "FormSubmission.h" 41 #include "Frame.h" 42 #include "FrameLoadRequest.h" 43 #include "FrameLoader.h" 44 #include "FrameLoaderStateMachine.h" 45 #include "HTMLFormElement.h" 46 #include "HTMLFrameOwnerElement.h" 47 #include "HistoryItem.h" 48 #include "Page.h" 49 #include "UserGestureIndicator.h" 50 #include <wtf/CurrentTime.h> 51 52 namespace WebCore { 53 54 unsigned NavigationDisablerForBeforeUnload::s_navigationDisableCount = 0; 55 56 class ScheduledNavigation { 57 WTF_MAKE_NONCOPYABLE(ScheduledNavigation); WTF_MAKE_FAST_ALLOCATED; 58 public: 59 ScheduledNavigation(double delay, bool lockHistory, bool lockBackForwardList, bool wasDuringLoad, bool isLocationChange) 60 : m_delay(delay) 61 , m_lockHistory(lockHistory) 62 , m_lockBackForwardList(lockBackForwardList) 63 , m_wasDuringLoad(wasDuringLoad) 64 , m_isLocationChange(isLocationChange) 65 , m_wasUserGesture(ScriptController::processingUserGesture()) 66 { 67 } 68 virtual ~ScheduledNavigation() { } 69 70 virtual void fire(Frame*) = 0; 71 72 virtual bool shouldStartTimer(Frame*) { return true; } 73 virtual void didStartTimer(Frame*, Timer<NavigationScheduler>*) { } 74 virtual void didStopTimer(Frame*, bool /* newLoadInProgress */) { } 75 76 double delay() const { return m_delay; } 77 bool lockHistory() const { return m_lockHistory; } 78 bool lockBackForwardList() const { return m_lockBackForwardList; } 79 bool wasDuringLoad() const { return m_wasDuringLoad; } 80 bool isLocationChange() const { return m_isLocationChange; } 81 bool wasUserGesture() const { return m_wasUserGesture; } 82 83 protected: 84 void clearUserGesture() { m_wasUserGesture = false; } 85 86 private: 87 double m_delay; 88 bool m_lockHistory; 89 bool m_lockBackForwardList; 90 bool m_wasDuringLoad; 91 bool m_isLocationChange; 92 bool m_wasUserGesture; 93 }; 94 95 class ScheduledURLNavigation : public ScheduledNavigation { 96 protected: 97 ScheduledURLNavigation(double delay, PassRefPtr<SecurityOrigin> securityOrigin, const String& url, const String& referrer, bool lockHistory, bool lockBackForwardList, bool duringLoad, bool isLocationChange) 98 : ScheduledNavigation(delay, lockHistory, lockBackForwardList, duringLoad, isLocationChange) 99 , m_securityOrigin(securityOrigin) 100 , m_url(url) 101 , m_referrer(referrer) 102 , m_haveToldClient(false) 103 { 104 } 105 106 virtual void fire(Frame* frame) 107 { 108 UserGestureIndicator gestureIndicator(wasUserGesture() ? DefinitelyProcessingUserGesture : DefinitelyNotProcessingUserGesture); 109 frame->loader()->changeLocation(m_securityOrigin, KURL(ParsedURLString, m_url), m_referrer, lockHistory(), lockBackForwardList(), false); 110 } 111 112 virtual void didStartTimer(Frame* frame, Timer<NavigationScheduler>* timer) 113 { 114 if (m_haveToldClient) 115 return; 116 m_haveToldClient = true; 117 frame->loader()->clientRedirected(KURL(ParsedURLString, m_url), delay(), currentTime() + timer->nextFireInterval(), lockBackForwardList()); 118 } 119 120 virtual void didStopTimer(Frame* frame, bool newLoadInProgress) 121 { 122 if (!m_haveToldClient) 123 return; 124 frame->loader()->clientRedirectCancelledOrFinished(newLoadInProgress); 125 } 126 127 SecurityOrigin* securityOrigin() const { return m_securityOrigin.get(); } 128 String url() const { return m_url; } 129 String referrer() const { return m_referrer; } 130 131 private: 132 RefPtr<SecurityOrigin> m_securityOrigin; 133 String m_url; 134 String m_referrer; 135 bool m_haveToldClient; 136 }; 137 138 class ScheduledRedirect : public ScheduledURLNavigation { 139 public: 140 ScheduledRedirect(double delay, PassRefPtr<SecurityOrigin> securityOrigin, const String& url, bool lockHistory, bool lockBackForwardList) 141 : ScheduledURLNavigation(delay, securityOrigin, url, String(), lockHistory, lockBackForwardList, false, false) 142 { 143 clearUserGesture(); 144 } 145 146 virtual bool shouldStartTimer(Frame* frame) { return frame->loader()->allAncestorsAreComplete(); } 147 }; 148 149 class ScheduledLocationChange : public ScheduledURLNavigation { 150 public: 151 ScheduledLocationChange(PassRefPtr<SecurityOrigin> securityOrigin, const String& url, const String& referrer, bool lockHistory, bool lockBackForwardList, bool duringLoad) 152 : ScheduledURLNavigation(0.0, securityOrigin, url, referrer, lockHistory, lockBackForwardList, duringLoad, true) { } 153 }; 154 155 class ScheduledRefresh : public ScheduledURLNavigation { 156 public: 157 ScheduledRefresh(PassRefPtr<SecurityOrigin> securityOrigin, const String& url, const String& referrer) 158 : ScheduledURLNavigation(0.0, securityOrigin, url, referrer, true, true, false, true) 159 { 160 } 161 162 virtual void fire(Frame* frame) 163 { 164 UserGestureIndicator gestureIndicator(wasUserGesture() ? DefinitelyProcessingUserGesture : DefinitelyNotProcessingUserGesture); 165 frame->loader()->changeLocation(securityOrigin(), KURL(ParsedURLString, url()), referrer(), lockHistory(), lockBackForwardList(), true); 166 } 167 }; 168 169 class ScheduledHistoryNavigation : public ScheduledNavigation { 170 public: 171 explicit ScheduledHistoryNavigation(int historySteps) 172 : ScheduledNavigation(0, false, false, false, true) 173 , m_historySteps(historySteps) 174 { 175 } 176 177 virtual void fire(Frame* frame) 178 { 179 UserGestureIndicator gestureIndicator(wasUserGesture() ? DefinitelyProcessingUserGesture : DefinitelyNotProcessingUserGesture); 180 181 if (!m_historySteps) { 182 // Special case for go(0) from a frame -> reload only the frame 183 // To follow Firefox and IE's behavior, history reload can only navigate the self frame. 184 frame->loader()->urlSelected(frame->document()->url(), "_self", 0, lockHistory(), lockBackForwardList(), SendReferrer); 185 return; 186 } 187 // go(i!=0) from a frame navigates into the history of the frame only, 188 // in both IE and NS (but not in Mozilla). We can't easily do that. 189 frame->page()->backForward()->goBackOrForward(m_historySteps); 190 } 191 192 private: 193 int m_historySteps; 194 }; 195 196 class ScheduledFormSubmission : public ScheduledNavigation { 197 public: 198 ScheduledFormSubmission(PassRefPtr<FormSubmission> submission, bool lockBackForwardList, bool duringLoad) 199 : ScheduledNavigation(0, submission->lockHistory(), lockBackForwardList, duringLoad, true) 200 , m_submission(submission) 201 , m_haveToldClient(false) 202 { 203 ASSERT(m_submission->state()); 204 } 205 206 virtual void fire(Frame* frame) 207 { 208 UserGestureIndicator gestureIndicator(wasUserGesture() ? DefinitelyProcessingUserGesture : DefinitelyNotProcessingUserGesture); 209 210 // The submitForm function will find a target frame before using the redirection timer. 211 // Now that the timer has fired, we need to repeat the security check which normally is done when 212 // selecting a target, in case conditions have changed. Other code paths avoid this by targeting 213 // without leaving a time window. If we fail the check just silently drop the form submission. 214 Frame* requestingFrame = m_submission->state()->sourceFrame(); 215 if (!requestingFrame->loader()->shouldAllowNavigation(frame)) 216 return; 217 FrameLoadRequest frameRequest(requestingFrame->document()->securityOrigin()); 218 m_submission->populateFrameLoadRequest(frameRequest); 219 frame->loader()->loadFrameRequest(frameRequest, lockHistory(), lockBackForwardList(), m_submission->event(), m_submission->state(), SendReferrer); 220 } 221 222 virtual void didStartTimer(Frame* frame, Timer<NavigationScheduler>* timer) 223 { 224 if (m_haveToldClient) 225 return; 226 m_haveToldClient = true; 227 frame->loader()->clientRedirected(m_submission->requestURL(), delay(), currentTime() + timer->nextFireInterval(), lockBackForwardList()); 228 } 229 230 virtual void didStopTimer(Frame* frame, bool newLoadInProgress) 231 { 232 if (!m_haveToldClient) 233 return; 234 frame->loader()->clientRedirectCancelledOrFinished(newLoadInProgress); 235 } 236 237 private: 238 RefPtr<FormSubmission> m_submission; 239 bool m_haveToldClient; 240 }; 241 242 NavigationScheduler::NavigationScheduler(Frame* frame) 243 : m_frame(frame) 244 , m_timer(this, &NavigationScheduler::timerFired) 245 { 246 } 247 248 NavigationScheduler::~NavigationScheduler() 249 { 250 } 251 252 bool NavigationScheduler::redirectScheduledDuringLoad() 253 { 254 return m_redirect && m_redirect->wasDuringLoad(); 255 } 256 257 bool NavigationScheduler::locationChangePending() 258 { 259 return m_redirect && m_redirect->isLocationChange(); 260 } 261 262 void NavigationScheduler::clear() 263 { 264 m_timer.stop(); 265 m_redirect.clear(); 266 } 267 268 inline bool NavigationScheduler::shouldScheduleNavigation() const 269 { 270 return m_frame->page(); 271 } 272 273 inline bool NavigationScheduler::shouldScheduleNavigation(const String& url) const 274 { 275 return shouldScheduleNavigation() && (protocolIsJavaScript(url) || NavigationDisablerForBeforeUnload::isNavigationAllowed()); 276 } 277 278 void NavigationScheduler::scheduleRedirect(double delay, const String& url) 279 { 280 if (!shouldScheduleNavigation(url)) 281 return; 282 if (delay < 0 || delay > INT_MAX / 1000) 283 return; 284 if (url.isEmpty()) 285 return; 286 287 // We want a new back/forward list item if the refresh timeout is > 1 second. 288 if (!m_redirect || delay <= m_redirect->delay()) 289 schedule(adoptPtr(new ScheduledRedirect(delay, m_frame->document()->securityOrigin(), url, true, delay <= 1))); 290 } 291 292 bool NavigationScheduler::mustLockBackForwardList(Frame* targetFrame) 293 { 294 // Non-user navigation before the page has finished firing onload should not create a new back/forward item. 295 // See https://webkit.org/b/42861 for the original motivation for this. 296 if (!ScriptController::processingUserGesture() && targetFrame->loader()->documentLoader() && !targetFrame->loader()->documentLoader()->wasOnloadHandled()) 297 return true; 298 299 // Navigation of a subframe during loading of an ancestor frame does not create a new back/forward item. 300 // The definition of "during load" is any time before all handlers for the load event have been run. 301 // See https://bugs.webkit.org/show_bug.cgi?id=14957 for the original motivation for this. 302 for (Frame* ancestor = targetFrame->tree()->parent(); ancestor; ancestor = ancestor->tree()->parent()) { 303 Document* document = ancestor->document(); 304 if (!ancestor->loader()->isComplete() || (document && document->processingLoadEvent())) 305 return true; 306 } 307 return false; 308 } 309 310 void NavigationScheduler::scheduleLocationChange(PassRefPtr<SecurityOrigin> securityOrigin, const String& url, const String& referrer, bool lockHistory, bool lockBackForwardList) 311 { 312 if (!shouldScheduleNavigation(url)) 313 return; 314 if (url.isEmpty()) 315 return; 316 317 lockBackForwardList = lockBackForwardList || mustLockBackForwardList(m_frame); 318 319 FrameLoader* loader = m_frame->loader(); 320 321 // If the URL we're going to navigate to is the same as the current one, except for the 322 // fragment part, we don't need to schedule the location change. 323 KURL parsedURL(ParsedURLString, url); 324 if (parsedURL.hasFragmentIdentifier() && equalIgnoringFragmentIdentifier(m_frame->document()->url(), parsedURL)) { 325 loader->changeLocation(securityOrigin, loader->completeURL(url), referrer, lockHistory, lockBackForwardList); 326 return; 327 } 328 329 // Handle a location change of a page with no document as a special case. 330 // This may happen when a frame changes the location of another frame. 331 bool duringLoad = !loader->stateMachine()->committedFirstRealDocumentLoad(); 332 333 schedule(adoptPtr(new ScheduledLocationChange(securityOrigin, url, referrer, lockHistory, lockBackForwardList, duringLoad))); 334 } 335 336 void NavigationScheduler::scheduleFormSubmission(PassRefPtr<FormSubmission> submission) 337 { 338 ASSERT(m_frame->page()); 339 340 // FIXME: Do we need special handling for form submissions where the URL is the same 341 // as the current one except for the fragment part? See scheduleLocationChange above. 342 343 // Handle a location change of a page with no document as a special case. 344 // This may happen when a frame changes the location of another frame. 345 bool duringLoad = !m_frame->loader()->stateMachine()->committedFirstRealDocumentLoad(); 346 347 // If this is a child frame and the form submission was triggered by a script, lock the back/forward list 348 // to match IE and Opera. 349 // See https://bugs.webkit.org/show_bug.cgi?id=32383 for the original motivation for this. 350 bool lockBackForwardList = mustLockBackForwardList(m_frame) 351 || (submission->state()->formSubmissionTrigger() == SubmittedByJavaScript 352 && m_frame->tree()->parent() && !ScriptController::processingUserGesture()); 353 354 schedule(adoptPtr(new ScheduledFormSubmission(submission, lockBackForwardList, duringLoad))); 355 } 356 357 void NavigationScheduler::scheduleRefresh() 358 { 359 if (!shouldScheduleNavigation()) 360 return; 361 const KURL& url = m_frame->document()->url(); 362 if (url.isEmpty()) 363 return; 364 365 schedule(adoptPtr(new ScheduledRefresh(m_frame->document()->securityOrigin(), url.string(), m_frame->loader()->outgoingReferrer()))); 366 } 367 368 void NavigationScheduler::scheduleHistoryNavigation(int steps) 369 { 370 if (!shouldScheduleNavigation()) 371 return; 372 373 // Invalid history navigations (such as history.forward() during a new load) have the side effect of cancelling any scheduled 374 // redirects. We also avoid the possibility of cancelling the current load by avoiding the scheduled redirection altogether. 375 BackForwardController* backForward = m_frame->page()->backForward(); 376 if (steps > backForward->forwardCount() || -steps > backForward->backCount()) { 377 cancel(); 378 return; 379 } 380 381 // In all other cases, schedule the history traversal to occur asynchronously. 382 schedule(adoptPtr(new ScheduledHistoryNavigation(steps))); 383 } 384 385 void NavigationScheduler::timerFired(Timer<NavigationScheduler>*) 386 { 387 if (!m_frame->page()) 388 return; 389 if (m_frame->page()->defersLoading()) 390 return; 391 392 OwnPtr<ScheduledNavigation> redirect(m_redirect.release()); 393 redirect->fire(m_frame); 394 } 395 396 void NavigationScheduler::schedule(PassOwnPtr<ScheduledNavigation> redirect) 397 { 398 ASSERT(m_frame->page()); 399 400 // If a redirect was scheduled during a load, then stop the current load. 401 // Otherwise when the current load transitions from a provisional to a 402 // committed state, pending redirects may be cancelled. 403 if (redirect->wasDuringLoad()) { 404 if (DocumentLoader* provisionalDocumentLoader = m_frame->loader()->provisionalDocumentLoader()) 405 provisionalDocumentLoader->stopLoading(); 406 m_frame->loader()->stopLoading(UnloadEventPolicyUnloadAndPageHide); 407 } 408 409 cancel(); 410 m_redirect = redirect; 411 412 if (!m_frame->loader()->isComplete() && m_redirect->isLocationChange()) 413 m_frame->loader()->completed(); 414 415 startTimer(); 416 } 417 418 void NavigationScheduler::startTimer() 419 { 420 if (!m_redirect) 421 return; 422 423 ASSERT(m_frame->page()); 424 if (m_timer.isActive()) 425 return; 426 if (!m_redirect->shouldStartTimer(m_frame)) 427 return; 428 429 m_timer.startOneShot(m_redirect->delay()); 430 m_redirect->didStartTimer(m_frame, &m_timer); 431 } 432 433 void NavigationScheduler::cancel(bool newLoadInProgress) 434 { 435 m_timer.stop(); 436 437 OwnPtr<ScheduledNavigation> redirect(m_redirect.release()); 438 if (redirect) 439 redirect->didStopTimer(m_frame, newLoadInProgress); 440 } 441 442 } // namespace WebCore 443