/* -*- 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 "mozilla/ArrayUtils.h" #include "mozilla/BasePrincipal.h" #include "mozilla/BasicEvents.h" #include "mozilla/CheckedInt.h" #include "mozilla/Span.h" #include "mozilla/StaticPrefs_dom.h" #include "DataTransfer.h" #include "nsISupportsPrimitives.h" #include "nsIScriptSecurityManager.h" #include "mozilla/dom/DOMStringList.h" #include "nsArray.h" #include "nsError.h" #include "nsIDragService.h" #include "nsIClipboard.h" #include "nsIXPConnect.h" #include "nsContentUtils.h" #include "nsIContent.h" #include "nsIObjectInputStream.h" #include "nsIObjectOutputStream.h" #include "nsIStorageStream.h" #include "nsStringStream.h" #include "nsCRT.h" #include "nsIScriptObjectPrincipal.h" #include "nsIScriptContext.h" #include "mozilla/dom/Document.h" #include "nsIScriptGlobalObject.h" #include "nsQueryObject.h" #include "nsVariant.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/DataTransferBinding.h" #include "mozilla/dom/DataTransferItemList.h" #include "mozilla/dom/Directory.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/FileList.h" #include "mozilla/dom/IPCBlobUtils.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/OSFileSystem.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/Unused.h" #include "nsComponentManagerUtils.h" #include "nsNetUtil.h" #include "nsReadableUtils.h" namespace mozilla::dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DataTransfer) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DataTransfer) NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) NS_IMPL_CYCLE_COLLECTION_UNLINK(mItems) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDragTarget) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDragImage) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DataTransfer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mItems) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDragTarget) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDragImage) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransfer) NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransfer) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransfer) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(mozilla::dom::DataTransfer) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END // the size of the array const char DataTransfer::sEffects[8][9] = { "none", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"}; // Used for custom clipboard types. enum CustomClipboardTypeId { eCustomClipboardTypeId_None, eCustomClipboardTypeId_String }; static DataTransfer::Mode ModeForEvent(EventMessage aEventMessage) { switch (aEventMessage) { case eCut: case eCopy: case eDragStart: // For these events, we want to be able to add data to the data transfer, // Otherwise, the data is already present. return DataTransfer::Mode::ReadWrite; case eDrop: case ePaste: case ePasteNoFormatting: case eEditorInput: // For these events we want to be able to read the data which is stored in // the DataTransfer, rather than just the type information. return DataTransfer::Mode::ReadOnly; default: return StaticPrefs::dom_events_dataTransfer_protected_enabled() ? DataTransfer::Mode::Protected : DataTransfer::Mode::ReadOnly; } } DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, bool aIsExternal, int32_t aClipboardType) : mParent(aParent), mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), mEffectAllowed(nsIDragService::DRAGDROP_ACTION_UNINITIALIZED), mEventMessage(aEventMessage), mCursorState(false), mMode(ModeForEvent(aEventMessage)), mIsExternal(aIsExternal), mUserCancelled(false), mIsCrossDomainSubFrameDrop(false), mClipboardType(aClipboardType), mDragImageX(0), mDragImageY(0) { mItems = new DataTransferItemList(this); // For external usage, cache the data from the native clipboard or drag. if (mIsExternal && mMode != Mode::ReadWrite) { if (aEventMessage == ePasteNoFormatting) { mEventMessage = ePaste; CacheExternalClipboardFormats(true); } else if (aEventMessage == ePaste) { CacheExternalClipboardFormats(false); } else if (aEventMessage >= eDragDropEventFirst && aEventMessage <= eDragDropEventLast) { CacheExternalDragFormats(); } } } DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, nsITransferable* aTransferable) : mParent(aParent), mTransferable(aTransferable), mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), mEffectAllowed(nsIDragService::DRAGDROP_ACTION_UNINITIALIZED), mEventMessage(aEventMessage), mCursorState(false), mMode(ModeForEvent(aEventMessage)), mIsExternal(true), mUserCancelled(false), mIsCrossDomainSubFrameDrop(false), mClipboardType(-1), mDragImageX(0), mDragImageY(0) { mItems = new DataTransferItemList(this); // XXX Currently, we cannot make DataTransfer grabs mTransferable for long // time because nsITransferable is not cycle collectable but this may // be grabbed by JS. Additionally, the data initializing path is too // complicated (too optimized) for D&D and clipboard. They are cached // only formats first, then, data of all items will be filled by the // items later and by themselves. However, we shouldn't duplicate such // path for saving the maintenance cost. Therefore, we need to treat // that DataTransfer and its items are in external mode. Finally, // release mTransferable and make them in internal mode. CacheTransferableFormats(); FillAllExternalData(); // Now, we have all necessary data of mTransferable. So, we can work as // internal mode. mIsExternal = false; // Release mTransferable because it won't be referred anymore. mTransferable = nullptr; } DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, const nsAString& aString) : mParent(aParent), mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), mEffectAllowed(nsIDragService::DRAGDROP_ACTION_UNINITIALIZED), mEventMessage(aEventMessage), mCursorState(false), mMode(ModeForEvent(aEventMessage)), mIsExternal(false), mUserCancelled(false), mIsCrossDomainSubFrameDrop(false), mClipboardType(-1), mDragImageX(0), mDragImageY(0) { mItems = new DataTransferItemList(this); nsCOMPtr sysPrincipal = nsContentUtils::GetSystemPrincipal(); RefPtr variant = new nsVariantCC(); variant->SetAsAString(aString); DebugOnly rvIgnored = SetDataWithPrincipal(u"text/plain"_ns, variant, 0, sysPrincipal, false); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Failed to set given string to the DataTransfer object"); } DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, const uint32_t aEffectAllowed, bool aCursorState, bool aIsExternal, bool aUserCancelled, bool aIsCrossDomainSubFrameDrop, int32_t aClipboardType, DataTransferItemList* aItems, Element* aDragImage, uint32_t aDragImageX, uint32_t aDragImageY, bool aShowFailAnimation) : mParent(aParent), mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), mEffectAllowed(aEffectAllowed), mEventMessage(aEventMessage), mCursorState(aCursorState), mMode(ModeForEvent(aEventMessage)), mIsExternal(aIsExternal), mUserCancelled(aUserCancelled), mIsCrossDomainSubFrameDrop(aIsCrossDomainSubFrameDrop), mClipboardType(aClipboardType), mDragImage(aDragImage), mDragImageX(aDragImageX), mDragImageY(aDragImageY), mShowFailAnimation(aShowFailAnimation) { MOZ_ASSERT(mParent); MOZ_ASSERT(aItems); // We clone the items array after everything else, so that it has a valid // mParent value mItems = aItems->Clone(this); // The items are copied from aItems into mItems. There is no need to copy // the actual data in the items as the data transfer will be read only. The // dragstart event is the only time when items are // modifiable, but those events should have been using the first constructor // above. NS_ASSERTION(aEventMessage != eDragStart, "invalid event type for DataTransfer constructor"); } DataTransfer::~DataTransfer() = default; // static already_AddRefed DataTransfer::Constructor( const GlobalObject& aGlobal) { RefPtr transfer = new DataTransfer(aGlobal.GetAsSupports(), eCopy, /* is external */ false, /* clipboard type */ -1); transfer->mEffectAllowed = nsIDragService::DRAGDROP_ACTION_NONE; return transfer.forget(); } JSObject* DataTransfer::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return DataTransfer_Binding::Wrap(aCx, this, aGivenProto); } void DataTransfer::SetDropEffect(const nsAString& aDropEffect) { // the drop effect can only be 'none', 'copy', 'move' or 'link'. for (uint32_t e = 0; e <= nsIDragService::DRAGDROP_ACTION_LINK; e++) { if (aDropEffect.EqualsASCII(sEffects[e])) { // don't allow copyMove if (e != (nsIDragService::DRAGDROP_ACTION_COPY | nsIDragService::DRAGDROP_ACTION_MOVE)) { mDropEffect = e; } break; } } } void DataTransfer::SetEffectAllowed(const nsAString& aEffectAllowed) { if (aEffectAllowed.EqualsLiteral("uninitialized")) { mEffectAllowed = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED; return; } static_assert(nsIDragService::DRAGDROP_ACTION_NONE == 0, "DRAGDROP_ACTION_NONE constant is wrong"); static_assert(nsIDragService::DRAGDROP_ACTION_COPY == 1, "DRAGDROP_ACTION_COPY constant is wrong"); static_assert(nsIDragService::DRAGDROP_ACTION_MOVE == 2, "DRAGDROP_ACTION_MOVE constant is wrong"); static_assert(nsIDragService::DRAGDROP_ACTION_LINK == 4, "DRAGDROP_ACTION_LINK constant is wrong"); for (uint32_t e = 0; e < ArrayLength(sEffects); e++) { if (aEffectAllowed.EqualsASCII(sEffects[e])) { mEffectAllowed = e; break; } } } void DataTransfer::GetMozTriggeringPrincipalURISpec( nsAString& aPrincipalURISpec) { nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (!dragSession) { aPrincipalURISpec.Truncate(0); return; } nsCOMPtr principal; dragSession->GetTriggeringPrincipal(getter_AddRefs(principal)); if (!principal) { aPrincipalURISpec.Truncate(0); return; } nsAutoCString spec; principal->GetAsciiSpec(spec); CopyUTF8toUTF16(spec, aPrincipalURISpec); } nsIContentSecurityPolicy* DataTransfer::GetMozCSP() { nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (!dragSession) { return nullptr; } nsCOMPtr csp; dragSession->GetCsp(getter_AddRefs(csp)); return csp; } already_AddRefed DataTransfer::GetFiles( nsIPrincipal& aSubjectPrincipal) { return mItems->Files(&aSubjectPrincipal); } void DataTransfer::GetTypes(nsTArray& aTypes, CallerType aCallerType) const { // When called from bindings, aTypes will be empty, but since we might have // Gecko-internal callers too, clear it to be safe. aTypes.Clear(); return mItems->GetTypes(aTypes, aCallerType); } bool DataTransfer::HasType(const nsAString& aType) const { return mItems->HasType(aType); } bool DataTransfer::HasFile() const { return mItems->HasFile(); } void DataTransfer::GetData(const nsAString& aFormat, nsAString& aData, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) const { // return an empty string if data for the format was not found aData.Truncate(); nsCOMPtr data; nsresult rv = GetDataAtInternal(aFormat, 0, &aSubjectPrincipal, getter_AddRefs(data)); if (NS_FAILED(rv)) { if (rv != NS_ERROR_DOM_INDEX_SIZE_ERR) { aRv.Throw(rv); } return; } if (data) { nsAutoString stringdata; data->GetAsAString(stringdata); // for the URL type, parse out the first URI from the list. The URIs are // separated by newlines nsAutoString lowercaseFormat; nsContentUtils::ASCIIToLower(aFormat, lowercaseFormat); if (lowercaseFormat.EqualsLiteral("url")) { int32_t lastidx = 0, idx; int32_t length = stringdata.Length(); while (lastidx < length) { idx = stringdata.FindChar('\n', lastidx); // lines beginning with # are comments if (stringdata[lastidx] == '#') { if (idx == -1) { break; } } else { if (idx == -1) { aData.Assign(Substring(stringdata, lastidx)); } else { aData.Assign(Substring(stringdata, lastidx, idx - lastidx)); } aData = nsContentUtils::TrimWhitespace(aData, true); return; } lastidx = idx + 1; } } else { aData = stringdata; } } } void DataTransfer::SetData(const nsAString& aFormat, const nsAString& aData, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { RefPtr variant = new nsVariantCC(); variant->SetAsAString(aData); aRv = SetDataAtInternal(aFormat, variant, 0, &aSubjectPrincipal); } void DataTransfer::ClearData(const Optional& aFormat, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { if (IsReadOnly()) { aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR); return; } if (MozItemCount() == 0) { return; } if (aFormat.WasPassed()) { MozClearDataAtHelper(aFormat.Value(), 0, aSubjectPrincipal, aRv); } else { MozClearDataAtHelper(u""_ns, 0, aSubjectPrincipal, aRv); } } void DataTransfer::SetMozCursor(const nsAString& aCursorState) { // Lock the cursor to an arrow during the drag. mCursorState = aCursorState.EqualsLiteral("default"); } already_AddRefed DataTransfer::GetMozSourceNode() { nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (!dragSession) { return nullptr; } nsCOMPtr sourceNode; dragSession->GetSourceNode(getter_AddRefs(sourceNode)); if (sourceNode && !nsContentUtils::LegacyIsCallerNativeCode() && !nsContentUtils::CanCallerAccess(sourceNode)) { return nullptr; } return sourceNode.forget(); } already_AddRefed DataTransfer::GetSourceTopWindowContext() { nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (!dragSession) { return nullptr; } RefPtr sourceTopWindowContext; dragSession->GetSourceTopWindowContext( getter_AddRefs(sourceTopWindowContext)); return sourceTopWindowContext.forget(); } already_AddRefed DataTransfer::MozTypesAt( uint32_t aIndex, ErrorResult& aRv) const { // Only the first item is valid for clipboard events if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || mEventMessage == ePaste)) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } RefPtr types = new DOMStringList(); if (aIndex < MozItemCount()) { // note that you can retrieve the types regardless of their principal const nsTArray>& items = *mItems->MozItemsAt(aIndex); bool addFile = false; for (uint32_t i = 0; i < items.Length(); i++) { // NOTE: The reason why we get the internal type here is because we want // kFileMime to appear in the types list for backwards compatibility // reasons. nsAutoString type; items[i]->GetInternalType(type); if (NS_WARN_IF(!types->Add(type))) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } if (items[i]->Kind() == DataTransferItem::KIND_FILE) { addFile = true; } } if (addFile) { types->Add(u"Files"_ns); } } return types.forget(); } nsresult DataTransfer::GetDataAtNoSecurityCheck(const nsAString& aFormat, uint32_t aIndex, nsIVariant** aData) const { return GetDataAtInternal(aFormat, aIndex, nsContentUtils::GetSystemPrincipal(), aData); } nsresult DataTransfer::GetDataAtInternal(const nsAString& aFormat, uint32_t aIndex, nsIPrincipal* aSubjectPrincipal, nsIVariant** aData) const { *aData = nullptr; if (aFormat.IsEmpty()) { return NS_OK; } if (aIndex >= MozItemCount()) { return NS_ERROR_DOM_INDEX_SIZE_ERR; } // Only the first item is valid for clipboard events if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || mEventMessage == ePaste)) { return NS_ERROR_DOM_INDEX_SIZE_ERR; } nsAutoString format; GetRealFormat(aFormat, format); MOZ_ASSERT(aSubjectPrincipal); RefPtr item = mItems->MozItemByTypeAt(format, aIndex); if (!item) { // The index exists but there's no data for the specified format, in this // case we just return undefined return NS_OK; } // If we have chrome only content, and we aren't chrome, don't allow access if (!aSubjectPrincipal->IsSystemPrincipal() && item->ChromeOnly()) { return NS_OK; } // DataTransferItem::Data() handles the principal checks ErrorResult result; nsCOMPtr data = item->Data(aSubjectPrincipal, result); if (NS_WARN_IF(!data || result.Failed())) { return result.StealNSResult(); } data.forget(aData); return NS_OK; } void DataTransfer::MozGetDataAt(JSContext* aCx, const nsAString& aFormat, uint32_t aIndex, JS::MutableHandle aRetval, mozilla::ErrorResult& aRv) { nsCOMPtr data; aRv = GetDataAtInternal(aFormat, aIndex, nsContentUtils::GetSystemPrincipal(), getter_AddRefs(data)); if (aRv.Failed()) { return; } if (!data) { aRetval.setNull(); return; } JS::Rooted result(aCx); if (!VariantToJsval(aCx, data, aRetval)) { aRv = NS_ERROR_FAILURE; return; } } /* static */ bool DataTransfer::PrincipalMaySetData(const nsAString& aType, nsIVariant* aData, nsIPrincipal* aPrincipal) { if (!aPrincipal->IsSystemPrincipal()) { DataTransferItem::eKind kind = DataTransferItem::KindFromData(aData); if (kind == DataTransferItem::KIND_OTHER) { NS_WARNING("Disallowing adding non string/file types to DataTransfer"); return false; } // Don't allow adding internal types of the form */x-moz-*, but // special-case the url types as they are simple variations of urls. // In addition, allow x-moz-place flavors to be added by WebExtensions. if (FindInReadable(kInternal_Mimetype_Prefix, aType) && !StringBeginsWith(aType, u"text/x-moz-url"_ns)) { auto principal = BasePrincipal::Cast(aPrincipal); if (!principal->AddonPolicy() || !StringBeginsWith(aType, u"text/x-moz-place"_ns)) { NS_WARNING("Disallowing adding this type to DataTransfer"); return false; } } } return true; } void DataTransfer::TypesListMayHaveChanged() { DataTransfer_Binding::ClearCachedTypesValue(this); } already_AddRefed DataTransfer::MozCloneForEvent( const nsAString& aEvent, ErrorResult& aRv) { RefPtr atomEvt = NS_Atomize(aEvent); if (!atomEvt) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } EventMessage eventMessage = nsContentUtils::GetEventMessage(atomEvt); RefPtr dt; nsresult rv = Clone(mParent, eventMessage, false, false, getter_AddRefs(dt)); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } return dt.forget(); } // The order of the types matters. `kFileMime` needs to be one of the first two // types. static const char* kNonPlainTextExternalFormats[] = { kCustomTypesMime, kFileMime, kHTMLMime, kRTFMime, kURLMime, kURLDataMime, kTextMime, kPNGImageMime, kPDFJSMime}; /* static */ void DataTransfer::GetExternalClipboardFormats(const int32_t& aWhichClipboard, const bool& aPlainTextOnly, nsTArray* aResult) { MOZ_ASSERT(aResult); // NOTE: When you change this method, you may need to change // GetExternalTransferableFormats() too since those methods should // work similarly. nsCOMPtr clipboard = do_GetService("@mozilla.org/widget/clipboard;1"); if (!clipboard || aWhichClipboard < 0) { return; } if (aPlainTextOnly) { bool hasType; AutoTArray textMime = {nsDependentCString(kTextMime)}; nsresult rv = clipboard->HasDataMatchingFlavors(textMime, aWhichClipboard, &hasType); NS_SUCCEEDED(rv); if (hasType) { aResult->AppendElement(kTextMime); } return; } // If not plain text only, then instead check all the other types for (uint32_t f = 0; f < mozilla::ArrayLength(kNonPlainTextExternalFormats); ++f) { bool hasType; AutoTArray format = { nsDependentCString(kNonPlainTextExternalFormats[f])}; nsresult rv = clipboard->HasDataMatchingFlavors(format, aWhichClipboard, &hasType); NS_SUCCEEDED(rv); if (hasType) { aResult->AppendElement(kNonPlainTextExternalFormats[f]); } } } /* static */ void DataTransfer::GetExternalTransferableFormats( nsITransferable* aTransferable, bool aPlainTextOnly, nsTArray* aResult) { MOZ_ASSERT(aTransferable); MOZ_ASSERT(aResult); aResult->Clear(); // NOTE: When you change this method, you may need to change // GetExternalClipboardFormats() too since those methods should // work similarly. AutoTArray flavors; aTransferable->FlavorsTransferableCanExport(flavors); if (aPlainTextOnly) { auto index = flavors.IndexOf(nsLiteralCString(kTextMime)); if (index != flavors.NoIndex) { aResult->AppendElement(nsLiteralCString(kTextMime)); } return; } // If not plain text only, then instead check all the other types for (const char* format : kNonPlainTextExternalFormats) { auto index = flavors.IndexOf(nsCString(format)); if (index != flavors.NoIndex) { aResult->AppendElement(nsCString(format)); } } } nsresult DataTransfer::SetDataAtInternal(const nsAString& aFormat, nsIVariant* aData, uint32_t aIndex, nsIPrincipal* aSubjectPrincipal) { if (aFormat.IsEmpty()) { return NS_OK; } if (IsReadOnly()) { return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; } // Specifying an index less than the current length will replace an existing // item. Specifying an index equal to the current length will add a new item. if (aIndex > MozItemCount()) { return NS_ERROR_DOM_INDEX_SIZE_ERR; } // Only the first item is valid for clipboard events if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || mEventMessage == ePaste)) { return NS_ERROR_DOM_INDEX_SIZE_ERR; } // Don't allow the custom type to be assigned. if (aFormat.EqualsLiteral(kCustomTypesMime)) { return NS_ERROR_DOM_NOT_SUPPORTED_ERR; } if (!PrincipalMaySetData(aFormat, aData, aSubjectPrincipal)) { return NS_ERROR_DOM_SECURITY_ERR; } return SetDataWithPrincipal(aFormat, aData, aIndex, aSubjectPrincipal); } void DataTransfer::MozSetDataAt(JSContext* aCx, const nsAString& aFormat, JS::Handle aData, uint32_t aIndex, ErrorResult& aRv) { nsCOMPtr data; aRv = nsContentUtils::XPConnect()->JSValToVariant(aCx, aData, getter_AddRefs(data)); if (!aRv.Failed()) { aRv = SetDataAtInternal(aFormat, data, aIndex, nsContentUtils::GetSystemPrincipal()); } } void DataTransfer::MozClearDataAt(const nsAString& aFormat, uint32_t aIndex, ErrorResult& aRv) { if (IsReadOnly()) { aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR); return; } if (aIndex >= MozItemCount()) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return; } // Only the first item is valid for clipboard events if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || mEventMessage == ePaste)) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return; } MozClearDataAtHelper(aFormat, aIndex, *nsContentUtils::GetSystemPrincipal(), aRv); // If we just cleared the 0-th index, and there are still more than 1 indexes // remaining, MozClearDataAt should cause the 1st index to become the 0th // index. This should _only_ happen when the MozClearDataAt function is // explicitly called by script, as this behavior is inconsistent with spec. // (however, so is the MozClearDataAt API) if (aIndex == 0 && mItems->MozItemCount() > 1 && mItems->MozItemsAt(0)->Length() == 0) { mItems->PopIndexZero(); } } void DataTransfer::MozClearDataAtHelper(const nsAString& aFormat, uint32_t aIndex, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { MOZ_ASSERT(!IsReadOnly()); MOZ_ASSERT(aIndex < MozItemCount()); MOZ_ASSERT(aIndex == 0 || (mEventMessage != eCut && mEventMessage != eCopy && mEventMessage != ePaste)); nsAutoString format; GetRealFormat(aFormat, format); mItems->MozRemoveByTypeAt(format, aIndex, aSubjectPrincipal, aRv); } void DataTransfer::SetDragImage(Element& aImage, int32_t aX, int32_t aY) { if (!IsReadOnly()) { mDragImage = &aImage; mDragImageX = aX; mDragImageY = aY; } } void DataTransfer::UpdateDragImage(Element& aImage, int32_t aX, int32_t aY) { if (mEventMessage < eDragDropEventFirst || mEventMessage > eDragDropEventLast) { return; } nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (dragSession) { dragSession->UpdateDragImage(&aImage, aX, aY); } } void DataTransfer::AddElement(Element& aElement, ErrorResult& aRv) { if (IsReadOnly()) { aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR); return; } mDragTarget = &aElement; } nsresult DataTransfer::Clone(nsISupports* aParent, EventMessage aEventMessage, bool aUserCancelled, bool aIsCrossDomainSubFrameDrop, DataTransfer** aNewDataTransfer) { RefPtr newDataTransfer = new DataTransfer( aParent, aEventMessage, mEffectAllowed, mCursorState, mIsExternal, aUserCancelled, aIsCrossDomainSubFrameDrop, mClipboardType, mItems, mDragImage, mDragImageX, mDragImageY, mShowFailAnimation); newDataTransfer.forget(aNewDataTransfer); return NS_OK; } already_AddRefed DataTransfer::GetTransferables( nsINode* aDragTarget) { MOZ_ASSERT(aDragTarget); Document* doc = aDragTarget->GetComposedDoc(); if (!doc) { return nullptr; } return GetTransferables(doc->GetLoadContext()); } already_AddRefed DataTransfer::GetTransferables( nsILoadContext* aLoadContext) { nsCOMPtr transArray = nsArray::Create(); if (!transArray) { return nullptr; } uint32_t count = MozItemCount(); for (uint32_t i = 0; i < count; i++) { nsCOMPtr transferable = GetTransferable(i, aLoadContext); if (transferable) { transArray->AppendElement(transferable); } } return transArray.forget(); } already_AddRefed DataTransfer::GetTransferable( uint32_t aIndex, nsILoadContext* aLoadContext) { if (aIndex >= MozItemCount()) { return nullptr; } const nsTArray>& item = *mItems->MozItemsAt(aIndex); uint32_t count = item.Length(); if (!count) { return nullptr; } nsCOMPtr transferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); if (!transferable) { return nullptr; } transferable->Init(aLoadContext); nsCOMPtr storageStream; nsCOMPtr stream; bool added = false; bool handlingCustomFormats = true; // When writing the custom data, we need to ensure that there is sufficient // space for a (uint32_t) data ending type, and the null byte character at // the end of the nsCString. We claim that space upfront and store it in // baseLength. This value will be set to zero if a write error occurs // indicating that the data and length are no longer valid. const uint32_t baseLength = sizeof(uint32_t) + 1; uint32_t totalCustomLength = baseLength; /* * Two passes are made here to iterate over all of the types. First, look for * any types that are not in the list of known types. For this pass, * handlingCustomFormats will be true. Data that corresponds to unknown types * will be pulled out and inserted into a single type (kCustomTypesMime) by * writing the data into a stream. * * The second pass will iterate over the formats looking for known types. * These are added as is. The unknown types are all then inserted as a single * type (kCustomTypesMime) in the same position of the first custom type. This * model is used to maintain the format order as best as possible. * * The format of the kCustomTypesMime type is one or more of the following * stored sequentially: * <32-bit> type (only none or string is supported) * <32-bit> length of format * format * <32-bit> length of data * data * A type of eCustomClipboardTypeId_None ends the list, without any following * data. */ do { for (uint32_t f = 0; f < count; f++) { RefPtr formatitem = item[f]; nsCOMPtr variant = formatitem->DataNoSecurityCheck(); if (!variant) { // skip empty items continue; } nsAutoString type; formatitem->GetInternalType(type); // If the data is of one of the well-known formats, use it directly. bool isCustomFormat = true; for (const char* format : kKnownFormats) { if (type.EqualsASCII(format)) { isCustomFormat = false; break; } } uint32_t lengthInBytes; nsCOMPtr convertedData; if (handlingCustomFormats) { if (!ConvertFromVariant(variant, getter_AddRefs(convertedData), &lengthInBytes)) { continue; } // When handling custom types, add the data to the stream if this is a // custom type. If totalCustomLength is 0, then a write error occurred // on a previous item, so ignore any others. if (isCustomFormat && totalCustomLength > 0) { // If it isn't a string, just ignore it. The dataTransfer is cached in // the drag sesion during drag-and-drop, so non-strings will be // available when dragging locally. nsCOMPtr str(do_QueryInterface(convertedData)); if (str) { nsAutoString data; str->GetData(data); if (!stream) { // Create a storage stream to write to. NS_NewStorageStream(1024, UINT32_MAX, getter_AddRefs(storageStream)); nsCOMPtr outputStream; storageStream->GetOutputStream(0, getter_AddRefs(outputStream)); stream = NS_NewObjectOutputStream(outputStream); } CheckedInt formatLength = CheckedInt(type.Length()) * sizeof(nsString::char_type); // The total size of the stream is the format length, the data // length, two integers to hold the lengths and one integer for // the string flag. Guard against large data by ignoring any that // don't fit. CheckedInt newSize = formatLength + totalCustomLength + lengthInBytes + (sizeof(uint32_t) * 3); if (newSize.isValid()) { // If a write error occurs, set totalCustomLength to 0 so that // further processing gets ignored. nsresult rv = stream->Write32(eCustomClipboardTypeId_String); if (NS_WARN_IF(NS_FAILED(rv))) { totalCustomLength = 0; continue; } rv = stream->Write32(formatLength.value()); if (NS_WARN_IF(NS_FAILED(rv))) { totalCustomLength = 0; continue; } MOZ_ASSERT(formatLength.isValid() && formatLength.value() == type.Length() * sizeof(nsString::char_type), "Why is formatLength off?"); rv = stream->WriteBytes( AsBytes(Span(type.BeginReading(), type.Length()))); if (NS_WARN_IF(NS_FAILED(rv))) { totalCustomLength = 0; continue; } rv = stream->Write32(lengthInBytes); if (NS_WARN_IF(NS_FAILED(rv))) { totalCustomLength = 0; continue; } // XXXbz it's not obvious to me that lengthInBytes is the actual // length of "data" if the variant contained an nsISupportsString // as VTYPE_INTERFACE, say. We used lengthInBytes above for // sizing, so just keep doing that. rv = stream->WriteBytes( Span(reinterpret_cast(data.BeginReading()), lengthInBytes)); if (NS_WARN_IF(NS_FAILED(rv))) { totalCustomLength = 0; continue; } totalCustomLength = newSize.value(); } } } } else if (isCustomFormat && stream) { // This is the second pass of the loop (handlingCustomFormats is false). // When encountering the first custom format, append all of the stream // at this position. If totalCustomLength is 0 indicating a write error // occurred, or no data has been added to it, don't output anything, if (totalCustomLength > baseLength) { // Write out an end of data terminator. nsresult rv = stream->Write32(eCustomClipboardTypeId_None); if (NS_SUCCEEDED(rv)) { nsCOMPtr inputStream; storageStream->NewInputStream(0, getter_AddRefs(inputStream)); RefPtr stringBuffer = nsStringBuffer::Alloc(totalCustomLength); // Subtract off the null terminator when reading. totalCustomLength--; // Read the data from the stream and add a null-terminator as // ToString needs it. uint32_t amountRead; rv = inputStream->Read(static_cast(stringBuffer->Data()), totalCustomLength, &amountRead); if (NS_SUCCEEDED(rv)) { static_cast(stringBuffer->Data())[amountRead] = 0; nsCString str; stringBuffer->ToString(totalCustomLength, str); nsCOMPtr strSupports( do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); strSupports->SetData(str); nsresult rv = transferable->SetTransferData(kCustomTypesMime, strSupports); if (NS_FAILED(rv)) { return nullptr; } added = true; } } } // Clear the stream so it doesn't get used again. stream = nullptr; } else { // This is the second pass of the loop and a known type is encountered. // Add it as is. if (!ConvertFromVariant(variant, getter_AddRefs(convertedData), &lengthInBytes)) { continue; } NS_ConvertUTF16toUTF8 format(type); // If a converter is set for a format, set the converter for the // transferable and don't add the item nsCOMPtr converter = do_QueryInterface(convertedData); if (converter) { transferable->AddDataFlavor(format.get()); transferable->SetConverter(converter); continue; } nsresult rv = transferable->SetTransferData(format.get(), convertedData); if (NS_FAILED(rv)) { return nullptr; } added = true; } } handlingCustomFormats = !handlingCustomFormats; } while (!handlingCustomFormats); // only return the transferable if data was successfully added to it if (added) { return transferable.forget(); } return nullptr; } bool DataTransfer::ConvertFromVariant(nsIVariant* aVariant, nsISupports** aSupports, uint32_t* aLength) const { *aSupports = nullptr; *aLength = 0; uint16_t type = aVariant->GetDataType(); if (type == nsIDataType::VTYPE_INTERFACE || type == nsIDataType::VTYPE_INTERFACE_IS) { nsCOMPtr data; if (NS_FAILED(aVariant->GetAsISupports(getter_AddRefs(data)))) { return false; } // For flavour data providers, use 0 as the length. if (nsCOMPtr fdp = do_QueryInterface(data)) { fdp.forget(aSupports); *aLength = 0; return true; } // Only use the underlying BlobImpl for transferables. if (RefPtr blob = do_QueryObject(data)) { RefPtr blobImpl = blob->Impl(); blobImpl.forget(aSupports); } else { data.forget(aSupports); } *aLength = sizeof(nsISupports*); return true; } nsAutoString str; nsresult rv = aVariant->GetAsAString(str); if (NS_FAILED(rv)) { return false; } nsCOMPtr strSupports( do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); if (!strSupports) { return false; } strSupports->SetData(str); strSupports.forget(aSupports); // each character is two bytes *aLength = str.Length() * 2; return true; } void DataTransfer::Disconnect() { SetMode(Mode::Protected); if (StaticPrefs::dom_events_dataTransfer_protected_enabled()) { ClearAll(); } } void DataTransfer::ClearAll() { mItems->ClearAllItems(); } uint32_t DataTransfer::MozItemCount() const { return mItems->MozItemCount(); } nsresult DataTransfer::SetDataWithPrincipal(const nsAString& aFormat, nsIVariant* aData, uint32_t aIndex, nsIPrincipal* aPrincipal, bool aHidden) { nsAutoString format; GetRealFormat(aFormat, format); ErrorResult rv; RefPtr item = mItems->SetDataWithPrincipal(format, aData, aIndex, aPrincipal, /* aInsertOnly = */ false, aHidden, rv); return rv.StealNSResult(); } void DataTransfer::SetDataWithPrincipalFromOtherProcess( const nsAString& aFormat, nsIVariant* aData, uint32_t aIndex, nsIPrincipal* aPrincipal, bool aHidden) { if (aFormat.EqualsLiteral(kCustomTypesMime)) { FillInExternalCustomTypes(aData, aIndex, aPrincipal); } else { nsAutoString format; GetRealFormat(aFormat, format); ErrorResult rv; RefPtr item = mItems->SetDataWithPrincipal(format, aData, aIndex, aPrincipal, /* aInsertOnly = */ false, aHidden, rv); if (NS_WARN_IF(rv.Failed())) { rv.SuppressException(); } } } void DataTransfer::GetRealFormat(const nsAString& aInFormat, nsAString& aOutFormat) const { // For compatibility, treat text/unicode as equivalent to text/plain nsAutoString lowercaseFormat; nsContentUtils::ASCIIToLower(aInFormat, lowercaseFormat); if (lowercaseFormat.EqualsLiteral("text") || lowercaseFormat.EqualsLiteral("text/unicode")) { aOutFormat.AssignLiteral("text/plain"); return; } if (lowercaseFormat.EqualsLiteral("url")) { aOutFormat.AssignLiteral("text/uri-list"); return; } aOutFormat.Assign(lowercaseFormat); } nsresult DataTransfer::CacheExternalData(const char* aFormat, uint32_t aIndex, nsIPrincipal* aPrincipal, bool aHidden) { ErrorResult rv; RefPtr item; if (strcmp(aFormat, kTextMime) == 0) { item = mItems->SetDataWithPrincipal(u"text/plain"_ns, nullptr, aIndex, aPrincipal, false, aHidden, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } return NS_OK; } if (strcmp(aFormat, kURLDataMime) == 0) { item = mItems->SetDataWithPrincipal(u"text/uri-list"_ns, nullptr, aIndex, aPrincipal, false, aHidden, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } return NS_OK; } nsAutoString format; GetRealFormat(NS_ConvertUTF8toUTF16(aFormat), format); item = mItems->SetDataWithPrincipal(format, nullptr, aIndex, aPrincipal, false, aHidden, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } return NS_OK; } void DataTransfer::CacheExternalDragFormats() { // Called during the constructor to cache the formats available from an // external drag. The data associated with each format will be set to null. // This data will instead only be retrieved in FillInExternalDragData when // asked for, as it may be time consuming for the source application to // generate it. nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (!dragSession) { return; } // make sure that the system principal is used for external drags nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); nsCOMPtr sysPrincipal; ssm->GetSystemPrincipal(getter_AddRefs(sysPrincipal)); // there isn't a way to get a list of the formats that might be available on // all platforms, so just check for the types that can actually be imported // XXXndeakin there are some other formats but those are platform specific. // NOTE: kFileMime must have index 0 // TODO: should this be `kNonPlainTextExternalFormats` instead? static const char* formats[] = {kFileMime, kHTMLMime, kURLMime, kURLDataMime, kTextMime, kPNGImageMime}; uint32_t count; dragSession->GetNumDropItems(&count); for (uint32_t c = 0; c < count; c++) { bool hasFileData = false; dragSession->IsDataFlavorSupported(kFileMime, &hasFileData); // First, check for the special format that holds custom types. bool supported; dragSession->IsDataFlavorSupported(kCustomTypesMime, &supported); if (supported) { FillInExternalCustomTypes(c, sysPrincipal); } for (uint32_t f = 0; f < ArrayLength(formats); f++) { // IsDataFlavorSupported doesn't take an index as an argument and just // checks if any of the items support a particular flavor, even though // the GetData method does take an index. Here, we just assume that // every item being dragged has the same set of flavors. bool supported; dragSession->IsDataFlavorSupported(formats[f], &supported); // if the format is supported, add an item to the array with null as // the data. When retrieved, GetRealData will read the data. if (supported) { CacheExternalData(formats[f], c, sysPrincipal, /* hidden = */ f && hasFileData); } } } } void DataTransfer::CacheExternalClipboardFormats(bool aPlainTextOnly) { // Called during the constructor for paste events to cache the formats // available on the clipboard. As with CacheExternalDragFormats, the // data will only be retrieved when needed. NS_ASSERTION(mEventMessage == ePaste, "caching clipboard data for invalid event"); nsCOMPtr sysPrincipal = nsContentUtils::GetSystemPrincipal(); nsTArray typesArray; if (XRE_IsContentProcess()) { ContentChild::GetSingleton()->SendGetExternalClipboardFormats( mClipboardType, aPlainTextOnly, &typesArray); } else { GetExternalClipboardFormats(mClipboardType, aPlainTextOnly, &typesArray); } if (aPlainTextOnly) { // The only thing that will be in types is kTextMime MOZ_ASSERT(typesArray.IsEmpty() || typesArray.Length() == 1); if (typesArray.Length() == 1) { CacheExternalData(kTextMime, 0, sysPrincipal, false); } return; } CacheExternalData(typesArray, sysPrincipal); } void DataTransfer::CacheTransferableFormats() { nsCOMPtr sysPrincipal = nsContentUtils::GetSystemPrincipal(); AutoTArray typesArray; GetExternalTransferableFormats(mTransferable, false, &typesArray); CacheExternalData(typesArray, sysPrincipal); } void DataTransfer::CacheExternalData(const nsTArray& aTypes, nsIPrincipal* aPrincipal) { bool hasFileData = false; for (const nsCString& type : aTypes) { if (type.EqualsLiteral(kCustomTypesMime)) { FillInExternalCustomTypes(0, aPrincipal); } else if (type.EqualsLiteral(kFileMime) && XRE_IsContentProcess() && !StaticPrefs::dom_events_dataTransfer_mozFile_enabled()) { // We will be ignoring any application/x-moz-file files found in the paste // datatransfer within e10s, as they will fail top be sent over IPC. // Because of that, we will unset hasFileData, whether or not it would // have been set. (bug 1308007) hasFileData = false; continue; } else { // We expect that if kFileMime is supported, then it will be the either at // index 0 or at index 1 in the aTypes returned by // GetExternalClipboardFormats if (type.EqualsLiteral(kFileMime)) { hasFileData = true; } // If we aren't the file data, and we have file data, we want to be hidden CacheExternalData( type.get(), 0, aPrincipal, /* hidden = */ !type.EqualsLiteral(kFileMime) && hasFileData); } } } void DataTransfer::FillAllExternalData() { if (mIsExternal) { for (uint32_t i = 0; i < MozItemCount(); ++i) { const nsTArray>& items = *mItems->MozItemsAt(i); for (uint32_t j = 0; j < items.Length(); ++j) { MOZ_ASSERT(items[j]->Index() == i); items[j]->FillInExternalData(); } } } } void DataTransfer::FillInExternalCustomTypes(uint32_t aIndex, nsIPrincipal* aPrincipal) { RefPtr item = new DataTransferItem( this, NS_LITERAL_STRING_FROM_CSTRING(kCustomTypesMime), DataTransferItem::KIND_STRING); item->SetIndex(aIndex); nsCOMPtr variant = item->DataNoSecurityCheck(); if (!variant) { return; } FillInExternalCustomTypes(variant, aIndex, aPrincipal); } void DataTransfer::FillInExternalCustomTypes(nsIVariant* aData, uint32_t aIndex, nsIPrincipal* aPrincipal) { char* chrs; uint32_t len = 0; nsresult rv = aData->GetAsStringWithSize(&len, &chrs); if (NS_FAILED(rv)) { return; } CheckedInt checkedLen(len); if (!checkedLen.isValid()) { return; } nsCOMPtr stringStream; NS_NewByteInputStream(getter_AddRefs(stringStream), Span(chrs, checkedLen.value()), NS_ASSIGNMENT_ADOPT); nsCOMPtr stream = NS_NewObjectInputStream(stringStream); uint32_t type; do { rv = stream->Read32(&type); NS_ENSURE_SUCCESS_VOID(rv); if (type == eCustomClipboardTypeId_String) { uint32_t formatLength; rv = stream->Read32(&formatLength); NS_ENSURE_SUCCESS_VOID(rv); char* formatBytes; rv = stream->ReadBytes(formatLength, &formatBytes); NS_ENSURE_SUCCESS_VOID(rv); nsAutoString format; format.Adopt(reinterpret_cast(formatBytes), formatLength / sizeof(char16_t)); uint32_t dataLength; rv = stream->Read32(&dataLength); NS_ENSURE_SUCCESS_VOID(rv); char* dataBytes; rv = stream->ReadBytes(dataLength, &dataBytes); NS_ENSURE_SUCCESS_VOID(rv); nsAutoString data; data.Adopt(reinterpret_cast(dataBytes), dataLength / sizeof(char16_t)); RefPtr variant = new nsVariantCC(); rv = variant->SetAsAString(data); NS_ENSURE_SUCCESS_VOID(rv); SetDataWithPrincipal(format, variant, aIndex, aPrincipal); } } while (type != eCustomClipboardTypeId_None); } void DataTransfer::SetMode(DataTransfer::Mode aMode) { if (!StaticPrefs::dom_events_dataTransfer_protected_enabled() && aMode == Mode::Protected) { mMode = Mode::ReadOnly; } else { mMode = aMode; } } } // namespace mozilla::dom