toolkit/components/downloads/DownloadPlatform.cpp
author Jean-Yves Avenard <jyavenard@mozilla.com>
Mon, 08 Oct 2018 13:24:19 +0200
changeset 498410 2851d7aead763576969cf47383066977ae75f4b3
parent 490374 acfaacaca4cbe91eb413abb206d2187f304fad0d
child 508163 6f3709b3878117466168c40affa7bca0b60cf75b
permissions -rw-r--r--
Bug 1495025 - P2. Use lambda for callback. r=cpearce I find it easier to read than a function pointer making you search elsewhere to see what it's about Depends on D7294 Differential Revision: https://phabricator.services.mozilla.com/D7295

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "DownloadPlatform.h"
#include "nsAutoPtr.h"
#include "nsNetUtil.h"
#include "nsString.h"
#include "nsINestedURI.h"
#include "nsIProtocolHandler.h"
#include "nsIURI.h"
#include "nsIFile.h"
#include "nsIObserverService.h"
#include "nsISupportsPrimitives.h"
#include "nsDirectoryServiceDefs.h"
#include "nsThreadUtils.h"
#include "xpcpublic.h"

#include "mozilla/dom/Promise.h"
#include "mozilla/LazyIdleThread.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"

#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs"

// The amount of time, in milliseconds, that our IO thread will stay alive after the last event it processes.
#define DEFAULT_THREAD_TIMEOUT_MS 10000

#ifdef XP_WIN
#include <shlobj.h>
#include <urlmon.h>
#include "nsILocalFileWin.h"
#endif

#ifdef XP_MACOSX
#include <CoreFoundation/CoreFoundation.h>
#include "../../../xpcom/io/CocoaFileUtils.h"
#endif

#ifdef MOZ_WIDGET_ANDROID
#include "FennecJNIWrappers.h"
#endif

#ifdef MOZ_WIDGET_GTK
#include <gtk/gtk.h>
#endif

using namespace mozilla;
using dom::Promise;

DownloadPlatform *DownloadPlatform::gDownloadPlatformService = nullptr;

NS_IMPL_ISUPPORTS(DownloadPlatform, mozIDownloadPlatform);

DownloadPlatform* DownloadPlatform::GetDownloadPlatform()
{
  if (!gDownloadPlatformService) {
    gDownloadPlatformService = new DownloadPlatform();
  }

  NS_ADDREF(gDownloadPlatformService);

#if defined(MOZ_WIDGET_GTK)
  g_type_init();
#endif

  return gDownloadPlatformService;
}

#ifdef MOZ_WIDGET_GTK
static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data)
{
  GError *err = nullptr;
  g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err);
  if (err) {
#ifdef DEBUG
    NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__);
#endif
    g_error_free(err);
  }
}
#endif

#ifdef XP_MACOSX
// Caller is responsible for freeing any result (CF Create Rule)
CFURLRef CreateCFURLFromNSIURI(nsIURI *aURI) {
  nsAutoCString spec;
  if (aURI) {
    aURI->GetSpec(spec);
  }

  CFStringRef urlStr = ::CFStringCreateWithCString(kCFAllocatorDefault,
                                                   spec.get(),
                                                   kCFStringEncodingUTF8);
  if (!urlStr) {
    return NULL;
  }

  CFURLRef url = ::CFURLCreateWithString(kCFAllocatorDefault,
                                         urlStr,
                                         NULL);

  ::CFRelease(urlStr);

  return url;
}
#endif

DownloadPlatform::DownloadPlatform()
{
  mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS,
                                 NS_LITERAL_CSTRING("DownloadPlatform"));
}

nsresult DownloadPlatform::DownloadDone(nsIURI* aSource, nsIURI* aReferrer, nsIFile* aTarget,
                                        const nsACString& aContentType, bool aIsPrivate,
                                        JSContext* aCx, Promise** aPromise)
{

  nsIGlobalObject* globalObject =
    xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));

  if (NS_WARN_IF(!globalObject)) {
    return NS_ERROR_FAILURE;
  }

  ErrorResult result;
  RefPtr<Promise> promise = Promise::Create(globalObject, result);

  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }

  nsresult rv = NS_OK;
  bool pendingAsyncOperations = false;

#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) \
 || defined(MOZ_WIDGET_GTK)

  nsAutoString path;
  if (aTarget && NS_SUCCEEDED(aTarget->GetPath(path))) {
#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID)
    // On Windows and Gtk, add the download to the system's "recent documents"
    // list, with a pref to disable.
    {
      bool addToRecentDocs = Preferences::GetBool(PREF_BDM_ADDTORECENTDOCS);
#ifdef MOZ_WIDGET_ANDROID
      if (jni::IsFennec() && addToRecentDocs) {
        java::DownloadsIntegration::ScanMedia(path, aContentType);
      }
#else
      if (addToRecentDocs && !aIsPrivate) {
#ifdef XP_WIN
        ::SHAddToRecentDocs(SHARD_PATHW, path.get());
#elif defined(MOZ_WIDGET_GTK)
        GtkRecentManager* manager = gtk_recent_manager_get_default();

        gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(),
                                       nullptr, nullptr);
        if (uri) {
          gtk_recent_manager_add_item(manager, uri);
          g_free(uri);
        }
#endif
      }
#endif
#ifdef MOZ_WIDGET_GTK
      // Use GIO to store the source URI for later display in the file manager.
      GFile* gio_file = g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get());
      nsCString source_uri;
      nsresult rv = aSource->GetSpec(source_uri);
      NS_ENSURE_SUCCESS(rv, rv);
      GFileInfo *file_info = g_file_info_new();
      g_file_info_set_attribute_string(file_info, "metadata::download-uri", source_uri.get());
      g_file_set_attributes_async(gio_file,
                                  file_info,
                                  G_FILE_QUERY_INFO_NONE,
                                  G_PRIORITY_DEFAULT,
                                  nullptr, gio_set_metadata_done, nullptr);
      g_object_unref(file_info);
      g_object_unref(gio_file);
