Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "DataTransferItemList.h"
#include "nsContentUtils.h"
#include "nsIGlobalObject.h"
#include "nsIScriptObjectPrincipal.h"
#include "nsIScriptGlobalObject.h"
#include "nsIScriptContext.h"
#include "nsQueryObject.h"
#include "nsVariant.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/EventForwards.h"
#include "mozilla/storage/Variant.h"
#include "mozilla/dom/DataTransferItemListBinding.h"
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DataTransferItemList, mDataTransfer,
mItems, mIndexedItems, mFiles)
NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransferItemList)
NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransferItemList)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransferItemList)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
JSObject* DataTransferItemList::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return DataTransferItemList_Binding::Wrap(aCx, this, aGivenProto);
}
already_AddRefed<DataTransferItemList> DataTransferItemList::Clone(
DataTransfer* aDataTransfer) const {
RefPtr<DataTransferItemList> list = new DataTransferItemList(aDataTransfer);
// We need to clone the mItems and mIndexedItems lists while keeping the same
// correspondences between the mIndexedItems and mItems lists (namely, if an
// item is in mIndexedItems, and mItems it must have the same new identity)
// First, we copy over indexedItems, and clone every entry. Then, we go over
// mItems. For every entry, we use its mIndex property to locate it in
// mIndexedItems on the original DataTransferItemList, and then copy over the
// reference from the same index pair on the new DataTransferItemList
list->mIndexedItems.SetLength(mIndexedItems.Length());
list->mItems.SetLength(mItems.Length());
// Copy over mIndexedItems, cloning every entry
for (uint32_t i = 0; i < mIndexedItems.Length(); i++) {
const nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i];
nsTArray<RefPtr<DataTransferItem>>& newItems = list->mIndexedItems[i];
newItems.SetLength(items.Length());
for (uint32_t j = 0; j < items.Length(); j++) {
newItems[j] = items[j]->Clone(aDataTransfer);
}
}
// Copy over mItems, getting the actual entries from mIndexedItems
for (uint32_t i = 0; i < mItems.Length(); i++) {
uint32_t index = mItems[i]->Index();
MOZ_ASSERT(index < mIndexedItems.Length());
uint32_t subIndex = mIndexedItems[index].IndexOf(mItems[i]);
// Copy over the reference
list->mItems[i] = list->mIndexedItems[index][subIndex];
}
return list.forget();
}
void DataTransferItemList::Remove(uint32_t aIndex,
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv) {
if (mDataTransfer->IsReadOnly()) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
if (aIndex >= Length()) {
return;
}
ClearDataHelper(mItems[aIndex], aIndex, -1, aSubjectPrincipal, aRv);
}
DataTransferItem* DataTransferItemList::IndexedGetter(uint32_t aIndex,
bool& aFound) const {
if (aIndex >= mItems.Length()) {
aFound = false;
return nullptr;
}
MOZ_ASSERT(mItems[aIndex]);
aFound = true;
return mItems[aIndex];
}
uint32_t DataTransferItemList::MozItemCount() const {
uint32_t length = mIndexedItems.Length();
// XXX: Compat hack - Index 0 always exists due to changes in internals, but
// if it is empty, scripts using the moz* APIs should see it as not existing.
if (length == 1 && mIndexedItems[0].IsEmpty()) {
return 0;
}
return length;
}
void DataTransferItemList::Clear(nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv) {
if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
return;
}
uint32_t count = Length();
for (uint32_t i = 0; i < count; i++) {
// We always remove the last item first, to avoid moving items around in
// memory as much
Remove(Length() - 1, aSubjectPrincipal, aRv);
ENSURE_SUCCESS_VOID(aRv);
}
MOZ_ASSERT(Length() == 0);
}
DataTransferItem* DataTransferItemList::Add(const nsAString& aData,
const nsAString& aType,
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv) {
if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
return nullptr;
}
RefPtr<nsVariantCC> data(new nsVariantCC());
data->SetAsAString(aData);
nsAutoString format;
mDataTransfer->GetRealFormat(aType, format);
if (!DataTransfer::PrincipalMaySetData(format, data, &aSubjectPrincipal)) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return nullptr;
}
// We add the textual data to index 0. We set aInsertOnly to true, as we don't
// want to update an existing entry if it is already present, as per the spec.
RefPtr<DataTransferItem> item =
SetDataWithPrincipal(format, data, 0, &aSubjectPrincipal,
/* aInsertOnly = */ true,
/* aHidden = */ false, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
MOZ_ASSERT(item->Kind() != DataTransferItem::KIND_FILE);
return item;
}
DataTransferItem* DataTransferItemList::Add(File& aData,
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv) {
if (mDataTransfer->IsReadOnly()) {
return nullptr;
}
nsCOMPtr<nsISupports> supports = do_QueryObject(&aData);
nsCOMPtr<nsIWritableVariant> data = new nsVariantCC();
data->SetAsISupports(supports);
nsAutoString type;
aData.GetType(type);
if (!DataTransfer::PrincipalMaySetData(type, data, &aSubjectPrincipal)) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return nullptr;
}
// We need to add this as a new item, as multiple files can't exist in the
// same item in the Moz DataTransfer layout. It will be appended at the end of
// the internal specced layout.
uint32_t index = mIndexedItems.Length();
RefPtr<DataTransferItem> item =
SetDataWithPrincipal(type, data, index, &aSubjectPrincipal,
/* aInsertOnly = */ true,
/* aHidden = */ false, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
MOZ_ASSERT(item->Kind() == DataTransferItem::KIND_FILE);
return item;
}
already_AddRefed<FileList> DataTransferItemList::Files(
nsIPrincipal* aPrincipal) {
// The DataTransfer can hold data with varying principals, coming from
// different windows. This means that permissions checks need to be made when
// accessing data from the DataTransfer. With the accessor methods, this is
// checked by DataTransferItem::Data(), however with files, we keep a cached
// live copy of the files list for spec compliance.
//
// A DataTransfer is only exposed to one webpage, chrome code and expanded
// principals related to WebExtensions content scripts or user scripts.
// The chrome code should be able to see all files on the DataTransfer, while
// the webpage and WebExtensions content scripts and user scripts should only
// be able to see the files they can see.
//
// As chrome code doesn't need as strict spec compliance as web visible code,
// we generate a new FileList object every time you access the Files list from
// chrome code, but re-use the cached one when accessing from content code.
//
// For WebExtensions content scripts (expanded principals subsuming both
// the attached web page principal and the extension principal) and
// WebExtensions user scripts (expanded principals subsuming the attached
// web page principal but not the extension principal) we also don't cache
// the FileList as for chrome code (because the webpage principal and other
// extension content scripts/user scripts principals would not be able to
// access the cached FileList when accessed by a different expanded principal
// first, see Bug 1707214).
//
// It is not legal to expose an identical DataTransfer object is to multiple
// different principals without using the `Clone` method or similar to copy it
// first. If that happens, this method will assert, and return nullptr in
// release builds. If this functionality is required in the future, a more
// advanced caching mechanism for the FileList objects will be required.
RefPtr<FileList> files;
if (aPrincipal->IsSystemPrincipal() ||
// WebExtensions content scripts and user scripts.
nsContentUtils::IsExpandedPrincipal(aPrincipal)) {
files = new FileList(mDataTransfer);
GenerateFiles(files, aPrincipal);
return files.forget();
}
if (!mFiles) {
mFiles = new FileList(mDataTransfer);
mFilesPrincipal = aPrincipal;
RegenerateFiles();
}
if (!aPrincipal->Subsumes(mFilesPrincipal)) {
MOZ_ASSERT(false,
"This DataTransfer should only be accessed by the system "
"and a single principal");
return nullptr;
}
files = mFiles;
return files.forget();
}
void DataTransferItemList::MozRemoveByTypeAt(const nsAString& aType,
uint32_t aIndex,
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv) {
if (NS_WARN_IF(mDataTransfer->IsReadOnly() ||
aIndex >= mIndexedItems.Length())) {
return;
}
bool removeAll = aType.IsEmpty();
nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aIndex];
uint32_t count = items.Length();
// We remove the last item of the list repeatedly - that way we don't
// have to worry about modifying the loop iterator
if (removeAll) {
for (uint32_t i = 0; i < count; ++i) {
uint32_t index = items.Length() - 1;
MOZ_ASSERT(index == count - i - 1);
ClearDataHelper(items[index], -1, index, aSubjectPrincipal, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
}
// items is no longer a valid reference, as removing the last element from
// it via ClearDataHelper invalidated it. so we can't MOZ_ASSERT that the
// length is now 0.
return;
}
for (uint32_t i = 0; i < count; ++i) {
// NOTE: As this is a moz-prefixed API, it works based on internal types.
nsAutoString type;
items[i]->GetInternalType(type);
if (type == aType) {
ClearDataHelper(items[i], -1, i, aSubjectPrincipal, aRv);
return;
}
}
}
DataTransferItem* DataTransferItemList::MozItemByTypeAt(const nsAString& aType,
uint32_t aIndex) {
if (NS_WARN_IF(aIndex >= mIndexedItems.Length())) {
return nullptr;
}
uint32_t count = mIndexedItems[aIndex].Length();
for (uint32_t i = 0; i < count; i++) {
RefPtr<DataTransferItem> item = mIndexedItems[aIndex][i];
// NOTE: As this is a moz-prefixed API it works on internal types
nsString type;
item->GetInternalType(type);
if (type.Equals(aType)) {
return item;
}
}
return nullptr;
}
already_AddRefed<DataTransferItem> DataTransferItemList::SetDataWithPrincipal(
const nsAString& aType, nsIVariant* aData, uint32_t aIndex,
nsIPrincipal* aPrincipal, bool aInsertOnly, bool aHidden,
ErrorResult& aRv) {
if (aIndex < mIndexedItems.Length()) {
nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aIndex];
uint32_t count = items.Length();
for (uint32_t i = 0; i < count; i++) {
RefPtr<DataTransferItem> item = items[i];
nsString type;
item->GetInternalType(type);
if (type.Equals(aType)) {
if (NS_WARN_IF(aInsertOnly)) {
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return nullptr;
}
// don't allow replacing data that has a stronger principal
bool subsumes;
if (NS_WARN_IF(item->Principal() && aPrincipal &&
(NS_FAILED(aPrincipal->Subsumes(item->Principal(),
&subsumes)) ||
!subsumes))) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return nullptr;
}
item->SetPrincipal(aPrincipal);
DataTransferItem::eKind oldKind = item->Kind();
item->SetData(aData);
mDataTransfer->TypesListMayHaveChanged();
if (aIndex != 0) {
// If the item changes from being a file to not a file or vice-versa,
// its presence in the mItems array may need to change.
if (item->Kind() == DataTransferItem::KIND_FILE &&
oldKind != DataTransferItem::KIND_FILE) {
// not file => file
mItems.AppendElement(item);
} else if (item->Kind() != DataTransferItem::KIND_FILE &&
oldKind == DataTransferItem::KIND_FILE) {
// file => not file
mItems.RemoveElement(item);
}
}
// Regenerate the Files array if we have modified a file's status
if (item->Kind() == DataTransferItem::KIND_FILE ||
oldKind == DataTransferItem::KIND_FILE) {
RegenerateFiles();
}
return item.forget();
}
}
} else {
// Make sure that we aren't adding past the end of the mIndexedItems array.
// XXX Should this be a MOZ_ASSERT instead?
aIndex = mIndexedItems.Length();
}
// Add the new item
RefPtr<DataTransferItem> item =
AppendNewItem(aIndex, aType, aData, aPrincipal, aHidden);
if (item->Kind() == DataTransferItem::KIND_FILE) {
RegenerateFiles();
}
return item.forget();
}
DataTransferItem* DataTransferItemList::AppendNewItem(uint32_t aIndex,
const nsAString& aType,
nsIVariant* aData,
nsIPrincipal* aPrincipal,
bool aHidden) {
if (mIndexedItems.Length() <= aIndex) {
MOZ_ASSERT(mIndexedItems.Length() == aIndex);
mIndexedItems.AppendElement();
}
RefPtr<DataTransferItem> item = new DataTransferItem(mDataTransfer, aType);
item->SetIndex(aIndex);
item->SetPrincipal(aPrincipal);
item->SetData(aData);
item->SetChromeOnly(aHidden);
mIndexedItems[aIndex].AppendElement(item);
// We only want to add the item to the main mItems list if the index we are
// adding to is 0, or the item we are adding is a file. If we add an item
// which is not a file to a non-zero index, invariants could be broken.
// (namely the invariant that there are not 2 non-file entries in the items
// array with the same type).
//
// We also want to update our DataTransfer's type list any time we're adding a
// KIND_FILE item, or an item at index 0.
if (item->Kind() == DataTransferItem::KIND_FILE || aIndex == 0) {
if (!aHidden) {
mItems.AppendElement(item);
}
mDataTransfer->TypesListMayHaveChanged();
}
return item;
}
void DataTransferItemList::GetTypes(nsTArray<nsString>& aTypes,
CallerType aCallerType) const {
MOZ_ASSERT(aTypes.IsEmpty());
if (mIndexedItems.IsEmpty()) {
return;
}
bool foundFile = false;
for (const RefPtr<DataTransferItem>& item : mIndexedItems[0]) {
MOZ_ASSERT(item);
// XXX Why don't we check the caller type with item's permission only
// for "Files"?
if (!foundFile) {
foundFile = item->Kind() == DataTransferItem::KIND_FILE;
}
if (item->ChromeOnly() && aCallerType != CallerType::System) {
continue;
}
// 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;
item->GetInternalType(type);
if (item->Kind() != DataTransferItem::KIND_FILE ||
type.EqualsASCII(kFileMime)) {
aTypes.AppendElement(type);
}
}
// Additional files will be added at a non-zero index.
if (!foundFile) {
for (uint32_t i = 1; i < mIndexedItems.Length(); i++) {
for (const RefPtr<DataTransferItem>& item : mIndexedItems[i]) {
MOZ_ASSERT(item);
foundFile = item->Kind() == DataTransferItem::KIND_FILE;
if (foundFile) {
break;
}
}
}
}
if (foundFile) {
aTypes.AppendElement(u"Files"_ns);
}
}
bool DataTransferItemList::HasType(const nsAString& aType) const {
MOZ_ASSERT(!aType.EqualsASCII("Files"), "Use HasFile instead");
if (mIndexedItems.IsEmpty()) {
return false;
}
for (const RefPtr<DataTransferItem>& item : mIndexedItems[0]) {
if (item->IsInternalType(aType)) {
return true;
}
}
return false;
}
bool DataTransferItemList::HasFile() const {
if (mIndexedItems.IsEmpty()) {
return false;
}
for (const RefPtr<DataTransferItem>& item : mIndexedItems[0]) {
if (item->Kind() == DataTransferItem::KIND_FILE) {
return true;
}
}
return false;
}
const nsTArray<RefPtr<DataTransferItem>>* DataTransferItemList::MozItemsAt(
uint32_t aIndex) // -- INDEXED
{
if (aIndex >= mIndexedItems.Length()) {
return nullptr;
}
return &mIndexedItems[aIndex];
}
void DataTransferItemList::PopIndexZero() {
MOZ_ASSERT(mIndexedItems.Length() > 1);
MOZ_ASSERT(mIndexedItems[0].IsEmpty());
mIndexedItems.RemoveElementAt(0);
// Update the index of every element which has now been shifted
for (uint32_t i = 0; i < mIndexedItems.Length(); i++) {
nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i];
for (uint32_t j = 0; j < items.Length(); j++) {
items[j]->SetIndex(i);
}
}
}
void DataTransferItemList::ClearAllItems() {
// We always need to have index 0, so don't delete that one
mItems.Clear();
mIndexedItems.Clear();
mIndexedItems.SetLength(1);
mDataTransfer->TypesListMayHaveChanged();
// Re-generate files (into an empty list)
RegenerateFiles();
}
void DataTransferItemList::ClearDataHelper(DataTransferItem* aItem,
uint32_t aIndexHint,
uint32_t aMozOffsetHint,
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv) {
MOZ_ASSERT(aItem);
if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
return;
}
if (aItem->Principal() && !aSubjectPrincipal.Subsumes(aItem->Principal())) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return;
}
// Check if the aIndexHint is actually the index, and then remove the item
// from aItems
bool found;
if (IndexedGetter(aIndexHint, found) == aItem) {
mItems.RemoveElementAt(aIndexHint);
} else {
mItems.RemoveElement(aItem);
}
// Check if the aMozIndexHint and aMozOffsetHint are actually the index and
// offset, and then remove them from mIndexedItems
MOZ_ASSERT(aItem->Index() < mIndexedItems.Length());
nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aItem->Index()];
if (aMozOffsetHint < items.Length() && aItem == items[aMozOffsetHint]) {
items.RemoveElementAt(aMozOffsetHint);
} else {
items.RemoveElement(aItem);
}
mDataTransfer->TypesListMayHaveChanged();
// Check if we should remove the index. We never remove index 0.
if (items.Length() == 0 && aItem->Index() != 0) {
mIndexedItems.RemoveElementAt(aItem->Index());
// Update the index of every element which has now been shifted
for (uint32_t i = aItem->Index(); i < mIndexedItems.Length(); i++) {
nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i];
for (uint32_t j = 0; j < items.Length(); j++) {
items[j]->SetIndex(i);
}
}
}
// Give the removed item the invalid index
aItem->SetIndex(-1);
if (aItem->Kind() == DataTransferItem::KIND_FILE) {
RegenerateFiles();
}
}
void DataTransferItemList::RegenerateFiles() {
// We don't want to regenerate the files list unless we already have a files
// list. That way we can avoid the unnecessary work if the user never touches
// the files list.
if (mFiles) {
// We clear the list rather than performing smaller updates, because it
// simplifies the logic greatly on this code path, which should be very
// infrequently used.
mFiles->Clear();
DataTransferItemList::GenerateFiles(mFiles, mFilesPrincipal);
}
}
void DataTransferItemList::GenerateFiles(FileList* aFiles,
nsIPrincipal* aFilesPrincipal) {
MOZ_ASSERT(aFiles);
MOZ_ASSERT(aFilesPrincipal);
// For non-system principals, the Files list should be empty if the
// DataTransfer is protected.
if (!aFilesPrincipal->IsSystemPrincipal() && mDataTransfer->IsProtected()) {
return;
}
uint32_t count = Length();
for (uint32_t i = 0; i < count; i++) {
bool found;
RefPtr<DataTransferItem> item = IndexedGetter(i, found);
MOZ_ASSERT(found);
if (item->Kind() == DataTransferItem::KIND_FILE) {
RefPtr<File> file = item->GetAsFile(*aFilesPrincipal, IgnoreErrors());
if (NS_WARN_IF(!file)) {
continue;
}
aFiles->Append(file);
}
}
}
} // namespace mozilla::dom