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