1/*
2 * Copyright (C) 2012, 2014 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB. If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21#include "WebKitDownload.h"
22
23#include "DownloadProxy.h"
24#include "WebErrors.h"
25#include "WebKitDownloadPrivate.h"
26#include "WebKitPrivate.h"
27#include "WebKitURIRequestPrivate.h"
28#include "WebKitURIResponsePrivate.h"
29#include <WebCore/ResourceResponse.h>
30#include <glib/gi18n-lib.h>
31#include <wtf/glib/GRefPtr.h>
32#include <wtf/glib/GUniquePtr.h>
33#include <wtf/glib/WTFGType.h>
34#include <wtf/text/CString.h>
35
36using namespace WebKit;
37using namespace WebCore;
38
39/**
40 * SECTION: WebKitDownload
41 * @Short_description: Object used to communicate with the application when downloading
42 * @Title: WebKitDownload
43 *
44 * #WebKitDownload carries information about a download request and
45 * response, including a #WebKitURIRequest and a #WebKitURIResponse
46 * objects. The application may use this object to control the
47 * download process, or to simply figure out what is to be downloaded,
48 * and handle the download process itself.
49 *
50 */
51
52enum {
53 RECEIVED_DATA,
54 FINISHED,
55 FAILED,
56 DECIDE_DESTINATION,
57 CREATED_DESTINATION,
58
59 LAST_SIGNAL
60};
61
62enum {
63 PROP_0,
64
65 PROP_DESTINATION,
66 PROP_RESPONSE,
67 PROP_ESTIMATED_PROGRESS,
68 PROP_ALLOW_OVERWRITE
69};
70
71struct _WebKitDownloadPrivate {
72 ~_WebKitDownloadPrivate()
73 {
74 if (webView)
75 g_object_remove_weak_pointer(G_OBJECT(webView), reinterpret_cast<void**>(&webView));
76 }
77
78 RefPtr<DownloadProxy> download;
79
80 GRefPtr<WebKitURIRequest> request;
81 GRefPtr<WebKitURIResponse> response;
82 WebKitWebView* webView;
83 CString destinationURI;
84 guint64 currentSize;
85 bool isCancelled;
86 GUniquePtr<GTimer> timer;
87 gdouble lastProgress;
88 gdouble lastElapsed;
89 bool allowOverwrite;
90};
91
92static guint signals[LAST_SIGNAL] = { 0, };
93
94WEBKIT_DEFINE_TYPE(WebKitDownload, webkit_download, G_TYPE_OBJECT)
95
96static void webkitDownloadSetProperty(GObject* object, guint propId, const GValue* value, GParamSpec* paramSpec)
97{
98 WebKitDownload* download = WEBKIT_DOWNLOAD(object);
99
100 switch (propId) {
101 case PROP_ALLOW_OVERWRITE:
102 webkit_download_set_allow_overwrite(download, g_value_get_boolean(value));
103 break;
104 default:
105 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
106 }
107}
108
109static void webkitDownloadGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* paramSpec)
110{
111 WebKitDownload* download = WEBKIT_DOWNLOAD(object);
112
113 switch (propId) {
114 case PROP_DESTINATION:
115 g_value_set_string(value, webkit_download_get_destination(download));
116 break;
117 case PROP_RESPONSE:
118 g_value_set_object(value, webkit_download_get_response(download));
119 break;
120 case PROP_ESTIMATED_PROGRESS:
121 g_value_set_double(value, webkit_download_get_estimated_progress(download));
122 break;
123 case PROP_ALLOW_OVERWRITE:
124 g_value_set_boolean(value, webkit_download_get_allow_overwrite(download));
125 break;
126 default:
127 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
128 }
129}
130
131static gboolean webkitDownloadDecideDestination(WebKitDownload* download, const gchar* suggestedFilename)
132{
133 if (!download->priv->destinationURI.isNull())
134 return FALSE;
135
136 GUniquePtr<char> filename(g_strdelimit(g_strdup(suggestedFilename), G_DIR_SEPARATOR_S, '_'));
137 const gchar *downloadsDir = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD);
138 if (!downloadsDir) {
139 // If we don't have XDG user dirs info, set just to HOME.
140 downloadsDir = g_get_home_dir();
141 }
142 GUniquePtr<char> destination(g_build_filename(downloadsDir, filename.get(), NULL));
143 GUniquePtr<char> destinationURI(g_filename_to_uri(destination.get(), 0, 0));
144 download->priv->destinationURI = destinationURI.get();
145 g_object_notify(G_OBJECT(download), "destination");
146 return TRUE;
147}
148
149static void webkit_download_class_init(WebKitDownloadClass* downloadClass)
150{
151 GObjectClass* objectClass = G_OBJECT_CLASS(downloadClass);
152 objectClass->set_property = webkitDownloadSetProperty;
153 objectClass->get_property = webkitDownloadGetProperty;
154
155 downloadClass->decide_destination = webkitDownloadDecideDestination;
156
157 /**
158 * WebKitDownload:destination:
159 *
160 * The local URI to where the download will be saved.
161 */
162 g_object_class_install_property(objectClass,
163 PROP_DESTINATION,
164 g_param_spec_string("destination",
165 _("Destination"),
166 _("The local URI to where the download will be saved"),
167 0,
168 WEBKIT_PARAM_READABLE));
169
170 /**
171 * WebKitDownload:response:
172 *
173 * The #WebKitURIResponse associated with this download.
174 */
175 g_object_class_install_property(objectClass,
176 PROP_RESPONSE,
177 g_param_spec_object("response",
178 _("Response"),
179 _("The response of the download"),
180 WEBKIT_TYPE_URI_RESPONSE,
181 WEBKIT_PARAM_READABLE));
182
183 /**
184 * WebKitDownload:estimated-progress:
185 *
186 * An estimate of the percent completion for the download operation.
187 * This value will range from 0.0 to 1.0. The value is an estimate
188 * based on the total number of bytes expected to be received for
189 * a download.
190 * If you need a more accurate progress information you can connect to
191 * #WebKitDownload::received-data signal to track the progress.
192 */
193 g_object_class_install_property(objectClass,
194 PROP_ESTIMATED_PROGRESS,
195 g_param_spec_double("estimated-progress",
196 _("Estimated Progress"),
197 _("Determines the current progress of the download"),
198 0.0, 1.0, 1.0,
199 WEBKIT_PARAM_READABLE));
200
201 /**
202 * WebKitDownload:allow-overwrite:
203 *
204 * Whether or not the download is allowed to overwrite an existing file on
205 * disk. If this property is %FALSE and the destination already exists,
206 * the download will fail.
207 *
208 * Since: 2.6
209 */
210 g_object_class_install_property(
211 objectClass,
212 PROP_ALLOW_OVERWRITE,
213 g_param_spec_boolean(
214 "allow-overwrite",
215 _("Allow Overwrite"),
216 _("Whether the destination may be overwritten"),
217 FALSE,
218 WEBKIT_PARAM_READWRITE));
219
220 /**
221 * WebKitDownload::received-data:
222 * @download: the #WebKitDownload
223 * @data_length: the length of data received in bytes
224 *
225 * This signal is emitted after response is received,
226 * every time new data has been written to the destination. It's
227 * useful to know the progress of the download operation.
228 */
229 signals[RECEIVED_DATA] = g_signal_new(
230 "received-data",
231 G_TYPE_FROM_CLASS(objectClass),
232 G_SIGNAL_RUN_LAST,
233 0, nullptr, nullptr,
234 g_cclosure_marshal_generic,
235 G_TYPE_NONE, 1,
236 G_TYPE_UINT64);
237
238 /**
239 * WebKitDownload::finished:
240 * @download: the #WebKitDownload
241 *
242 * This signal is emitted when download finishes successfully or due to an error.
243 * In case of errors #WebKitDownload::failed signal is emitted before this one.
244 */
245 signals[FINISHED] =
246 g_signal_new("finished",
247 G_TYPE_FROM_CLASS(objectClass),
248 G_SIGNAL_RUN_LAST,
249 0, 0, 0,
250 g_cclosure_marshal_VOID__VOID,
251 G_TYPE_NONE, 0);
252
253 /**
254 * WebKitDownload::failed:
255 * @download: the #WebKitDownload
256 * @error: the #GError that was triggered
257 *
258 * This signal is emitted when an error occurs during the download
259 * operation. The given @error, of the domain %WEBKIT_DOWNLOAD_ERROR,
260 * contains further details of the failure. If the download is cancelled
261 * with webkit_download_cancel(), this signal is emitted with error
262 * %WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER. The download operation finishes
263 * after an error and #WebKitDownload::finished signal is emitted after this one.
264 */
265 signals[FAILED] =
266 g_signal_new(
267 "failed",
268 G_TYPE_FROM_CLASS(objectClass),
269 G_SIGNAL_RUN_LAST,
270 0, 0, 0,
271 g_cclosure_marshal_VOID__BOXED,
272 G_TYPE_NONE, 1,
273 G_TYPE_ERROR | G_SIGNAL_TYPE_STATIC_SCOPE);
274
275 /**
276 * WebKitDownload::decide-destination:
277 * @download: the #WebKitDownload
278 * @suggested_filename: the filename suggested for the download
279 *
280 * This signal is emitted after response is received to
281 * decide a destination URI for the download. If this signal is not
282 * handled the file will be downloaded to %G_USER_DIRECTORY_DOWNLOAD
283 * directory using @suggested_filename.
284 *
285 * Returns: %TRUE to stop other handlers from being invoked for the event.
286 * %FALSE to propagate the event further.
287 */
288 signals[DECIDE_DESTINATION] = g_signal_new(
289 "decide-destination",
290 G_TYPE_FROM_CLASS(objectClass),
291 G_SIGNAL_RUN_LAST,
292 G_STRUCT_OFFSET(WebKitDownloadClass, decide_destination),
293 g_signal_accumulator_true_handled, NULL,
294 g_cclosure_marshal_generic,
295 G_TYPE_BOOLEAN, 1,
296 G_TYPE_STRING);
297
298 /**
299 * WebKitDownload::created-destination:
300 * @download: the #WebKitDownload
301 * @destination: the destination URI
302 *
303 * This signal is emitted after #WebKitDownload::decide-destination and before
304 * #WebKitDownload::received-data to notify that destination file has been
305 * created successfully at @destination.
306 */
307 signals[CREATED_DESTINATION] =
308 g_signal_new(
309 "created-destination",
310 G_TYPE_FROM_CLASS(objectClass),
311 G_SIGNAL_RUN_LAST,
312 0, 0, 0,
313 g_cclosure_marshal_VOID__STRING,
314 G_TYPE_NONE, 1,
315 G_TYPE_STRING);
316}
317
318WebKitDownload* webkitDownloadCreate(DownloadProxy* downloadProxy)
319{
320 ASSERT(downloadProxy);
321 WebKitDownload* download = WEBKIT_DOWNLOAD(g_object_new(WEBKIT_TYPE_DOWNLOAD, NULL));
322 download->priv->download = downloadProxy;
323 return download;
324}
325
326static void webkitDownloadUpdateRequest(WebKitDownload* download)
327{
328 download->priv->request = adoptGRef(webkitURIRequestCreateForResourceRequest(download->priv->download->request()));
329}
330
331void webkitDownloadStarted(WebKitDownload* download)
332{
333 // Update with the final request if needed.
334 if (download->priv->request)
335 webkitDownloadUpdateRequest(download);
336}
337
338void webkitDownloadSetResponse(WebKitDownload* download, WebKitURIResponse* response)
339{
340 download->priv->response = response;
341 g_object_notify(G_OBJECT(download), "response");
342}
343
344void webkitDownloadSetWebView(WebKitDownload* download, WebKitWebView* webView)
345{
346 download->priv->webView = webView;
347 g_object_add_weak_pointer(G_OBJECT(webView), reinterpret_cast<void**>(&download->priv->webView));
348}
349
350bool webkitDownloadIsCancelled(WebKitDownload* download)
351{
352 return download->priv->isCancelled;
353}
354
355void webkitDownloadNotifyProgress(WebKitDownload* download, guint64 bytesReceived)
356{
357 WebKitDownloadPrivate* priv = download->priv;
358 if (priv->isCancelled)
359 return;
360
361 if (!download->priv->timer)
362 download->priv->timer.reset(g_timer_new());
363
364 priv->currentSize += bytesReceived;
365 g_signal_emit(download, signals[RECEIVED_DATA], 0, bytesReceived);
366
367 // Throttle progress notification to not consume high amounts of
368 // CPU on fast links, except when the last notification occurred
369 // more than 0.016 secs ago (60 FPS), or the last notified progress
370 // is passed in 1% or we reached the end.
371 gdouble currentElapsed = g_timer_elapsed(priv->timer.get(), 0);
372 gdouble currentProgress = webkit_download_get_estimated_progress(download);
373
374 if (priv->lastElapsed
375 && priv->lastProgress
376 && (currentElapsed - priv->lastElapsed) < 0.016
377 && (currentProgress - priv->lastProgress) < 0.01
378 && currentProgress < 1.0) {
379 return;
380 }
381 priv->lastElapsed = currentElapsed;
382 priv->lastProgress = currentProgress;
383 g_object_notify(G_OBJECT(download), "estimated-progress");
384}
385
386void webkitDownloadFailed(WebKitDownload* download, const ResourceError& resourceError)
387{
388 GUniquePtr<GError> webError(g_error_new_literal(g_quark_from_string(resourceError.domain().utf8().data()),
389 toWebKitError(resourceError.errorCode()), resourceError.localizedDescription().utf8().data()));
390 if (download->priv->timer)
391 g_timer_stop(download->priv->timer.get());
392
393 g_signal_emit(download, signals[FAILED], 0, webError.get());
394 g_signal_emit(download, signals[FINISHED], 0, NULL);
395}
396
397void webkitDownloadCancelled(WebKitDownload* download)
398{
399 WebKitDownloadPrivate* priv = download->priv;
400 webkitDownloadFailed(download, downloadCancelledByUserError(priv->response ? webkitURIResponseGetResourceResponse(priv->response.get()) : ResourceResponse()));
401}
402
403void webkitDownloadFinished(WebKitDownload* download)
404{
405 if (download->priv->isCancelled) {
406 // Since cancellation is asynchronous, didFinish might be called even
407 // if the download was cancelled. User cancelled the download,
408 // so we should fail with cancelled error even if the download
409 // actually finished successfully.
410 webkitDownloadCancelled(download);
411 return;
412 }
413 if (download->priv->timer)
414 g_timer_stop(download->priv->timer.get());
415 g_signal_emit(download, signals[FINISHED], 0, NULL);
416}
417
418String webkitDownloadDecideDestinationWithSuggestedFilename(WebKitDownload* download, const CString& suggestedFilename, bool& allowOverwrite)
419{
420 if (download->priv->isCancelled)
421 return emptyString();
422 gboolean returnValue;
423 g_signal_emit(download, signals[DECIDE_DESTINATION], 0, suggestedFilename.data(), &returnValue);
424 allowOverwrite = download->priv->allowOverwrite;
425 GUniquePtr<char> destinationPath(g_filename_from_uri(download->priv->destinationURI.data(), nullptr, nullptr));
426 if (!destinationPath)
427 return emptyString();
428 return String::fromUTF8(destinationPath.get());
429}
430
431void webkitDownloadDestinationCreated(WebKitDownload* download, const String& destinationPath)
432{
433 if (download->priv->isCancelled)
434 return;
435 GUniquePtr<char> destinationURI(g_filename_to_uri(destinationPath.utf8().data(), nullptr, nullptr));
436 ASSERT(destinationURI);
437 g_signal_emit(download, signals[CREATED_DESTINATION], 0, destinationURI.get());
438}
439
440/**
441 * webkit_download_get_request:
442 * @download: a #WebKitDownload
443 *
444 * Retrieves the #WebKitURIRequest object that backs the download
445 * process.
446 *
447 * Returns: (transfer none): the #WebKitURIRequest of @download
448 */
449WebKitURIRequest* webkit_download_get_request(WebKitDownload* download)
450{
451 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), nullptr);
452
453 WebKitDownloadPrivate* priv = download->priv;
454 if (!priv->request)
455 webkitDownloadUpdateRequest(download);
456 return priv->request.get();
457}
458
459/**
460 * webkit_download_get_destination:
461 * @download: a #WebKitDownload
462 *
463 * Obtains the URI to which the downloaded file will be written. You
464 * can connect to #WebKitDownload::created-destination to make
465 * sure this method returns a valid destination.
466 *
467 * Returns: the destination URI or %NULL
468 */
469const gchar* webkit_download_get_destination(WebKitDownload* download)
470{
471 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
472
473 return download->priv->destinationURI.data();
474}
475
476/**
477 * webkit_download_set_destination:
478 * @download: a #WebKitDownload
479 * @uri: the destination URI
480 *
481 * Sets the URI to which the downloaded file will be written.
482 * This method should be called before the download transfer
483 * starts or it will not have any effect on the ongoing download
484 * operation. To set the destination using the filename suggested
485 * by the server connect to #WebKitDownload::decide-destination
486 * signal and call webkit_download_set_destination(). If you want to
487 * set a fixed destination URI that doesn't depend on the suggested
488 * filename you can connect to notify::response signal and call
489 * webkit_download_set_destination().
490 * If #WebKitDownload::decide-destination signal is not handled
491 * and destination URI is not set when the download transfer starts,
492 * the file will be saved with the filename suggested by the server in
493 * %G_USER_DIRECTORY_DOWNLOAD directory.
494 */
495void webkit_download_set_destination(WebKitDownload* download, const gchar* uri)
496{
497 g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
498 g_return_if_fail(uri);
499
500 WebKitDownloadPrivate* priv = download->priv;
501 if (priv->destinationURI == uri)
502 return;
503
504 priv->destinationURI = uri;
505 g_object_notify(G_OBJECT(download), "destination");
506}
507
508/**
509 * webkit_download_get_response:
510 * @download: a #WebKitDownload
511 *
512 * Retrieves the #WebKitURIResponse object that backs the download
513 * process. This method returns %NULL if called before the response
514 * is received from the server. You can connect to notify::response
515 * signal to be notified when the response is received.
516 *
517 * Returns: (transfer none): the #WebKitURIResponse, or %NULL if
518 * the response hasn't been received yet.
519 */
520WebKitURIResponse* webkit_download_get_response(WebKitDownload* download)
521{
522 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
523
524 return download->priv->response.get();
525}
526
527/**
528 * webkit_download_cancel:
529 * @download: a #WebKitDownload
530 *
531 * Cancels the download. When the ongoing download
532 * operation is effectively cancelled the signal
533 * #WebKitDownload::failed is emitted with
534 * %WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER error.
535 */
536void webkit_download_cancel(WebKitDownload* download)
537{
538 g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
539
540 download->priv->isCancelled = true;
541 download->priv->download->cancel();
542}
543
544/**
545 * webkit_download_get_estimated_progress:
546 * @download: a #WebKitDownload
547 *
548 * Gets the value of the #WebKitDownload:estimated-progress property.
549 * You can monitor the estimated progress of the download operation by
550 * connecting to the notify::estimated-progress signal of @download.
551 *
552 * Returns: an estimate of the of the percent complete for a download
553 * as a range from 0.0 to 1.0.
554 */
555gdouble webkit_download_get_estimated_progress(WebKitDownload* download)
556{
557 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
558
559 WebKitDownloadPrivate* priv = download->priv;
560 if (!priv->response)
561 return 0;
562
563 guint64 contentLength = webkit_uri_response_get_content_length(priv->response.get());
564 if (!contentLength)
565 return 0;
566
567 return static_cast<gdouble>(priv->currentSize) / static_cast<gdouble>(contentLength);
568}
569
570/**
571 * webkit_download_get_elapsed_time:
572 * @download: a #WebKitDownload
573 *
574 * Gets the elapsed time in seconds, including any fractional part.
575 * If the download finished, had an error or was cancelled this is
576 * the time between its start and the event.
577 *
578 * Returns: seconds since the download was started
579 */
580gdouble webkit_download_get_elapsed_time(WebKitDownload* download)
581{
582 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
583
584 WebKitDownloadPrivate* priv = download->priv;
585 if (!priv->timer)
586 return 0;
587
588 return g_timer_elapsed(priv->timer.get(), 0);
589}
590
591/**
592 * webkit_download_get_received_data_length:
593 * @download: a #WebKitDownload
594 *
595 * Gets the length of the data already downloaded for @download
596 * in bytes.
597 *
598 * Returns: the amount of bytes already downloaded.
599 */
600guint64 webkit_download_get_received_data_length(WebKitDownload* download)
601{
602 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
603
604 return download->priv->currentSize;
605}
606
607/**
608 * webkit_download_get_web_view:
609 * @download: a #WebKitDownload
610 *
611 * Get the #WebKitWebView that initiated the download.
612 *
613 * Returns: (transfer none): the #WebKitWebView that initiated @download,
614 * or %NULL if @download was not initiated by a #WebKitWebView.
615 */
616WebKitWebView* webkit_download_get_web_view(WebKitDownload* download)
617{
618 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
619
620 return download->priv->webView;
621}
622
623/**
624 * webkit_download_get_allow_overwrite:
625 * @download: a #WebKitDownload
626 *
627 * Returns the current value of the #WebKitDownload:allow-overwrite property,
628 * which determines whether the download will overwrite an existing file on
629 * disk, or if it will fail if the destination already exists.
630 *
631 * Returns: the current value of the #WebKitDownload:allow-overwrite property
632 *
633 * Since: 2.6
634 */
635gboolean webkit_download_get_allow_overwrite(WebKitDownload* download)
636{
637 g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), FALSE);
638
639 return download->priv->allowOverwrite;
640}
641
642/**
643 * webkit_download_set_allow_overwrite:
644 * @download: a #WebKitDownload
645 * @allowed: the new value for the #WebKitDownload:allow-overwrite property
646 *
647 * Sets the #WebKitDownload:allow-overwrite property, which determines whether
648 * the download may overwrite an existing file on disk, or if it will fail if
649 * the destination already exists.
650 *
651 * Since: 2.6
652 */
653void webkit_download_set_allow_overwrite(WebKitDownload* download, gboolean allowed)
654{
655 g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
656
657 if (allowed == download->priv->allowOverwrite)
658 return;
659
660 download->priv->allowOverwrite = allowed;
661 g_object_notify(G_OBJECT(download), "allow-overwrite");
662}
663