1/*
2 * Copyright (C) 2007 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 COMPUTER, 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 COMPUTER, 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 "PageCache.h"
28
29#include "ApplicationCacheHost.h"
30#include "BackForwardController.h"
31#include "MemoryCache.h"
32#include "CachedPage.h"
33#include "DOMWindow.h"
34#include "DeviceMotionController.h"
35#include "DeviceOrientationController.h"
36#include "Document.h"
37#include "DocumentLoader.h"
38#include "Frame.h"
39#include "FrameLoader.h"
40#include "FrameLoaderClient.h"
41#include "FrameLoaderStateMachine.h"
42#include "HistoryItem.h"
43#include "Logging.h"
44#include "Page.h"
45#include "Settings.h"
46#include "SharedWorkerRepository.h"
47#include "SystemTime.h"
48#include <wtf/CurrentTime.h>
49#include <wtf/text/CString.h>
50#include <wtf/text/StringConcatenate.h>
51
52using namespace std;
53
54namespace WebCore {
55
56static const double autoreleaseInterval = 3;
57
58#ifndef NDEBUG
59
60static String& pageCacheLogPrefix(int indentLevel)
61{
62    static int previousIndent = -1;
63    DEFINE_STATIC_LOCAL(String, prefix, ());
64
65    if (indentLevel != previousIndent) {
66        previousIndent = indentLevel;
67        prefix.truncate(0);
68        for (int i = 0; i < previousIndent; ++i)
69            prefix += "    ";
70    }
71
72    return prefix;
73}
74
75static void pageCacheLog(const String& prefix, const String& message)
76{
77    LOG(PageCache, "%s%s", prefix.utf8().data(), message.utf8().data());
78}
79
80#define PCLOG(...) pageCacheLog(pageCacheLogPrefix(indentLevel), makeString(__VA_ARGS__))
81
82static bool logCanCacheFrameDecision(Frame* frame, int indentLevel)
83{
84    // Only bother logging for frames that have actually loaded and have content.
85    if (frame->loader()->stateMachine()->creatingInitialEmptyDocument())
86        return false;
87    KURL currentURL = frame->loader()->documentLoader() ? frame->loader()->documentLoader()->url() : KURL();
88    if (currentURL.isEmpty())
89        return false;
90
91    PCLOG("+---");
92    KURL newURL = frame->loader()->provisionalDocumentLoader() ? frame->loader()->provisionalDocumentLoader()->url() : KURL();
93    if (!newURL.isEmpty())
94        PCLOG(" Determining if frame can be cached navigating from (", currentURL.string(), ") to (", newURL.string(), "):");
95    else
96        PCLOG(" Determining if subframe with URL (", currentURL.string(), ") can be cached:");
97
98    bool cannotCache = false;
99
100    do {
101        if (!frame->loader()->documentLoader()) {
102            PCLOG("   -There is no DocumentLoader object");
103            cannotCache = true;
104            break;
105        }
106        if (!frame->loader()->documentLoader()->mainDocumentError().isNull()) {
107            PCLOG("   -Main document has an error");
108            cannotCache = true;
109        }
110        if (frame->loader()->subframeLoader()->containsPlugins()) {
111            PCLOG("   -Frame contains plugins");
112            cannotCache = true;
113        }
114        if (frame->document()->url().protocolIs("https")) {
115            PCLOG("   -Frame is HTTPS");
116            cannotCache = true;
117        }
118        if (frame->domWindow() && frame->domWindow()->hasEventListeners(eventNames().unloadEvent)) {
119            PCLOG("   -Frame has an unload event listener");
120            cannotCache = true;
121        }
122#if ENABLE(DATABASE)
123        if (frame->document()->hasOpenDatabases()) {
124            PCLOG("   -Frame has open database handles");
125            cannotCache = true;
126        }
127#endif
128#if ENABLE(SHARED_WORKERS)
129        if (SharedWorkerRepository::hasSharedWorkers(frame->document())) {
130            PCLOG("   -Frame has associated SharedWorkers");
131            cannotCache = true;
132        }
133#endif
134        if (frame->document()->usingGeolocation()) {
135            PCLOG("   -Frame uses Geolocation");
136            cannotCache = true;
137        }
138        if (!frame->loader()->history()->currentItem()) {
139            PCLOG("   -No current history item");
140            cannotCache = true;
141        }
142        if (frame->loader()->quickRedirectComing()) {
143            PCLOG("   -Quick redirect is coming");
144            cannotCache = true;
145        }
146        if (frame->loader()->documentLoader()->isLoadingInAPISense()) {
147            PCLOG("   -DocumentLoader is still loading in API sense");
148            cannotCache = true;
149        }
150        if (frame->loader()->documentLoader()->isStopping()) {
151            PCLOG("   -DocumentLoader is in the middle of stopping");
152            cannotCache = true;
153        }
154        if (!frame->document()->canSuspendActiveDOMObjects()) {
155            PCLOG("   -The document cannot suspect its active DOM Objects");
156            cannotCache = true;
157        }
158#if ENABLE(OFFLINE_WEB_APPLICATIONS)
159        if (!frame->loader()->documentLoader()->applicationCacheHost()->canCacheInPageCache()) {
160            PCLOG("   -The DocumentLoader uses an application cache");
161            cannotCache = true;
162        }
163#endif
164        if (!frame->loader()->client()->canCachePage()) {
165            PCLOG("   -The client says this frame cannot be cached");
166            cannotCache = true;
167        }
168    } while (false);
169
170    for (Frame* child = frame->tree()->firstChild(); child; child = child->tree()->nextSibling())
171        if (!logCanCacheFrameDecision(child, indentLevel + 1))
172            cannotCache = true;
173
174    PCLOG(cannotCache ? " Frame CANNOT be cached" : " Frame CAN be cached");
175    PCLOG("+---");
176
177    return !cannotCache;
178}
179
180static void logCanCachePageDecision(Page* page)
181{
182    // Only bother logging for main frames that have actually loaded and have content.
183    if (page->mainFrame()->loader()->stateMachine()->creatingInitialEmptyDocument())
184        return;
185    KURL currentURL = page->mainFrame()->loader()->documentLoader() ? page->mainFrame()->loader()->documentLoader()->url() : KURL();
186    if (currentURL.isEmpty())
187        return;
188
189    int indentLevel = 0;
190    PCLOG("--------\n Determining if page can be cached:");
191
192    bool cannotCache = !logCanCacheFrameDecision(page->mainFrame(), 1);
193
194    FrameLoadType loadType = page->mainFrame()->loader()->loadType();
195    if (!page->backForward()->isActive()) {
196        PCLOG("   -The back/forward list is disabled or has 0 capacity");
197        cannotCache = true;
198    }
199    if (!page->settings()->usesPageCache()) {
200        PCLOG("   -Page settings says b/f cache disabled");
201        cannotCache = true;
202    }
203#if ENABLE(DEVICE_ORIENTATION)
204    if (page->deviceMotionController() && page->deviceMotionController()->isActive()) {
205        PCLOG("   -Page is using DeviceMotion");
206        cannotCache = true;
207    }
208    if (page->deviceOrientationController() && page->deviceOrientationController()->isActive()) {
209        PCLOG("   -Page is using DeviceOrientation");
210        cannotCache = true;
211    }
212#endif
213    if (loadType == FrameLoadTypeReload) {
214        PCLOG("   -Load type is: Reload");
215        cannotCache = true;
216    }
217    if (loadType == FrameLoadTypeReloadFromOrigin) {
218        PCLOG("   -Load type is: Reload from origin");
219        cannotCache = true;
220    }
221    if (loadType == FrameLoadTypeSame) {
222        PCLOG("   -Load type is: Same");
223        cannotCache = true;
224    }
225
226    PCLOG(cannotCache ? " Page CANNOT be cached\n--------" : " Page CAN be cached\n--------");
227}
228
229#endif
230
231PageCache* pageCache()
232{
233    static PageCache* staticPageCache = new PageCache;
234    return staticPageCache;
235}
236
237PageCache::PageCache()
238    : m_capacity(0)
239    , m_size(0)
240    , m_head(0)
241    , m_tail(0)
242    , m_autoreleaseTimer(this, &PageCache::releaseAutoreleasedPagesNowOrReschedule)
243{
244}
245
246bool PageCache::canCachePageContainingThisFrame(Frame* frame)
247{
248    for (Frame* child = frame->tree()->firstChild(); child; child = child->tree()->nextSibling()) {
249        if (!canCachePageContainingThisFrame(child))
250            return false;
251    }
252
253    return frame->loader()->documentLoader()
254        && frame->loader()->documentLoader()->mainDocumentError().isNull()
255        // Do not cache error pages (these can be recognized as pages with substitute data or unreachable URLs).
256        && !(frame->loader()->documentLoader()->substituteData().isValid() && !frame->loader()->documentLoader()->substituteData().failingURL().isEmpty())
257        // FIXME: If we ever change this so that frames with plug-ins will be cached,
258        // we need to make sure that we don't cache frames that have outstanding NPObjects
259        // (objects created by the plug-in). Since there is no way to pause/resume a Netscape plug-in,
260        // they would need to be destroyed and then recreated, and there is no way that we can recreate
261        // the right NPObjects. See <rdar://problem/5197041> for more information.
262        && !frame->loader()->subframeLoader()->containsPlugins()
263        && !frame->document()->url().protocolIs("https")
264        && (!frame->domWindow() || !frame->domWindow()->hasEventListeners(eventNames().unloadEvent))
265#if ENABLE(DATABASE)
266        && !frame->document()->hasOpenDatabases()
267#endif
268#if ENABLE(SHARED_WORKERS)
269        && !SharedWorkerRepository::hasSharedWorkers(frame->document())
270#endif
271        && !frame->document()->usingGeolocation()
272        && frame->loader()->history()->currentItem()
273        && !frame->loader()->quickRedirectComing()
274        && !frame->loader()->documentLoader()->isLoadingInAPISense()
275        && !frame->loader()->documentLoader()->isStopping()
276        && frame->document()->canSuspendActiveDOMObjects()
277#if ENABLE(OFFLINE_WEB_APPLICATIONS)
278        // FIXME: We should investigating caching frames that have an associated
279        // application cache. <rdar://problem/5917899> tracks that work.
280        && frame->loader()->documentLoader()->applicationCacheHost()->canCacheInPageCache()
281#endif
282#if ENABLE(WML)
283        && !frame->document()->containsWMLContent()
284        && !frame->document()->isWMLDocument()
285#endif
286        && frame->loader()->client()->canCachePage();
287}
288
289bool PageCache::canCache(Page* page)
290{
291    if (!page)
292        return false;
293
294#ifndef NDEBUG
295    logCanCachePageDecision(page);
296#endif
297
298    // Cache the page, if possible.
299    // Don't write to the cache if in the middle of a redirect, since we will want to
300    // store the final page we end up on.
301    // No point writing to the cache on a reload or loadSame, since we will just write
302    // over it again when we leave that page.
303    // FIXME: <rdar://problem/4886592> - We should work out the complexities of caching pages with frames as they
304    // are the most interesting pages on the web, and often those that would benefit the most from caching!
305    FrameLoadType loadType = page->mainFrame()->loader()->loadType();
306
307    return canCachePageContainingThisFrame(page->mainFrame())
308        && page->backForward()->isActive()
309        && page->settings()->usesPageCache()
310#if ENABLE(DEVICE_ORIENTATION)
311        && !(page->deviceMotionController() && page->deviceMotionController()->isActive())
312        && !(page->deviceOrientationController() && page->deviceOrientationController()->isActive())
313#endif
314        && loadType != FrameLoadTypeReload
315        && loadType != FrameLoadTypeReloadFromOrigin
316        && loadType != FrameLoadTypeSame;
317}
318
319void PageCache::setCapacity(int capacity)
320{
321    ASSERT(capacity >= 0);
322    m_capacity = max(capacity, 0);
323
324    prune();
325}
326
327int PageCache::frameCount() const
328{
329    int frameCount = 0;
330    for (HistoryItem* current = m_head; current; current = current->m_next) {
331        ++frameCount;
332        ASSERT(current->m_cachedPage);
333        frameCount += current->m_cachedPage ? current->m_cachedPage->cachedMainFrame()->descendantFrameCount() : 0;
334    }
335
336    return frameCount;
337}
338
339int PageCache::autoreleasedPageCount() const
340{
341    return m_autoreleaseSet.size();
342}
343
344void PageCache::markPagesForVistedLinkStyleRecalc()
345{
346    for (HistoryItem* current = m_head; current; current = current->m_next)
347        current->m_cachedPage->markForVistedLinkStyleRecalc();
348}
349
350void PageCache::add(PassRefPtr<HistoryItem> prpItem, Page* page)
351{
352    ASSERT(prpItem);
353    ASSERT(page);
354    ASSERT(canCache(page));
355
356    HistoryItem* item = prpItem.releaseRef(); // Balanced in remove().
357
358    // Remove stale cache entry if necessary.
359    if (item->m_cachedPage)
360        remove(item);
361
362    item->m_cachedPage = CachedPage::create(page);
363    addToLRUList(item);
364    ++m_size;
365
366    prune();
367}
368
369CachedPage* PageCache::get(HistoryItem* item)
370{
371    if (!item)
372        return 0;
373
374    if (CachedPage* cachedPage = item->m_cachedPage.get()) {
375        // FIXME: 1800 should not be hardcoded, it should come from
376        // WebKitBackForwardCacheExpirationIntervalKey in WebKit.
377        // Or we should remove WebKitBackForwardCacheExpirationIntervalKey.
378        if (currentTime() - cachedPage->timeStamp() <= 1800)
379            return cachedPage;
380
381        LOG(PageCache, "Not restoring page for %s from back/forward cache because cache entry has expired", item->url().string().ascii().data());
382        pageCache()->remove(item);
383    }
384    return 0;
385}
386
387void PageCache::remove(HistoryItem* item)
388{
389    // Safely ignore attempts to remove items not in the cache.
390    if (!item || !item->m_cachedPage)
391        return;
392
393    autorelease(item->m_cachedPage.release());
394    removeFromLRUList(item);
395    --m_size;
396
397    item->deref(); // Balanced in add().
398}
399
400void PageCache::prune()
401{
402    while (m_size > m_capacity) {
403        ASSERT(m_tail && m_tail->m_cachedPage);
404        remove(m_tail);
405    }
406}
407
408void PageCache::addToLRUList(HistoryItem* item)
409{
410    item->m_next = m_head;
411    item->m_prev = 0;
412
413    if (m_head) {
414        ASSERT(m_tail);
415        m_head->m_prev = item;
416    } else {
417        ASSERT(!m_tail);
418        m_tail = item;
419    }
420
421    m_head = item;
422}
423
424void PageCache::removeFromLRUList(HistoryItem* item)
425{
426    if (!item->m_next) {
427        ASSERT(item == m_tail);
428        m_tail = item->m_prev;
429    } else {
430        ASSERT(item != m_tail);
431        item->m_next->m_prev = item->m_prev;
432    }
433
434    if (!item->m_prev) {
435        ASSERT(item == m_head);
436        m_head = item->m_next;
437    } else {
438        ASSERT(item != m_head);
439        item->m_prev->m_next = item->m_next;
440    }
441}
442
443void PageCache::releaseAutoreleasedPagesNowOrReschedule(Timer<PageCache>* timer)
444{
445    double loadDelta = currentTime() - FrameLoader::timeOfLastCompletedLoad();
446    float userDelta = userIdleTime();
447
448    // FIXME: <rdar://problem/5211190> This limit of 42 risks growing the page cache far beyond its nominal capacity.
449    if ((userDelta < 0.5 || loadDelta < 1.25) && m_autoreleaseSet.size() < 42) {
450        LOG(PageCache, "WebCorePageCache: Postponing releaseAutoreleasedPagesNowOrReschedule() - %f since last load, %f since last input, %i objects pending release", loadDelta, userDelta, m_autoreleaseSet.size());
451        timer->startOneShot(autoreleaseInterval);
452        return;
453    }
454
455    LOG(PageCache, "WebCorePageCache: Releasing page caches - %f seconds since last load, %f since last input, %i objects pending release", loadDelta, userDelta, m_autoreleaseSet.size());
456    releaseAutoreleasedPagesNow();
457}
458
459void PageCache::releaseAutoreleasedPagesNow()
460{
461    m_autoreleaseTimer.stop();
462
463    // Postpone dead pruning until all our resources have gone dead.
464    memoryCache()->setPruneEnabled(false);
465
466    CachedPageSet tmp;
467    tmp.swap(m_autoreleaseSet);
468
469    CachedPageSet::iterator end = tmp.end();
470    for (CachedPageSet::iterator it = tmp.begin(); it != end; ++it)
471        (*it)->destroy();
472
473    // Now do the prune.
474    memoryCache()->setPruneEnabled(true);
475    memoryCache()->prune();
476}
477
478void PageCache::autorelease(PassRefPtr<CachedPage> page)
479{
480    ASSERT(page);
481    ASSERT(!m_autoreleaseSet.contains(page.get()));
482    m_autoreleaseSet.add(page);
483    if (!m_autoreleaseTimer.isActive())
484        m_autoreleaseTimer.startOneShot(autoreleaseInterval);
485}
486
487} // namespace WebCore
488