2008-02-11 Alp Toker <alp@atoker.com>
[WebKit-https.git] / WebCore / platform / network / curl / ResourceHandleManager.cpp
1 /*
2  * Copyright (C) 2004, 2006 Apple Computer, Inc.  All rights reserved.
3  * Copyright (C) 2006 Michael Emmel mike.emmel@gmail.com
4  * Copyright (C) 2007 Alp Toker <alp.toker@collabora.co.uk>
5  * Copyright (C) 2007 Holger Hans Peter Freyther
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
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  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
25  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #include "config.h"
31 #include "ResourceHandleManager.h"
32
33 #include "CString.h"
34 #include "FileSystem.h"
35 #include "MIMETypeRegistry.h"
36 #include "NotImplemented.h"
37 #include "ResourceHandle.h"
38 #include "ResourceHandleInternal.h"
39 #include "HTTPParsers.h"
40 #include "Base64.h"
41
42 #include <errno.h>
43 #include <wtf/Vector.h>
44
45 #if PLATFORM(GTK)
46     #if GLIB_CHECK_VERSION(2,12,0)
47         #define USE_GLIB_BASE64
48     #endif
49 #endif
50
51 namespace WebCore {
52
53 const int selectTimeoutMS = 5;
54 const double pollTimeSeconds = 0.05;
55 const int maxRunningJobs = 5;
56
57 ResourceHandleManager::ResourceHandleManager()
58     : m_downloadTimer(this, &ResourceHandleManager::downloadTimerCallback)
59     , m_cookieJarFileName(0)
60     , m_runningJobs(0)
61 {
62     curl_global_init(CURL_GLOBAL_ALL);
63     m_curlMultiHandle = curl_multi_init();
64     m_curlShareHandle = curl_share_init();
65     curl_share_setopt(m_curlShareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
66     curl_share_setopt(m_curlShareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
67 }
68
69 ResourceHandleManager::~ResourceHandleManager()
70 {
71     curl_multi_cleanup(m_curlMultiHandle);
72     curl_share_cleanup(m_curlShareHandle);
73     if (m_cookieJarFileName)
74         free(m_cookieJarFileName);
75 }
76
77 void ResourceHandleManager::setCookieJarFileName(const char* cookieJarFileName)
78 {
79     m_cookieJarFileName = strdup(cookieJarFileName);
80 }
81
82 ResourceHandleManager* ResourceHandleManager::sharedInstance()
83 {
84     static ResourceHandleManager* sharedInstance = 0;
85     if (!sharedInstance)
86         sharedInstance = new ResourceHandleManager();
87     return sharedInstance;
88 }
89
90 // called with data after all headers have been processed via headerCallback
91 static size_t writeCallback(void* ptr, size_t size, size_t nmemb, void* data)
92 {
93     ResourceHandle* job = static_cast<ResourceHandle*>(data);
94     ResourceHandleInternal* d = job->getInternal();
95     if (d->m_cancelled)
96         return 0;
97     size_t totalSize = size * nmemb;
98
99     // this shouldn't be necessary but apparently is. CURL writes the data
100     // of html page even if it is a redirect that was handled internally
101     // can be observed e.g. on gmail.com
102     CURL* h = d->m_handle;
103     long httpCode = 0;
104     CURLcode err = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &httpCode);
105     if (CURLE_OK == err && httpCode >= 300 && httpCode < 400)
106         return totalSize;
107
108     // since the code in headerCallback will not have run for local files
109     // the code to set the URL and fire didReceiveResponse is never run,
110     // which means the ResourceLoader's response does not contain the URL.
111     // Run the code here for local files to resolve the issue.
112     // TODO: See if there is a better approach for handling this.
113     if (!d->m_response.responseFired()) {
114         const char* hdr;
115         err = curl_easy_getinfo(h, CURLINFO_EFFECTIVE_URL, &hdr);
116         d->m_response.setUrl(KURL(hdr));
117         if (d->client())
118             d->client()->didReceiveResponse(job, d->m_response);
119         d->m_response.setResponseFired(true);
120     }
121
122     if (d->client())
123         d->client()->didReceiveData(job, static_cast<char*>(ptr), totalSize, 0);
124     return totalSize;
125 }
126
127 /*
128  * This is being called for each HTTP header in the response. This includes '\r\n'
129  * for the last line of the header.
130  *
131  * We will add each HTTP Header to the ResourceResponse and on the termination
132  * of the header (\r\n) we will parse Content-Type and Content-Disposition and
133  * update the ResourceResponse and then send it away.
134  *
135  */
136 static size_t headerCallback(char* ptr, size_t size, size_t nmemb, void* data)
137 {
138     ResourceHandle* job = static_cast<ResourceHandle*>(data);
139     ResourceHandleInternal* d = job->getInternal();
140     if (d->m_cancelled)
141         return 0;
142     size_t totalSize = size * nmemb;
143     ResourceHandleClient* client = d->client();
144
145     String header(static_cast<const char*>(ptr), totalSize);
146
147     /*
148      * a) We can finish and send the ResourceResponse
149      * b) We will add the current header to the HTTPHeaderMap of the ResourceResponse
150      */
151     if (header == String("\r\n")) {
152         CURL* h = d->m_handle;
153         CURLcode err;
154
155         double contentLength = 0;
156         err = curl_easy_getinfo(h, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &contentLength);
157         d->m_response.setExpectedContentLength(static_cast<long long int>(contentLength));
158
159         const char* hdr;
160         err = curl_easy_getinfo(h, CURLINFO_EFFECTIVE_URL, &hdr);
161         d->m_response.setUrl(KURL(hdr));
162
163         long httpCode = 0;
164         err = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &httpCode);
165         d->m_response.setHTTPStatusCode(httpCode);
166
167         d->m_response.setMimeType(extractMIMETypeFromMediaType(d->m_response.httpHeaderField("Content-Type")));
168         d->m_response.setTextEncodingName(extractCharsetFromMediaType(d->m_response.httpHeaderField("Content-Type")));
169         d->m_response.setSuggestedFilename(filenameFromHTTPContentDisposition(d->m_response.httpHeaderField("Content-Disposition")));
170
171         // HTTP redirection
172         if (httpCode >= 300 && httpCode < 400) {
173             String location = d->m_response.httpHeaderField("location");
174             if (!location.isEmpty()) {
175                 KURL newURL = KURL(job->request().url(), location.deprecatedString());
176
177                 ResourceRequest redirectedRequest = job->request();
178                 redirectedRequest.setURL(newURL);
179                 if (client)
180                     client->willSendRequest(job, redirectedRequest, d->m_response);
181
182                 d->m_request.setURL(newURL);
183
184                 return totalSize;
185             }
186         }
187
188         if (client)
189             client->didReceiveResponse(job, d->m_response);
190         d->m_response.setResponseFired(true);
191
192     } else {
193         int splitPos = header.find(":");
194         if (splitPos != -1)
195             d->m_response.setHTTPHeaderField(header.left(splitPos), header.substring(splitPos+1).stripWhiteSpace());
196     }
197
198     return totalSize;
199 }
200
201 /* This is called to obtain HTTP POST or PUT data.
202    Iterate through FormData elements and upload files.
203    Carefully respect the given buffer size and fill the rest of the data at the next calls.
204 */
205 size_t readCallback(void* ptr, size_t size, size_t nmemb, void* data)
206 {
207     ResourceHandle* job = static_cast<ResourceHandle*>(data);
208     ResourceHandleInternal* d = job->getInternal();
209     if (d->m_cancelled)
210         return 0;
211
212     size_t sent = 0;
213     size_t toSend = size * nmemb;
214     if (!toSend)
215         return 0;
216
217     Vector<FormDataElement> elements = job->request().httpBody()->elements();
218     if (d->m_formDataElementIndex >= elements.size())
219         return 0;
220
221     FormDataElement element = elements[d->m_formDataElementIndex];
222
223     if (element.m_type == FormDataElement::encodedFile) {
224         if (!d->m_file)
225             d->m_file = fopen(element.m_filename.utf8().data(), "rb");
226
227         if (!d->m_file) {
228             // FIXME: show a user error?
229 #ifndef NDEBUG
230             printf("Failed while trying to open %s for upload\n", element.m_filename.utf8().data());
231 #endif
232             job->cancel();
233             return 0;
234         }
235
236         sent = fread(ptr, size, nmemb, d->m_file);
237         if (!size && ferror(d->m_file)) {
238             // FIXME: show a user error?
239 #ifndef NDEBUG
240             printf("Failed while trying to read %s for upload\n", element.m_filename.utf8().data());
241 #endif
242             job->cancel();
243             return 0;
244         }
245         if (feof(d->m_file)) {
246             fclose(d->m_file);
247             d->m_file = 0;
248             d->m_formDataElementIndex++;
249         }
250     } else {
251         size_t elementSize = element.m_data.size() - d->m_formDataElementDataOffset;
252         sent = elementSize > toSend ? toSend : elementSize;
253         memcpy(ptr, element.m_data.data() + d->m_formDataElementDataOffset, sent);
254         if (elementSize > sent)
255             d->m_formDataElementDataOffset += sent;
256         else {
257             d->m_formDataElementDataOffset = 0;
258             d->m_formDataElementIndex++;
259         }
260     }
261
262     return sent;
263 }
264
265 void ResourceHandleManager::downloadTimerCallback(Timer<ResourceHandleManager>* timer)
266 {
267     startScheduledJobs();
268
269     fd_set fdread;
270     fd_set fdwrite;
271     fd_set fdexcep;
272     int maxfd = 0;
273
274     struct timeval timeout;
275     timeout.tv_sec = 0;
276     timeout.tv_usec = selectTimeoutMS * 1000;       // select waits microseconds
277
278     // Temporarily disable timers since signals may interrupt select(), raising EINTR errors on some platforms
279     setDeferringTimers(true);
280     int rc = 0;
281     do {
282         FD_ZERO(&fdread);
283         FD_ZERO(&fdwrite);
284         FD_ZERO(&fdexcep);
285         curl_multi_fdset(m_curlMultiHandle, &fdread, &fdwrite, &fdexcep, &maxfd);
286         // When the 3 file descriptors are empty, winsock will return -1
287         // and bail out, stopping the file download. So make sure we
288         // have valid file descriptors before calling select.
289         if (maxfd >= 0)
290             rc = ::select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
291     } while (rc == -1 && errno == EINTR);
292     setDeferringTimers(false);
293
294     if (-1 == rc) {
295 #ifndef NDEBUG
296         perror("bad: select() returned -1: ");
297 #endif
298         return;
299     }
300
301     int runningHandles = 0;
302     while (curl_multi_perform(m_curlMultiHandle, &runningHandles) == CURLM_CALL_MULTI_PERFORM) { }
303
304     // check the curl messages indicating completed transfers
305     // and free their resources
306     while (true) {
307         int messagesInQueue;
308         CURLMsg* msg = curl_multi_info_read(m_curlMultiHandle, &messagesInQueue);
309         if (!msg)
310             break;
311
312         // find the node which has same d->m_handle as completed transfer
313         CURL* handle = msg->easy_handle;
314         ASSERT(handle);
315         ResourceHandle* job = 0;
316         CURLcode err = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &job);
317         ASSERT(CURLE_OK == err);
318         ASSERT(job);
319         if (!job)
320             continue;
321         ResourceHandleInternal* d = job->getInternal();
322         ASSERT(d->m_handle == handle);
323
324         if (d->m_cancelled) {
325             removeFromCurl(job);
326             continue;
327         }
328
329         if (CURLMSG_DONE != msg->msg)
330             continue;
331
332         if (CURLE_OK == msg->data.result) {
333             if (d->client())
334                 d->client()->didFinishLoading(job);
335         } else {
336 #ifndef NDEBUG
337             char* url = 0;
338             curl_easy_getinfo(d->m_handle, CURLINFO_EFFECTIVE_URL, &url);
339             printf("Curl ERROR for url='%s', error: '%s'\n", url, curl_easy_strerror(msg->data.result));
340 #endif
341             if (d->client())
342                 d->client()->didFail(job, ResourceError());
343         }
344
345         removeFromCurl(job);
346     }
347
348     bool started = startScheduledJobs(); // new jobs might have been added in the meantime
349
350     if (!m_downloadTimer.isActive() && (started || (runningHandles > 0)))
351         m_downloadTimer.startOneShot(pollTimeSeconds);
352 }
353
354 void ResourceHandleManager::removeFromCurl(ResourceHandle* job)
355 {
356     ResourceHandleInternal* d = job->getInternal();
357     ASSERT(d->m_handle);
358     if (!d->m_handle)
359         return;
360     m_runningJobs--;
361     curl_multi_remove_handle(m_curlMultiHandle, d->m_handle);
362     curl_easy_cleanup(d->m_handle);
363     d->m_handle = 0;
364 }
365
366 void ResourceHandleManager::setupPUT(ResourceHandle*, struct curl_slist**)
367 {
368     notImplemented();
369 }
370
371 /* Calculate the length of the POST.
372    Force chunked data transfer if size of files can't be obtained.
373  */
374 void ResourceHandleManager::setupPOST(ResourceHandle* job, struct curl_slist** headers)
375 {
376     ResourceHandleInternal* d = job->getInternal();
377     Vector<FormDataElement> elements = job->request().httpBody()->elements();
378     size_t numElements = elements.size();
379
380     if (!numElements)
381         return;
382
383     // Do not stream for simple POST data
384     if (numElements == 1) {
385         job->request().httpBody()->flatten(d->m_postBytes);
386         if (d->m_postBytes.size() != 0) {
387             curl_easy_setopt(d->m_handle, CURLOPT_POST, TRUE);
388             curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDSIZE, d->m_postBytes.size());
389             curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDS, d->m_postBytes.data());
390         }
391         return;
392     }
393
394     // Obtain the total size of the POST
395     static const long long maxCurlOffT = (1LL << (sizeof(curl_off_t) * 8 - 1)) - 1;
396     curl_off_t size = 0;
397     bool chunkedTransfer = false;
398     for (size_t i = 0; i < numElements; i++) {
399         FormDataElement element = elements[i];
400         if (element.m_type == FormDataElement::encodedFile) {
401             long long fileSizeResult;
402             if (getFileSize(element.m_filename, fileSizeResult)) {
403                 if (fileSizeResult > maxCurlOffT) {
404                     // File size is too big for specifying it to cURL
405                     chunkedTransfer = true;
406                     break;
407                 }
408                 size += fileSizeResult;
409             } else {
410                 chunkedTransfer = true;
411                 break;
412             }
413         } else
414             size += elements[i].m_data.size();
415     }
416
417     curl_easy_setopt(d->m_handle, CURLOPT_POST, TRUE);
418
419     // cURL guesses that we want chunked encoding as long as we specify the header
420     if (chunkedTransfer)
421         *headers = curl_slist_append(*headers, "Transfer-Encoding: chunked");
422     else
423         curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDSIZE_LARGE, size);
424
425     curl_easy_setopt(d->m_handle, CURLOPT_READFUNCTION, readCallback);
426     curl_easy_setopt(d->m_handle, CURLOPT_READDATA, job);
427 }
428
429 void ResourceHandleManager::add(ResourceHandle* job)
430 {
431     // we can be called from within curl, so to avoid re-entrancy issues
432     // schedule this job to be added the next time we enter curl download loop
433     m_resourceHandleList.append(job);
434     if (!m_downloadTimer.isActive())
435         m_downloadTimer.startOneShot(pollTimeSeconds);
436 }
437
438 bool ResourceHandleManager::removeScheduledJob(ResourceHandle* job)
439 {
440     int size = m_resourceHandleList.size();
441     for (int i = 0; i < size; i++) {
442         if (job == m_resourceHandleList[i]) {
443             m_resourceHandleList.remove(i);
444             return true;
445         }
446     }
447     return false;
448 }
449
450 bool ResourceHandleManager::startScheduledJobs()
451 {
452     // TODO: Create a separate stack of jobs for each domain.
453
454     bool started = false;
455     while (!m_resourceHandleList.isEmpty() && m_runningJobs < maxRunningJobs) {
456         ResourceHandle* job = m_resourceHandleList[0];
457         startJob(job);
458         m_resourceHandleList.remove(0);
459         started = true;
460     }
461     return started;
462 }
463
464 static void parseDataUrl(ResourceHandle* handle)
465 {
466     DeprecatedString data = handle->request().url().deprecatedString();
467
468     ASSERT(data.startsWith("data:", false));
469
470     DeprecatedString header;
471     bool base64 = false;
472
473     int index = data.find(',');
474     if (index != -1) {
475         header = data.mid(5, index - 5).lower();
476         data = data.mid(index + 1);
477
478         if (header.endsWith(";base64")) {
479             base64 = true;
480             header = header.left(header.length() - 7);
481         }
482     } else
483         data = DeprecatedString();
484
485     data = KURL::decode_string(data);
486
487     if (base64 && !data.isEmpty()) {
488         // Use the GLib Base64 if available, since WebCore's decoder isn't
489         // general-purpose and fails on Acid3 test 97 (whitespace).
490 #ifdef USE_GLIB_BASE64
491         gsize outLength;
492         guchar* out = g_base64_decode(data.ascii(), &outLength);
493         data = DeprecatedString(reinterpret_cast<char*>(out), outLength);
494         g_free(out);
495 #else
496         Vector<char> out;
497         if (base64Decode(data.ascii(), data.length(), out))
498             data = DeprecatedString(out.data(), out.size());
499         else
500             data = DeprecatedString();
501 #endif
502     }
503
504     if (header.isEmpty())
505         header = "text/plain;charset=US-ASCII";
506
507     ResourceHandleClient* client = handle->getInternal()->client();
508
509     ResourceResponse response;
510
511     response.setMimeType(extractMIMETypeFromMediaType(header));
512     response.setTextEncodingName(extractCharsetFromMediaType(header));
513     response.setExpectedContentLength(data.length());
514     response.setHTTPStatusCode(200);
515
516     client->didReceiveResponse(handle, response);
517
518     if (!data.isEmpty())
519         client->didReceiveData(handle, data.ascii(), data.length(), 0);
520
521     client->didFinishLoading(handle);
522 }
523
524 void ResourceHandleManager::startJob(ResourceHandle* job)
525 {
526     KURL kurl = job->request().url();
527     String protocol = kurl.protocol();
528
529     if (equalIgnoringCase(protocol, "data")) {
530         parseDataUrl(job);
531         return;
532     }
533
534     // Remove any fragment part, otherwise curl will send it as part of the request.
535     kurl.setRef("");
536
537     ResourceHandleInternal* d = job->getInternal();
538     DeprecatedString url = kurl.deprecatedString();
539
540     if (kurl.isLocalFile()) {
541         DeprecatedString query = kurl.query();
542         // Remove any query part sent to a local file.
543         if (!query.isEmpty())
544             url = url.left(url.find(query));
545         // Determine the MIME type based on the path.
546         d->m_response.setMimeType(MIMETypeRegistry::getMIMETypeForPath(String(url)));
547     }
548
549     d->m_handle = curl_easy_init();
550 #ifndef NDEBUG
551     if (getenv("DEBUG_CURL"))
552         curl_easy_setopt(d->m_handle, CURLOPT_VERBOSE, 1);
553 #endif
554     curl_easy_setopt(d->m_handle, CURLOPT_PRIVATE, job);
555     curl_easy_setopt(d->m_handle, CURLOPT_ERRORBUFFER, m_curlErrorBuffer);
556     curl_easy_setopt(d->m_handle, CURLOPT_WRITEFUNCTION, writeCallback);
557     curl_easy_setopt(d->m_handle, CURLOPT_WRITEDATA, job);
558     curl_easy_setopt(d->m_handle, CURLOPT_HEADERFUNCTION, headerCallback);
559     curl_easy_setopt(d->m_handle, CURLOPT_WRITEHEADER, job);
560     curl_easy_setopt(d->m_handle, CURLOPT_AUTOREFERER, 1);
561     curl_easy_setopt(d->m_handle, CURLOPT_FOLLOWLOCATION, 1);
562     curl_easy_setopt(d->m_handle, CURLOPT_MAXREDIRS, 10);
563     curl_easy_setopt(d->m_handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
564     curl_easy_setopt(d->m_handle, CURLOPT_SHARE, m_curlShareHandle);
565     curl_easy_setopt(d->m_handle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5); // 5 minutes
566     // enable gzip and deflate through Accept-Encoding:
567     curl_easy_setopt(d->m_handle, CURLOPT_ENCODING, "");
568
569     // url must remain valid through the request
570     ASSERT(!d->m_url);
571     d->m_url = strdup(url.ascii());
572     curl_easy_setopt(d->m_handle, CURLOPT_URL, d->m_url);
573
574     if (m_cookieJarFileName) {
575         curl_easy_setopt(d->m_handle, CURLOPT_COOKIEFILE, m_cookieJarFileName);
576         curl_easy_setopt(d->m_handle, CURLOPT_COOKIEJAR, m_cookieJarFileName);
577     }
578
579     struct curl_slist* headers = 0;
580     if (job->request().httpHeaderFields().size() > 0) {
581         HTTPHeaderMap customHeaders = job->request().httpHeaderFields();
582         HTTPHeaderMap::const_iterator end = customHeaders.end();
583         for (HTTPHeaderMap::const_iterator it = customHeaders.begin(); it != end; ++it) {
584             String key = it->first;
585             String value = it->second;
586             String headerString(key);
587             headerString.append(": ");
588             headerString.append(value);
589             CString headerLatin1 = headerString.latin1();
590             headers = curl_slist_append(headers, headerLatin1.data());
591         }
592     }
593
594     if ("GET" == job->request().httpMethod())
595         curl_easy_setopt(d->m_handle, CURLOPT_HTTPGET, TRUE);
596     else if ("POST" == job->request().httpMethod())
597         setupPOST(job, &headers);
598     else if ("PUT" == job->request().httpMethod())
599         setupPUT(job, &headers);
600     else if ("HEAD" == job->request().httpMethod())
601         curl_easy_setopt(d->m_handle, CURLOPT_NOBODY, TRUE);
602
603     if (headers) {
604         curl_easy_setopt(d->m_handle, CURLOPT_HTTPHEADER, headers);
605         d->m_customHeaders = headers;
606     }
607
608     m_runningJobs++;
609     CURLMcode ret = curl_multi_add_handle(m_curlMultiHandle, d->m_handle);
610     // don't call perform, because events must be async
611     // timeout will occur and do curl_multi_perform
612     if (ret && ret != CURLM_CALL_MULTI_PERFORM) {
613 #ifndef NDEBUG
614         printf("Error %d starting job %s\n", ret, job->request().url().deprecatedString().ascii());
615 #endif
616         job->cancel();
617         return;
618     }
619 }
620
621 void ResourceHandleManager::cancel(ResourceHandle* job)
622 {
623     if (removeScheduledJob(job))
624         return;
625
626     ResourceHandleInternal* d = job->getInternal();
627     d->m_cancelled = true;
628     if (!m_downloadTimer.isActive())
629         m_downloadTimer.startOneShot(pollTimeSeconds);
630 }
631
632 } // namespace WebCore