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