/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "nsReadableUtils.h"

// Local Includes
#include "nsContentAreaDragDrop.h"

// Helper Classes
#include "nsString.h"

// Interfaces needed to be included
#include "nsCopySupport.h"
#include "nsISelectionController.h"
#include "nsPIDOMWindow.h"
#include "nsIFormControl.h"
#include "nsITransferable.h"
#include "nsComponentManagerUtils.h"
#include "nsXPCOM.h"
#include "nsISupportsPrimitives.h"
#include "nsServiceManagerUtils.h"
#include "nsNetUtil.h"
#include "nsIFile.h"
#include "nsFrameLoader.h"
#include "nsFrameLoaderOwner.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsIContentPolicy.h"
#include "nsIImageLoadingContent.h"
#include "nsUnicharUtils.h"
#include "nsIURL.h"
#include "nsIURIMutator.h"
#include "mozilla/dom/Document.h"
#include "nsICookieJarSettings.h"
#include "nsIPrincipal.h"
#include "nsIWebBrowserPersist.h"
#include "nsEscape.h"
#include "nsContentUtils.h"
#include "nsIMIMEService.h"
#include "imgIContainer.h"
#include "imgIRequest.h"
#include "mozilla/dom/DataTransfer.h"
#include "nsIMIMEInfo.h"
#include "nsRange.h"
#include "BrowserParent.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLAreaElement.h"
#include "mozilla/dom/HTMLAnchorElement.h"
#include "mozilla/dom/Selection.h"
#include "nsVariant.h"
#include "nsQueryObject.h"

using namespace mozilla;
using namespace mozilla::dom;
using mozilla::IgnoreErrors;

class MOZ_STACK_CLASS DragDataProducer {
 public:
  DragDataProducer(nsPIDOMWindowOuter* aWindow, nsIContent* aTarget,
                   nsIContent* aSelectionTargetNode, bool aIsAltKeyPressed);
  nsresult Produce(DataTransfer* aDataTransfer, bool* aCanDrag,
                   Selection** aSelection, nsIContent** aDragNode,
                   nsIPrincipal** aPrincipal, nsIContentSecurityPolicy** aCsp,
                   nsICookieJarSettings** aCookieJarSettings);

 private:
  // @param aHidden true, iff the data should be hidden from non-chrome code.
  void AddString(DataTransfer* aDataTransfer, const nsAString& aFlavor,
                 const nsAString& aData, nsIPrincipal* aPrincipal,
                 bool aHidden = false);
  nsresult AddStringsToDataTransfer(nsIContent* aDragNode,
                                    DataTransfer* aDataTransfer);
  nsresult GetImageData(imgIContainer* aImage, imgIRequest* aRequest);
  static nsresult GetDraggableSelectionData(Selection* inSelection,
                                            nsIContent* inRealTargetNode,
                                            nsIContent** outImageOrLinkNode,
                                            bool* outDragSelectedText);
  [[nodiscard]] static nsresult GetAnchorURL(nsIContent* inNode,
                                             nsAString& outURL);
  static void CreateLinkText(const nsAString& inURL, const nsAString& inText,
                             nsAString& outLinkText);

  nsCOMPtr<nsPIDOMWindowOuter> mWindow;
  nsCOMPtr<nsIContent> mTarget;
  nsCOMPtr<nsIContent> mSelectionTargetNode;
  bool mIsAltKeyPressed;

  nsString mUrlString;
  nsString mImageSourceString;
  nsString mImageDestFileName;
#if defined(XP_MACOSX)
  nsString mImageRequestMime;
#endif
  nsString mTitleString;
  // will be filled automatically if you fill urlstring
  nsString mHtmlString;
  nsString mContextString;
  nsString mInfoString;

  bool mIsAnchor;
  nsCOMPtr<imgIContainer> mImage;
};

