/* -*- 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 "DataTransferItem.h" #include "DataTransferItemList.h" #include "mozilla/Attributes.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ContentEvents.h" #include "mozilla/EventForwards.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/BlobImpl.h" #include "mozilla/dom/DataTransferItemBinding.h" #include "mozilla/dom/Directory.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/FileSystem.h" #include "mozilla/dom/FileSystemDirectoryEntry.h" #include "mozilla/dom/FileSystemFileEntry.h" #include "imgIContainer.h" #include "imgITools.h" #include "nsComponentManagerUtils.h" #include "nsIClipboard.h" #include "nsIFile.h" #include "nsIInputStream.h" #include "nsISupportsPrimitives.h" #include "nsIScriptObjectPrincipal.h" #include "nsNetUtil.h" #include "nsQueryObject.h" #include "nsContentUtils.h" #include "nsThreadUtils.h" #include "nsVariant.h" namespace { struct FileMimeNameData { const char* mMimeName; const char* mFileName; }; FileMimeNameData kFileMimeNameMap[] = { {kFileMime, "GenericFileName"}, {kPNGImageMime, "GenericImageNamePNG"}, }; } // anonymous namespace namespace mozilla::dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DataTransferItem, mData, mPrincipal, mDataTransfer, mCachedFile) NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransferItem) NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransferItem) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransferItem) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END JSObject* DataTransferItem::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return DataTransferItem_Binding::Wrap(aCx, this, aGivenProto); } already_AddRefed DataTransferItem::Clone( DataTransfer* aDataTransfer) const { MOZ_ASSERT(aDataTransfer); RefPtr it = new DataTransferItem(aDataTransfer, mType); // Copy over all of the fields it->mKind = mKind; it->mIndex = mIndex; it->mData = mData; it->mPrincipal = mPrincipal; it->mChromeOnly = mChromeOnly; return it.forget(); } void DataTransferItem::SetData(nsIVariant* aData) { // Invalidate our file cache, we will regenerate it with the new data mCachedFile = nullptr; if (!aData) { // We are holding a temporary null which will later be filled. // These are provided by the system, and have guaranteed properties about // their kind based on their type. MOZ_ASSERT(!mType.IsEmpty()); // This type should not be provided by the OS. MOZ_ASSERT(!mType.EqualsASCII(kNativeImageMime)); mKind = KIND_STRING; for (uint32_t i = 0; i < ArrayLength(kFileMimeNameMap); ++i) { if (mType.EqualsASCII(kFileMimeNameMap[i].mMimeName)) { mKind = KIND_FILE; break; } } mData = nullptr; return; } mData = aData; mKind = KindFromData(mData); } /* static */ DataTransferItem::eKind DataTransferItem::KindFromData( nsIVariant* aData) { nsCOMPtr supports; nsresult rv = aData->GetAsISupports(getter_AddRefs(supports)); if (NS_SUCCEEDED(rv) && supports) { // Check if we have one of the supported file data formats if (RefPtr(do_QueryObject(supports)) || nsCOMPtr(do_QueryInterface(supports)) || nsCOMPtr(do_QueryInterface(supports))) { return KIND_FILE; } if (StaticPrefs::dom_events_dataTransfer_imageAsFile_enabled()) { // Firefox internally uses imgIContainer to represent images being // copied/dragged. These need to be encoded to PNG files. if (nsCOMPtr(do_QueryInterface(supports))) { return KIND_FILE; } } } nsAutoString string; // If we can't get the data type as a string, that means that the object // should be considered to be of the "other" type. This is impossible // through the APIs defined by the spec, but we provide extra Moz* APIs, // which allow setting of non-string data. We determine whether we can // consider it a string, by calling GetAsAString, and checking for success. rv = aData->GetAsAString(string); if (NS_SUCCEEDED(rv)) { return KIND_STRING; } return KIND_OTHER; } void DataTransferItem::FillInExternalData() { if (mData) { return; } NS_ConvertUTF16toUTF8 utf8format(mType); const char* format = utf8format.get(); if (strcmp(format, "text/plain") == 0) { format = kUnicodeMime; } else if (strcmp(format, "text/uri-list") == 0) { format = kURLDataMime; } nsCOMPtr trans = mDataTransfer->GetTransferable(); if (!trans) { trans = do_CreateInstance("@mozilla.org/widget/transferable;1"); if (NS_WARN_IF(!trans)) { return; } trans->Init(nullptr); trans->AddDataFlavor(format); if (mDataTransfer->GetEventMessage() == ePaste) { MOZ_ASSERT(mIndex == 0, "index in clipboard must be 0"); nsCOMPtr clipboard = do_GetService("@mozilla.org/widget/clipboard;1"); if (!clipboard || mDataTransfer->ClipboardType() < 0) { return; } nsresult rv = clipboard->GetData(trans, mDataTransfer->ClipboardType()); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } else { nsCOMPtr dragSession = nsContentUtils::GetDragSession(); if (!dragSession) { return; } nsresult rv = dragSession->GetData(trans, mIndex); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } } nsCOMPtr data; nsresult rv = trans->GetTransferData(format, getter_AddRefs(data)); if (NS_WARN_IF(NS_FAILED(rv) || !data)) { return; } // Fill the variant RefPtr variant = new nsVariantCC(); eKind oldKind = Kind(); if (oldKind == KIND_FILE) { // Because this is an external piece of data, mType is one of kFileMime, // kPNGImageMime, kJPEGImageMime, or kGIFImageMime. Some of these types // are passed in as a nsIInputStream which must be converted to a // dom::File before storing. if (nsCOMPtr istream = do_QueryInterface(data)) { RefPtr file = CreateFileFromInputStream(istream); if (NS_WARN_IF(!file)) { return; } data = do_QueryObject(file); } variant->SetAsISupports(data); } else { // We have an external piece of string data. Extract it and store it in the // variant MOZ_ASSERT(oldKind == KIND_STRING); nsCOMPtr supportsstr = do_QueryInterface(data); if (supportsstr) { nsAutoString str; supportsstr->GetData(str); variant->SetAsAString(str); } else { nsCOMPtr supportscstr = do_QueryInterface(data); if (supportscstr) { nsAutoCString str; supportscstr->GetData(str); variant->SetAsACString(str); } } } SetData(variant); if (oldKind != Kind()) { NS_WARNING( "Clipboard data provided by the OS does not match predicted kind"); mDataTransfer->TypesListMayHaveChanged(); } } void DataTransferItem::GetType(nsAString& aType) { // If we don't have a File, we can just put whatever our recorded internal // type is. if (Kind() != KIND_FILE) { aType = mType; return; } // If we do have a File, then we need to look at our File object to discover // what its mime type is. We can use the System Principal here, as this // information should be avaliable even if the data is currently inaccessible // (for example during a dragover). // // XXX: This seems inefficient, as it seems like we should be able to get this // data without getting the entire File object, which may require talking to // the OS. ErrorResult rv; RefPtr file = GetAsFile(*nsContentUtils::GetSystemPrincipal(), rv); MOZ_ASSERT(!rv.Failed(), "Failed to get file data with system principal"); // If we don't actually have a file, fall back to returning the internal type. if (NS_WARN_IF(!file)) { aType = mType; return; } file->GetType(aType); } already_AddRefed DataTransferItem::GetAsFile( nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { // This is done even if we have an mCachedFile, as it performs the necessary // permissions checks to ensure that we are allowed to access this type. nsCOMPtr data = Data(&aSubjectPrincipal, aRv); if (NS_WARN_IF(!data || aRv.Failed())) { return nullptr; } // We have to check our kind after getting the data, because if we have // external data and the OS lied to us (which unfortunately does happen // sometimes), then we might not have the same type of data as we did coming // into this function. if (NS_WARN_IF(mKind != KIND_FILE)) { return nullptr; } // Generate the dom::File from the stored data, caching it so that the // same object is returned in the future. if (!mCachedFile) { nsCOMPtr supports; aRv = data->GetAsISupports(getter_AddRefs(supports)); MOZ_ASSERT(!aRv.Failed() && supports, "File objects should be stored as nsISupports variants"); if (aRv.Failed() || !supports) { return nullptr; } if (RefPtr blob = do_QueryObject(supports)) { mCachedFile = blob->ToFile(); } else { nsCOMPtr global = GetGlobalFromDataTransfer(); if (NS_WARN_IF(!global)) { return nullptr; } if (nsCOMPtr blobImpl = do_QueryInterface(supports)) { MOZ_ASSERT(blobImpl->IsFile()); mCachedFile = File::Create(global, blobImpl); if (NS_WARN_IF(!mCachedFile)) { return nullptr; } } else if (nsCOMPtr ifile = do_QueryInterface(supports)) { mCachedFile = File::CreateFromFile(global, ifile); if (NS_WARN_IF(!mCachedFile)) { return nullptr; } } else if (nsCOMPtr img = do_QueryInterface(supports)) { nsCOMPtr imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); nsCOMPtr inputStream; nsresult rv = imgTools->EncodeImage(img, "image/png"_ns, u""_ns, getter_AddRefs(inputStream)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } mCachedFile = CreateFileFromInputStream( inputStream, "GenericImageNamePNG", u"image/png"_ns); if (NS_WARN_IF(!mCachedFile)) { return nullptr; } } else { MOZ_ASSERT(false, "One of the above code paths should be taken"); return nullptr; } } } RefPtr file = mCachedFile; return file.forget(); } already_AddRefed DataTransferItem::GetAsEntry( nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { RefPtr file = GetAsFile(aSubjectPrincipal, aRv); if (NS_WARN_IF(aRv.Failed()) || !file) { return nullptr; } nsCOMPtr global = GetGlobalFromDataTransfer(); if (NS_WARN_IF(!global)) { return nullptr; } RefPtr fs = FileSystem::Create(global); RefPtr entry; BlobImpl* impl = file->Impl(); MOZ_ASSERT(impl); if (impl->IsDirectory()) { nsAutoString fullpath; impl->GetMozFullPathInternal(fullpath, aRv); if (aRv.Failed()) { aRv.SuppressException(); return nullptr; } nsCOMPtr directoryFile; // fullPath is already in unicode, we don't have to use // NS_NewNativeLocalFile. nsresult rv = NS_NewLocalFile(fullpath, true, getter_AddRefs(directoryFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } RefPtr directory = Directory::Create(global, directoryFile); entry = new FileSystemDirectoryEntry(global, directory, nullptr, fs); } else { entry = new FileSystemFileEntry(global, file, nullptr, fs); } Sequence> entries; if (!entries.AppendElement(entry, fallible)) { return nullptr; } fs->CreateRoot(entries); return entry.forget(); } already_AddRefed DataTransferItem::CreateFileFromInputStream( nsIInputStream* aStream) { const char* key = nullptr; for (uint32_t i = 0; i < ArrayLength(kFileMimeNameMap); ++i) { if (mType.EqualsASCII(kFileMimeNameMap[i].mMimeName)) { key = kFileMimeNameMap[i].mFileName; break; } } if (!key) { MOZ_ASSERT_UNREACHABLE("Unsupported mime type"); key = "GenericFileName"; } return CreateFileFromInputStream(aStream, key, mType); } already_AddRefed DataTransferItem::CreateFileFromInputStream( nsIInputStream* aStream, const char* aFileNameKey, const nsAString& aContentType) { nsAutoString fileName; nsresult rv = nsContentUtils::GetLocalizedString( nsContentUtils::eDOM_PROPERTIES, aFileNameKey, fileName); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } uint64_t available; void* data = nullptr; rv = NS_ReadInputStreamToBuffer(aStream, &data, -1, &available); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } nsCOMPtr global = GetGlobalFromDataTransfer(); if (NS_WARN_IF(!global)) { return nullptr; } return File::CreateMemoryFileWithLastModifiedNow(global, data, available, fileName, aContentType); } void DataTransferItem::GetAsString(FunctionStringCallback* aCallback, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { if (!aCallback) { return; } // Theoretically this should be done inside of the runnable, as it might be an // expensive operation on some systems, however we wouldn't get access to the // NS_ERROR_DOM_SECURITY_ERROR messages which may be raised by this method. nsCOMPtr data = Data(&aSubjectPrincipal, aRv); if (NS_WARN_IF(!data || aRv.Failed())) { return; } // We have to check our kind after getting the data, because if we have // external data and the OS lied to us (which unfortunately does happen // sometimes), then we might not have the same type of data as we did coming // into this function. if (NS_WARN_IF(mKind != KIND_STRING)) { return; } nsAutoString stringData; nsresult rv = data->GetAsAString(stringData); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // Dispatch the callback to the main thread class GASRunnable final : public Runnable { public: GASRunnable(FunctionStringCallback* aCallback, const nsAString& aStringData) : mozilla::Runnable("GASRunnable"), mCallback(aCallback), mStringData(aStringData) {} // MOZ_CAN_RUN_SCRIPT_BOUNDARY until runnables are opted into // MOZ_CAN_RUN_SCRIPT. See bug 1535398. MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { ErrorResult rv; mCallback->Call(mStringData, rv); NS_WARNING_ASSERTION(!rv.Failed(), "callback failed"); return rv.StealNSResult(); } private: const RefPtr mCallback; nsString mStringData; }; RefPtr runnable = new GASRunnable(aCallback, stringData); // DataTransfer.mParent might be EventTarget, nsIGlobalObject, ClipboardEvent // nsPIDOMWindowOuter, null nsISupports* parent = mDataTransfer->GetParentObject(); nsCOMPtr global = do_QueryInterface(parent); if (parent && !global) { if (nsCOMPtr target = do_QueryInterface(parent)) { global = target->GetOwnerGlobal(); } else if (RefPtr event = do_QueryObject(parent)) { global = event->GetParentObject(); } } if (global) { rv = global->Dispatch(TaskCategory::Other, runnable.forget()); } else { rv = NS_DispatchToMainThread(runnable); } if (NS_FAILED(rv)) { NS_WARNING( "Dispatch to main thread Failed in " "DataTransferItem::GetAsString!"); } } already_AddRefed DataTransferItem::DataNoSecurityCheck() { if (!mData) { FillInExternalData(); } nsCOMPtr data = mData; return data.forget(); } already_AddRefed DataTransferItem::Data(nsIPrincipal* aPrincipal, ErrorResult& aRv) { MOZ_ASSERT(aPrincipal); // If the inbound principal is system, we can skip the below checks, as // they will trivially succeed. if (aPrincipal->IsSystemPrincipal()) { return DataNoSecurityCheck(); } // We should not allow raw data to be accessed from a Protected DataTransfer. // We don't prevent this access if the accessing document is Chrome. if (mDataTransfer->IsProtected()) { return nullptr; } nsCOMPtr variant = DataNoSecurityCheck(); MOZ_ASSERT(!ChromeOnly(), "Non-chrome code shouldn't see a ChromeOnly DataTransferItem"); if (ChromeOnly()) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } bool checkItemPrincipal = mDataTransfer->IsCrossDomainSubFrameDrop() || (mDataTransfer->GetEventMessage() != eDrop && mDataTransfer->GetEventMessage() != ePaste && mDataTransfer->GetEventMessage() != eEditorInput); // Check if the caller is allowed to access the drag data. Callers with // chrome privileges can always read the data. During the // drop event, allow retrieving the data except in the case where the // source of the drag is in a child frame of the caller. In that case, // we only allow access to data of the same principal. During other events, // only allow access to the data with the same principal. // // We don't want to fail with an exception in this siutation, rather we want // to just pretend as though the stored data is "nullptr". This is consistent // with Chrome's behavior and is less surprising for web applications which // don't expect execptions to be raised when performing certain operations. if (Principal() && checkItemPrincipal && !aPrincipal->Subsumes(Principal())) { return nullptr; } if (!variant) { return nullptr; } nsCOMPtr data; nsresult rv = variant->GetAsISupports(getter_AddRefs(data)); if (NS_SUCCEEDED(rv) && data) { nsCOMPtr pt = do_QueryInterface(data); if (pt) { nsIGlobalObject* go = pt->GetOwnerGlobal(); if (NS_WARN_IF(!go)) { return nullptr; } nsCOMPtr sp = do_QueryInterface(go); MOZ_ASSERT(sp, "This cannot fail on the main thread."); nsIPrincipal* dataPrincipal = sp->GetPrincipal(); if (NS_WARN_IF(!dataPrincipal || !aPrincipal->Equals(dataPrincipal))) { return nullptr; } } } return variant.forget(); } already_AddRefed DataTransferItem::GetGlobalFromDataTransfer() { nsCOMPtr global; // This is annoying, but DataTransfer may have various things as parent. nsCOMPtr target = do_QueryInterface(mDataTransfer->GetParentObject()); if (target) { global = target->GetOwnerGlobal(); } else { RefPtr event = do_QueryObject(mDataTransfer->GetParentObject()); if (event) { global = event->GetParentObject(); } } return global.forget(); } } // namespace mozilla::dom