#endif
    }
#endif

#ifdef XP_MACOSX
    // On OS X, make the downloads stack bounce.
    CFStringRef observedObject = ::CFStringCreateWithCString(kCFAllocatorDefault,
                                             NS_ConvertUTF16toUTF8(path).get(),
                                             kCFStringEncodingUTF8);
    CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
    ::CFNotificationCenterPostNotification(center, CFSTR("com.apple.DownloadFileFinished"),
                                           observedObject, nullptr, TRUE);
    ::CFRelease(observedObject);

    // Add OS X origin and referrer file metadata
    CFStringRef pathCFStr = NULL;
    if (!path.IsEmpty()) {
      pathCFStr = ::CFStringCreateWithCharacters(kCFAllocatorDefault,
                                                 (const UniChar*)path.get(),
                                                 path.Length());
    }
    if (pathCFStr && !aIsPrivate) {
      bool isFromWeb = IsURLPossiblyFromWeb(aSource);
      nsCOMPtr<nsIURI> source(aSource);
      nsCOMPtr<nsIURI> referrer(aReferrer);

      rv = mIOThread->Dispatch(NS_NewRunnableFunction(
        "DownloadPlatform::DownloadDone",
        [pathCFStr, isFromWeb, source, referrer, promise]() mutable {
          CFURLRef sourceCFURL = CreateCFURLFromNSIURI(source);
          CFURLRef referrerCFURL = CreateCFURLFromNSIURI(referrer);

          CocoaFileUtils::AddOriginMetadataToFile(pathCFStr,
                                                  sourceCFURL,
                                                  referrerCFURL);
          CocoaFileUtils::AddQuarantineMetadataToFile(pathCFStr,
                                                      sourceCFURL,
                                                      referrerCFURL,
                                                      isFromWeb);
          ::CFRelease(pathCFStr);
          if (sourceCFURL) {
            ::CFRelease(sourceCFURL);
          }
          if (referrerCFURL) {
            ::CFRelease(referrerCFURL);
          }

          DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
            "DownloadPlatform::DownloadDoneResolve",
            [promise = std::move(promise)]() {
              promise->MaybeResolveWithUndefined();
            }
          ));
          MOZ_ASSERT(NS_SUCCEEDED(rv));
          // In non-debug builds, if we've for some reason failed to dispatch
          // a runnable to the main thread to resolve the Promise, then it's
          // unlikely we can reject it either. At that point, the Promise
          // is going to remain in pending limbo until its global goes away.
        }
      ));

      if (NS_SUCCEEDED(rv)) {
        pendingAsyncOperations = true;
      }
    }
#endif
  }

#endif

  if (!pendingAsyncOperations) {
    promise->MaybeResolveWithUndefined();
  }
  promise.forget(aPromise);
  return rv;
}

nsresult DownloadPlatform::MapUrlToZone(const nsAString& aURL,
                                        uint32_t* aZone)
{
#ifdef XP_WIN
  RefPtr<IInternetSecurityManager> inetSecMgr;
  if (FAILED(CoCreateInstance(CLSID_InternetSecurityManager, NULL,
                              CLSCTX_ALL, IID_IInternetSecurityManager,
                              getter_AddRefs(inetSecMgr)))) {
    return NS_ERROR_UNEXPECTED;
  }

  DWORD zone;
  if (inetSecMgr->MapUrlToZone(PromiseFlatString(aURL).get(),
                               &zone, 0) != S_OK) {
    return NS_ERROR_UNEXPECTED;
  } else {
    *aZone = zone;
  }

  return NS_OK;
#else
  return NS_ERROR_NOT_IMPLEMENTED;
#endif
}

// Check if a URI is likely to be web-based, by checking its URI flags.
// If in doubt (e.g. if anything fails during the check) claims things
// are from the web.
bool DownloadPlatform::IsURLPossiblyFromWeb(nsIURI* aURI)
{
  nsCOMPtr<nsIIOService> ios = do_GetIOService();
  nsCOMPtr<nsIURI> uri = aURI;
  if (!ios) {
    return true;
  }

  while (uri) {
    // We're not using nsIIOService::ProtocolHasFlags because it doesn't
    // take per-URI flags into account. We're also not using
    // NS_URIChainHasFlags because we're checking for *any* of 3 flags
    // to be present on *all* of the nested URIs, which it can't do.
    nsAutoCString scheme;
    nsresult rv = uri->GetScheme(scheme);
    if (NS_FAILED(rv)) {
      return true;
    }
    nsCOMPtr<nsIProtocolHandler> ph;
    rv = ios->GetProtocolHandler(scheme.get(), getter_AddRefs(ph));
    if (NS_FAILED(rv)) {
      return true;
    }
    uint32_t flags;
    rv = ph->DoGetProtocolFlags(uri, &flags);
    if (NS_FAILED(rv)) {
      return true;
    }
    // If not dangerous to load, not a UI resource and not a local file,
    // assume this is from the web:
    if (!(flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) &&
        !(flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) &&
        !(flags & nsIProtocolHandler::URI_IS_LOCAL_FILE)) {
      return true;
    }
    // Otherwise, check if the URI is nested, and if so go through
    // the loop again:
    nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
    uri = nullptr;
    if (nestedURI) {
      rv = nestedURI->GetInnerURI(getter_AddRefs(uri));
      if (NS_FAILED(rv)) {
        return true;
      }
    }
  }
  return false;
}