nsresult nsContentAreaDragDrop::GetDragData(
    nsPIDOMWindowOuter* aWindow, nsIContent* aTarget,
    nsIContent* aSelectionTargetNode, bool aIsAltKeyPressed,
    DataTransfer* aDataTransfer, bool* aCanDrag, Selection** aSelection,
    nsIContent** aDragNode, nsIPrincipal** aPrincipal,
    nsIContentSecurityPolicy** aCsp,
    nsICookieJarSettings** aCookieJarSettings) {
  NS_ENSURE_TRUE(aSelectionTargetNode, NS_ERROR_INVALID_ARG);

  *aCanDrag = true;

  DragDataProducer provider(aWindow, aTarget, aSelectionTargetNode,
                            aIsAltKeyPressed);
  return provider.Produce(aDataTransfer, aCanDrag, aSelection, aDragNode,
                          aPrincipal, aCsp, aCookieJarSettings);
}

NS_IMPL_ISUPPORTS(nsContentAreaDragDropDataProvider, nsIFlavorDataProvider)

// SaveURIToFile
// used on platforms where it's possible to drag items (e.g. images)
// into the file system
nsresult nsContentAreaDragDropDataProvider::SaveURIToFile(
    nsIURI* inSourceURI, nsIPrincipal* inTriggeringPrincipal,
    nsICookieJarSettings* inCookieJarSettings, nsIFile* inDestFile,
    nsContentPolicyType inContentPolicyType, bool isPrivate) {
  nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(inSourceURI);
  if (!sourceURL) {
    return NS_ERROR_NO_INTERFACE;
  }

  nsresult rv = inDestFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
  NS_ENSURE_SUCCESS(rv, rv);

  // we rely on the fact that the WPB is refcounted by the channel etc,
  // so we don't keep a ref to it. It will die when finished.
  nsCOMPtr<nsIWebBrowserPersist> persist = do_CreateInstance(
      "@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  persist->SetPersistFlags(
      nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION);

  // referrer policy can be anything since the referrer is nullptr
  return persist->SaveURI(inSourceURI, inTriggeringPrincipal, 0, nullptr,
                          inCookieJarSettings, nullptr, nullptr, inDestFile,
                          inContentPolicyType, isPrivate);
}

/*
 * Check if the provided filename extension is valid for the MIME type and
 * return the MIME type's primary extension.
 *
 * @param aExtension           [in]  the extension to check
 * @param aMimeType            [in]  the MIME type to check the extension with
 * @param aIsValidExtension    [out] true if |aExtension| is valid for
 *                                   |aMimeType|
 * @param aPrimaryExtension    [out] the primary extension for the MIME type
 *                                   to potentially be used as a replacement
 *                                   for |aExtension|
 */
