The failure mode in the attached crashtest is an inconsistency in the flattened tree. Specifically, we null out mVideoControls in an nsVideoFrame, but defer the UnbindFromTree call on that NAC element, which measn that its mParent still points to the nsVideoFrame's mContent. Because all this stuff runs off of script runners, and the anonymous content destroyer is not guaranteed to run before other potential script runners, we end up running arbitrary script while the tree mismatch exists. This script calls back into ProcessPendingRestyles, which causes trouble. We could build a separate deferral mechanism, but it's not clear that we actually need to defer the unbind anymore. The deferred unbind was added in bug 489008, which predated a lot of simplifications in layout/dom interaction. MozReview-Commit-ID: 1JYAhiXKVJC
517 lines
16 KiB
C++
517 lines
16 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* 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 "nsFileControlFrame.h"
|
|
|
|
#include "nsGkAtoms.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsIDocument.h"
|
|
#include "mozilla/dom/NodeInfo.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/DataTransfer.h"
|
|
#include "mozilla/dom/HTMLButtonElement.h"
|
|
#include "mozilla/dom/HTMLInputElement.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "nsNodeInfoManager.h"
|
|
#include "nsContentCreatorFunctions.h"
|
|
#include "nsContentUtils.h"
|
|
#include "mozilla/EventStates.h"
|
|
#include "mozilla/dom/DOMStringList.h"
|
|
#include "mozilla/dom/Directory.h"
|
|
#include "mozilla/dom/FileList.h"
|
|
#include "nsIDOMDragEvent.h"
|
|
#include "nsIDOMFileList.h"
|
|
#include "nsContentList.h"
|
|
#include "nsIDOMMutationEvent.h"
|
|
#include "nsTextNode.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::dom;
|
|
|
|
nsIFrame*
|
|
NS_NewFileControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
|
|
{
|
|
return new (aPresShell) nsFileControlFrame(aContext);
|
|
}
|
|
|
|
NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame)
|
|
|
|
nsFileControlFrame::nsFileControlFrame(nsStyleContext* aContext)
|
|
: nsBlockFrame(aContext, kClassID)
|
|
{
|
|
AddStateBits(NS_BLOCK_FLOAT_MGR);
|
|
}
|
|
|
|
|
|
void
|
|
nsFileControlFrame::Init(nsIContent* aContent,
|
|
nsContainerFrame* aParent,
|
|
nsIFrame* aPrevInFlow)
|
|
{
|
|
nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
|
|
|
|
mMouseListener = new DnDListener(this);
|
|
}
|
|
|
|
void
|
|
nsFileControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
|
|
{
|
|
ENSURE_TRUE(mContent);
|
|
|
|
// Remove the events.
|
|
if (mContent) {
|
|
mContent->RemoveSystemEventListener(NS_LITERAL_STRING("drop"),
|
|
mMouseListener, false);
|
|
mContent->RemoveSystemEventListener(NS_LITERAL_STRING("dragover"),
|
|
mMouseListener, false);
|
|
}
|
|
|
|
DestroyAnonymousContent(mTextContent.forget());
|
|
DestroyAnonymousContent(mBrowseFilesOrDirs.forget());
|
|
|
|
mMouseListener->ForgetFrame();
|
|
nsBlockFrame::DestroyFrom(aDestructRoot);
|
|
}
|
|
|
|
static already_AddRefed<Element>
|
|
MakeAnonButton(nsIDocument* aDoc, const char* labelKey,
|
|
HTMLInputElement* aInputElement,
|
|
const nsAString& aAccessKey)
|
|
{
|
|
RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button);
|
|
// NOTE: SetIsNativeAnonymousRoot() has to be called before setting any
|
|
// attribute.
|
|
button->SetIsNativeAnonymousRoot();
|
|
button->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
|
|
NS_LITERAL_STRING("button"), false);
|
|
|
|
// Set the file picking button text depending on the current locale.
|
|
nsAutoString buttonTxt;
|
|
nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
|
|
labelKey, buttonTxt);
|
|
|
|
// Set the browse button text. It's a bit of a pain to do because we want to
|
|
// make sure we are not notifying.
|
|
RefPtr<nsTextNode> textContent =
|
|
new nsTextNode(button->NodeInfo()->NodeInfoManager());
|
|
|
|
textContent->SetText(buttonTxt, false);
|
|
|
|
nsresult rv = button->AppendChildTo(textContent, false);
|
|
if (NS_FAILED(rv)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Make sure access key and tab order for the element actually redirect to the
|
|
// file picking button.
|
|
RefPtr<HTMLButtonElement> buttonElement =
|
|
HTMLButtonElement::FromContentOrNull(button);
|
|
|
|
if (!aAccessKey.IsEmpty()) {
|
|
IgnoredErrorResult ignored;
|
|
buttonElement->SetAccessKey(aAccessKey, ignored);
|
|
}
|
|
|
|
// Both elements are given the same tab index so that the user can tab
|
|
// to the file control at the correct index, and then between the two
|
|
// buttons.
|
|
IgnoredErrorResult ignored;
|
|
buttonElement->SetTabIndex(aInputElement->TabIndex(), ignored);
|
|
|
|
return button.forget();
|
|
}
|
|
|
|
nsresult
|
|
nsFileControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
|
|
{
|
|
nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc();
|
|
|
|
RefPtr<HTMLInputElement> fileContent = HTMLInputElement::FromContentOrNull(mContent);
|
|
|
|
// The access key is transferred to the "Choose files..." button only. In
|
|
// effect that access key allows access to the control via that button, then
|
|
// the user can tab between the two buttons.
|
|
nsAutoString accessKey;
|
|
fileContent->GetAccessKey(accessKey);
|
|
|
|
mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent, accessKey);
|
|
if (!mBrowseFilesOrDirs || !aElements.AppendElement(mBrowseFilesOrDirs)) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
// Create and setup the text showing the selected files.
|
|
RefPtr<NodeInfo> nodeInfo;
|
|
nodeInfo = doc->NodeInfoManager()->GetNodeInfo(nsGkAtoms::label, nullptr,
|
|
kNameSpaceID_XUL,
|
|
nsIDOMNode::ELEMENT_NODE);
|
|
NS_TrustedNewXULElement(getter_AddRefs(mTextContent), nodeInfo.forget());
|
|
// NOTE: SetIsNativeAnonymousRoot() has to be called before setting any
|
|
// attribute.
|
|
mTextContent->SetIsNativeAnonymousRoot();
|
|
mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::crop,
|
|
NS_LITERAL_STRING("center"), false);
|
|
|
|
// Update the displayed text to reflect the current element's value.
|
|
nsAutoString value;
|
|
HTMLInputElement::FromContent(mContent)->GetDisplayFileName(value);
|
|
UpdateDisplayedValue(value, false);
|
|
|
|
if (!aElements.AppendElement(mTextContent)) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
// We should be able to interact with the element by doing drag and drop.
|
|
mContent->AddSystemEventListener(NS_LITERAL_STRING("drop"),
|
|
mMouseListener, false);
|
|
mContent->AddSystemEventListener(NS_LITERAL_STRING("dragover"),
|
|
mMouseListener, false);
|
|
|
|
SyncDisabledState();
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsFileControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
|
|
uint32_t aFilter)
|
|
{
|
|
if (mBrowseFilesOrDirs) {
|
|
aElements.AppendElement(mBrowseFilesOrDirs);
|
|
}
|
|
|
|
if (mTextContent) {
|
|
aElements.AppendElement(mTextContent);
|
|
}
|
|
}
|
|
|
|
NS_QUERYFRAME_HEAD(nsFileControlFrame)
|
|
NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
|
|
NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
|
|
NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
|
|
|
|
void
|
|
nsFileControlFrame::SetFocus(bool aOn, bool aRepaint)
|
|
{
|
|
}
|
|
|
|
static void
|
|
AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray,
|
|
BlobImpl* aBlobImpl,
|
|
nsIContent* aContent)
|
|
{
|
|
MOZ_ASSERT(aBlobImpl);
|
|
MOZ_ASSERT(aBlobImpl->IsDirectory());
|
|
|
|
nsAutoString fullpath;
|
|
ErrorResult err;
|
|
aBlobImpl->GetMozFullPath(fullpath, SystemCallerGuarantee(), err);
|
|
if (err.Failed()) {
|
|
err.SuppressException();
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsIFile> file;
|
|
nsresult rv = NS_NewLocalFile(fullpath, true, getter_AddRefs(file));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return;
|
|
}
|
|
|
|
nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow();
|
|
if (!inner || !inner->IsCurrentInnerWindow()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<Directory> directory =
|
|
Directory::Create(inner, file);
|
|
MOZ_ASSERT(directory);
|
|
|
|
OwningFileOrDirectory* element = aArray.AppendElement();
|
|
element->SetAsDirectory() = directory;
|
|
}
|
|
|
|
/**
|
|
* This is called when we receive a drop or a dragover.
|
|
*/
|
|
NS_IMETHODIMP
|
|
nsFileControlFrame::DnDListener::HandleEvent(nsIDOMEvent* aEvent)
|
|
{
|
|
NS_ASSERTION(mFrame, "We should have been unregistered");
|
|
|
|
bool defaultPrevented = false;
|
|
aEvent->GetDefaultPrevented(&defaultPrevented);
|
|
if (defaultPrevented) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
|
|
if (!dragEvent) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsIDOMDataTransfer> dataTransfer;
|
|
dragEvent->GetDataTransfer(getter_AddRefs(dataTransfer));
|
|
if (!IsValidDropData(dataTransfer)) {
|
|
return NS_OK;
|
|
}
|
|
|
|
|
|
nsCOMPtr<nsIContent> content = mFrame->GetContent();
|
|
bool supportsMultiple = content && content->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple);
|
|
if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) {
|
|
dataTransfer->SetDropEffect(NS_LITERAL_STRING("none"));
|
|
aEvent->StopPropagation();
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoString eventType;
|
|
aEvent->GetType(eventType);
|
|
if (eventType.EqualsLiteral("dragover")) {
|
|
// Prevent default if we can accept this drag data
|
|
aEvent->PreventDefault();
|
|
return NS_OK;
|
|
}
|
|
|
|
if (eventType.EqualsLiteral("drop")) {
|
|
aEvent->StopPropagation();
|
|
aEvent->PreventDefault();
|
|
|
|
NS_ASSERTION(content, "The frame has no content???");
|
|
|
|
HTMLInputElement* inputElement = HTMLInputElement::FromContent(content);
|
|
NS_ASSERTION(inputElement, "No input element for this file upload control frame!");
|
|
|
|
nsCOMPtr<nsIDOMFileList> fileList;
|
|
dataTransfer->GetFiles(getter_AddRefs(fileList));
|
|
|
|
RefPtr<BlobImpl> webkitDir;
|
|
nsresult rv =
|
|
GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir));
|
|
NS_ENSURE_SUCCESS(rv, NS_OK);
|
|
|
|
nsTArray<OwningFileOrDirectory> array;
|
|
if (webkitDir) {
|
|
AppendBlobImplAsDirectory(array, webkitDir, content);
|
|
inputElement->MozSetDndFilesAndDirectories(array);
|
|
} else {
|
|
bool blinkFileSystemEnabled =
|
|
Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false);
|
|
bool dirPickerEnabled =
|
|
Preferences::GetBool("dom.input.dirpicker", false);
|
|
if (blinkFileSystemEnabled || dirPickerEnabled) {
|
|
FileList* files = static_cast<FileList*>(fileList.get());
|
|
if (files) {
|
|
for (uint32_t i = 0; i < files->Length(); ++i) {
|
|
File* file = files->Item(i);
|
|
if (file) {
|
|
if (file->Impl() && file->Impl()->IsDirectory()) {
|
|
AppendBlobImplAsDirectory(array, file->Impl(), content);
|
|
} else {
|
|
OwningFileOrDirectory* element = array.AppendElement();
|
|
element->SetAsFile() = file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Entries API.
|
|
if (blinkFileSystemEnabled) {
|
|
// This is rather ugly. Pass the directories as Files using SetFiles,
|
|
// but then if blink filesystem API is enabled, it wants
|
|
// FileOrDirectory array.
|
|
inputElement->SetFiles(fileList, true);
|
|
inputElement->UpdateEntries(array);
|
|
}
|
|
// Directory Upload API
|
|
else if (dirPickerEnabled) {
|
|
inputElement->SetFilesOrDirectories(array, true);
|
|
}
|
|
// Normal DnD
|
|
else {
|
|
inputElement->SetFiles(fileList, true);
|
|
}
|
|
|
|
nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content,
|
|
NS_LITERAL_STRING("input"), true,
|
|
false);
|
|
nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content,
|
|
NS_LITERAL_STRING("change"), true,
|
|
false);
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory(nsIDOMFileList* aFileList,
|
|
BlobImpl** aBlobImpl)
|
|
{
|
|
*aBlobImpl = nullptr;
|
|
|
|
HTMLInputElement* inputElement =
|
|
HTMLInputElement::FromContent(mFrame->GetContent());
|
|
bool webkitDirPicker =
|
|
Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
|
|
inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory);
|
|
if (!webkitDirPicker) {
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!aFileList) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
FileList* files = static_cast<FileList*>(aFileList);
|
|
// webkitdirectory doesn't care about the length of the file list but
|
|
// only about the first item on it.
|
|
uint32_t len = files->Length();
|
|
if (len) {
|
|
File* file = files->Item(0);
|
|
if (file) {
|
|
BlobImpl* impl = file->Impl();
|
|
if (impl && impl->IsDirectory()) {
|
|
RefPtr<BlobImpl> retVal = impl;
|
|
retVal.swap(*aBlobImpl);
|
|
return NS_OK;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
bool
|
|
nsFileControlFrame::DnDListener::IsValidDropData(nsIDOMDataTransfer* aDOMDataTransfer)
|
|
{
|
|
nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer);
|
|
NS_ENSURE_TRUE(dataTransfer, false);
|
|
|
|
// We only support dropping files onto a file upload control
|
|
nsTArray<nsString> types;
|
|
dataTransfer->GetTypes(types, CallerType::System);
|
|
|
|
return types.Contains(NS_LITERAL_STRING("Files"));
|
|
}
|
|
|
|
bool
|
|
nsFileControlFrame::DnDListener::CanDropTheseFiles(nsIDOMDataTransfer* aDOMDataTransfer,
|
|
bool aSupportsMultiple)
|
|
{
|
|
nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer);
|
|
NS_ENSURE_TRUE(dataTransfer, false);
|
|
|
|
nsCOMPtr<nsIDOMFileList> fileList;
|
|
dataTransfer->GetFiles(getter_AddRefs(fileList));
|
|
|
|
RefPtr<BlobImpl> webkitDir;
|
|
nsresult rv =
|
|
GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir));
|
|
// Just check if either there isn't webkitdirectory attribute, or
|
|
// fileList has a directory which can be dropped to the element.
|
|
// No need to use webkitDir for anything here.
|
|
NS_ENSURE_SUCCESS(rv, false);
|
|
|
|
uint32_t listLength = 0;
|
|
if (fileList) {
|
|
fileList->GetLength(&listLength);
|
|
}
|
|
return listLength <= 1 || aSupportsMultiple;
|
|
}
|
|
|
|
nscoord
|
|
nsFileControlFrame::GetMinISize(gfxContext *aRenderingContext)
|
|
{
|
|
nscoord result;
|
|
DISPLAY_MIN_WIDTH(this, result);
|
|
|
|
// Our min width is our pref width
|
|
result = GetPrefISize(aRenderingContext);
|
|
return result;
|
|
}
|
|
|
|
void
|
|
nsFileControlFrame::SyncDisabledState()
|
|
{
|
|
EventStates eventStates = mContent->AsElement()->State();
|
|
if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
|
|
mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
|
|
EmptyString(), true);
|
|
} else {
|
|
mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
nsFileControlFrame::AttributeChanged(int32_t aNameSpaceID,
|
|
nsIAtom* aAttribute,
|
|
int32_t aModType)
|
|
{
|
|
if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::tabindex) {
|
|
if (aModType == nsIDOMMutationEvent::REMOVAL) {
|
|
mBrowseFilesOrDirs->UnsetAttr(aNameSpaceID, aAttribute, true);
|
|
} else {
|
|
nsAutoString value;
|
|
mContent->GetAttr(aNameSpaceID, aAttribute, value);
|
|
mBrowseFilesOrDirs->SetAttr(aNameSpaceID, aAttribute, value, true);
|
|
}
|
|
}
|
|
|
|
return nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
|
|
}
|
|
|
|
void
|
|
nsFileControlFrame::ContentStatesChanged(EventStates aStates)
|
|
{
|
|
if (aStates.HasState(NS_EVENT_STATE_DISABLED)) {
|
|
nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this));
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG_FRAME_DUMP
|
|
nsresult
|
|
nsFileControlFrame::GetFrameName(nsAString& aResult) const
|
|
{
|
|
return MakeFrameName(NS_LITERAL_STRING("FileControl"), aResult);
|
|
}
|
|
#endif
|
|
|
|
void
|
|
nsFileControlFrame::UpdateDisplayedValue(const nsAString& aValue, bool aNotify)
|
|
{
|
|
mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, aNotify);
|
|
}
|
|
|
|
nsresult
|
|
nsFileControlFrame::SetFormProperty(nsIAtom* aName,
|
|
const nsAString& aValue)
|
|
{
|
|
if (nsGkAtoms::value == aName) {
|
|
UpdateDisplayedValue(aValue, true);
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsFileControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
|
const nsDisplayListSet& aLists)
|
|
{
|
|
BuildDisplayListForInline(aBuilder, aLists);
|
|
}
|
|
|
|
#ifdef ACCESSIBILITY
|
|
a11y::AccType
|
|
nsFileControlFrame::AccessibleType()
|
|
{
|
|
return a11y::eHTMLFileInputType;
|
|
}
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Mouse listener implementation
|
|
|
|
NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener,
|
|
nsIDOMEventListener)
|