/* -*- 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/dom/HTMLInputElement.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Components.h"
#include "mozilla/dom/AutocompleteInfoBinding.h"
#include "mozilla/dom/BlobImpl.h"
#include "mozilla/dom/CustomEvent.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/DocumentOrShadowRoot.h"
#include "mozilla/dom/ElementBinding.h"
#include "mozilla/dom/FileSystemUtils.h"
#include "mozilla/dom/FormData.h"
#include "mozilla/dom/GetFilesHelper.h"
#include "mozilla/dom/NumericInputTypes.h"
#include "mozilla/dom/WindowContext.h"
#include "mozilla/dom/InputType.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/MutationEventBinding.h"
#include "mozilla/dom/WheelEventBinding.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/ImageInputTelemetry.h"
#include "mozilla/Maybe.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_signon.h"
#include "mozilla/TextUtils.h"
#include "mozilla/Try.h"
#include "mozilla/Unused.h"
#include "nsAttrValueInlines.h"
#include "nsCRTGlue.h"
#include "nsIFilePicker.h"
#include "nsNetUtil.h"
#include "nsQueryObject.h"
#include "HTMLDataListElement.h"
#include "HTMLFormSubmissionConstants.h"
#include "mozilla/Telemetry.h"
#include "nsBaseCommandController.h"
#include "nsIStringBundle.h"
#include "nsFocusManager.h"
#include "nsColorControlFrame.h"
#include "nsFileControlFrame.h"
#include "nsNumberControlFrame.h"
#include "nsSearchControlFrame.h"
#include "nsPIDOMWindow.h"
#include "nsRepeatService.h"
#include "mozilla/dom/ProgressEvent.h"
#include "nsGkAtoms.h"
#include "nsStyleConsts.h"
#include "nsPresContext.h"
#include "nsIFormControl.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/HTMLDataListElement.h"
#include "mozilla/dom/HTMLOptionElement.h"
#include "nsIFrame.h"
#include "nsRangeFrame.h"
#include "nsError.h"
#include "nsIEditor.h"
#include "nsIPromptCollection.h"
#include "mozilla/PresState.h"
#include "nsLinebreakConverter.h" //to strip out carriage returns
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsLayoutUtils.h"
#include "nsVariant.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/MappedDeclarationsBuilder.h"
#include "mozilla/InternalMutationEvent.h"
#include "mozilla/TextControlState.h"
#include "mozilla/TextEditor.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TouchEvents.h"
#include <algorithm>
// input type=radio
#include "mozilla/dom/RadioGroupContainer.h"
#include "nsIRadioVisitor.h"
#include "nsRadioVisitor.h"
// input type=file
#include "mozilla/dom/FileSystemEntry.h"
#include "mozilla/dom/FileSystem.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/FileList.h"
#include "nsIFile.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIContentPrefService2.h"
#include "nsIMIMEService.h"
#include "nsIObserverService.h"
// input type=image
#include "nsImageLoadingContent.h"
#include "imgRequestProxy.h"
#include "mozAutoDocUpdate.h"
#include "nsContentCreatorFunctions.h"
#include "nsContentUtils.h"
#include "mozilla/dom/DirectionalityUtils.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/Preferences.h"
#include "mozilla/MathAlgorithms.h"
#include <limits>
#include "nsIColorPicker.h"
#include "nsIStringEnumerator.h"
#include "HTMLSplitOnSpacesTokenizer.h"
#include "nsIMIMEInfo.h"
#include "nsFrameSelection.h"
#include "nsXULControllers.h"
// input type=date
#include "js/Date.h"
NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Input)
// XXX align=left, hspace, vspace, border? other nav4 attrs
namespace mozilla::dom {
// First bits are needed for the control type.
#define NS_OUTER_ACTIVATE_EVENT (1 << 9)
#define NS_ORIGINAL_CHECKED_VALUE (1 << 10)
// (1 << 11 is unused)
#define NS_ORIGINAL_INDETERMINATE_VALUE (1 << 12)
#define NS_PRE_HANDLE_BLUR_EVENT (1 << 13)
#define NS_IN_SUBMIT_CLICK (1 << 15)
#define NS_CONTROL_TYPE(bits) \
((bits) & ~(NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | \
NS_ORIGINAL_INDETERMINATE_VALUE | NS_PRE_HANDLE_BLUR_EVENT | \
NS_IN_SUBMIT_CLICK))
// whether textfields should be selected once focused:
// -1: no, 1: yes, 0: uninitialized
static int32_t gSelectTextFieldOnFocus;
UploadLastDir* HTMLInputElement::gUploadLastDir;
static const nsAttrValue::EnumTable kInputTypeTable[] = {
{
"button", FormControlType::InputButton},
{
"checkbox", FormControlType::InputCheckbox},
{
"color", FormControlType::InputColor},
{
"date", FormControlType::InputDate},
{
"datetime-local", FormControlType::InputDatetimeLocal},
{
"email", FormControlType::InputEmail},
{
"file", FormControlType::InputFile},
{
"hidden", FormControlType::InputHidden},
{
"reset", FormControlType::InputReset},
{
"image", FormControlType::InputImage},
{
"month", FormControlType::InputMonth},
{
"number", FormControlType::InputNumber},
{
"password", FormControlType::InputPassword},
{
"radio", FormControlType::InputRadio},
{
"range", FormControlType::InputRange},
{
"search", FormControlType::InputSearch},
{
"submit", FormControlType::InputSubmit},
{
"tel", FormControlType::InputTel},
{
"time", FormControlType::InputTime},
{
"url", FormControlType::InputUrl},
{
"week", FormControlType::InputWeek},
// "text" must be last for ParseAttribute to work right. If you add things
// before it, please update kInputDefaultType.
{
"text", FormControlType::InputText},
{nullptr, 0}};
// Default type is 'text'.
static const nsAttrValue::EnumTable* kInputDefaultType =
&kInputTypeTable[std::size(kInputTypeTable) - 2];
static const nsAttrValue::EnumTable kCaptureTable[] = {
{
"user", nsIFilePicker::captureUser},
{
"environment", nsIFilePicker::captureEnv},
{
"", nsIFilePicker::captureDefault},
{nullptr, nsIFilePicker::captureNone}};
static const nsAttrValue::EnumTable* kCaptureDefault = &kCaptureTable[2];
using namespace blink;
constexpr Decimal HTMLInputElement::kStepScaleFactorDate(86400000_d);
constexpr Decimal HTMLInputElement::kStepScaleFactorNumberRange(1_d);
constexpr Decimal HTMLInputElement::kStepScaleFactorTime(1000_d);
constexpr Decimal HTMLInputElement::kStepScaleFactorMonth(1_d);
constexpr Decimal HTMLInputElement::kStepScaleFactorWeek(7 * 86400000_d);
constexpr Decimal HTMLInputElement::kDefaultStepBase(0_d);
constexpr Decimal HTMLInputElement::kDefaultStepBaseWeek(-259200000_d);
constexpr Decimal HTMLInputElement::kDefaultStep(1_d);
constexpr Decimal HTMLInputElement::kDefaultStepTime(60_d);
constexpr Decimal HTMLInputElement::kStepAny(0_d);
const double HTMLInputElement::kMinimumYear = 1;
const double HTMLInputElement::kMaximumYear = 275760;
const double HTMLInputElement::kMaximumWeekInMaximumYear = 37;
const double HTMLInputElement::kMaximumDayInMaximumYear = 13;
const double HTMLInputElement::kMaximumMonthInMaximumYear = 9;
const double HTMLInputElement::kMaximumWeekInYear = 53;
const double HTMLInputElement::kMsPerDay = 24 * 60 * 60 * 1000;
// An helper class for the dispatching of the 'change' event.
// This class is used when the FilePicker finished its task (or when files and
// directories are set by some chrome/test only method).
// The task of this class is to postpone the dispatching of 'change' and 'input'
// events at the end of the exploration of the directories.
class DispatchChangeEventCallback final :
public GetFilesCallback {
public:
explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement)
: mInputElement(aInputElement) {
MOZ_ASSERT(aInputElement);
}
virtual void Callback(
nsresult aStatus,
const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
if (!mInputElement->GetOwnerGlobal()) {
return;
}
nsTArray<OwningFileOrDirectory> array;
for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) {
OwningFileOrDirectory* element = array.AppendElement();
RefPtr<File> file =
File::Create(mInputElement->GetOwnerGlobal(), aBlobImpls[i]);
if (NS_WARN_IF(!file)) {
return;
}
element->SetAsFile() = file;
}
mInputElement->SetFilesOrDirectories(array,
true);
Unused << NS_WARN_IF(NS_FAILED(DispatchEvents()));
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
nsresult DispatchEvents() {
RefPtr<HTMLInputElement> inputElement(mInputElement);
nsresult rv = nsContentUtils::DispatchInputEvent(inputElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to dispatch input event");
mInputElement->SetUserInteracted(
true);
rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
mInputElement, u
"change"_ns,
CanBubble::eYes, Cancelable::eNo);
return rv;
}
private:
RefPtr<HTMLInputElement> mInputElement;
};
struct HTMLInputElement::FileData {
/**
* The value of the input if it is a file input. This is the list of files or
* directories DOM objects used when uploading a file. It is vital that this
* is kept separate from mValue so that it won't be possible to 'leak' the
* value from a text-input to a file-input. Additionally, the logic for this
* value is kept as simple as possible to avoid accidental errors where the
* wrong filename is used. Therefor the list of filenames is always owned by
* this member, never by the frame. Whenever the frame wants to change the
* filename it has to call SetFilesOrDirectories to update this member.
*/
nsTArray<OwningFileOrDirectory> mFilesOrDirectories;
RefPtr<GetFilesHelper> mGetFilesRecursiveHelper;
RefPtr<GetFilesHelper> mGetFilesNonRecursiveHelper;
/**
* Hack for bug 1086684: Stash the .value when we're a file picker.
*/
nsString mFirstFilePath;
RefPtr<FileList> mFileList;
Sequence<RefPtr<FileSystemEntry>> mEntries;
nsString mStaticDocFileList;
void ClearGetFilesHelpers() {
if (mGetFilesRecursiveHelper) {
mGetFilesRecursiveHelper->Unlink();
mGetFilesRecursiveHelper = nullptr;
}
if (mGetFilesNonRecursiveHelper) {
mGetFilesNonRecursiveHelper->Unlink();
mGetFilesNonRecursiveHelper = nullptr;
}
}
// Cycle Collection support.
void Traverse(nsCycleCollectionTraversalCallback& cb) {
FileData* tmp =
this;
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFilesOrDirectories)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileList)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEntries)
if (mGetFilesRecursiveHelper) {
mGetFilesRecursiveHelper->Traverse(cb);
}
if (mGetFilesNonRecursiveHelper) {
mGetFilesNonRecursiveHelper->Traverse(cb);
}
}
void Unlink() {
FileData* tmp =
this;
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFilesOrDirectories)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFileList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEntries)
ClearGetFilesHelpers();
}
};
HTMLInputElement::nsFilePickerShownCallback::nsFilePickerShownCallback(
HTMLInputElement* aInput, nsIFilePicker* aFilePicker)
: mFilePicker(aFilePicker), mInput(aInput) {}
NS_IMPL_ISUPPORTS(UploadLastDir::ContentPrefCallback, nsIContentPrefCallback2)
NS_IMETHODIMP
UploadLastDir::ContentPrefCallback::HandleCompletion(uint16_t aReason) {
nsCOMPtr<nsIFile> localFile;
nsAutoString prefStr;
if (aReason == nsIContentPrefCallback2::COMPLETE_ERROR || !mResult) {
Preferences::GetString(
"dom.input.fallbackUploadDir", prefStr);
}
if (prefStr.IsEmpty() && mResult) {
nsCOMPtr<nsIVariant> pref;
mResult->GetValue(getter_AddRefs(pref));
pref->GetAsAString(prefStr);
}
if (!prefStr.IsEmpty()) {
nsresult rv = NS_NewLocalFile(prefStr, getter_AddRefs(localFile));
Unused << NS_WARN_IF(NS_FAILED(rv));
}
if (localFile) {
mFilePicker->SetDisplayDirectory(localFile);
}
else {
// If no custom directory was set through the pref, default to
// "desktop" directory for each platform.
mFilePicker->SetDisplaySpecialDirectory(
NS_LITERAL_STRING_FROM_CSTRING(NS_OS_DESKTOP_DIR));
}
mFilePicker->Open(mFpCallback);
return NS_OK;
}
NS_IMETHODIMP
UploadLastDir::ContentPrefCallback::HandleResult(nsIContentPref* pref) {
mResult = pref;
return NS_OK;
}
NS_IMETHODIMP
UploadLastDir::ContentPrefCallback::HandleError(nsresult error) {
// HandleCompletion is always called (even with HandleError was called),
// so we don't need to do anything special here.
return NS_OK;
}
namespace {
/**
* This may return nullptr if the DOM File's implementation of
* File::mozFullPathInternal does not successfully return a non-empty
* string that is a valid path. This can happen on Firefox OS, for example,
* where the file picker can create Blobs.
*/
static already_AddRefed<nsIFile> LastUsedDirectory(
const OwningFileOrDirectory& aData) {
if (aData.IsFile()) {
nsAutoString path;
ErrorResult error;
aData.GetAsFile()->GetMozFullPathInternal(path, error);
if (error.Failed() || path.IsEmpty()) {
error.SuppressException();
return nullptr;
}
nsCOMPtr<nsIFile> localFile;
nsresult rv = NS_NewLocalFile(path, getter_AddRefs(localFile));
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
nsCOMPtr<nsIFile> parentFile;
rv = localFile->GetParent(getter_AddRefs(parentFile));
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
return parentFile.forget();
}
MOZ_ASSERT(aData.IsDirectory());
nsCOMPtr<nsIFile> localFile = aData.GetAsDirectory()->GetInternalNsIFile();
MOZ_ASSERT(localFile);
return localFile.forget();
}
void GetDOMFileOrDirectoryName(
const OwningFileOrDirectory& aData,
nsAString& aName) {
if (aData.IsFile()) {
aData.GetAsFile()->GetName(aName);
}
else {
MOZ_ASSERT(aData.IsDirectory());
ErrorResult rv;
aData.GetAsDirectory()->GetName(aName, rv);
if (NS_WARN_IF(rv.Failed())) {
rv.SuppressException();
}
}
}
void GetDOMFileOrDirectoryPath(
const OwningFileOrDirectory& aData,
nsAString& aPath, ErrorResult& aRv) {
if (aData.IsFile()) {
aData.GetAsFile()->GetMozFullPathInternal(aPath, aRv);
}
else {
MOZ_ASSERT(aData.IsDirectory());
aData.GetAsDirectory()->GetFullRealPath(aPath);
}
}
}
// namespace
NS_IMETHODIMP
HTMLInputElement::nsFilePickerShownCallback::Done(
nsIFilePicker::ResultCode aResult) {
mInput->PickerClosed();
if (aResult == nsIFilePicker::returnCancel) {
RefPtr<HTMLInputElement> inputElement(mInput);
return nsContentUtils::DispatchTrustedEvent(
inputElement->OwnerDoc(), inputElement, u
"cancel"_ns, CanBubble::eYes,
Cancelable::eNo);
}
mInput->OwnerDoc()->NotifyUserGestureActivation();
nsIFilePicker::Mode mode;
mFilePicker->GetMode(&mode);
// Collect new selected filenames
nsTArray<OwningFileOrDirectory> newFilesOrDirectories;
if (mode == nsIFilePicker::modeOpenMultiple) {
nsCOMPtr<nsISimpleEnumerator> iter;
nsresult rv =
mFilePicker->GetDomFileOrDirectoryEnumerator(getter_AddRefs(iter));
NS_ENSURE_SUCCESS(rv, rv);
if (!iter) {
return NS_OK;
}
nsCOMPtr<nsISupports> tmp;
bool hasMore =
true;
while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) {
iter->GetNext(getter_AddRefs(tmp));
RefPtr<Blob> domBlob = do_QueryObject(tmp);
MOZ_ASSERT(domBlob,
"Null file object from FilePicker's file enumerator?");
if (!domBlob) {
continue;
}
OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
element->SetAsFile() = domBlob->ToFile();
ImageInputTelemetry::MaybeRecordFilePickerImageInputTelemetry(domBlob);
}
}
else {
MOZ_ASSERT(mode == nsIFilePicker::modeOpen ||
mode == nsIFilePicker::modeGetFolder);
nsCOMPtr<nsISupports> tmp;
nsresult rv = mFilePicker->GetDomFileOrDirectory(getter_AddRefs(tmp));
NS_ENSURE_SUCCESS(rv, rv);
if (!tmp) {
return NS_OK;
}
// Show a prompt to get user confirmation before allowing folder access.
// This is to prevent sites from tricking the user into uploading files.
// See Bug 1338637.
if (mode == nsIFilePicker::modeGetFolder) {
nsCOMPtr<nsIPromptCollection> prompter =
do_GetService(
"@mozilla.org/embedcomp/prompt-collection;1");
if (!prompter) {
return NS_ERROR_NOT_AVAILABLE;
}
bool confirmed =
false;
BrowsingContext* bc = mInput->OwnerDoc()->GetBrowsingContext();
// Get directory name
RefPtr<Directory> directory =
static_cast<Directory*>(tmp.get());
nsAutoString directoryName;
ErrorResult error;
directory->GetName(directoryName, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
rv = prompter->ConfirmFolderUpload(bc, directoryName, &confirmed);
NS_ENSURE_SUCCESS(rv, rv);
if (!confirmed) {
// User aborted upload
return NS_OK;
}
}
RefPtr<Blob> blob = do_QueryObject(tmp);
if (blob) {
RefPtr<File> file = blob->ToFile();
MOZ_ASSERT(file);
OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
element->SetAsFile() = file;
ImageInputTelemetry::MaybeRecordFilePickerImageInputTelemetry(blob);
}
else if (tmp) {
RefPtr<Directory> directory =
static_cast<Directory*>(tmp.get());
OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
element->SetAsDirectory() = directory;
}
}
if (newFilesOrDirectories.IsEmpty()) {
return NS_OK;
}
// Store the last used directory using the content pref service:
nsCOMPtr<nsIFile> lastUsedDir = LastUsedDirectory(newFilesOrDirectories[0]);
if (lastUsedDir) {
HTMLInputElement::gUploadLastDir->StoreLastUsedDirectory(mInput->OwnerDoc(),
lastUsedDir);
}
// The text control frame (if there is one) isn't going to send a change
// event because it will think this is done by a script.
// So, we can safely send one by ourself.
mInput->SetFilesOrDirectories(newFilesOrDirectories,
true);
// mInput(HTMLInputElement) has no scriptGlobalObject, don't create
// DispatchChangeEventCallback
if (!mInput->GetOwnerGlobal()) {
return NS_OK;
}
RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
new DispatchChangeEventCallback(mInput);
if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
mInput->HasAttr(nsGkAtoms::webkitdirectory)) {
ErrorResult error;
GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(
true, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
helper->AddCallback(dispatchChangeEventCallback);
return NS_OK;
}
return dispatchChangeEventCallback->DispatchEvents();
}
NS_IMPL_ISUPPORTS(HTMLInputElement::nsFilePickerShownCallback,
nsIFilePickerShownCallback)
class nsColorPickerShownCallback final :
public nsIColorPickerShownCallback {
~nsColorPickerShownCallback() =
default;
public:
nsColorPickerShownCallback(HTMLInputElement* aInput,
nsIColorPicker* aColorPicker)
: mInput(aInput), mColorPicker(aColorPicker), mValueChanged(
false) {}
NS_DECL_ISUPPORTS
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Update(
const nsAString& aColor) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Done(
const nsAString& aColor) override;
private:
/**
* Updates the internals of the object using aColor as the new value.
* If aTrustedUpdate is true, it will consider that aColor is a new value.
* Otherwise, it will check that aColor is different from the current value.
*/
MOZ_CAN_RUN_SCRIPT
nsresult UpdateInternal(
const nsAString& aColor,
bool aTrustedUpdate);
RefPtr<HTMLInputElement> mInput;
nsCOMPtr<nsIColorPicker> mColorPicker;
bool mValueChanged;
};
nsresult nsColorPickerShownCallback::UpdateInternal(
const nsAString& aColor,
bool aTrustedUpdate) {
bool valueChanged =
false;
nsAutoString oldValue;
if (aTrustedUpdate) {
mInput->OwnerDoc()->NotifyUserGestureActivation();
valueChanged =
true;
}
else {
mInput->GetValue(oldValue, CallerType::System);
}
mInput->SetValue(aColor, CallerType::System, IgnoreErrors());
if (!aTrustedUpdate) {
nsAutoString newValue;
mInput->GetValue(newValue, CallerType::System);
if (!oldValue.Equals(newValue)) {
valueChanged =
true;
}
}
if (!valueChanged) {
return NS_OK;
}
mValueChanged =
true;
RefPtr<HTMLInputElement> input(mInput);
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(input);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
return NS_OK;
}
NS_IMETHODIMP
nsColorPickerShownCallback::Update(
const nsAString& aColor) {
return UpdateInternal(aColor,
true);
}
NS_IMETHODIMP
nsColorPickerShownCallback::Done(
const nsAString& aColor) {
/**
* When Done() is called, we might be at the end of a serie of Update() calls
* in which case mValueChanged is set to true and a change event will have to
* be fired but we might also be in a one shot Done() call situation in which
* case we should fire a change event iif the value actually changed.
* UpdateInternal(bool) is taking care of that logic for us.
*/
nsresult rv = NS_OK;
mInput->PickerClosed();
if (!aColor.IsEmpty()) {
UpdateInternal(aColor,
false);
}
if (mValueChanged) {
mInput->SetUserInteracted(
true);
rv = nsContentUtils::DispatchTrustedEvent(
mInput->OwnerDoc(),
static_cast<Element*>(mInput.get()), u
"change"_ns,
CanBubble::eYes, Cancelable::eNo);
}
return rv;
}
NS_IMPL_ISUPPORTS(nsColorPickerShownCallback, nsIColorPickerShownCallback)
static bool IsPickerBlocked(Document* aDoc) {
if (aDoc->ConsumeTransientUserGestureActivation()) {
return false;
}
nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
"DOM"_ns, aDoc,
nsContentUtils::eDOM_PROPERTIES,
"InputPickerBlockedNoUserActivation");
return true;
}
nsTArray<nsString> HTMLInputElement::GetColorsFromList() {
RefPtr<HTMLDataListElement> dataList = GetList();
if (!dataList) {
return {};
}
nsTArray<nsString> colors;
RefPtr<nsContentList> options = dataList->Options();
uint32_t length = options->Length(
true);
for (uint32_t i = 0; i < length; ++i) {
auto* option = HTMLOptionElement::FromNodeOrNull(options->Item(i,
false));
if (!option) {
continue;
}
nsString value;
option->GetValue(value);
if (IsValidSimpleColor(value)) {
ToLowerCase(value);
colors.AppendElement(value);
}
}
return colors;
}
nsresult HTMLInputElement::InitColorPicker() {
MOZ_ASSERT(IsMutable());
if (mPickerRunning) {
NS_WARNING(
"Just one nsIColorPicker is allowed");
return NS_ERROR_FAILURE;
}
nsCOMPtr<Document> doc = OwnerDoc();
RefPtr<BrowsingContext> bc = doc->GetBrowsingContext();
if (!bc) {
return NS_ERROR_FAILURE;
}
if (IsPickerBlocked(doc)) {
return NS_OK;
}
// Get Loc title
nsAutoString title;
nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"ColorPicker", title);
nsCOMPtr<nsIColorPicker> colorPicker =
do_CreateInstance(
"@mozilla.org/colorpicker;1");
if (!colorPicker) {
return NS_ERROR_FAILURE;
}
nsAutoString initialValue;
GetNonFileValueInternal(initialValue);
nsTArray<nsString> colors = GetColorsFromList();
nsresult rv = colorPicker->Init(bc, title, initialValue, colors);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIColorPickerShownCallback> callback =
new nsColorPickerShownCallback(
this, colorPicker);
rv = colorPicker->Open(callback);
if (NS_SUCCEEDED(rv)) {
mPickerRunning =
true;
SetStates(ElementState::OPEN,
true);
}
return rv;
}
nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
MOZ_ASSERT(IsMutable());
if (mPickerRunning) {
NS_WARNING(
"Just one nsIFilePicker is allowed");
return NS_ERROR_FAILURE;
}
// Get parent nsPIDOMWindow object.
nsCOMPtr<Document> doc = OwnerDoc();
RefPtr<BrowsingContext> bc = doc->GetBrowsingContext();
if (!bc) {
return NS_ERROR_FAILURE;
}
if (IsPickerBlocked(doc)) {
return NS_OK;
}
// Get Loc title
nsAutoString title;
nsAutoString okButtonLabel;
if (aType == FILE_PICKER_DIRECTORY) {
nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"DirectoryUpload", doc, title);
nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"DirectoryPickerOkButtonLabel", doc,
okButtonLabel);
}
else {
nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"FileUpload", doc, title);
}
nsCOMPtr<nsIFilePicker> filePicker =
do_CreateInstance(
"@mozilla.org/filepicker;1");
if (!filePicker)
return NS_ERROR_FAILURE;
nsIFilePicker::Mode mode;
if (aType == FILE_PICKER_DIRECTORY) {
mode = nsIFilePicker::modeGetFolder;
}
else if (HasAttr(nsGkAtoms::multiple)) {
mode = nsIFilePicker::modeOpenMultiple;
}
else {
mode = nsIFilePicker::modeOpen;
}
nsresult rv = filePicker->Init(bc, title, mode);
NS_ENSURE_SUCCESS(rv, rv);
if (!okButtonLabel.IsEmpty()) {
filePicker->SetOkButtonLabel(okButtonLabel);
}
// Native directory pickers ignore file type filters, so we don't spend
// cycles adding them for FILE_PICKER_DIRECTORY.
if (HasAttr(nsGkAtoms::accept) && aType != FILE_PICKER_DIRECTORY) {
SetFilePickerFiltersFromAccept(filePicker);
if (StaticPrefs::dom_capture_enabled()) {
if (
const nsAttrValue* captureVal = GetParsedAttr(nsGkAtoms::capture)) {
filePicker->SetCapture(
static_cast<nsIFilePicker::CaptureTarget>(
captureVal->GetEnumValue()));
}
}
}
else {
filePicker->AppendFilters(nsIFilePicker::filterAll);
}
// Set default directory and filename
nsAutoString defaultName;
const nsTArray<OwningFileOrDirectory>& oldFiles =
GetFilesOrDirectoriesInternal();
nsCOMPtr<nsIFilePickerShownCallback> callback =
new HTMLInputElement::nsFilePickerShownCallback(
this, filePicker);
if (!oldFiles.IsEmpty() && aType != FILE_PICKER_DIRECTORY) {
nsAutoString path;
nsCOMPtr<nsIFile> parentFile = LastUsedDirectory(oldFiles[0]);
if (parentFile) {
filePicker->SetDisplayDirectory(parentFile);
}
// Unfortunately nsIFilePicker doesn't allow multiple files to be
// default-selected, so only select something by default if exactly
// one file was selected before.
if (oldFiles.Length() == 1) {
nsAutoString leafName;
GetDOMFileOrDirectoryName(oldFiles[0], leafName);
if (!leafName.IsEmpty()) {
filePicker->SetDefaultString(leafName);
}
}
rv = filePicker->Open(callback);
if (NS_SUCCEEDED(rv)) {
mPickerRunning =
true;
SetStates(ElementState::OPEN,
true);
}
return rv;
}
HTMLInputElement::gUploadLastDir->FetchDirectoryAndDisplayPicker(
doc, filePicker, callback);
mPickerRunning =
true;
SetStates(ElementState::OPEN,
true);
return NS_OK;
}
#define CPS_PREF_NAME u
"browser.upload.lastDir"_ns
NS_IMPL_ISUPPORTS(UploadLastDir, nsIObserver, nsISupportsWeakReference)
void HTMLInputElement::InitUploadLastDir() {
gUploadLastDir =
new UploadLastDir();
NS_ADDREF(gUploadLastDir);
nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
if (observerService && gUploadLastDir) {
observerService->AddObserver(gUploadLastDir,
"browser:purge-session-history",
true);
}
}
void HTMLInputElement::DestroyUploadLastDir() { NS_IF_RELEASE(gUploadLastDir); }
nsresult UploadLastDir::FetchDirectoryAndDisplayPicker(
Document* aDoc, nsIFilePicker* aFilePicker,
nsIFilePickerShownCallback* aFpCallback) {
MOZ_ASSERT(aDoc,
"aDoc is null");
MOZ_ASSERT(aFilePicker,
"aFilePicker is null");
MOZ_ASSERT(aFpCallback,
"aFpCallback is null");
nsIURI* docURI = aDoc->GetDocumentURI();
MOZ_ASSERT(docURI,
"docURI is null");
nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext();
nsCOMPtr<nsIContentPrefCallback2> prefCallback =
new UploadLastDir::ContentPrefCallback(aFilePicker, aFpCallback);
// Attempt to get the CPS, if it's not present we'll fallback to use the
// Desktop folder
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
if (!contentPrefService) {
prefCallback->HandleCompletion(nsIContentPrefCallback2::COMPLETE_ERROR);
return NS_OK;
}
nsAutoCString cstrSpec;
docURI->GetSpec(cstrSpec);
NS_ConvertUTF8toUTF16 spec(cstrSpec);
contentPrefService->GetByDomainAndName(spec, CPS_PREF_NAME, loadContext,
prefCallback);
return NS_OK;
}
nsresult UploadLastDir::StoreLastUsedDirectory(Document* aDoc, nsIFile* aDir) {
MOZ_ASSERT(aDoc,
"aDoc is null");
if (!aDir) {
return NS_OK;
}
nsCOMPtr<nsIURI> docURI = aDoc->GetDocumentURI();
MOZ_ASSERT(docURI,
"docURI is null");
// Attempt to get the CPS, if it's not present we'll just return
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
if (!contentPrefService)
return NS_ERROR_NOT_AVAILABLE;
nsAutoCString cstrSpec;
docURI->GetSpec(cstrSpec);
NS_ConvertUTF8toUTF16 spec(cstrSpec);
// Find the parent of aFile, and store it
nsString unicodePath;
aDir->GetPath(unicodePath);
if (unicodePath.IsEmpty())
// nothing to do
return NS_OK;
RefPtr<nsVariantCC> prefValue =
new nsVariantCC();
prefValue->SetAsAString(unicodePath);
// Use the document's current load context to ensure that the content pref
// service doesn't persistently store this directory for this domain if the
// user is using private browsing:
nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext();
return contentPrefService->Set(spec, CPS_PREF_NAME, prefValue, loadContext,
nullptr);
}
NS_IMETHODIMP
UploadLastDir::Observe(nsISupports* aSubject,
char const* aTopic,
char16_t
const* aData) {
if (strcmp(aTopic,
"browser:purge-session-history") == 0) {
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
if (contentPrefService)
contentPrefService->RemoveByName(CPS_PREF_NAME, nullptr, nullptr);
}
return NS_OK;
}
#ifdef ACCESSIBILITY
// Helper method
static nsresult FireEventForAccessibility(HTMLInputElement* aTarget,
EventMessage aEventMessage);
#endif
//
// construction, destruction
//
HTMLInputElement::HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo,
FromParser aFromParser, FromClone aFromClone)
: TextControlElement(std::move(aNodeInfo), aFromParser,
FormControlType(kInputDefaultType->value)),
mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown),
mAutocompleteInfoState(nsContentUtils::eAutocompleteAttrState_Unknown),
mDisabledChanged(
false),
mValueChanged(
false),
mUserInteracted(
false),
mLastValueChangeWasInteractive(
false),
mCheckedChanged(
false),
mChecked(
false),
mShouldInitChecked(
false),
mDoneCreating(aFromParser == NOT_FROM_PARSER &&
aFromClone == FromClone::No),
mInInternalActivate(
false),
mCheckedIsToggled(
false),
mIndeterminate(
false),
mInhibitRestoration(aFromParser & FROM_PARSER_FRAGMENT),
mHasRange(
false),
mIsDraggingRange(
false),
mNumberControlSpinnerIsSpinning(
false),
mNumberControlSpinnerSpinsUp(
false),
mPickerRunning(
false),
mIsPreviewEnabled(
false),
mHasBeenTypePassword(
false),
mHasPatternAttribute(
false),
mRadioGroupContainer(nullptr) {
// If size is above 512, mozjemalloc allocates 1kB, see
// memory/build/mozjemalloc.cpp
static_assert(
sizeof(HTMLInputElement) <= 512,
"Keep the size of HTMLInputElement under 512 to avoid "
"performance regression!");
// We are in a type=text but we create TextControlState lazily.
mInputData.mState = nullptr;
void* memory = mInputTypeMem;
mInputType = InputType::Create(
this, mType, memory);
if (!gUploadLastDir) HTMLInputElement::InitUploadLastDir();
// Set up our default state. By default we're enabled (since we're a control
// type that can be disabled but not actually disabled right now), optional,
// read-write, and valid. Also by default we don't have to show validity UI
// and so forth.
AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ |
ElementState::VALID | ElementState::VALUE_EMPTY |
ElementState::READWRITE);
RemoveStatesSilently(ElementState::READONLY);
UpdateApzAwareFlag();
}
HTMLInputElement::~HTMLInputElement() {
if (mNumberControlSpinnerIsSpinning) {
StopNumberControlSpinnerSpin(eDisallowDispatchingEvents);
}
nsImageLoadingContent::Destroy();
FreeData();
}
void HTMLInputElement::FreeData() {
if (!IsSingleLineTextControl(
false)) {
free(mInputData.mValue);
mInputData.mValue = nullptr;
}
else if (mInputData.mState) {
// XXX Passing nullptr to UnbindFromFrame doesn't do anything!
UnbindFromFrame(nullptr);
mInputData.mState->Destroy();
mInputData.mState = nullptr;
}
if (mInputType) {
mInputType->DropReference();
mInputType = nullptr;
}
}
void HTMLInputElement::EnsureEditorState() {
MOZ_ASSERT(IsSingleLineTextControl(
false));
if (!mInputData.mState) {
mInputData.mState = TextControlState::Construct(
this);
}
}
TextControlState* HTMLInputElement::GetEditorState()
const {
if (!IsSingleLineTextControl(
false)) {
return nullptr;
}
// We've postponed allocating TextControlState, doing that in a const
// method is fine.
const_cast<HTMLInputElement*>(
this)->EnsureEditorState();
MOZ_ASSERT(mInputData.mState,
"Single line text controls need to have a state"
" associated with them");
return mInputData.mState;
}
// nsISupports
NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLInputElement)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLInputElement,
TextControlElement)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers)
if (tmp->IsSingleLineTextControl(
false) && tmp->mInputData.mState) {
tmp->mInputData.mState->Traverse(cb);
}
if (tmp->mFileData) {
tmp->mFileData->Traverse(cb);
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLInputElement,
TextControlElement)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers)
if (tmp->IsSingleLineTextControl(
false) && tmp->mInputData.mState) {
tmp->mInputData.mState->Unlink();
}
if (tmp->mFileData) {
tmp->mFileData->Unlink();
}
// XXX should unlink more?
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLInputElement,
TextControlElement,
imgINotificationObserver,
nsIImageLoadingContent,
nsIConstraintValidation)
// nsINode
nsresult HTMLInputElement::Clone(dom::NodeInfo* aNodeInfo,
nsINode** aResult)
const {
*aResult = nullptr;
RefPtr<HTMLInputElement> it =
new (aNodeInfo->NodeInfoManager())
HTMLInputElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER, FromClone::Yes);
nsresult rv =
const_cast<HTMLInputElement*>(
this)->CopyInnerTo(it);
NS_ENSURE_SUCCESS(rv, rv);
switch (GetValueMode()) {
case VALUE_MODE_VALUE:
if (mValueChanged) {
// We don't have our default value anymore. Set our value on
// the clone.
nsAutoString value;
GetNonFileValueInternal(value);
// SetValueInternal handles setting the VALUE_CHANGED bit for us
if (NS_WARN_IF(
NS_FAILED(rv = it->SetValueInternal(
value, {ValueSetterOption::SetValueChanged})))) {
return rv;
}
}
break;
case VALUE_MODE_FILENAME:
if (it->OwnerDoc()->IsStaticDocument()) {
// We're going to be used in print preview. Since the doc is static
// we can just grab the pretty string and use it as wallpaper
GetDisplayFileName(it->mFileData->mStaticDocFileList);
}
else {
it->mFileData->ClearGetFilesHelpers();
it->mFileData->mFilesOrDirectories.Clear();
it->mFileData->mFilesOrDirectories.AppendElements(
mFileData->mFilesOrDirectories);
}
break;
case VALUE_MODE_DEFAULT_ON:
case VALUE_MODE_DEFAULT:
break;
}
if (mCheckedChanged) {
// We no longer have our original checked state. Set our
// checked state on the clone.
it->DoSetChecked(mChecked,
/* aNotify */ false,
/* aSetValueChanged */ true);
// Then tell DoneCreatingElement() not to overwrite:
it->mShouldInitChecked =
false;
}
it->mIndeterminate = mIndeterminate;
it->DoneCreatingElement();
it->SetLastValueChangeWasInteractive(mLastValueChangeWasInteractive);
it.forget(aResult);
return NS_OK;
}
void HTMLInputElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
const nsAttrValue* aValue,
bool aNotify) {
if (aNameSpaceID == kNameSpaceID_None) {
if (aNotify && aName == nsGkAtoms::disabled) {
mDisabledChanged =
true;
}
// When name or type changes, radio should be removed from radio group.
// If we are not done creating the radio, we also should not do it.
if (mType == FormControlType::InputRadio) {
if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
(mForm || mDoneCreating)) {
RemoveFromRadioGroup();
}
else if (aName == nsGkAtoms::required) {
auto* container = GetCurrentRadioGroupContainer();
if (container && ((aValue && !HasAttr(aNameSpaceID, aName)) ||
(!aValue && HasAttr(aNameSpaceID, aName)))) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
container->RadioRequiredWillChange(name, !!aValue);
}
}
}
if (aName == nsGkAtoms::webkitdirectory) {
Telemetry::Accumulate(Telemetry::WEBKIT_DIRECTORY_USED,
true);
}
}
return nsGenericHTMLFormControlElementWithState::BeforeSetAttr(
aNameSpaceID, aName, aValue, aNotify);
}
void HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
const nsAttrValue* aValue,
const nsAttrValue* aOldValue,
nsIPrincipal* aSubjectPrincipal,
bool aNotify) {
if (aNameSpaceID == kNameSpaceID_None) {
bool needValidityUpdate =
false;
if (aName == nsGkAtoms::src) {
mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
this, aValue ? aValue->GetStringValue() : EmptyString(),
aSubjectPrincipal);
if (aNotify && mType == FormControlType::InputImage) {
if (aValue) {
// Mark channel as urgent-start before load image if the image load is
// initiated by a user interaction.
mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
LoadImage(aValue->GetStringValue(),
true, aNotify,
eImageLoadType_Normal, mSrcTriggeringPrincipal);
}
else {
// Null value means the attr got unset; drop the image
CancelImageRequests(aNotify);
}
}
}
if (aName == nsGkAtoms::value) {
// If the element has a value in value mode, the value content attribute
// is the default value. So if the elements value didn't change from the
// default, we have to re-set it.
if (!mValueChanged && GetValueMode() == VALUE_MODE_VALUE) {
SetDefaultValueAsValue();
}
else if (GetValueMode() == VALUE_MODE_DEFAULT) {
ResetDirFormAssociatedElement(
this, aNotify, HasDirAuto());
}
// GetStepBase() depends on the `value` attribute if `min` is not present,
// even if the value doesn't change.
UpdateStepMismatchValidityState();
needValidityUpdate =
true;
}
// Checked must be set no matter what type of control it is, since
// mChecked must reflect the new value
if (aName == nsGkAtoms::checked) {
if (IsRadioOrCheckbox()) {
SetStates(ElementState::
DEFAULT, !!aValue, aNotify);
}
if (!mCheckedChanged) {
// Delay setting checked if we are creating this element (wait
// until everything is set)
if (!mDoneCreating) {
mShouldInitChecked =
true;
}
else {
DoSetChecked(!!aValue, aNotify,
/* aSetValueChanged */ false);
}
}
needValidityUpdate =
true;
}
if (aName == nsGkAtoms::type) {
FormControlType newType;
if (!aValue) {
// We're now a text input.
newType = FormControlType(kInputDefaultType->value);
}
else {
newType = FormControlType(aValue->GetEnumValue());
}
if (newType != mType) {
HandleTypeChange(newType, aNotify);
needValidityUpdate =
true;
}
}
// When name or type changes, radio should be added to radio group.
// If we are not done creating the radio, we also should not do it.
if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
mType == FormControlType::InputRadio && (mForm || mDoneCreating)) {
AddToRadioGroup();
UpdateValueMissingValidityStateForRadio(
false);
needValidityUpdate =
true;
}
if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled ||
aName == nsGkAtoms::readonly) {
if (aName == nsGkAtoms::disabled) {
// This *has* to be called *before* validity state check because
// UpdateBarredFromConstraintValidation and
// UpdateValueMissingValidityState depend on our disabled state.
UpdateDisabledState(aNotify);
}
if (aName == nsGkAtoms::required && DoesRequiredApply()) {
// This *has* to be called *before* UpdateValueMissingValidityState
// because UpdateValueMissingValidityState depends on our required
// state.
UpdateRequiredState(!!aValue, aNotify);
}
if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) {
UpdateReadOnlyState(aNotify);
}
UpdateValueMissingValidityState();
// This *has* to be called *after* validity has changed.
if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
UpdateBarredFromConstraintValidation();
}
needValidityUpdate =
true;
}
else if (aName == nsGkAtoms::maxlength) {
UpdateTooLongValidityState();
needValidityUpdate =
true;
}
else if (aName == nsGkAtoms::minlength) {
UpdateTooShortValidityState();
needValidityUpdate =
true;
}
else if (aName == nsGkAtoms::pattern) {
// Although pattern attribute only applies to single line text controls,
// we set this flag for all input types to save having to check the type
// here.
mHasPatternAttribute = !!aValue;
if (mDoneCreating) {
UpdatePatternMismatchValidityState();
}
needValidityUpdate =
true;
}
else if (aName == nsGkAtoms::multiple) {
UpdateTypeMismatchValidityState();
needValidityUpdate =
true;
}
else if (aName == nsGkAtoms::max) {
UpdateHasRange(aNotify);
mInputType->MinMaxStepAttrChanged();
// Validity state must be updated *after* the UpdateValueDueToAttrChange
// call above or else the following assert will not be valid.
// We don't assert the state of underflow during creation since
// DoneCreatingElement sanitizes.
UpdateRangeOverflowValidityState();
needValidityUpdate =
true;
MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
!GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
"HTML5 spec does not allow underflow for type=range");
}
else if (aName == nsGkAtoms::min) {
UpdateHasRange(aNotify);
mInputType->MinMaxStepAttrChanged();
// See corresponding @max comment
UpdateRangeUnderflowValidityState();
UpdateStepMismatchValidityState();
needValidityUpdate =
true;
MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
!GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
"HTML5 spec does not allow underflow for type=range");
}
else if (aName == nsGkAtoms::step) {
mInputType->MinMaxStepAttrChanged();
// See corresponding @max comment
UpdateStepMismatchValidityState();
needValidityUpdate =
true;
MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
!GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
"HTML5 spec does not allow underflow for type=range");
}
else if (aName == nsGkAtoms::dir && aValue &&
aValue->Equals(nsGkAtoms::_
auto, eIgnoreCase)) {
ResetDirFormAssociatedElement(
this, aNotify,
true);
}
else if (aName == nsGkAtoms::lang) {
// FIXME(emilio, bug 1651070): This doesn't account for lang changes on
// ancestors.
if (mType == FormControlType::InputNumber) {
// The validity of our value may have changed based on the locale.
UpdateValidityState();
needValidityUpdate =
true;
}
}
else if (aName == nsGkAtoms::autocomplete) {
// Clear the cached @autocomplete attribute and autocompleteInfo state.
mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown;
mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown;
}
else if (aName == nsGkAtoms::placeholder) {
// Full addition / removals of the attribute reconstruct right now.
if (nsTextControlFrame* f = do_QueryFrame(GetPrimaryFrame())) {
f->PlaceholderChanged(aOldValue, aValue);
}
UpdatePlaceholderShownState();
needValidityUpdate =
true;
}
if (CreatesDateTimeWidget()) {
if (aName == nsGkAtoms::value || aName == nsGkAtoms::readonly ||
aName == nsGkAtoms::tabindex || aName == nsGkAtoms::required ||
aName == nsGkAtoms::disabled) {
// If original target is this and not the inner text control, we should
// pass the focus to the inner text control.
if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) {
AsyncEventDispatcher::RunDOMEventWhenSafe(
*dateTimeBoxElement,
aName == nsGkAtoms::value ? u
"MozDateTimeValueChanged"_ns
: u
"MozDateTimeAttributeChanged"_ns,
CanBubble::eNo, ChromeOnlyDispatch::eNo);
}
}
}
if (needValidityUpdate) {
UpdateValidityElementStates(aNotify);
}
}
return nsGenericHTMLFormControlElementWithState::AfterSetAttr(
aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
}
void HTMLInputElement::BeforeSetForm(HTMLFormElement* aForm,
bool aBindToTree) {
// No need to remove from radio group if we are just binding to tree.
if (mType == FormControlType::InputRadio && !aBindToTree) {
RemoveFromRadioGroup();
}
// Dispatch event when <input> @form is set
if (!aBindToTree) {
MaybeDispatchLoginManagerEvents(aForm);
}
}
void HTMLInputElement::AfterClearForm(
bool aUnbindOrDelete) {
MOZ_ASSERT(!mForm);
// Do not add back to radio group if we are releasing or unbinding from tree.
if (mType == FormControlType::InputRadio && !aUnbindOrDelete &&
!GetCurrentRadioGroupContainer()) {
AddToRadioGroup();
UpdateValueMissingValidityStateForRadio(
false);
}
}
void HTMLInputElement::ResultForDialogSubmit(nsAString& aResult) {
if (mType == FormControlType::InputImage) {
// Get a property set by the frame to find out where it was clicked.
const auto* lastClickedPoint =
static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint));
int32_t x, y;
if (lastClickedPoint) {
x = lastClickedPoint->x;
y = lastClickedPoint->y;
}
else {
x = y = 0;
}
aResult.AppendInt(x);
aResult.AppendLiteral(
",");
aResult.AppendInt(y);
}
else {
GetAttr(nsGkAtoms::value, aResult);
}
}
void HTMLInputElement::GetAutocomplete(nsAString& aValue) {
if (!DoesAutocompleteApply()) {
return;
}
aValue.Truncate();
const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(
attributeVal, aValue, mAutocompleteAttrState);
}
void HTMLInputElement::GetAutocompleteInfo(Nullable<AutocompleteInfo>& aInfo) {
if (!DoesAutocompleteApply()) {
aInfo.SetNull();
return;
}
const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
mAutocompleteInfoState = nsContentUtils::SerializeAutocompleteAttribute(
attributeVal, aInfo.SetValue(), mAutocompleteInfoState,
true);
}
void HTMLInputElement::GetCapture(nsAString& aValue) {
GetEnumAttr(nsGkAtoms::capture, kCaptureDefault->tag, aValue);
}
void HTMLInputElement::GetFormEnctype(nsAString& aValue) {
GetEnumAttr(nsGkAtoms::formenctype,
"", kFormDefaultEnctype->tag, aValue);
}
void HTMLInputElement::GetFormMethod(nsAString& aValue) {
GetEnumAttr(nsGkAtoms::formmethod,
"", kFormDefaultMethod->tag, aValue);
}
void HTMLInputElement::GetType(nsAString& aValue)
const {
GetEnumAttr(nsGkAtoms::type, kInputDefaultType->tag, aValue);
}
int32_t HTMLInputElement::TabIndexDefault() {
return 0; }
uint32_t HTMLInputElement::Height() {
if (mType != FormControlType::InputImage) {
return 0;
}
return GetWidthHeightForImage().height;
}
void HTMLInputElement::SetIndeterminateInternal(
bool aValue,
bool aShouldInvalidate) {
mIndeterminate = aValue;
if (mType != FormControlType::InputCheckbox) {
return;
}
SetStates(ElementState::INDETERMINATE, aValue);
if (aShouldInvalidate) {
// Repaint the frame
if (nsIFrame* frame = GetPrimaryFrame()) {
frame->InvalidateFrameSubtree();
}
}
}
void HTMLInputElement::SetIndeterminate(
bool aValue) {
SetIndeterminateInternal(aValue,
true);
}
uint32_t HTMLInputElement::Width() {
if (mType != FormControlType::InputImage) {
return 0;
}
return GetWidthHeightForImage().width;
}
bool HTMLInputElement::SanitizesOnValueGetter()
const {
// Don't return non-sanitized value for datetime types, email, or number.
return mType == FormControlType::InputEmail ||
mType == FormControlType::InputNumber || IsDateTimeInputType(mType);
}
void HTMLInputElement::GetValue(nsAString& aValue, CallerType aCallerType) {
GetValueInternal(aValue, aCallerType);
// In the case where we need to sanitize an input value without affecting
// the displayed user's input, we instead sanitize only on .value accesses.
// For the more general case of input elements displaying text that isn't
// their current value, see bug 805049.
if (SanitizesOnValueGetter()) {
SanitizeValue(aValue, SanitizationKind::ForValueGetter);
}
}
void HTMLInputElement::GetValueInternal(nsAString& aValue,
CallerType aCallerType)
const {
if (mType != FormControlType::InputFile) {
GetNonFileValueInternal(aValue);
return;
}
if (aCallerType == CallerType::System) {
aValue.Assign(mFileData->mFirstFilePath);
return;
}
if (mFileData->mFilesOrDirectories.IsEmpty()) {
aValue.Truncate();
return;
}
nsAutoString file;
GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], file);
if (file.IsEmpty()) {
aValue.Truncate();
return;
}
aValue.AssignLiteral(
"C:\\fakepath\\");
aValue.Append(file);
}
void HTMLInputElement::GetNonFileValueInternal(nsAString& aValue)
const {
switch (GetValueMode()) {
case VALUE_MODE_VALUE:
if (IsSingleLineTextControl(
false)) {
if (mInputData.mState) {
mInputData.mState->GetValue(aValue,
true,
/* aForDisplay = */ false);
}
else {
// Value hasn't been set yet.
aValue.Truncate();
}
}
else if (!aValue.Assign(mInputData.mValue, fallible)) {
aValue.Truncate();
}
return;
case VALUE_MODE_FILENAME:
MOZ_ASSERT_UNREACHABLE(
"Someone screwed up here");
// We'll just return empty string if someone does screw up.
aValue.Truncate();
return;
case VALUE_MODE_DEFAULT:
// Treat defaultValue as value.
GetAttr(nsGkAtoms::value, aValue);
return;
case VALUE_MODE_DEFAULT_ON:
// Treat default value as value and returns "on" if no value.
if (!GetAttr(nsGkAtoms::value, aValue)) {
aValue.AssignLiteral(
"on");
}
return;
}
}
void HTMLInputElement::ClearFiles(
bool aSetValueChanged) {
nsTArray<OwningFileOrDirectory> data;
SetFilesOrDirectories(data, aSetValueChanged);
}
int32_t HTMLInputElement::MonthsSinceJan1970(uint32_t aYear,
uint32_t aMonth)
const {
return (aYear - 1970) * 12 + aMonth - 1;
}
/* static */
Decimal HTMLInputElement::StringToDecimal(
const nsAString& aValue) {
if (!IsAscii(aValue)) {
return Decimal::nan();
}
NS_LossyConvertUTF16toASCII asciiString(aValue);
std::string stdString(asciiString.get(), asciiString.Length());
auto decimal = Decimal::fromString(stdString);
if (!decimal.isFinite()) {
return Decimal::nan();
}
// Numbers are considered finite IEEE 754 Double-precision floating point
// values, but decimal supports a bigger range.
static const Decimal maxDouble =
Decimal::fromDouble(std::numeric_limits<
double>::max());
if (decimal < -maxDouble || decimal > maxDouble) {
return Decimal::nan();
}
return decimal;
}
Decimal HTMLInputElement::GetValueAsDecimal()
const {
nsAutoString stringValue;
GetNonFileValueInternal(stringValue);
Decimal result = mInputType->ConvertStringToNumber(stringValue).mResult;
return result.isFinite() ? result : Decimal::nan();
}
void HTMLInputElement::SetValue(
const nsAString& aValue, CallerType aCallerType,
ErrorResult& aRv) {
// check security. Note that setting the value to the empty string is always
// OK and gives pages a way to clear a file input if necessary.
if (mType == FormControlType::InputFile) {
if (!aValue.IsEmpty()) {
if (aCallerType != CallerType::System) {
// setting the value of a "FILE" input widget requires
// chrome privilege
aRv.
Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
Sequence<nsString> list;
if (!list.AppendElement(aValue, fallible)) {
aRv.
Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
MozSetFileNameArray(list, aRv);
return;
}
ClearFiles(
true);
}
else {
if (MayFireChangeOnBlur()) {
// If the value has been set by a script, we basically want to keep the
// current change event state. If the element is ready to fire a change
// event, we should keep it that way. Otherwise, we should make sure the
// element will not fire any event because of the script interaction.
//
// NOTE: this is currently quite expensive work (too much string
// manipulation). We should probably optimize that.
nsAutoString currentValue;
GetNonFileValueInternal(currentValue);
nsresult rv = SetValueInternal(
aValue, ¤tValue,
{ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
ValueSetterOption::MoveCursorToEndIfValueChanged});
if (NS_FAILED(rv)) {
aRv.
Throw(rv);
return;
}
if (mFocusedValue.Equals(currentValue)) {
GetValue(mFocusedValue, aCallerType);
}
}
else {
nsresult rv = SetValueInternal(
aValue,
{ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
ValueSetterOption::MoveCursorToEndIfValueChanged});
if (NS_FAILED(rv)) {
aRv.
Throw(rv);
return;
}
}
}
}
HTMLDataListElement* HTMLInputElement::GetList()
const {
nsAutoString dataListId;
GetAttr(nsGkAtoms::list_, dataListId);
if (dataListId.IsEmpty()) {
return nullptr;
}
DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
if (!docOrShadow) {
return nullptr;
}
return HTMLDataListElement::FromNodeOrNull(
docOrShadow->GetElementById(dataListId));
}
void HTMLInputElement::SetValue(Decimal aValue, CallerType aCallerType) {
MOZ_ASSERT(!aValue.isInfinity(),
"aValue must not be Infinity!");
if (aValue.isNaN()) {
SetValue(u
""_ns, aCallerType, IgnoreErrors());
return;
}
nsAutoString value;
mInputType->ConvertNumberToString(aValue, value);
SetValue(value, aCallerType, IgnoreErrors());
}
void HTMLInputElement::GetValueAsDate(JSContext* aCx,
JS::MutableHandle<JSObject*> aObject,
ErrorResult& aRv) {
aObject.set(nullptr);
if (!IsDateTimeInputType(mType)) {
return;
}
Maybe<JS::ClippedTime> time;
switch (mType) {
case FormControlType::InputDate: {
uint32_t year, month, day;
nsAutoString value;
GetNonFileValueInternal(value);
if (!ParseDate(value, &year, &month, &day)) {
return;
}
time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day)));
break;
}
case FormControlType::InputTime: {
uint32_t millisecond;
nsAutoString value;
GetNonFileValueInternal(value);
if (!ParseTime(value, &millisecond)) {
return;
}
time.emplace(JS::TimeClip(millisecond));
MOZ_ASSERT(time->toDouble() == millisecond,
"HTML times are restricted to the day after the epoch and "
"never clip");
break;
}
case FormControlType::InputMonth: {
uint32_t year, month;
nsAutoString value;
GetNonFileValueInternal(value);
if (!ParseMonth(value, &year, &month)) {
return;
}
time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, 1)));
break;
}
case FormControlType::InputWeek: {
uint32_t year, week;
nsAutoString value;
GetNonFileValueInternal(value);
if (!ParseWeek(value, &year, &week)) {
return;
}
double days = DaysSinceEpochFromWeek(year, week);
time.emplace(JS::TimeClip(days * kMsPerDay));
break;
}
case FormControlType::InputDatetimeLocal: {
uint32_t year, month, day, timeInMs;
nsAutoString value;
GetNonFileValueInternal(value);
if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) {
return;
}
time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)));
break;
}
default:
break;
}
if (time) {
aObject.set(JS::NewDateObject(aCx, *time));
if (!aObject) {
aRv.NoteJSContextException(aCx);
}
return;
}
MOZ_ASSERT(
false,
"Unrecognized input type");
aRv.
Throw(NS_ERROR_UNEXPECTED);
}
void HTMLInputElement::SetValueAsDate(JSContext* aCx,
JS::Handle<JSObject*> aObj,
ErrorResult& aRv) {
if (!IsDateTimeInputType(mType)) {
aRv.
Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
if (aObj) {
bool isDate;
if (!JS::ObjectIsDate(aCx, aObj, &isDate)) {
aRv.NoteJSContextException(aCx);
return;
}
if (!isDate) {
aRv.ThrowTypeError(
"Value being assigned is not a date.");
return;
}
}
double milliseconds;
if (aObj) {
if (!js::DateGetMsecSinceEpoch(aCx, aObj, &milliseconds)) {
aRv.NoteJSContextException(aCx);
return;
}
}
else {
milliseconds = UnspecifiedNaN<
double>();
}
// At this point we know we're not a file input, so we can just pass "not
// system" as the caller type, since the caller type only matters in the file
// input case.
if (std::isnan(milliseconds)) {
SetValue(u
""_ns, CallerType::NonSystem, aRv);
return;
}
if (mType != FormControlType::InputMonth) {
SetValue(Decimal::fromDouble(milliseconds), CallerType::NonSystem);
return;
}
// type=month expects the value to be number of months.
double year = JS::YearFromTime(milliseconds);
double month = JS::MonthFromTime(milliseconds);
if (std::isnan(year) || std::isnan(month)) {
SetValue(u
""_ns, CallerType::NonSystem, aRv);
return;
}
int32_t months = MonthsSinceJan1970(year, month + 1);
SetValue(Decimal(int32_t(months)), CallerType::NonSystem);
}
void HTMLInputElement::SetValueAsNumber(
double aValueAsNumber,
ErrorResult& aRv) {
// TODO: return TypeError when HTMLInputElement is converted to WebIDL, see
// bug 825197.
if (std::isinf(aValueAsNumber)) {
aRv.
Throw(NS_ERROR_INVALID_ARG);
return;
}
if (!DoesValueAsNumberApply()) {
aRv.
Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
// At this point we know we're not a file input, so we can just pass "not
// system" as the caller type, since the caller type only matters in the file
// input case.
SetValue(Decimal::fromDouble(aValueAsNumber), CallerType::NonSystem);
}
Decimal HTMLInputElement::GetMinimum()
const {
MOZ_ASSERT(
DoesValueAsNumberApply(),
"GetMinimum() should only be used for types that allow .valueAsNumber");
// Only type=range has a default minimum
Decimal defaultMinimum =
mType == FormControlType::InputRange ? Decimal(0) : Decimal::nan();
if (!HasAttr(nsGkAtoms::min)) {
return defaultMinimum;
}
nsAutoString minStr;
GetAttr(nsGkAtoms::min, minStr);
Decimal min = mInputType->ConvertStringToNumber(minStr).mResult;
return min.isFinite() ? min : defaultMinimum;
}
Decimal HTMLInputElement::GetMaximum()
const {
MOZ_ASSERT(
DoesValueAsNumberApply(),
"GetMaximum() should only be used for types that allow .valueAsNumber");
// Only type=range has a default maximum
Decimal defaultMaximum =
mType == FormControlType::InputRange ? Decimal(100) : Decimal::nan();
if (!HasAttr(nsGkAtoms::max)) {
return defaultMaximum;
}
nsAutoString maxStr;
GetAttr(nsGkAtoms::max, maxStr);
Decimal max = mInputType->ConvertStringToNumber(maxStr).mResult;
return max.isFinite() ? max : defaultMaximum;
}
Decimal HTMLInputElement::GetStepBase()
const {
MOZ_ASSERT(IsDateTimeInputType(mType) ||
mType == FormControlType::InputNumber ||
mType == FormControlType::InputRange,
"Check that kDefaultStepBase is correct for this new type");
// Do NOT use GetMinimum here - the spec says to use "the min content
// attribute", not "the minimum".
nsAutoString minStr;
if (GetAttr(nsGkAtoms::min, minStr)) {
Decimal min = mInputType->ConvertStringToNumber(minStr).mResult;
if (min.isFinite()) {
return min;
}
}
// If @min is not a double, we should use @value.
nsAutoString valueStr;
if (GetAttr(nsGkAtoms::value, valueStr)) {
Decimal value = mInputType->ConvertStringToNumber(valueStr).mResult;
if (value.isFinite()) {
return value;
}
}
if (mType == FormControlType::InputWeek) {
return kDefaultStepBaseWeek;
}
return kDefaultStepBase;
}
Decimal HTMLInputElement::GetValueIfStepped(int32_t aStep,
StepCallerType aCallerType,
ErrorResult& aRv) {
constexpr
auto kNaN = Decimal::nan();
if (!DoStepDownStepUpApply()) {
aRv.ThrowInvalidStateError(
"Step doesn't apply to this input type");
--> --------------------
--> maximum size reached
--> --------------------