nsresult CheckAndGetExtensionForMime(const nsCString& aExtension,
                                     const nsCString& aMimeType,
                                     bool* aIsValidExtension,
                                     nsACString* aPrimaryExtension) {
  nsresult rv;

  nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
  if (NS_WARN_IF(!mimeService)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsIMIMEInfo> mimeInfo;
  rv = mimeService->GetFromTypeAndExtension(aMimeType, ""_ns,
                                            getter_AddRefs(mimeInfo));
  NS_ENSURE_SUCCESS(rv, rv);

  mimeInfo->GetPrimaryExtension(*aPrimaryExtension);

  if (aExtension.IsEmpty()) {
    *aIsValidExtension = false;
    return NS_OK;
  }

  rv = mimeInfo->ExtensionExists(aExtension, aIsValidExtension);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

// This is our nsIFlavorDataProvider callback. There are several
// assumptions here that make this work:
//
// 1. Someone put a kFilePromiseURLMime flavor into the transferable
//    with the source URI of the file to save (as a string). We did
//    that in AddStringsToDataTransfer.
//
// 2. Someone put a kFilePromiseDirectoryMime flavor into the
//    transferable with an nsIFile for the directory we are to
//    save in. That has to be done by platform-specific code (in
//    widget), which gets the destination directory from
//    OS-specific drag information.
//
NS_IMETHODIMP
nsContentAreaDragDropDataProvider::GetFlavorData(nsITransferable* aTransferable,
                                                 const char* aFlavor,
                                                 nsISupports** aData) {
  NS_ENSURE_ARG_POINTER(aData);
  *aData = nullptr;

  nsresult rv = NS_ERROR_NOT_IMPLEMENTED;

  if (strcmp(aFlavor, kFilePromiseMime) == 0) {
    // get the URI from the kFilePromiseURLMime flavor
    NS_ENSURE_ARG(aTransferable);
    nsCOMPtr<nsISupports> tmp;
    rv = aTransferable->GetTransferData(kFilePromiseURLMime,
                                        getter_AddRefs(tmp));
    NS_ENSURE_SUCCESS(rv, rv);
    nsCOMPtr<nsISupportsString> supportsString = do_QueryInterface(tmp);
    if (!supportsString) return NS_ERROR_FAILURE;

    nsAutoString sourceURLString;
    supportsString->GetData(sourceURLString);
    if (sourceURLString.IsEmpty()) return NS_ERROR_FAILURE;

    nsCOMPtr<nsIURI> sourceURI;
    rv = NS_NewURI(getter_AddRefs(sourceURI), sourceURLString);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = aTransferable->GetTransferData(kFilePromiseDestFilename,
                                        getter_AddRefs(tmp));
    NS_ENSURE_SUCCESS(rv, rv);
    supportsString = do_QueryInterface(tmp);
    if (!supportsString) return NS_ERROR_FAILURE;

    nsAutoString targetFilename;
    supportsString->GetData(targetFilename);
    if (targetFilename.IsEmpty()) return NS_ERROR_FAILURE;

#if defined(XP_MACOSX)
    // Use the image request's MIME type to ensure the filename's
    // extension is compatible with the OS's handler for this type.
    // If it isn't, or is missing, replace the extension with the
    // primary extension. On Mac, do this in the parent process
    // because sandboxing blocks access to MIME-handler info from
    // content processes.
    if (XRE_IsParentProcess()) {
      rv = aTransferable->GetTransferData(kImageRequestMime,
                                          getter_AddRefs(tmp));
      NS_ENSURE_SUCCESS(rv, rv);
      supportsString = do_QueryInterface(tmp);
      if (!supportsString) return NS_ERROR_FAILURE;

      nsAutoString contentType;
      supportsString->GetData(contentType);

      nsCOMPtr<nsIMIMEService> mimeService =
          do_GetService("@mozilla.org/mime;1");
      if (NS_WARN_IF(!mimeService)) {
        return NS_ERROR_FAILURE;
      }

      mimeService->ValidateFileNameForSaving(
          targetFilename, NS_ConvertUTF16toUTF8(contentType),
          nsIMIMEService::VALIDATE_DEFAULT, targetFilename);
    } else {
      // make the filename safe for the filesystem
      targetFilename.ReplaceChar(
          u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, u'-');
    }
#endif /* defined(XP_MACOSX) */

    // get the target directory from the kFilePromiseDirectoryMime
    // flavor
    nsCOMPtr<nsISupports> dirPrimitive;
    rv = aTransferable->GetTransferData(kFilePromiseDirectoryMime,
                                        getter_AddRefs(dirPrimitive));
    NS_ENSURE_SUCCESS(rv, rv);
    nsCOMPtr<nsIFile> destDirectory = do_QueryInterface(dirPrimitive);
    if (!destDirectory) return NS_ERROR_FAILURE;

    nsCOMPtr<nsIFile> file;
    rv = destDirectory->Clone(getter_AddRefs(file));
    NS_ENSURE_SUCCESS(rv, rv);

    file->Append(targetFilename);

    bool isPrivate = aTransferable->GetIsPrivateData();

    nsCOMPtr<nsIPrincipal> principal = aTransferable->GetRequestingPrincipal();
    nsContentPolicyType contentPolicyType =
        aTransferable->GetContentPolicyType();
    nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
        aTransferable->GetCookieJarSettings();
    rv = SaveURIToFile(sourceURI, principal, cookieJarSettings, file,
                       contentPolicyType, isPrivate);
    // send back an nsIFile
    if (NS_SUCCEEDED(rv)) {
      CallQueryInterface(file, aData);
    }
  }

  return rv;
}

DragDataProducer::DragDataProducer(nsPIDOMWindowOuter* aWindow,
                                   nsIContent* aTarget,
                                   nsIContent* aSelectionTargetNode,
                                   bool aIsAltKeyPressed)
    : mWindow(aWindow),
      mTarget(aTarget),
      mSelectionTargetNode(aSelectionTargetNode),
      mIsAltKeyPressed(aIsAltKeyPressed),
      mIsAnchor(false) {}

static nsIContent* FindDragTarget(nsIContent* aContent) {
  for (nsIContent* content = aContent; content;
       content = content->GetFlattenedTreeParent()) {
    if (nsContentUtils::ContentIsDraggable(content)) {
      return content;
    }
  }
  return nullptr;
}

//
// GetAnchorURL
//
nsresult DragDataProducer::GetAnchorURL(nsIContent* aContent, nsAString& aURL) {
  aURL.Truncate();
  auto* element = Element::FromNodeOrNull(aContent);
  if (!element || !element->IsLink()) {
    return NS_OK;
  }

  nsCOMPtr<nsIURI> linkURI = element->GetHrefURI();
  if (!linkURI) {
    return NS_OK;
  }

  nsAutoCString spec;
  nsresult rv = linkURI->GetSpec(spec);
  NS_ENSURE_SUCCESS(rv, rv);
  nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
  rv = secMan->CheckLoadURIStrWithPrincipal(aContent->NodePrincipal(), spec, 0);
  NS_ENSURE_SUCCESS(rv, rv);
  CopyUTF8toUTF16(spec, aURL);
  return NS_OK;
}

//
// CreateLinkText
//
// Creates the html for an anchor in the form
//  <a href="inURL">inText</a>
//
void DragDataProducer::CreateLinkText(const nsAString& inURL,
                                      const nsAString& inText,
                                      nsAString& outLinkText) {
  // use a temp var in case |inText| is the same string as
  // |outLinkText| to avoid overwriting it while building up the
  // string in pieces.
  nsAutoString linkText(u"<a href=\""_ns + inURL + u"\">"_ns + inText +
                        u"</a>"_ns);

  outLinkText = linkText;
}

nsresult DragDataProducer::GetImageData(imgIContainer* aImage,
                                        imgIRequest* aRequest) {
  nsCOMPtr<nsIURI> imgUri = aRequest->GetURI();

  nsCOMPtr<nsIURL> imgUrl(do_QueryInterface(imgUri));
  if (imgUrl) {
    nsAutoCString spec;
    nsresult rv = imgUrl->GetSpec(spec);
    NS_ENSURE_SUCCESS(rv, rv);

    // pass out the image source string
    CopyUTF8toUTF16(spec, mImageSourceString);

    nsCString mimeType;
    aRequest->GetMimeType(getter_Copies(mimeType));

    nsAutoCString fileName;
    aRequest->GetFileName(fileName);

#if defined(XP_MACOSX)
    // Save the MIME type so we can make sure the extension
    // is compatible (and replace it if it isn't) when the
    // image is dropped. On Mac, we need to get the OS MIME
    // handler information in the parent due to sandboxing.
    CopyUTF8toUTF16(mimeType, mImageRequestMime);
    CopyUTF8toUTF16(fileName, mImageDestFileName);
#else
    nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
    if (NS_WARN_IF(!mimeService)) {
      return NS_ERROR_FAILURE;
    }

    CopyUTF8toUTF16(fileName, mImageDestFileName);
    mimeService->ValidateFileNameForSaving(mImageDestFileName, mimeType,
                                           nsIMIMEService::VALIDATE_DEFAULT,
                                           mImageDestFileName);
#endif

    // and the image object
    mImage = aImage;
  }

  return NS_OK;
}

nsresult DragDataProducer::Produce(DataTransfer* aDataTransfer, bool* aCanDrag,
                                   Selection** aSelection,
                                   nsIContent** aDragNode,
                                   nsIPrincipal** aPrincipal,
                                   nsIContentSecurityPolicy** aCsp,
                                   nsICookieJarSettings** aCookieJarSettings) {
  MOZ_ASSERT(aCanDrag && aSelection && aDataTransfer && aDragNode,
             "null pointer passed to Produce");
  NS_ASSERTION(mWindow, "window not set");
  NS_ASSERTION(mSelectionTargetNode,
               "selection target node should have been set");

  *aDragNode = nullptr;

  nsresult rv;
  nsIContent* dragNode = nullptr;
  *aSelection = nullptr;

  // Find the selection to see what we could be dragging and if what we're
  // dragging is in what is selected. If this is an editable textbox, use
  // the textbox's selection, otherwise use the window's selection.
  RefPtr<Selection> selection;
  nsIContent* editingElement = mSelectionTargetNode->IsEditable()
                                   ? mSelectionTargetNode->GetEditingHost()
                                   : nullptr;
  RefPtr<TextControlElement> textControlElement =
      TextControlElement::GetTextControlElementFromEditingHost(editingElement);
  if (textControlElement) {
    nsISelectionController* selcon =
        textControlElement->GetSelectionController();
    if (selcon) {
      selection =
          selcon->GetSelection(nsISelectionController::SELECTION_NORMAL);
    }

    if (!selection) return NS_OK;
  } else {
    selection = mWindow->GetSelection();
    if (!selection) return NS_OK;

    // Check if the node is inside a form control. Don't set aCanDrag to false
    // however, as we still want to allow the drag.
    nsCOMPtr<nsIContent> findFormNode = mSelectionTargetNode;
    nsIContent* findFormParent = findFormNode->GetParent();
    while (findFormParent) {
      nsCOMPtr<nsIFormControl> form(do_QueryInterface(findFormParent));
      if (form && !form->AllowDraggableChildren()) {
        return NS_OK;
      }
      findFormParent = findFormParent->GetParent();
    }
  }

  // if set, serialize the content under this node
  nsCOMPtr<nsIContent> nodeToSerialize;

  BrowsingContext* bc = mWindow->GetBrowsingContext();
  const bool isChromeShell = bc && bc->IsChrome();

  // In chrome shells, only allow dragging inside editable areas.
  if (isChromeShell && !editingElement) {
    // This path should already be filtered out in
    // EventStateManager::DetermineDragTargetAndDefaultData.
    MOZ_ASSERT_UNREACHABLE("Shouldn't be generating drag data for chrome");
    return NS_OK;
  }

  if (isChromeShell && textControlElement) {
    // Only use the selection if the target node is in the selection.
    if (!selection->ContainsNode(*mSelectionTargetNode, false, IgnoreErrors()))
      return NS_OK;

    selection.swap(*aSelection);
  } else {
    // In content shells, a number of checks are made below to determine
    // whether an image or a link is being dragged. If so, add additional
    // data to the data transfer. This is also done for chrome shells, but
    // only when in a non-textbox editor.

    bool haveSelectedContent = false;

    // possible parent link node
    nsCOMPtr<nsIContent> parentLink;
    nsCOMPtr<nsIContent> draggedNode;

    {
      // only drag form elements by using the alt key,
      // otherwise buttons and select widgets are hard to use

      // Note that while <object> elements implement nsIFormControl, we should
      // really allow dragging them if they happen to be images.
      nsCOMPtr<nsIFormControl> form(do_QueryInterface(mTarget));
      if (form && !mIsAltKeyPressed &&
          form->ControlType() != FormControlType::Object) {
        *aCanDrag = false;
        return NS_OK;
      }

      draggedNode = FindDragTarget(mTarget);
    }

    nsCOMPtr<nsIImageLoadingContent> image;

    nsCOMPtr<nsIContent> selectedImageOrLinkNode;
    GetDraggableSelectionData(selection, mSelectionTargetNode,
                              getter_AddRefs(selectedImageOrLinkNode),
                              &haveSelectedContent);

    // either plain text or anchor text is selected
    if (haveSelectedContent) {
      selection.swap(*aSelection);
    } else if (selectedImageOrLinkNode) {
      // an image is selected
      image = do_QueryInterface(selectedImageOrLinkNode);
    } else {
      // nothing is selected -
      //
      // look for draggable elements under the mouse
      //
      // if the alt key is down, don't start a drag if we're in an
      // anchor because we want to do selection.
      parentLink = nsContentUtils::GetClosestLinkInFlatTree(draggedNode);
      if (parentLink && mIsAltKeyPressed) {
        *aCanDrag = false;
        return NS_OK;
      }
      image = do_QueryInterface(draggedNode);
    }

    {
      // set for linked images, and links
      nsCOMPtr<nsIContent> linkNode;
      if (const auto* areaElem = HTMLAreaElement::FromNodeOrNull(draggedNode)) {
        // use the alt text (or, if missing, the href) as the title
        areaElem->GetAttr(nsGkAtoms::alt, mTitleString);
        if (mTitleString.IsEmpty()) {
          // this can be a relative link
          areaElem->GetAttr(nsGkAtoms::href, mTitleString);
        }

        // gives an absolute link
        nsresult rv = GetAnchorURL(draggedNode, mUrlString);
        NS_ENSURE_SUCCESS(rv, rv);

        // we'll generate HTML like <a href="absurl">alt text</a>
        mIsAnchor = true;

        mHtmlString.AssignLiteral("<a href=\"");
        mHtmlString.Append(mUrlString);
        mHtmlString.AppendLiteral("\">");
        mHtmlString.Append(mTitleString);
        mHtmlString.AppendLiteral("</a>");

        dragNode = draggedNode;
      } else if (image) {
        // grab the href as the url, use alt text as the title of the
        // area if it's there.  the drag data is the image tag and src
        // attribute.
        nsCOMPtr<nsIURI> imageURI;
        image->GetCurrentURI(getter_AddRefs(imageURI));
        nsCOMPtr<Element> imageElement(do_QueryInterface(image));
        if (imageURI) {
          nsAutoCString spec;
          rv = imageURI->GetSpec(spec);
          NS_ENSURE_SUCCESS(rv, rv);
          nsIScriptSecurityManager* secMan =
              nsContentUtils::GetSecurityManager();
          rv = secMan->CheckLoadURIStrWithPrincipal(
              imageElement->NodePrincipal(), spec, 0);
          NS_ENSURE_SUCCESS(rv, rv);
          mIsAnchor = true;
          CopyUTF8toUTF16(spec, mUrlString);
        }

        // XXXbz Shouldn't we use the "title" attr for title?  Using
        // "alt" seems very wrong....
        // XXXbz Also, what if this is an nsIImageLoadingContent
        // that's not an <html:img>?
        if (imageElement) {
          imageElement->GetAttr(nsGkAtoms::alt, mTitleString);
        }

        if (mTitleString.IsEmpty()) {
          mTitleString = mUrlString;
        }

        nsCOMPtr<imgIRequest> imgRequest;

        // grab the image data, and its request.
        nsCOMPtr<imgIContainer> img = nsContentUtils::GetImageFromContent(
            image, getter_AddRefs(imgRequest));
        if (imgRequest) {
          rv = GetImageData(img, imgRequest);
          NS_ENSURE_SUCCESS(rv, rv);
        }

        if (parentLink) {
          // If we are dragging around an image in an anchor, then we
          // are dragging the entire anchor
          linkNode = parentLink;
          nodeToSerialize = linkNode;
        } else {
          nodeToSerialize = draggedNode;
        }
        dragNode = nodeToSerialize;
      } else if (parentLink) {
        // parentLink will always be null if there's selected content
        linkNode = parentLink;
        nodeToSerialize = linkNode;
      } else if (!haveSelectedContent) {
        // nothing draggable
        return NS_OK;
      }

      if (linkNode) {
        rv = GetAnchorURL(linkNode, mUrlString);
        NS_ENSURE_SUCCESS(rv, rv);
        mIsAnchor = true;
        dragNode = linkNode;
      }
    }
  }

  if (nodeToSerialize || *aSelection) {
    mHtmlString.Truncate();
    mContextString.Truncate();
    mInfoString.Truncate();
    mTitleString.Truncate();

    nsCOMPtr<Document> doc = mWindow->GetDoc();
    NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);

    nsCOMPtr<nsIContentSecurityPolicy> csp = doc->GetCsp();
    if (csp) {
      NS_IF_ADDREF(*aCsp = csp);
    }

    nsCOMPtr<nsICookieJarSettings> cookieJarSettings = doc->CookieJarSettings();
    if (cookieJarSettings) {
      NS_IF_ADDREF(*aCookieJarSettings = cookieJarSettings);
    }

    // if we have selected text, use it in preference to the node
    nsCOMPtr<nsITransferable> transferable;
    if (*aSelection) {
      rv = nsCopySupport::GetTransferableForSelection(
          *aSelection, doc, getter_AddRefs(transferable));
    } else {
      rv = nsCopySupport::GetTransferableForNode(nodeToSerialize, doc,
                                                 getter_AddRefs(transferable));
    }
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsISupports> supports;
    nsCOMPtr<nsISupportsString> data;
    rv = transferable->GetTransferData(kHTMLMime, getter_AddRefs(supports));
    data = do_QueryInterface(supports);
    if (NS_SUCCEEDED(rv)) {
      data->GetData(mHtmlString);
    }
    rv = transferable->GetTransferData(kHTMLContext, getter_AddRefs(supports));
    data = do_QueryInterface(supports);
    if (NS_SUCCEEDED(rv)) {
      data->GetData(mContextString);
    }
    rv = transferable->GetTransferData(kHTMLInfo, getter_AddRefs(supports));
    data = do_QueryInterface(supports);
    if (NS_SUCCEEDED(rv)) {
      data->GetData(mInfoString);
    }
    rv = transferable->GetTransferData(kTextMime, getter_AddRefs(supports));
    data = do_QueryInterface(supports);
    NS_ENSURE_SUCCESS(rv, rv);  // require plain text at a minimum
    data->GetData(mTitleString);
  }

  // default text value is the URL
  if (mTitleString.IsEmpty()) {
    mTitleString = mUrlString;
  }

  // if we haven't constructed a html version, make one now
  if (mHtmlString.IsEmpty() && !mUrlString.IsEmpty())
    CreateLinkText(mUrlString, mTitleString, mHtmlString);

  // if there is no drag node, which will be the case for a selection, just
  // use the selection target node.
  rv = AddStringsToDataTransfer(
      dragNode ? dragNode : mSelectionTargetNode.get(), aDataTransfer);
  NS_ENSURE_SUCCESS(rv, rv);

  NS_IF_ADDREF(*aDragNode = dragNode);
  return NS_OK;
}

void DragDataProducer::AddString(DataTransfer* aDataTransfer,
                                 const nsAString& aFlavor,
                                 const nsAString& aData,
                                 nsIPrincipal* aPrincipal, bool aHidden) {
  RefPtr<nsVariantCC> variant = new nsVariantCC();
  variant->SetAsAString(aData);
  aDataTransfer->SetDataWithPrincipal(aFlavor, variant, 0, aPrincipal, aHidden);
}

nsresult DragDataProducer::AddStringsToDataTransfer(
    nsIContent* aDragNode, DataTransfer* aDataTransfer) {
  NS_ASSERTION(aDragNode, "adding strings for null node");

  // set all of the data to have the principal of the node where the data came
  // from
  nsIPrincipal* principal = aDragNode->NodePrincipal();

  // add a special flavor if we're an anchor to indicate that we have
  // a URL in the drag data
  if (!mUrlString.IsEmpty() && mIsAnchor) {
    nsAutoString dragData(mUrlString);
    dragData.Append('\n');
    // Remove leading and trailing newlines in the title and replace them with
    // space in remaining positions - they confuse PlacesUtils::unwrapNodes
    // that expects url\ntitle formatted data for x-moz-url.
    nsAutoString title(mTitleString);
    title.Trim("\r\n");
    title.ReplaceChar(u"\r\n", ' ');
    dragData += title;

    AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kURLMime), dragData,
              principal);
    AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kURLDataMime),
              mUrlString, principal);
    AddString(aDataTransfer,
              NS_LITERAL_STRING_FROM_CSTRING(kURLDescriptionMime), mTitleString,
              principal);
    AddString(aDataTransfer, u"text/uri-list"_ns, mUrlString, principal);
  }

  // add a special flavor for the html context data
  if (!mContextString.IsEmpty())
    AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext),
              mContextString, principal);

  // add a special flavor if we have html info data
  if (!mInfoString.IsEmpty())
    AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kHTMLInfo),
              mInfoString, principal);

  // add the full html
  if (!mHtmlString.IsEmpty())
    AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kHTMLMime),
              mHtmlString, principal);

  // add the plain text. we use the url for text/plain data if an anchor is
  // being dragged, rather than the title text of the link or the alt text for
  // an anchor image.
  AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kTextMime),
            mIsAnchor ? mUrlString : mTitleString, principal);

  // add image data, if present. For now, all we're going to do with
  // this is turn it into a native data flavor, so indicate that with
  // a new flavor so as not to confuse anyone who is really registered
  // for image/gif or image/jpg.
  if (mImage) {
    RefPtr<nsVariantCC> variant = new nsVariantCC();
    variant->SetAsISupports(mImage);
    aDataTransfer->SetDataWithPrincipal(
        NS_LITERAL_STRING_FROM_CSTRING(kNativeImageMime), variant, 0,
        principal);

    // assume the image comes from a file, and add a file promise. We
    // register ourselves as a nsIFlavorDataProvider, and will use the
    // GetFlavorData callback to save the image to disk.

    nsCOMPtr<nsIFlavorDataProvider> dataProvider =
        new nsContentAreaDragDropDataProvider();
    if (dataProvider) {
      RefPtr<nsVariantCC> variant = new nsVariantCC();
      variant->SetAsISupports(dataProvider);
      aDataTransfer->SetDataWithPrincipal(
          NS_LITERAL_STRING_FROM_CSTRING(kFilePromiseMime), variant, 0,
          principal);
    }

    AddString(aDataTransfer,
              NS_LITERAL_STRING_FROM_CSTRING(kFilePromiseURLMime),
              mImageSourceString, principal);
    AddString(aDataTransfer,
              NS_LITERAL_STRING_FROM_CSTRING(kFilePromiseDestFilename),
              mImageDestFileName, principal);
#if defined(XP_MACOSX)
    AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kImageRequestMime),
              mImageRequestMime, principal, /* aHidden= */ true);
