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