blob: a75a8c020598533a4c3875db06a4e0260e0c14d3 [file] [log] [blame]
/*
* Copyright (C) 2012, 2014 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "WebKitDownload.h"
#include "DownloadProxy.h"
#include "WebErrors.h"
#include "WebKitDownloadPrivate.h"
#include "WebKitPrivate.h"
#include "WebKitURIRequestPrivate.h"
#include "WebKitURIResponsePrivate.h"
#include <WebCore/ResourceResponse.h>
#include <glib/gi18n-lib.h>
#include <wtf/glib/GRefPtr.h>
#include <wtf/glib/GUniquePtr.h>
#include <wtf/glib/WTFGType.h>
#include <wtf/text/CString.h>
using namespace WebKit;
using namespace WebCore;
/**
* SECTION: WebKitDownload
* @Short_description: Object used to communicate with the application when downloading
* @Title: WebKitDownload
*
* #WebKitDownload carries information about a download request and
* response, including a #WebKitURIRequest and a #WebKitURIResponse
* objects. The application may use this object to control the
* download process, or to simply figure out what is to be downloaded,
* and handle the download process itself.
*
*/
enum {
RECEIVED_DATA,
FINISHED,
FAILED,
DECIDE_DESTINATION,
CREATED_DESTINATION,
LAST_SIGNAL
};
enum {
PROP_0,
PROP_DESTINATION,
PROP_RESPONSE,
PROP_ESTIMATED_PROGRESS,
PROP_ALLOW_OVERWRITE
};
struct _WebKitDownloadPrivate {
~_WebKitDownloadPrivate()
{
if (webView)
g_object_remove_weak_pointer(G_OBJECT(webView), reinterpret_cast<void**>(&webView));
}
RefPtr<DownloadProxy> download;
GRefPtr<WebKitURIRequest> request;
GRefPtr<WebKitURIResponse> response;
WebKitWebView* webView;
CString destinationURI;
guint64 currentSize;
bool isCancelled;
GUniquePtr<GTimer> timer;
gdouble lastProgress;
gdouble lastElapsed;
bool allowOverwrite;
};
static guint signals[LAST_SIGNAL] = { 0, };
WEBKIT_DEFINE_TYPE(WebKitDownload, webkit_download, G_TYPE_OBJECT)
static void webkitDownloadSetProperty(GObject* object, guint propId, const GValue* value, GParamSpec* paramSpec)
{
WebKitDownload* download = WEBKIT_DOWNLOAD(object);
switch (propId) {
case PROP_ALLOW_OVERWRITE:
webkit_download_set_allow_overwrite(download, g_value_get_boolean(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
}
}
static void webkitDownloadGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* paramSpec)
{
WebKitDownload* download = WEBKIT_DOWNLOAD(object);
switch (propId) {
case PROP_DESTINATION:
g_value_set_string(value, webkit_download_get_destination(download));
break;
case PROP_RESPONSE:
g_value_set_object(value, webkit_download_get_response(download));
break;
case PROP_ESTIMATED_PROGRESS:
g_value_set_double(value, webkit_download_get_estimated_progress(download));
break;
case PROP_ALLOW_OVERWRITE:
g_value_set_boolean(value, webkit_download_get_allow_overwrite(download));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
}
}
static gboolean webkitDownloadDecideDestination(WebKitDownload* download, const gchar* suggestedFilename)
{
if (!download->priv->destinationURI.isNull())
return FALSE;
GUniquePtr<char> filename(g_strdelimit(g_strdup(suggestedFilename), G_DIR_SEPARATOR_S, '_'));
const gchar *downloadsDir = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD);
if (!downloadsDir) {
// If we don't have XDG user dirs info, set just to HOME.
downloadsDir = g_get_home_dir();
}
GUniquePtr<char> destination(g_build_filename(downloadsDir, filename.get(), NULL));
GUniquePtr<char> destinationURI(g_filename_to_uri(destination.get(), 0, 0));
download->priv->destinationURI = destinationURI.get();
g_object_notify(G_OBJECT(download), "destination");
return TRUE;
}
static void webkit_download_class_init(WebKitDownloadClass* downloadClass)
{
GObjectClass* objectClass = G_OBJECT_CLASS(downloadClass);
objectClass->set_property = webkitDownloadSetProperty;
objectClass->get_property = webkitDownloadGetProperty;
downloadClass->decide_destination = webkitDownloadDecideDestination;
/**
* WebKitDownload:destination:
*
* The local URI to where the download will be saved.
*/
g_object_class_install_property(objectClass,
PROP_DESTINATION,
g_param_spec_string("destination",
_("Destination"),
_("The local URI to where the download will be saved"),
0,
WEBKIT_PARAM_READABLE));
/**
* WebKitDownload:response:
*
* The #WebKitURIResponse associated with this download.
*/
g_object_class_install_property(objectClass,
PROP_RESPONSE,
g_param_spec_object("response",
_("Response"),
_("The response of the download"),
WEBKIT_TYPE_URI_RESPONSE,
WEBKIT_PARAM_READABLE));
/**
* WebKitDownload:estimated-progress:
*
* An estimate of the percent completion for the download operation.
* This value will range from 0.0 to 1.0. The value is an estimate
* based on the total number of bytes expected to be received for
* a download.
* If you need a more accurate progress information you can connect to
* #WebKitDownload::received-data signal to track the progress.
*/
g_object_class_install_property(objectClass,
PROP_ESTIMATED_PROGRESS,
g_param_spec_double("estimated-progress",
_("Estimated Progress"),
_("Determines the current progress of the download"),
0.0, 1.0, 1.0,
WEBKIT_PARAM_READABLE));
/**
* WebKitDownload:allow-overwrite:
*
* Whether or not the download is allowed to overwrite an existing file on
* disk. If this property is %FALSE and the destination already exists,
* the download will fail.
*
* Since: 2.6
*/
g_object_class_install_property(
objectClass,
PROP_ALLOW_OVERWRITE,
g_param_spec_boolean(
"allow-overwrite",
_("Allow Overwrite"),
_("Whether the destination may be overwritten"),
FALSE,
WEBKIT_PARAM_READWRITE));
/**
* WebKitDownload::received-data:
* @download: the #WebKitDownload
* @data_length: the length of data received in bytes
*
* This signal is emitted after response is received,
* every time new data has been written to the destination. It's
* useful to know the progress of the download operation.
*/
signals[RECEIVED_DATA] = g_signal_new(
"received-data",
G_TYPE_FROM_CLASS(objectClass),
G_SIGNAL_RUN_LAST,
0, nullptr, nullptr,
g_cclosure_marshal_generic,
G_TYPE_NONE, 1,
G_TYPE_UINT64);
/**
* WebKitDownload::finished:
* @download: the #WebKitDownload
*
* This signal is emitted when download finishes successfully or due to an error.
* In case of errors #WebKitDownload::failed signal is emitted before this one.
*/
signals[FINISHED] =
g_signal_new("finished",
G_TYPE_FROM_CLASS(objectClass),
G_SIGNAL_RUN_LAST,
0, 0, 0,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
/**
* WebKitDownload::failed:
* @download: the #WebKitDownload
* @error: the #GError that was triggered
*
* This signal is emitted when an error occurs during the download
* operation. The given @error, of the domain %WEBKIT_DOWNLOAD_ERROR,
* contains further details of the failure. If the download is cancelled
* with webkit_download_cancel(), this signal is emitted with error
* %WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER. The download operation finishes
* after an error and #WebKitDownload::finished signal is emitted after this one.
*/
signals[FAILED] =
g_signal_new(
"failed",
G_TYPE_FROM_CLASS(objectClass),
G_SIGNAL_RUN_LAST,
0, 0, 0,
g_cclosure_marshal_VOID__BOXED,
G_TYPE_NONE, 1,
G_TYPE_ERROR | G_SIGNAL_TYPE_STATIC_SCOPE);
/**
* WebKitDownload::decide-destination:
* @download: the #WebKitDownload
* @suggested_filename: the filename suggested for the download
*
* This signal is emitted after response is received to
* decide a destination URI for the download. If this signal is not
* handled the file will be downloaded to %G_USER_DIRECTORY_DOWNLOAD
* directory using @suggested_filename.
*
* Returns: %TRUE to stop other handlers from being invoked for the event.
* %FALSE to propagate the event further.
*/
signals[DECIDE_DESTINATION] = g_signal_new(
"decide-destination",
G_TYPE_FROM_CLASS(objectClass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET(WebKitDownloadClass, decide_destination),
g_signal_accumulator_true_handled, NULL,
g_cclosure_marshal_generic,
G_TYPE_BOOLEAN, 1,
G_TYPE_STRING);
/**
* WebKitDownload::created-destination:
* @download: the #WebKitDownload
* @destination: the destination URI
*
* This signal is emitted after #WebKitDownload::decide-destination and before
* #WebKitDownload::received-data to notify that destination file has been
* created successfully at @destination.
*/
signals[CREATED_DESTINATION] =
g_signal_new(
"created-destination",
G_TYPE_FROM_CLASS(objectClass),
G_SIGNAL_RUN_LAST,
0, 0, 0,
g_cclosure_marshal_VOID__STRING,
G_TYPE_NONE, 1,
G_TYPE_STRING);
}
WebKitDownload* webkitDownloadCreate(DownloadProxy* downloadProxy)
{
ASSERT(downloadProxy);
WebKitDownload* download = WEBKIT_DOWNLOAD(g_object_new(WEBKIT_TYPE_DOWNLOAD, NULL));
download->priv->download = downloadProxy;
return download;
}
static void webkitDownloadUpdateRequest(WebKitDownload* download)
{
download->priv->request = adoptGRef(webkitURIRequestCreateForResourceRequest(download->priv->download->request()));
}
void webkitDownloadStarted(WebKitDownload* download)
{
// Update with the final request if needed.
if (download->priv->request)
webkitDownloadUpdateRequest(download);
}
void webkitDownloadSetResponse(WebKitDownload* download, WebKitURIResponse* response)
{
download->priv->response = response;
g_object_notify(G_OBJECT(download), "response");
}
void webkitDownloadSetWebView(WebKitDownload* download, WebKitWebView* webView)
{
download->priv->webView = webView;
g_object_add_weak_pointer(G_OBJECT(webView), reinterpret_cast<void**>(&download->priv->webView));
}
bool webkitDownloadIsCancelled(WebKitDownload* download)
{
return download->priv->isCancelled;
}
void webkitDownloadNotifyProgress(WebKitDownload* download, guint64 bytesReceived)
{
WebKitDownloadPrivate* priv = download->priv;
if (priv->isCancelled)
return;
if (!download->priv->timer)
download->priv->timer.reset(g_timer_new());
priv->currentSize += bytesReceived;
g_signal_emit(download, signals[RECEIVED_DATA], 0, bytesReceived);
// Throttle progress notification to not consume high amounts of
// CPU on fast links, except when the last notification occurred
// more than 0.016 secs ago (60 FPS), or the last notified progress
// is passed in 1% or we reached the end.
gdouble currentElapsed = g_timer_elapsed(priv->timer.get(), 0);
gdouble currentProgress = webkit_download_get_estimated_progress(download);
if (priv->lastElapsed
&& priv->lastProgress
&& (currentElapsed - priv->lastElapsed) < 0.016
&& (currentProgress - priv->lastProgress) < 0.01
&& currentProgress < 1.0) {
return;
}
priv->lastElapsed = currentElapsed;
priv->lastProgress = currentProgress;
g_object_notify(G_OBJECT(download), "estimated-progress");
}
void webkitDownloadFailed(WebKitDownload* download, const ResourceError& resourceError)
{
GUniquePtr<GError> webError(g_error_new_literal(g_quark_from_string(resourceError.domain().utf8().data()),
toWebKitError(resourceError.errorCode()), resourceError.localizedDescription().utf8().data()));
if (download->priv->timer)
g_timer_stop(download->priv->timer.get());
g_signal_emit(download, signals[FAILED], 0, webError.get());
g_signal_emit(download, signals[FINISHED], 0, NULL);
}
void webkitDownloadCancelled(WebKitDownload* download)
{
WebKitDownloadPrivate* priv = download->priv;
webkitDownloadFailed(download, downloadCancelledByUserError(priv->response ? webkitURIResponseGetResourceResponse(priv->response.get()) : ResourceResponse()));
}
void webkitDownloadFinished(WebKitDownload* download)
{
if (download->priv->isCancelled) {
// Since cancellation is asynchronous, didFinish might be called even
// if the download was cancelled. User cancelled the download,
// so we should fail with cancelled error even if the download
// actually finished successfully.
webkitDownloadCancelled(download);
return;
}
if (download->priv->timer)
g_timer_stop(download->priv->timer.get());
g_signal_emit(download, signals[FINISHED], 0, NULL);
}
String webkitDownloadDecideDestinationWithSuggestedFilename(WebKitDownload* download, const CString& suggestedFilename, bool& allowOverwrite)
{
if (download->priv->isCancelled)
return emptyString();
gboolean returnValue;
g_signal_emit(download, signals[DECIDE_DESTINATION], 0, suggestedFilename.data(), &returnValue);
allowOverwrite = download->priv->allowOverwrite;
GUniquePtr<char> destinationPath(g_filename_from_uri(download->priv->destinationURI.data(), nullptr, nullptr));
if (!destinationPath)
return emptyString();
return String::fromUTF8(destinationPath.get());
}
void webkitDownloadDestinationCreated(WebKitDownload* download, const String& destinationPath)
{
if (download->priv->isCancelled)
return;
GUniquePtr<char> destinationURI(g_filename_to_uri(destinationPath.utf8().data(), nullptr, nullptr));
ASSERT(destinationURI);
g_signal_emit(download, signals[CREATED_DESTINATION], 0, destinationURI.get());
}
/**
* webkit_download_get_request:
* @download: a #WebKitDownload
*
* Retrieves the #WebKitURIRequest object that backs the download
* process.
*
* Returns: (transfer none): the #WebKitURIRequest of @download
*/
WebKitURIRequest* webkit_download_get_request(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), nullptr);
WebKitDownloadPrivate* priv = download->priv;
if (!priv->request)
webkitDownloadUpdateRequest(download);
return priv->request.get();
}
/**
* webkit_download_get_destination:
* @download: a #WebKitDownload
*
* Obtains the URI to which the downloaded file will be written. You
* can connect to #WebKitDownload::created-destination to make
* sure this method returns a valid destination.
*
* Returns: the destination URI or %NULL
*/
const gchar* webkit_download_get_destination(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
return download->priv->destinationURI.data();
}
/**
* webkit_download_set_destination:
* @download: a #WebKitDownload
* @uri: the destination URI
*
* Sets the URI to which the downloaded file will be written.
* This method should be called before the download transfer
* starts or it will not have any effect on the ongoing download
* operation. To set the destination using the filename suggested
* by the server connect to #WebKitDownload::decide-destination
* signal and call webkit_download_set_destination(). If you want to
* set a fixed destination URI that doesn't depend on the suggested
* filename you can connect to notify::response signal and call
* webkit_download_set_destination().
* If #WebKitDownload::decide-destination signal is not handled
* and destination URI is not set when the download transfer starts,
* the file will be saved with the filename suggested by the server in
* %G_USER_DIRECTORY_DOWNLOAD directory.
*/
void webkit_download_set_destination(WebKitDownload* download, const gchar* uri)
{
g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
g_return_if_fail(uri);
WebKitDownloadPrivate* priv = download->priv;
if (priv->destinationURI == uri)
return;
priv->destinationURI = uri;
g_object_notify(G_OBJECT(download), "destination");
}
/**
* webkit_download_get_response:
* @download: a #WebKitDownload
*
* Retrieves the #WebKitURIResponse object that backs the download
* process. This method returns %NULL if called before the response
* is received from the server. You can connect to notify::response
* signal to be notified when the response is received.
*
* Returns: (transfer none): the #WebKitURIResponse, or %NULL if
* the response hasn't been received yet.
*/
WebKitURIResponse* webkit_download_get_response(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
return download->priv->response.get();
}
/**
* webkit_download_cancel:
* @download: a #WebKitDownload
*
* Cancels the download. When the ongoing download
* operation is effectively cancelled the signal
* #WebKitDownload::failed is emitted with
* %WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER error.
*/
void webkit_download_cancel(WebKitDownload* download)
{
g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
download->priv->isCancelled = true;
download->priv->download->cancel();
}
/**
* webkit_download_get_estimated_progress:
* @download: a #WebKitDownload
*
* Gets the value of the #WebKitDownload:estimated-progress property.
* You can monitor the estimated progress of the download operation by
* connecting to the notify::estimated-progress signal of @download.
*
* Returns: an estimate of the of the percent complete for a download
* as a range from 0.0 to 1.0.
*/
gdouble webkit_download_get_estimated_progress(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
WebKitDownloadPrivate* priv = download->priv;
if (!priv->response)
return 0;
guint64 contentLength = webkit_uri_response_get_content_length(priv->response.get());
if (!contentLength)
return 0;
return static_cast<gdouble>(priv->currentSize) / static_cast<gdouble>(contentLength);
}
/**
* webkit_download_get_elapsed_time:
* @download: a #WebKitDownload
*
* Gets the elapsed time in seconds, including any fractional part.
* If the download finished, had an error or was cancelled this is
* the time between its start and the event.
*
* Returns: seconds since the download was started
*/
gdouble webkit_download_get_elapsed_time(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
WebKitDownloadPrivate* priv = download->priv;
if (!priv->timer)
return 0;
return g_timer_elapsed(priv->timer.get(), 0);
}
/**
* webkit_download_get_received_data_length:
* @download: a #WebKitDownload
*
* Gets the length of the data already downloaded for @download
* in bytes.
*
* Returns: the amount of bytes already downloaded.
*/
guint64 webkit_download_get_received_data_length(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
return download->priv->currentSize;
}
/**
* webkit_download_get_web_view:
* @download: a #WebKitDownload
*
* Get the #WebKitWebView that initiated the download.
*
* Returns: (transfer none): the #WebKitWebView that initiated @download,
* or %NULL if @download was not initiated by a #WebKitWebView.
*/
WebKitWebView* webkit_download_get_web_view(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);
return download->priv->webView;
}
/**
* webkit_download_get_allow_overwrite:
* @download: a #WebKitDownload
*
* Returns the current value of the #WebKitDownload:allow-overwrite property,
* which determines whether the download will overwrite an existing file on
* disk, or if it will fail if the destination already exists.
*
* Returns: the current value of the #WebKitDownload:allow-overwrite property
*
* Since: 2.6
*/
gboolean webkit_download_get_allow_overwrite(WebKitDownload* download)
{
g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), FALSE);
return download->priv->allowOverwrite;
}
/**
* webkit_download_set_allow_overwrite:
* @download: a #WebKitDownload
* @allowed: the new value for the #WebKitDownload:allow-overwrite property
*
* Sets the #WebKitDownload:allow-overwrite property, which determines whether
* the download may overwrite an existing file on disk, or if it will fail if
* the destination already exists.
*
* Since: 2.6
*/
void webkit_download_set_allow_overwrite(WebKitDownload* download, gboolean allowed)
{
g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
if (allowed == download->priv->allowOverwrite)
return;
download->priv->allowOverwrite = allowed;
g_object_notify(G_OBJECT(download), "allow-overwrite");
}