#endif

    // if not an anchor, add the image url
    if (!mIsAnchor) {
      AddString(aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kURLDataMime),
                mUrlString, principal);
      AddString(aDataTransfer, u"text/uri-list"_ns, mUrlString, principal);
    }
  }

  return NS_OK;
}

// note that this can return NS_OK, but a null out param (by design)
// static
nsresult DragDataProducer::GetDraggableSelectionData(
    Selection* inSelection, nsIContent* inRealTargetNode,
    nsIContent** outImageOrLinkNode, bool* outDragSelectedText) {
  NS_ENSURE_ARG(inSelection);
  NS_ENSURE_ARG(inRealTargetNode);
  NS_ENSURE_ARG_POINTER(outImageOrLinkNode);

  *outImageOrLinkNode = nullptr;
  *outDragSelectedText = false;

  if (!inSelection->IsCollapsed()) {
    if (inSelection->ContainsNode(*inRealTargetNode, false, IgnoreErrors())) {
      // track down the anchor node, if any, for the url
      nsINode* selectionStart = inSelection->GetAnchorNode();
      nsINode* selectionEnd = inSelection->GetFocusNode();

      // look for a selection around a single node, like an image.
      // in this case, drag the image, rather than a serialization of the HTML
      // XXX generalize this to other draggable element types?
      if (selectionStart == selectionEnd) {
        nsCOMPtr<nsIContent> selStartContent =
            nsIContent::FromNodeOrNull(selectionStart);
        if (selStartContent && selStartContent->HasChildNodes()) {
          // see if just one node is selected
          uint32_t anchorOffset = inSelection->AnchorOffset();
          uint32_t focusOffset = inSelection->FocusOffset();
          if (anchorOffset == focusOffset + 1 ||
              focusOffset == anchorOffset + 1) {
            uint32_t childOffset = std::min(anchorOffset, focusOffset);
            nsIContent* childContent =
                selStartContent->GetChildAt_Deprecated(childOffset);
            // if we find an image, we'll fall into the node-dragging code,
            // rather the the selection-dragging code
            if (nsContentUtils::IsDraggableImage(childContent)) {
              NS_ADDREF(*outImageOrLinkNode = childContent);
              return NS_OK;
            }
          }
        }
      }

      // indicate that a link or text is selected
      *outDragSelectedText = true;
    }
  }

  return NS_OK;
}
