1/*
2 * Copyright (C) 2013 University of Szeged
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY UNIVERSITY OF SZEGED ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL UNIVERSITY OF SZEGED OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "config.h"
28
29#if USE(CURL)
30
31#include "CurlCacheManager.h"
32
33#include "FileSystem.h"
34#include "HTTPHeaderMap.h"
35#include "Logging.h"
36#include "ResourceHandleClient.h"
37#include "ResourceHandleInternal.h"
38#include "ResourceRequest.h"
39#include <wtf/HashMap.h>
40#include <wtf/text/CString.h>
41
42#define IO_BUFFERSIZE 4096
43
44namespace WebCore {
45
46CurlCacheManager& CurlCacheManager::getInstance()
47{
48    static CurlCacheManager instance;
49    return instance;
50}
51
52CurlCacheManager::CurlCacheManager()
53    : m_disabled(true)
54    , m_currentStorageSize(0)
55    , m_storageSizeLimit(52428800) // 50 * 1024 * 1024 bytes
56{
57    // Call setCacheDirectory() to enable the Cache Manager
58}
59
60CurlCacheManager::~CurlCacheManager()
61{
62    if (m_disabled)
63        return;
64
65    saveIndex();
66}
67
68void CurlCacheManager::setCacheDirectory(const String& directory)
69{
70    m_cacheDir = directory;
71    m_cacheDir.append("/");
72    if (m_cacheDir.isEmpty()) {
73        LOG(Network, "Cache Error: Cache location is not set! CacheManager disabled.\n");
74        m_disabled = true;
75        return;
76    }
77
78    if (!fileExists(m_cacheDir)) {
79        if (!makeAllDirectories(m_cacheDir)) {
80            LOG(Network, "Cache Error: Could not open or create cache directory! CacheManager disabled.\n");
81            m_disabled = true;
82            return;
83        }
84    }
85
86    m_disabled = false;
87    loadIndex();
88}
89
90void CurlCacheManager::setStorageSizeLimit(size_t sizeLimit)
91{
92    m_storageSizeLimit = sizeLimit;
93}
94
95void CurlCacheManager::loadIndex()
96{
97    if (m_disabled)
98        return;
99
100    String indexFilePath(m_cacheDir);
101    indexFilePath.append("index.dat");
102
103    PlatformFileHandle indexFile = openFile(indexFilePath, OpenForRead);
104    if (!isHandleValid(indexFile)) {
105        LOG(Network, "Cache Warning: Could not open %s for read\n", indexFilePath.latin1().data());
106        return;
107    }
108
109    long long filesize = -1;
110    if (!getFileSize(indexFilePath, filesize)) {
111        LOG(Network, "Cache Error: Could not get file size of %s\n", indexFilePath.latin1().data());
112        return;
113    }
114
115    // Load the file content into buffer
116    Vector<char> buffer;
117    buffer.resize(filesize);
118    int bufferPosition = 0;
119    int bufferReadSize = IO_BUFFERSIZE;
120    while (filesize > bufferPosition) {
121        if (filesize - bufferPosition < bufferReadSize)
122            bufferReadSize = filesize - bufferPosition;
123
124        readFromFile(indexFile, buffer.data() + bufferPosition, bufferReadSize);
125        bufferPosition += bufferReadSize;
126    }
127    closeFile(indexFile);
128
129    // Create strings from buffer
130    String headerContent = String(buffer.data(), buffer.size());
131    Vector<String> indexURLs;
132    headerContent.split("\n", indexURLs);
133    buffer.clear();
134
135    // Add entries to index
136    Vector<String>::const_iterator it = indexURLs.begin();
137    Vector<String>::const_iterator end = indexURLs.end();
138    if (indexURLs.size() > 1)
139        --end; // Last line is empty
140    while (it != end) {
141        String url = it->stripWhiteSpace();
142        auto cacheEntry = std::make_unique<CurlCacheEntry>(url, nullptr, m_cacheDir);
143
144        if (cacheEntry->isCached() && cacheEntry->entrySize() < m_storageSizeLimit) {
145            m_currentStorageSize += cacheEntry->entrySize();
146            makeRoomForNewEntry();
147            m_LRUEntryList.prependOrMoveToFirst(url);
148            m_index.set(url, WTF::move(cacheEntry));
149        } else
150            cacheEntry->invalidate();
151
152        ++it;
153    }
154}
155
156void CurlCacheManager::saveIndex()
157{
158    if (m_disabled)
159        return;
160
161    String indexFilePath(m_cacheDir);
162    indexFilePath.append("index.dat");
163
164    deleteFile(indexFilePath);
165    PlatformFileHandle indexFile = openFile(indexFilePath, OpenForWrite);
166    if (!isHandleValid(indexFile)) {
167        LOG(Network, "Cache Error: Could not open %s for write\n", indexFilePath.latin1().data());
168        return;
169    }
170
171    auto it = m_LRUEntryList.begin();
172    const auto& end = m_LRUEntryList.end();
173    while (it != end) {
174        const CString& urlLatin1 = it->latin1();
175        writeToFile(indexFile, urlLatin1.data(), urlLatin1.length());
176        writeToFile(indexFile, "\n", 1);
177        ++it;
178    }
179
180    closeFile(indexFile);
181}
182
183void CurlCacheManager::makeRoomForNewEntry()
184{
185    if (m_disabled)
186        return;
187
188    while ((m_currentStorageSize > m_storageSizeLimit) && m_LRUEntryList.size() > 0) {
189        ASSERT(m_index.find(m_LRUEntryList.last()) != m_index.end());
190        invalidateCacheEntry(m_LRUEntryList.last());
191    }
192}
193
194void CurlCacheManager::didReceiveResponse(ResourceHandle& job, ResourceResponse& response)
195{
196    if (m_disabled)
197        return;
198
199    ResourceHandleInternal* d = job.getInternal();
200    if (d->m_cancelled)
201        return;
202
203    const String& url = job.firstRequest().url().string();
204
205    if (response.httpStatusCode() == 304) {
206        readCachedData(url, &job, response);
207        m_LRUEntryList.prependOrMoveToFirst(url);
208    }
209    else if (response.httpStatusCode() == 200) {
210        auto it = m_index.find(url);
211        if (it != m_index.end() && it->value->isLoading())
212            return;
213
214        invalidateCacheEntry(url); // Invalidate existing entry on 200
215
216        auto cacheEntry = std::make_unique<CurlCacheEntry>(url, &job, m_cacheDir);
217        bool cacheable = cacheEntry->parseResponseHeaders(response);
218        if (cacheable) {
219            m_LRUEntryList.prependOrMoveToFirst(url);
220            m_index.set(url, WTF::move(cacheEntry));
221            saveResponseHeaders(url, response);
222        }
223    } else
224        invalidateCacheEntry(url);
225}
226
227void CurlCacheManager::didFinishLoading(ResourceHandle& job)
228{
229    if (m_disabled)
230        return;
231
232    const String& url = job.firstRequest().url().string();
233
234    auto it = m_index.find(url);
235    if (it != m_index.end())
236        it->value->didFinishLoading();
237}
238
239bool CurlCacheManager::isCached(const String& url) const
240{
241    if (m_disabled)
242        return false;
243
244    auto it = m_index.find(url);
245    if (it != m_index.end())
246        return it->value->isCached() && !it->value->isLoading();
247
248    return false;
249}
250
251HTTPHeaderMap& CurlCacheManager::requestHeaders(const String& url)
252{
253    ASSERT(isCached(url));
254    return m_index.find(url)->value->requestHeaders();
255}
256
257bool CurlCacheManager::getCachedResponse(const String& url, ResourceResponse& response)
258{
259    auto it = m_index.find(url);
260    if (it != m_index.end()) {
261        it->value->setResponseFromCachedHeaders(response);
262        return true;
263    }
264    return false;
265}
266
267void CurlCacheManager::didReceiveData(ResourceHandle& job, const char* data, size_t size)
268{
269    if (m_disabled)
270        return;
271
272    const String& url = job.firstRequest().url().string();
273
274    auto it = m_index.find(url);
275    if (it != m_index.end()) {
276        if (it->value->getJob() != &job)
277            return;
278
279        if (!it->value->saveCachedData(data, size))
280            invalidateCacheEntry(url);
281
282        else {
283            m_currentStorageSize += size;
284            m_LRUEntryList.prependOrMoveToFirst(url);
285            makeRoomForNewEntry();
286        }
287    }
288}
289
290void CurlCacheManager::saveResponseHeaders(const String& url, ResourceResponse& response)
291{
292    if (m_disabled)
293        return;
294
295    auto it = m_index.find(url);
296    if (it != m_index.end())
297        if (!it->value->saveResponseHeaders(response))
298            invalidateCacheEntry(url);
299}
300
301void CurlCacheManager::invalidateCacheEntry(const String& url)
302{
303    if (m_disabled)
304        return;
305
306    auto it = m_index.find(url);
307    if (it != m_index.end()) {
308        if (m_currentStorageSize < it->value->entrySize())
309            m_currentStorageSize = 0;
310        else
311            m_currentStorageSize -= it->value->entrySize();
312
313        it->value->invalidate();
314        m_index.remove(url);
315    }
316    m_LRUEntryList.remove(url);
317}
318
319void CurlCacheManager::didFail(ResourceHandle &job)
320{
321    const String& url = job.firstRequest().url().string();
322
323    invalidateCacheEntry(url);
324}
325
326void CurlCacheManager::readCachedData(const String& url, ResourceHandle* job, ResourceResponse& response)
327{
328    if (m_disabled)
329        return;
330
331    auto it = m_index.find(url);
332    if (it != m_index.end()) {
333        it->value->setResponseFromCachedHeaders(response);
334        m_LRUEntryList.prependOrMoveToFirst(url);
335        if (!it->value->readCachedData(job))
336            invalidateCacheEntry(url);
337    }
338}
339
340}
341
342#endif
343