/* -*- 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/DebugOnly.h" #include "mozilla/dom/Date.h" #include "mozilla/dom/Directory.h" #include "mozilla/dom/HTMLFormSubmission.h" #include "mozilla/dom/FileSystemUtils.h" #include "mozilla/dom/GetFilesHelper.h" #include "nsAttrValueInlines.h" #include "nsCRTGlue.h" #include "nsIDOMHTMLInputElement.h" #include "nsITextControlElement.h" #include "nsIDOMNSEditableElement.h" #include "nsIRadioVisitor.h" #include "nsIPhonetic.h" #include "HTMLFormSubmissionConstants.h" #include "mozilla/Telemetry.h" #include "nsIControllers.h" #include "nsIStringBundle.h" #include "nsFocusManager.h" #include "nsColorControlFrame.h" #include "nsNumberControlFrame.h" #include "nsPIDOMWindow.h" #include "nsRepeatService.h" #include "nsContentCID.h" #include "nsIComponentManager.h" #include "nsIDOMHTMLFormElement.h" #include "mozilla/dom/ProgressEvent.h" #include "nsGkAtoms.h" #include "nsStyleConsts.h" #include "nsPresContext.h" #include "nsMappedAttributes.h" #include "nsIFormControl.h" #include "nsIDocument.h" #include "nsIPresShell.h" #include "nsIFormControlFrame.h" #include "nsITextControlFrame.h" #include "nsIFrame.h" #include "nsRangeFrame.h" #include "nsIServiceManager.h" #include "nsError.h" #include "nsIEditor.h" #include "nsIIOService.h" #include "nsDocument.h" #include "nsAttrValueOrString.h" #include "nsDateTimeControlFrame.h" #include "nsPresState.h" #include "nsIDOMEvent.h" #include "nsIDOMNodeList.h" #include "nsIDOMHTMLCollection.h" #include "nsLinebreakConverter.h" //to strip out carriage returns #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "nsLayoutUtils.h" #include "nsVariant.h" #include "nsIDOMMutationEvent.h" #include "mozilla/ContentEvents.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventStates.h" #include "mozilla/InternalMutationEvent.h" #include "mozilla/TextEvents.h" #include "mozilla/TouchEvents.h" #include "nsRuleData.h" #include // input type=radio #include "nsIRadioGroupContainer.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 "nsNetCID.h" #include "nsNetUtil.h" #include "nsDirectoryServiceDefs.h" #include "nsIContentPrefService.h" #include "nsIMIMEService.h" #include "nsIObserverService.h" #include "nsIPopupWindowManager.h" #include "nsGlobalWindow.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 "nsRadioVisitor.h" #include "nsTextEditorState.h" #include "mozilla/LookAndFeel.h" #include "mozilla/Preferences.h" #include "mozilla/MathAlgorithms.h" #include "nsIIDNService.h" #include #include "nsIColorPicker.h" #include "nsIDatePicker.h" #include "nsIStringEnumerator.h" #include "HTMLSplitOnSpacesTokenizer.h" #include "nsIController.h" #include "nsIMIMEInfo.h" #include "nsFrameSelection.h" #include "nsIConsoleService.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 static NS_DEFINE_CID(kXULControllersCID, NS_XULCONTROLLERS_CID); // This must come outside of any namespace, or else it won't overload with the // double based version in nsMathUtils.h inline mozilla::Decimal NS_floorModulo(mozilla::Decimal x, mozilla::Decimal y) { return (x - y * (x / y).floor()); } namespace mozilla { namespace dom { // First bits are needed for the control type. #define NS_OUTER_ACTIVATE_EVENT (1 << 9) #define NS_ORIGINAL_CHECKED_VALUE (1 << 10) #define NS_NO_CONTENT_DISPATCH (1 << 11) #define NS_ORIGINAL_INDETERMINATE_VALUE (1 << 12) #define NS_CONTROL_TYPE(bits) ((bits) & ~( \ NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | NS_NO_CONTENT_DISPATCH | \ NS_ORIGINAL_INDETERMINATE_VALUE)) // 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", NS_FORM_INPUT_BUTTON }, { "checkbox", NS_FORM_INPUT_CHECKBOX }, { "color", NS_FORM_INPUT_COLOR }, { "date", NS_FORM_INPUT_DATE }, { "datetime-local", NS_FORM_INPUT_DATETIME_LOCAL }, { "email", NS_FORM_INPUT_EMAIL }, { "file", NS_FORM_INPUT_FILE }, { "hidden", NS_FORM_INPUT_HIDDEN }, { "reset", NS_FORM_INPUT_RESET }, { "image", NS_FORM_INPUT_IMAGE }, { "month", NS_FORM_INPUT_MONTH }, { "number", NS_FORM_INPUT_NUMBER }, { "password", NS_FORM_INPUT_PASSWORD }, { "radio", NS_FORM_INPUT_RADIO }, { "range", NS_FORM_INPUT_RANGE }, { "search", NS_FORM_INPUT_SEARCH }, { "submit", NS_FORM_INPUT_SUBMIT }, { "tel", NS_FORM_INPUT_TEL }, { "text", NS_FORM_INPUT_TEXT }, { "time", NS_FORM_INPUT_TIME }, { "url", NS_FORM_INPUT_URL }, { "week", NS_FORM_INPUT_WEEK }, { nullptr, 0 } }; // Default type is 'text'. static const nsAttrValue::EnumTable* kInputDefaultType = &kInputTypeTable[18]; static const uint8_t NS_INPUT_INPUTMODE_AUTO = 0; static const uint8_t NS_INPUT_INPUTMODE_NUMERIC = 1; static const uint8_t NS_INPUT_INPUTMODE_DIGIT = 2; static const uint8_t NS_INPUT_INPUTMODE_UPPERCASE = 3; static const uint8_t NS_INPUT_INPUTMODE_LOWERCASE = 4; static const uint8_t NS_INPUT_INPUTMODE_TITLECASE = 5; static const uint8_t NS_INPUT_INPUTMODE_AUTOCAPITALIZED = 6; static const nsAttrValue::EnumTable kInputInputmodeTable[] = { { "auto", NS_INPUT_INPUTMODE_AUTO }, { "numeric", NS_INPUT_INPUTMODE_NUMERIC }, { "digit", NS_INPUT_INPUTMODE_DIGIT }, { "uppercase", NS_INPUT_INPUTMODE_UPPERCASE }, { "lowercase", NS_INPUT_INPUTMODE_LOWERCASE }, { "titlecase", NS_INPUT_INPUTMODE_TITLECASE }, { "autocapitalized", NS_INPUT_INPUTMODE_AUTOCAPITALIZED }, { nullptr, 0 } }; // Default inputmode value is "auto". static const nsAttrValue::EnumTable* kInputDefaultInputmode = &kInputInputmodeTable[0]; const Decimal HTMLInputElement::kStepScaleFactorDate = Decimal(86400000); const Decimal HTMLInputElement::kStepScaleFactorNumberRange = Decimal(1); const Decimal HTMLInputElement::kStepScaleFactorTime = Decimal(1000); const Decimal HTMLInputElement::kStepScaleFactorMonth = Decimal(1); const Decimal HTMLInputElement::kStepScaleFactorWeek = Decimal(7 * 86400000); const Decimal HTMLInputElement::kDefaultStepBase = Decimal(0); const Decimal HTMLInputElement::kDefaultStepBaseWeek = Decimal(-259200000); const Decimal HTMLInputElement::kDefaultStep = Decimal(1); const Decimal HTMLInputElement::kDefaultStepTime = Decimal(60); const Decimal HTMLInputElement::kStepAny = Decimal(0); 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; #define NS_INPUT_ELEMENT_STATE_IID \ { /* dc3b3d14-23e2-4479-b513-7b369343e3a0 */ \ 0xdc3b3d14, \ 0x23e2, \ 0x4479, \ {0xb5, 0x13, 0x7b, 0x36, 0x93, 0x43, 0xe3, 0xa0} \ } #define PROGRESS_STR "progress" static const uint32_t kProgressEventInterval = 50; // ms // 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 Sequence>& aFiles) override { nsTArray array; for (uint32_t i = 0; i < aFiles.Length(); ++i) { OwningFileOrDirectory* element = array.AppendElement(); element->SetAsFile() = aFiles[i]; } mInputElement->SetFilesOrDirectories(array, true); Unused << NS_WARN_IF(NS_FAILED(DispatchEvents())); } nsresult DispatchEvents() { nsresult rv = NS_OK; rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(), static_cast(mInputElement.get()), NS_LITERAL_STRING("input"), true, false); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "DispatchTrustedEvent failed"); rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(), static_cast(mInputElement.get()), NS_LITERAL_STRING("change"), true, false); return rv; } private: RefPtr mInputElement; }; class HTMLInputElementState final : public nsISupports { public: NS_DECLARE_STATIC_IID_ACCESSOR(NS_INPUT_ELEMENT_STATE_IID) NS_DECL_ISUPPORTS bool IsCheckedSet() { return mCheckedSet; } bool GetChecked() { return mChecked; } void SetChecked(bool aChecked) { mChecked = aChecked; mCheckedSet = true; } const nsString& GetValue() { return mValue; } void SetValue(const nsAString& aValue) { mValue = aValue; } void GetFilesOrDirectories(nsPIDOMWindowInner* aWindow, nsTArray& aResult) const { for (uint32_t i = 0; i < mBlobImplsOrDirectoryPaths.Length(); ++i) { if (mBlobImplsOrDirectoryPaths[i].mType == BlobImplOrDirectoryPath::eBlobImpl) { RefPtr file = File::Create(aWindow, mBlobImplsOrDirectoryPaths[i].mBlobImpl); MOZ_ASSERT(file); OwningFileOrDirectory* element = aResult.AppendElement(); element->SetAsFile() = file; } else { MOZ_ASSERT(mBlobImplsOrDirectoryPaths[i].mType == BlobImplOrDirectoryPath::eDirectoryPath); nsCOMPtr file; nsresult rv = NS_NewLocalFile(mBlobImplsOrDirectoryPaths[i].mDirectoryPath, true, getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } RefPtr directory = Directory::Create(aWindow, file); MOZ_ASSERT(directory); OwningFileOrDirectory* element = aResult.AppendElement(); element->SetAsDirectory() = directory; } } } void SetFilesOrDirectories(const nsTArray& aArray) { mBlobImplsOrDirectoryPaths.Clear(); for (uint32_t i = 0; i < aArray.Length(); ++i) { if (aArray[i].IsFile()) { BlobImplOrDirectoryPath* data = mBlobImplsOrDirectoryPaths.AppendElement(); data->mBlobImpl = aArray[i].GetAsFile()->Impl(); data->mType = BlobImplOrDirectoryPath::eBlobImpl; } else { MOZ_ASSERT(aArray[i].IsDirectory()); nsAutoString fullPath; nsresult rv = aArray[i].GetAsDirectory()->GetFullRealPath(fullPath); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } BlobImplOrDirectoryPath* data = mBlobImplsOrDirectoryPaths.AppendElement(); data->mDirectoryPath = fullPath; data->mType = BlobImplOrDirectoryPath::eDirectoryPath; } } } HTMLInputElementState() : mValue() , mChecked(false) , mCheckedSet(false) {} protected: ~HTMLInputElementState() {} nsString mValue; struct BlobImplOrDirectoryPath { RefPtr mBlobImpl; nsString mDirectoryPath; enum { eBlobImpl, eDirectoryPath } mType; }; nsTArray mBlobImplsOrDirectoryPaths; bool mChecked; bool mCheckedSet; }; NS_DEFINE_STATIC_IID_ACCESSOR(HTMLInputElementState, NS_INPUT_ELEMENT_STATE_IID) NS_IMPL_ISUPPORTS(HTMLInputElementState, HTMLInputElementState) 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 localFile; nsAutoString prefStr; if (aReason == nsIContentPrefCallback2::COMPLETE_ERROR || !mResult) { prefStr = Preferences::GetString("dom.input.fallbackUploadDir"); if (prefStr.IsEmpty()) { // If no custom directory was set through the pref, default to // "desktop" directory for each platform. NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(localFile)); } } if (!localFile) { if (prefStr.IsEmpty() && mResult) { nsCOMPtr pref; mResult->GetValue(getter_AddRefs(pref)); pref->GetAsAString(prefStr); } localFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); localFile->InitWithPath(prefStr); } mFilePicker->SetDisplayDirectory(localFile); 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 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 localFile; nsresult rv = NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(path), true, getter_AddRefs(localFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } nsCOMPtr parentFile; rv = localFile->GetParent(getter_AddRefs(parentFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } return parentFile.forget(); } MOZ_ASSERT(aData.IsDirectory()); nsCOMPtr 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 /* static */ bool HTMLInputElement::ValueAsDateEnabled(JSContext* cx, JSObject* obj) { return IsExperimentalFormsEnabled() || IsDatePickerEnabled() || IsInputDateTimeEnabled(); } NS_IMETHODIMP HTMLInputElement::nsFilePickerShownCallback::Done(int16_t aResult) { mInput->PickerClosed(); if (aResult == nsIFilePicker::returnCancel) { return NS_OK; } int16_t mode; mFilePicker->GetMode(&mode); // Collect new selected filenames nsTArray newFilesOrDirectories; if (mode == static_cast(nsIFilePicker::modeOpenMultiple)) { nsCOMPtr iter; nsresult rv = mFilePicker->GetDomFileOrDirectoryEnumerator(getter_AddRefs(iter)); NS_ENSURE_SUCCESS(rv, rv); if (!iter) { return NS_OK; } nsCOMPtr tmp; bool hasMore = true; while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) { iter->GetNext(getter_AddRefs(tmp)); nsCOMPtr domBlob = do_QueryInterface(tmp); MOZ_ASSERT(domBlob, "Null file object from FilePicker's file enumerator?"); if (!domBlob) { continue; } OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement(); element->SetAsFile() = static_cast(domBlob.get()); } } else { MOZ_ASSERT(mode == static_cast(nsIFilePicker::modeOpen) || mode == static_cast(nsIFilePicker::modeGetFolder)); nsCOMPtr tmp; nsresult rv = mFilePicker->GetDomFileOrDirectory(getter_AddRefs(tmp)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr blob = do_QueryInterface(tmp); if (blob) { RefPtr file = static_cast(blob.get())->ToFile(); MOZ_ASSERT(file); OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement(); element->SetAsFile() = file; } else if (tmp) { RefPtr directory = static_cast(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 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); RefPtr dispatchChangeEventCallback = new DispatchChangeEventCallback(mInput); if (IsWebkitDirPickerEnabled() && mInput->HasAttr(kNameSpaceID_None, 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() {} public: nsColorPickerShownCallback(HTMLInputElement* aInput, nsIColorPicker* aColorPicker) : mInput(aInput) , mColorPicker(aColorPicker) , mValueChanged(false) {} NS_DECL_ISUPPORTS NS_IMETHOD Update(const nsAString& aColor) override; 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. */ nsresult UpdateInternal(const nsAString& aColor, bool aTrustedUpdate); RefPtr mInput; nsCOMPtr mColorPicker; bool mValueChanged; }; nsresult nsColorPickerShownCallback::UpdateInternal(const nsAString& aColor, bool aTrustedUpdate) { bool valueChanged = false; nsAutoString oldValue; if (aTrustedUpdate) { valueChanged = true; } else { mInput->GetValue(oldValue); } mInput->SetValue(aColor); if (!aTrustedUpdate) { nsAutoString newValue; mInput->GetValue(newValue); if (!oldValue.Equals(newValue)) { valueChanged = true; } } if (valueChanged) { mValueChanged = true; return nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(), static_cast(mInput.get()), NS_LITERAL_STRING("input"), true, false); } 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) { rv = nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(), static_cast(mInput.get()), NS_LITERAL_STRING("change"), true, false); } return rv; } NS_IMPL_ISUPPORTS(nsColorPickerShownCallback, nsIColorPickerShownCallback) class DatePickerShownCallback final : public nsIDatePickerShownCallback { ~DatePickerShownCallback() {} public: DatePickerShownCallback(HTMLInputElement* aInput, nsIDatePicker* aDatePicker) : mInput(aInput) , mDatePicker(aDatePicker) {} NS_DECL_ISUPPORTS NS_IMETHOD Done(const nsAString& aDate) override; NS_IMETHOD Cancel() override; private: RefPtr mInput; nsCOMPtr mDatePicker; }; NS_IMETHODIMP DatePickerShownCallback::Cancel() { mInput->PickerClosed(); return NS_OK; } NS_IMETHODIMP DatePickerShownCallback::Done(const nsAString& aDate) { nsAutoString oldValue; mInput->PickerClosed(); mInput->GetValue(oldValue); if(!oldValue.Equals(aDate)){ mInput->SetValue(aDate); nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(), static_cast(mInput.get()), NS_LITERAL_STRING("input"), true, false); return nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(), static_cast(mInput.get()), NS_LITERAL_STRING("change"), true, false); } return NS_OK; } NS_IMPL_ISUPPORTS(DatePickerShownCallback, nsIDatePickerShownCallback) bool HTMLInputElement::IsPopupBlocked() const { nsCOMPtr win = OwnerDoc()->GetWindow(); MOZ_ASSERT(win, "window should not be null"); if (!win) { return true; } // Check if page is allowed to open the popup if (win->GetPopupControlState() <= openControlled) { return false; } nsCOMPtr pm = do_GetService(NS_POPUPWINDOWMANAGER_CONTRACTID); if (!pm) { return true; } uint32_t permission; pm->TestPermission(OwnerDoc()->NodePrincipal(), &permission); return permission == nsIPopupWindowManager::DENY_POPUP; } nsresult HTMLInputElement::InitDatePicker() { if (!IsDatePickerEnabled()) { return NS_OK; } if (mPickerRunning) { NS_WARNING("Just one nsIDatePicker is allowed"); return NS_ERROR_FAILURE; } nsCOMPtr doc = OwnerDoc(); nsCOMPtr win = doc->GetWindow(); if (!win) { return NS_ERROR_FAILURE; } if (IsPopupBlocked()) { win->FirePopupBlockedEvent(doc, nullptr, EmptyString(), EmptyString()); return NS_OK; } // Get Loc title nsXPIDLString title; nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "DatePicker", title); nsresult rv; nsCOMPtr datePicker = do_CreateInstance("@mozilla.org/datepicker;1", &rv); if (!datePicker) { return rv; } nsAutoString initialValue; GetValueInternal(initialValue); rv = datePicker->Init(win, title, initialValue); nsCOMPtr callback = new DatePickerShownCallback(this, datePicker); rv = datePicker->Open(callback); if (NS_SUCCEEDED(rv)) { mPickerRunning = true; } return rv; } nsresult HTMLInputElement::InitColorPicker() { if (mPickerRunning) { NS_WARNING("Just one nsIColorPicker is allowed"); return NS_ERROR_FAILURE; } nsCOMPtr doc = OwnerDoc(); nsCOMPtr win = doc->GetWindow(); if (!win) { return NS_ERROR_FAILURE; } if (IsPopupBlocked()) { win->FirePopupBlockedEvent(doc, nullptr, EmptyString(), EmptyString()); return NS_OK; } // Get Loc title nsXPIDLString title; nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "ColorPicker", title); nsCOMPtr colorPicker = do_CreateInstance("@mozilla.org/colorpicker;1"); if (!colorPicker) { return NS_ERROR_FAILURE; } nsAutoString initialValue; GetValueInternal(initialValue); nsresult rv = colorPicker->Init(win, title, initialValue); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr callback = new nsColorPickerShownCallback(this, colorPicker); rv = colorPicker->Open(callback); if (NS_SUCCEEDED(rv)) { mPickerRunning = true; } return rv; } nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { if (mPickerRunning) { NS_WARNING("Just one nsIFilePicker is allowed"); return NS_ERROR_FAILURE; } // Get parent nsPIDOMWindow object. nsCOMPtr doc = OwnerDoc(); nsCOMPtr win = doc->GetWindow(); if (!win) { return NS_ERROR_FAILURE; } if (IsPopupBlocked()) { win->FirePopupBlockedEvent(doc, nullptr, EmptyString(), EmptyString()); return NS_OK; } // Get Loc title nsXPIDLString title; nsXPIDLString okButtonLabel; if (aType == FILE_PICKER_DIRECTORY) { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "DirectoryUpload", title); nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "DirectoryPickerOkButtonLabel", okButtonLabel); } else { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "FileUpload", title); } nsCOMPtr filePicker = do_CreateInstance("@mozilla.org/filepicker;1"); if (!filePicker) return NS_ERROR_FAILURE; int16_t mode; if (aType == FILE_PICKER_DIRECTORY) { mode = static_cast(nsIFilePicker::modeGetFolder); } else if (HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)) { mode = static_cast(nsIFilePicker::modeOpenMultiple); } else { mode = static_cast(nsIFilePicker::modeOpen); } nsresult rv = filePicker->Init(win, 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(kNameSpaceID_None, nsGkAtoms::accept) && aType != FILE_PICKER_DIRECTORY) { SetFilePickerFiltersFromAccept(filePicker); } else { filePicker->AppendFilters(nsIFilePicker::filterAll); } // Set default directory and filename nsAutoString defaultName; const nsTArray& oldFiles = GetFilesOrDirectoriesInternal(); nsCOMPtr callback = new HTMLInputElement::nsFilePickerShownCallback(this, filePicker); if (!oldFiles.IsEmpty() && aType != FILE_PICKER_DIRECTORY) { nsAutoString path; nsCOMPtr 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; } return rv; } HTMLInputElement::gUploadLastDir->FetchDirectoryAndDisplayPicker(doc, filePicker, callback); mPickerRunning = true; return NS_OK; } #define CPS_PREF_NAME NS_LITERAL_STRING("browser.upload.lastDir") NS_IMPL_ISUPPORTS(UploadLastDir, nsIObserver, nsISupportsWeakReference) void HTMLInputElement::InitUploadLastDir() { gUploadLastDir = new UploadLastDir(); NS_ADDREF(gUploadLastDir); nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService && gUploadLastDir) { observerService->AddObserver(gUploadLastDir, "browser:purge-session-history", true); } } void HTMLInputElement::DestroyUploadLastDir() { NS_IF_RELEASE(gUploadLastDir); } nsresult UploadLastDir::FetchDirectoryAndDisplayPicker(nsIDocument* aDoc, nsIFilePicker* aFilePicker, nsIFilePickerShownCallback* aFpCallback) { NS_PRECONDITION(aDoc, "aDoc is null"); NS_PRECONDITION(aFilePicker, "aFilePicker is null"); NS_PRECONDITION(aFpCallback, "aFpCallback is null"); nsIURI* docURI = aDoc->GetDocumentURI(); NS_PRECONDITION(docURI, "docURI is null"); nsCOMPtr loadContext = aDoc->GetLoadContext(); nsCOMPtr prefCallback = new UploadLastDir::ContentPrefCallback(aFilePicker, aFpCallback); #ifdef MOZ_B2G if (XRE_IsContentProcess()) { prefCallback->HandleCompletion(nsIContentPrefCallback2::COMPLETE_ERROR); return NS_OK; } #endif // Attempt to get the CPS, if it's not present we'll fallback to use the Desktop folder nsCOMPtr 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(nsIDocument* aDoc, nsIFile* aDir) { NS_PRECONDITION(aDoc, "aDoc is null"); if (!aDir) { return NS_OK; } #ifdef MOZ_B2G if (XRE_IsContentProcess()) { return NS_OK; } #endif nsCOMPtr docURI = aDoc->GetDocumentURI(); NS_PRECONDITION(docURI, "docURI is null"); // Attempt to get the CPS, if it's not present we'll just return nsCOMPtr 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 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 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 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(nsIDOMHTMLInputElement* aTarget, nsPresContext* aPresContext, const nsAString& aEventType); #endif // // construction, destruction // HTMLInputElement::HTMLInputElement(already_AddRefed& aNodeInfo, FromParser aFromParser, FromClone aFromClone) : nsGenericHTMLFormElementWithState(aNodeInfo) , mType(kInputDefaultType->value) , mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown) , mDisabledChanged(false) , mValueChanged(false) , mLastValueChangeWasInteractive(false) , mCheckedChanged(false) , mChecked(false) , mHandlingSelectEvent(false) , mShouldInitChecked(false) , mDoneCreating(aFromParser == NOT_FROM_PARSER && aFromClone == FromClone::no) , mInInternalActivate(false) , mCheckedIsToggled(false) , mIndeterminate(false) , mInhibitRestoration(aFromParser & FROM_PARSER_FRAGMENT) , mCanShowValidUI(true) , mCanShowInvalidUI(true) , mHasRange(false) , mIsDraggingRange(false) , mNumberControlSpinnerIsSpinning(false) , mNumberControlSpinnerSpinsUp(false) , mPickerRunning(false) , mSelectionCached(true) { // We are in a type=text so we now we currenty need a nsTextEditorState. mInputData.mState = new nsTextEditorState(this); 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, and valid. We are NOT readwrite by default // until someone calls UpdateEditableState on us, apparently! Also // by default we don't have to show validity UI and so forth. AddStatesSilently(NS_EVENT_STATE_ENABLED | NS_EVENT_STATE_OPTIONAL | NS_EVENT_STATE_VALID); UpdateApzAwareFlag(); } HTMLInputElement::~HTMLInputElement() { if (mNumberControlSpinnerIsSpinning) { StopNumberControlSpinnerSpin(eDisallowDispatchingEvents); } DestroyImageLoadingContent(); FreeData(); } void HTMLInputElement::FreeData() { if (!IsSingleLineTextControl(false)) { free(mInputData.mValue); mInputData.mValue = nullptr; } else { UnbindFromFrame(nullptr); delete mInputData.mState; mInputData.mState = nullptr; } } nsTextEditorState* HTMLInputElement::GetEditorState() const { if (!IsSingleLineTextControl(false)) { return nullptr; } 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, nsGenericHTMLFormElementWithState) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers) if (tmp->IsSingleLineTextControl(false)) { tmp->mInputData.mState->Traverse(cb); } NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFilesOrDirectories) if (tmp->mGetFilesRecursiveHelper) { tmp->mGetFilesRecursiveHelper->Traverse(cb); } if (tmp->mGetFilesNonRecursiveHelper) { tmp->mGetFilesNonRecursiveHelper->Traverse(cb); } NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileList) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEntries) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLInputElement, nsGenericHTMLFormElementWithState) NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity) NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers) NS_IMPL_CYCLE_COLLECTION_UNLINK(mFilesOrDirectories) NS_IMPL_CYCLE_COLLECTION_UNLINK(mFileList) NS_IMPL_CYCLE_COLLECTION_UNLINK(mEntries) if (tmp->IsSingleLineTextControl(false)) { tmp->mInputData.mState->Unlink(); } tmp->ClearGetFilesHelpers(); //XXX should unlink more? NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_ADDREF_INHERITED(HTMLInputElement, Element) NS_IMPL_RELEASE_INHERITED(HTMLInputElement, Element) // QueryInterface implementation for HTMLInputElement NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLInputElement) NS_INTERFACE_TABLE_INHERITED(HTMLInputElement, nsIDOMHTMLInputElement, nsITextControlElement, nsIPhonetic, imgINotificationObserver, nsIImageLoadingContent, imgIOnloadBlocker, nsIDOMNSEditableElement, nsIConstraintValidation) NS_INTERFACE_TABLE_TAIL_INHERITING(nsGenericHTMLFormElementWithState) // nsIConstraintValidation NS_IMPL_NSICONSTRAINTVALIDATION_EXCEPT_SETCUSTOMVALIDITY(HTMLInputElement) // nsIDOMNode nsresult HTMLInputElement::Clone(mozilla::dom::NodeInfo* aNodeInfo, nsINode** aResult) const { *aResult = nullptr; already_AddRefed ni = RefPtr(aNodeInfo).forget(); RefPtr it = new HTMLInputElement(ni, NOT_FROM_PARSER, FromClone::yes); nsresult rv = const_cast(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; GetValueInternal(value); // SetValueInternal handles setting the VALUE_CHANGED bit for us rv = it->SetValueInternal(value, nsTextEditorState::eSetValue_Notify); NS_ENSURE_SUCCESS(rv, 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->mStaticDocFileList); } else { it->ClearGetFilesHelpers(); it->mFilesOrDirectories.Clear(); it->mFilesOrDirectories.AppendElements(mFilesOrDirectories); } break; case VALUE_MODE_DEFAULT_ON: if (mCheckedChanged) { // We no longer have our original checked state. Set our // checked state on the clone. it->DoSetChecked(mChecked, false, true); // Then tell DoneCreatingElement() not to overwrite: it->mShouldInitChecked = false; } break; case VALUE_MODE_DEFAULT: if (mType == NS_FORM_INPUT_IMAGE && it->OwnerDoc()->IsStaticDocument()) { CreateStaticImageClone(it); } break; } it->DoneCreatingElement(); it->mLastValueChangeWasInteractive = mLastValueChangeWasInteractive; it.forget(aResult); return NS_OK; } nsresult HTMLInputElement::BeforeSetAttr(int32_t aNameSpaceID, nsIAtom* aName, nsAttrValueOrString* aValue, bool aNotify) { if (aNameSpaceID == kNameSpaceID_None) { // // When name or type changes, radio should be removed from radio group. // (type changes are handled in the form itself currently) // If we are not done creating the radio, we also should not do it. // if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) && mType == NS_FORM_INPUT_RADIO && (mForm || mDoneCreating)) { WillRemoveFromRadioGroup(); } else if (aNotify && aName == nsGkAtoms::src && mType == NS_FORM_INPUT_IMAGE) { if (aValue) { LoadImage(aValue->String(), true, aNotify, eImageLoadType_Normal); } else { // Null value means the attr got unset; drop the image CancelImageRequests(aNotify); } } else if (aNotify && aName == nsGkAtoms::disabled) { mDisabledChanged = true; } else if (aName == nsGkAtoms::dir && AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir, nsGkAtoms::_auto, eIgnoreCase)) { SetDirectionIfAuto(false, aNotify); } else if (mType == NS_FORM_INPUT_RADIO && aName == nsGkAtoms::required) { nsCOMPtr container = GetRadioGroupContainer(); if (container && ((aValue && !HasAttr(aNameSpaceID, aName)) || (!aValue && HasAttr(aNameSpaceID, aName)))) { nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); container->RadioRequiredWillChange(name, !!aValue); } } if (aName == nsGkAtoms::webkitdirectory) { Telemetry::Accumulate(Telemetry::WEBKIT_DIRECTORY_USED, true); } } return nsGenericHTMLFormElementWithState::BeforeSetAttr(aNameSpaceID, aName, aValue, aNotify); } nsresult HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName, const nsAttrValue* aValue, bool aNotify) { if (aNameSpaceID == kNameSpaceID_None) { // // When name or type changes, radio should be added to radio group. // (type changes are handled in the form itself currently) // If we are not done creating the radio, we also should not do it. // if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) && mType == NS_FORM_INPUT_RADIO && (mForm || mDoneCreating)) { AddedToRadioGroup(); UpdateValueMissingValidityStateForRadio(false); } // If @value is changed and BF_VALUE_CHANGED is false, @value is the value // of the element so, if the value of the element is different than @value, // we have to re-set it. This is only the case when GetValueMode() returns // VALUE_MODE_VALUE. if (aName == nsGkAtoms::value && !mValueChanged && GetValueMode() == VALUE_MODE_VALUE) { SetDefaultValueAsValue(); } // // Checked must be set no matter what type of control it is, since // mChecked must reflect the new value if (aName == nsGkAtoms::checked && !mCheckedChanged) { // Delay setting checked if we are creating this element (wait // until everything is set) if (!mDoneCreating) { mShouldInitChecked = true; } else { DoSetChecked(DefaultChecked(), true, true); SetCheckedChanged(false); } } if (aName == nsGkAtoms::type) { if (!aValue) { // We're now a text input. Note that we have to handle this manually, // since removing an attribute (which is what happened, since aValue is // null) doesn't call ParseAttribute. HandleTypeChange(kInputDefaultType->value); } UpdateBarredFromConstraintValidation(); if (mType != NS_FORM_INPUT_IMAGE) { // We're no longer an image input. Cancel our image requests, if we have // any. Note that doing this when we already weren't an image is ok -- // just does nothing. CancelImageRequests(aNotify); } else if (aNotify) { // We just got switched to be an image input; we should see // whether we have an image to load; nsAutoString src; if (GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) { LoadImage(src, false, aNotify, eImageLoadType_Normal); } } if (mType == NS_FORM_INPUT_PASSWORD && IsInComposedDoc()) { AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(this, NS_LITERAL_STRING("DOMInputPasswordAdded"), true, true); dispatcher->PostDOMEvent(); } } if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled || aName == nsGkAtoms::readonly) { UpdateValueMissingValidityState(); // This *has* to be called *after* validity has changed. if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) { UpdateBarredFromConstraintValidation(); } } else if (MinOrMaxLengthApplies() && aName == nsGkAtoms::maxlength) { UpdateTooLongValidityState(); } else if (MinOrMaxLengthApplies() && aName == nsGkAtoms::minlength) { UpdateTooShortValidityState(); } else if (aName == nsGkAtoms::pattern && mDoneCreating) { UpdatePatternMismatchValidityState(); } else if (aName == nsGkAtoms::multiple) { UpdateTypeMismatchValidityState(); } else if (aName == nsGkAtoms::max) { UpdateHasRange(); if (mType == NS_FORM_INPUT_RANGE) { // The value may need to change when @max changes since the value may // have been invalid and can now change to a valid value, or vice // versa. For example, consider: // . The valid range is 0 to 1 // while the nearest valid steps are -1 and 2 (the max value having // prevented there being a valid step in range). Changing @max to/from // 1 and a number greater than on equal to 3 should change whether we // have a step mismatch or not. // The value may also need to change between a value that results in // a step mismatch and a value that results in overflow. For example, // if @max in the example above were to change from 1 to -1. nsAutoString value; GetValue(value); nsresult rv = SetValueInternal(value, nsTextEditorState::eSetValue_Internal); NS_ENSURE_SUCCESS(rv, rv); } // Validity state must be updated *after* the SetValueInternal 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(); MOZ_ASSERT(!mDoneCreating || mType != NS_FORM_INPUT_RANGE || !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW), "HTML5 spec does not allow underflow for type=range"); } else if (aName == nsGkAtoms::min) { UpdateHasRange(); if (mType == NS_FORM_INPUT_RANGE) { // See @max comment nsAutoString value; GetValue(value); nsresult rv = SetValueInternal(value, nsTextEditorState::eSetValue_Internal); NS_ENSURE_SUCCESS(rv, rv); } // See corresponding @max comment UpdateRangeUnderflowValidityState(); UpdateStepMismatchValidityState(); MOZ_ASSERT(!mDoneCreating || mType != NS_FORM_INPUT_RANGE || !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW), "HTML5 spec does not allow underflow for type=range"); } else if (aName == nsGkAtoms::step) { if (mType == NS_FORM_INPUT_RANGE) { // See @max comment nsAutoString value; GetValue(value); nsresult rv = SetValueInternal(value, nsTextEditorState::eSetValue_Internal); NS_ENSURE_SUCCESS(rv, rv); } // See corresponding @max comment UpdateStepMismatchValidityState(); MOZ_ASSERT(!mDoneCreating || mType != NS_FORM_INPUT_RANGE || !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)) { SetDirectionIfAuto(true, aNotify); } else if (aName == nsGkAtoms::lang) { if (mType == NS_FORM_INPUT_NUMBER) { // Update the value that is displayed to the user to the new locale: nsAutoString value; GetValueInternal(value); nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { numberControlFrame->SetValueOfAnonTextControl(value); } } } else if (aName == nsGkAtoms::autocomplete) { // Clear the cached @autocomplete attribute state. mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown; } UpdateState(aNotify); } return nsGenericHTMLFormElementWithState::AfterSetAttr(aNameSpaceID, aName, aValue, aNotify); } // nsIDOMHTMLInputElement NS_IMETHODIMP HTMLInputElement::GetForm(nsIDOMHTMLFormElement** aForm) { return nsGenericHTMLFormElementWithState::GetForm(aForm); } NS_IMPL_STRING_ATTR(HTMLInputElement, DefaultValue, value) NS_IMPL_BOOL_ATTR(HTMLInputElement, DefaultChecked, checked) NS_IMPL_STRING_ATTR(HTMLInputElement, Accept, accept) NS_IMPL_STRING_ATTR(HTMLInputElement, Align, align) NS_IMPL_STRING_ATTR(HTMLInputElement, Alt, alt) NS_IMPL_BOOL_ATTR(HTMLInputElement, Autofocus, autofocus) //NS_IMPL_BOOL_ATTR(HTMLInputElement, Checked, checked) NS_IMPL_BOOL_ATTR(HTMLInputElement, Disabled, disabled) NS_IMPL_STRING_ATTR(HTMLInputElement, Max, max) NS_IMPL_STRING_ATTR(HTMLInputElement, Min, min) NS_IMPL_ACTION_ATTR(HTMLInputElement, FormAction, formaction) NS_IMPL_ENUM_ATTR_DEFAULT_MISSING_INVALID_VALUES(HTMLInputElement, FormEnctype, formenctype, "", kFormDefaultEnctype->tag) NS_IMPL_ENUM_ATTR_DEFAULT_MISSING_INVALID_VALUES(HTMLInputElement, FormMethod, formmethod, "", kFormDefaultMethod->tag) NS_IMPL_BOOL_ATTR(HTMLInputElement, FormNoValidate, formnovalidate) NS_IMPL_STRING_ATTR(HTMLInputElement, FormTarget, formtarget) NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLInputElement, InputMode, inputmode, kInputDefaultInputmode->tag) NS_IMPL_BOOL_ATTR(HTMLInputElement, Multiple, multiple) NS_IMPL_NON_NEGATIVE_INT_ATTR(HTMLInputElement, MaxLength, maxlength) NS_IMPL_NON_NEGATIVE_INT_ATTR(HTMLInputElement, MinLength, minlength) NS_IMPL_STRING_ATTR(HTMLInputElement, Name, name) NS_IMPL_BOOL_ATTR(HTMLInputElement, ReadOnly, readonly) NS_IMPL_BOOL_ATTR(HTMLInputElement, Required, required) NS_IMPL_URI_ATTR(HTMLInputElement, Src, src) NS_IMPL_STRING_ATTR(HTMLInputElement, Step, step) NS_IMPL_STRING_ATTR(HTMLInputElement, UseMap, usemap) //NS_IMPL_STRING_ATTR(HTMLInputElement, Value, value) NS_IMPL_UINT_ATTR_NON_ZERO_DEFAULT_VALUE(HTMLInputElement, Size, size, DEFAULT_COLS) NS_IMPL_STRING_ATTR(HTMLInputElement, Pattern, pattern) NS_IMPL_STRING_ATTR(HTMLInputElement, Placeholder, placeholder) NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLInputElement, Type, type, kInputDefaultType->tag) NS_IMETHODIMP HTMLInputElement::GetAutocomplete(nsAString& aValue) { if (!DoesAutocompleteApply()) { return NS_OK; } aValue.Truncate(0); const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(attributeVal, aValue, mAutocompleteAttrState); return NS_OK; } NS_IMETHODIMP HTMLInputElement::SetAutocomplete(const nsAString& aValue) { return SetAttr(kNameSpaceID_None, nsGkAtoms::autocomplete, nullptr, aValue, true); } void HTMLInputElement::GetAutocompleteInfo(Nullable& aInfo) { if (!DoesAutocompleteApply()) { aInfo.SetNull(); return; } const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(attributeVal, aInfo.SetValue(), mAutocompleteAttrState); } int32_t HTMLInputElement::TabIndexDefault() { return 0; } uint32_t HTMLInputElement::Height() { if (mType != NS_FORM_INPUT_IMAGE) { return 0; } return GetWidthHeightForImage(mCurrentRequest).height; } NS_IMETHODIMP HTMLInputElement::GetHeight(uint32_t* aHeight) { *aHeight = Height(); return NS_OK; } NS_IMETHODIMP HTMLInputElement::SetHeight(uint32_t aHeight) { ErrorResult rv; SetHeight(aHeight, rv); return rv.StealNSResult(); } NS_IMETHODIMP HTMLInputElement::GetIndeterminate(bool* aValue) { *aValue = Indeterminate(); return NS_OK; } void HTMLInputElement::SetIndeterminateInternal(bool aValue, bool aShouldInvalidate) { mIndeterminate = aValue; if (aShouldInvalidate) { // Repaint the frame nsIFrame* frame = GetPrimaryFrame(); if (frame) frame->InvalidateFrameSubtree(); } UpdateState(true); } NS_IMETHODIMP HTMLInputElement::SetIndeterminate(bool aValue) { SetIndeterminateInternal(aValue, true); return NS_OK; } uint32_t HTMLInputElement::Width() { if (mType != NS_FORM_INPUT_IMAGE) { return 0; } return GetWidthHeightForImage(mCurrentRequest).width; } NS_IMETHODIMP HTMLInputElement::GetWidth(uint32_t* aWidth) { *aWidth = Width(); return NS_OK; } NS_IMETHODIMP HTMLInputElement::SetWidth(uint32_t aWidth) { ErrorResult rv; SetWidth(aWidth, rv); return rv.StealNSResult(); } NS_IMETHODIMP HTMLInputElement::GetValue(nsAString& aValue) { nsresult rv = GetValueInternal(aValue); if (NS_FAILED(rv)) { return rv; } // Don't return non-sanitized value for types that are experimental on mobile // or datetime types if (IsExperimentalMobileType(mType) || IsDateTimeInputType(mType)) { SanitizeValue(aValue); } return NS_OK; } nsresult HTMLInputElement::GetValueInternal(nsAString& aValue) const { switch (GetValueMode()) { case VALUE_MODE_VALUE: if (IsSingleLineTextControl(false)) { mInputData.mState->GetValue(aValue, true); } else if (!aValue.Assign(mInputData.mValue, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; case VALUE_MODE_FILENAME: if (nsContentUtils::LegacyIsCallerChromeOrNativeCode()) { aValue.Assign(mFirstFilePath); } else { // Just return the leaf name if (mFilesOrDirectories.IsEmpty()) { aValue.Truncate(); } else { GetDOMFileOrDirectoryName(mFilesOrDirectories[0], aValue); } } return NS_OK; case VALUE_MODE_DEFAULT: // Treat defaultValue as value. GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue); return NS_OK; case VALUE_MODE_DEFAULT_ON: // Treat default value as value and returns "on" if no value. if (!GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue)) { aValue.AssignLiteral("on"); } return NS_OK; } // This return statement is required for some compilers. return NS_OK; } bool HTMLInputElement::IsValueEmpty() const { nsAutoString value; GetValueInternal(value); return value.IsEmpty(); } void HTMLInputElement::ClearFiles(bool aSetValueChanged) { nsTArray 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(); return Decimal::fromString(stdString); } bool HTMLInputElement::ConvertStringToNumber(nsAString& aValue, Decimal& aResultValue) const { MOZ_ASSERT(DoesValueAsNumberApply(), "ConvertStringToNumber only applies if .valueAsNumber applies"); switch (mType) { case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: { aResultValue = StringToDecimal(aValue); if (!aResultValue.isFinite()) { return false; } return true; } case NS_FORM_INPUT_DATE: { uint32_t year, month, day; if (!ParseDate(aValue, &year, &month, &day)) { return false; } JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day)); if (!time.isValid()) { return false; } aResultValue = Decimal::fromDouble(time.toDouble()); return true; } case NS_FORM_INPUT_TIME: uint32_t milliseconds; if (!ParseTime(aValue, &milliseconds)) { return false; } aResultValue = Decimal(int32_t(milliseconds)); return true; case NS_FORM_INPUT_MONTH: { uint32_t year, month; if (!ParseMonth(aValue, &year, &month)) { return false; } if (year < kMinimumYear || year > kMaximumYear) { return false; } // Maximum valid month is 275760-09. if (year == kMaximumYear && month > kMaximumMonthInMaximumYear) { return false; } int32_t months = MonthsSinceJan1970(year, month); aResultValue = Decimal(int32_t(months)); return true; } case NS_FORM_INPUT_WEEK: { uint32_t year, week; if (!ParseWeek(aValue, &year, &week)) { return false; } if (year < kMinimumYear || year > kMaximumYear) { return false; } // Maximum week is 275760-W37, the week of 275760-09-13. if (year == kMaximumYear && week > kMaximumWeekInMaximumYear) { return false; } double days = DaysSinceEpochFromWeek(year, week); aResultValue = Decimal::fromDouble(days * kMsPerDay); return true; } case NS_FORM_INPUT_DATETIME_LOCAL: { uint32_t year, month, day, timeInMs; if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) { return false; } JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)); if (!time.isValid()) { return false; } aResultValue = Decimal::fromDouble(time.toDouble()); return true; } default: MOZ_ASSERT(false, "Unrecognized input type"); return false; } } Decimal HTMLInputElement::GetValueAsDecimal() const { Decimal decimalValue; nsAutoString stringValue; GetValueInternal(stringValue); return !ConvertStringToNumber(stringValue, decimalValue) ? Decimal::nan() : decimalValue; } void HTMLInputElement::GetValue(nsAString& aValue, ErrorResult& aRv) { nsresult rv = GetValue(aValue); if (NS_FAILED(rv)) { aRv.Throw(rv); } } void HTMLInputElement::SetValue(const nsAString& aValue, 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 == NS_FORM_INPUT_FILE) { if (!aValue.IsEmpty()) { if (!nsContentUtils::IsCallerChrome()) { // setting the value of a "FILE" input widget requires // chrome privilege aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } Sequence list; if (!list.AppendElement(aValue, fallible)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return; } MozSetFileNameArray(list, aRv); return; } else { 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; GetValue(currentValue); nsresult rv = SetValueInternal(aValue, nsTextEditorState::eSetValue_ByContent | nsTextEditorState::eSetValue_Notify); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } if (mFocusedValue.Equals(currentValue)) { GetValue(mFocusedValue); } } else { nsresult rv = SetValueInternal(aValue, nsTextEditorState::eSetValue_ByContent | nsTextEditorState::eSetValue_Notify); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } } } } NS_IMETHODIMP HTMLInputElement::SetValue(const nsAString& aValue) { ErrorResult rv; SetValue(aValue, rv); return rv.StealNSResult(); } nsGenericHTMLElement* HTMLInputElement::GetList() const { nsAutoString dataListId; GetAttr(kNameSpaceID_None, nsGkAtoms::list, dataListId); if (dataListId.IsEmpty()) { return nullptr; } //XXXsmaug How should this all work in case input element is in Shadow DOM. nsIDocument* doc = GetUncomposedDoc(); if (!doc) { return nullptr; } Element* element = doc->GetElementById(dataListId); if (!element || !element->IsHTMLElement(nsGkAtoms::datalist)) { return nullptr; } return static_cast(element); } NS_IMETHODIMP HTMLInputElement::GetList(nsIDOMHTMLElement** aValue) { *aValue = nullptr; RefPtr element = GetList(); if (!element) { return NS_OK; } element.forget(aValue); return NS_OK; } void HTMLInputElement::SetValue(Decimal aValue) { MOZ_ASSERT(!aValue.isInfinity(), "aValue must not be Infinity!"); if (aValue.isNaN()) { SetValue(EmptyString()); return; } nsAutoString value; ConvertNumberToString(aValue, value); SetValue(value); } bool HTMLInputElement::ConvertNumberToString(Decimal aValue, nsAString& aResultString) const { MOZ_ASSERT(DoesValueAsNumberApply(), "ConvertNumberToString is only implemented for types implementing .valueAsNumber"); MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); aResultString.Truncate(); switch (mType) { case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: { char buf[32]; bool ok = aValue.toString(buf, ArrayLength(buf)); aResultString.AssignASCII(buf); MOZ_ASSERT(ok, "buf not big enough"); return ok; } case NS_FORM_INPUT_DATE: { // The specs (and our JS APIs) require |aValue| to be truncated. aValue = aValue.floor(); double year = JS::YearFromTime(aValue.toDouble()); double month = JS::MonthFromTime(aValue.toDouble()); double day = JS::DayFromTime(aValue.toDouble()); if (IsNaN(year) || IsNaN(month) || IsNaN(day)) { return false; } aResultString.AppendPrintf("%04.0f-%02.0f-%02.0f", year, month + 1, day); return true; } case NS_FORM_INPUT_TIME: { aValue = aValue.floor(); // Per spec, we need to truncate |aValue| and we should only represent // times inside a day [00:00, 24:00[, which means that we should do a // modulo on |aValue| using the number of milliseconds in a day (86400000). uint32_t value = NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); uint16_t milliseconds, seconds, minutes, hours; if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) { return false; } if (milliseconds != 0) { aResultString.AppendPrintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds); } else if (seconds != 0) { aResultString.AppendPrintf("%02d:%02d:%02d", hours, minutes, seconds); } else { aResultString.AppendPrintf("%02d:%02d", hours, minutes); } return true; } case NS_FORM_INPUT_MONTH: { aValue = aValue.floor(); double month = NS_floorModulo(aValue, Decimal(12)).toDouble(); month = (month < 0 ? month + 12 : month); double year = 1970 + (aValue.toDouble() - month) / 12; // Maximum valid month is 275760-09. if (year < kMinimumYear || year > kMaximumYear) { return false; } if (year == kMaximumYear && month > 8) { return false; } aResultString.AppendPrintf("%04.0f-%02.0f", year, month + 1); return true; } case NS_FORM_INPUT_WEEK: { aValue = aValue.floor(); // Based on ISO 8601 date. double year = JS::YearFromTime(aValue.toDouble()); double month = JS::MonthFromTime(aValue.toDouble()); double day = JS::DayFromTime(aValue.toDouble()); // Adding 1 since day starts from 0. double dayInYear = JS::DayWithinYear(aValue.toDouble(), year) + 1; // Adding 1 since month starts from 0. uint32_t isoWeekday = DayOfWeek(year, month + 1, day, true); // Target on Wednesday since ISO 8601 states that week 1 is the week // with the first Thursday of that year. uint32_t week = (dayInYear - isoWeekday + 10) / 7; if (week < 1) { year--; if (year < 1) { return false; } week = MaximumWeekInYear(year); } else if (week > MaximumWeekInYear(year)) { year++; if (year > kMaximumYear || (year == kMaximumYear && week > kMaximumWeekInMaximumYear)) { return false; } week = 1; } aResultString.AppendPrintf("%04.0f-W%02d", year, week); return true; } case NS_FORM_INPUT_DATETIME_LOCAL: { aValue = aValue.floor(); uint32_t timeValue = NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); uint16_t milliseconds, seconds, minutes, hours; if (!GetTimeFromMs(timeValue, &hours, &minutes, &seconds, &milliseconds)) { return false; } double year = JS::YearFromTime(aValue.toDouble()); double month = JS::MonthFromTime(aValue.toDouble()); double day = JS::DayFromTime(aValue.toDouble()); if (IsNaN(year) || IsNaN(month) || IsNaN(day)) { return false; } if (milliseconds != 0) { aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d", year, month + 1, day, hours, minutes, seconds, milliseconds); } else if (seconds != 0) { aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d", year, month + 1, day, hours, minutes, seconds); } else { aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d", year, month + 1, day, hours, minutes); } return true; } default: MOZ_ASSERT(false, "Unrecognized input type"); return false; } } Nullable HTMLInputElement::GetValueAsDate(ErrorResult& aRv) { if (!IsDateTimeInputType(mType)) { return Nullable(); } switch (mType) { case NS_FORM_INPUT_DATE: { uint32_t year, month, day; nsAutoString value; GetValueInternal(value); if (!ParseDate(value, &year, &month, &day)) { return Nullable(); } JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day)); return Nullable(Date(time)); } case NS_FORM_INPUT_TIME: { uint32_t millisecond; nsAutoString value; GetValueInternal(value); if (!ParseTime(value, &millisecond)) { return Nullable(); } JS::ClippedTime time = JS::TimeClip(millisecond); MOZ_ASSERT(time.toDouble() == millisecond, "HTML times are restricted to the day after the epoch and " "never clip"); return Nullable(Date(time)); } case NS_FORM_INPUT_MONTH: { uint32_t year, month; nsAutoString value; GetValueInternal(value); if (!ParseMonth(value, &year, &month)) { return Nullable(); } JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, 1)); return Nullable(Date(time)); } case NS_FORM_INPUT_WEEK: { uint32_t year, week; nsAutoString value; GetValueInternal(value); if (!ParseWeek(value, &year, &week)) { return Nullable(); } double days = DaysSinceEpochFromWeek(year, week); JS::ClippedTime time = JS::TimeClip(days * kMsPerDay); return Nullable(Date(time)); } case NS_FORM_INPUT_DATETIME_LOCAL: { uint32_t year, month, day, timeInMs; nsAutoString value; GetNonFileValueInternal(value); if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) { return Nullable(); } JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)); return Nullable(Date(time)); } } MOZ_ASSERT(false, "Unrecognized input type"); aRv.Throw(NS_ERROR_UNEXPECTED); return Nullable(); } void HTMLInputElement::SetValueAsDate(Nullable aDate, ErrorResult& aRv) { if (!IsDateTimeInputType(mType)) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (aDate.IsNull() || aDate.Value().IsUndefined()) { aRv = SetValue(EmptyString()); return; } double milliseconds = aDate.Value().TimeStamp().toDouble(); if (mType != NS_FORM_INPUT_MONTH) { SetValue(Decimal::fromDouble(milliseconds)); return; } // type=month expects the value to be number of months. double year = JS::YearFromTime(milliseconds); double month = JS::MonthFromTime(milliseconds); if (IsNaN(year) || IsNaN(month)) { SetValue(EmptyString()); return; } int32_t months = MonthsSinceJan1970(year, month + 1); SetValue(Decimal(int32_t(months))); } NS_IMETHODIMP HTMLInputElement::GetValueAsNumber(double* aValueAsNumber) { *aValueAsNumber = ValueAsNumber(); return NS_OK; } void HTMLInputElement::SetValueAsNumber(double aValueAsNumber, ErrorResult& aRv) { // TODO: return TypeError when HTMLInputElement is converted to WebIDL, see // bug 825197. if (IsInfinite(aValueAsNumber)) { aRv.Throw(NS_ERROR_INVALID_ARG); return; } if (!DoesValueAsNumberApply()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } SetValue(Decimal::fromDouble(aValueAsNumber)); } NS_IMETHODIMP HTMLInputElement::SetValueAsNumber(double aValueAsNumber) { ErrorResult rv; SetValueAsNumber(aValueAsNumber, rv); return rv.StealNSResult(); } 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 == NS_FORM_INPUT_RANGE ? Decimal(0) : Decimal::nan(); if (!HasAttr(kNameSpaceID_None, nsGkAtoms::min)) { return defaultMinimum; } nsAutoString minStr; GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr); Decimal min; return ConvertStringToNumber(minStr, min) ? 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 == NS_FORM_INPUT_RANGE ? Decimal(100) : Decimal::nan(); if (!HasAttr(kNameSpaceID_None, nsGkAtoms::max)) { return defaultMaximum; } nsAutoString maxStr; GetAttr(kNameSpaceID_None, nsGkAtoms::max, maxStr); Decimal max; return ConvertStringToNumber(maxStr, max) ? max : defaultMaximum; } Decimal HTMLInputElement::GetStepBase() const { MOZ_ASSERT(mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_DATE || mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_MONTH || mType == NS_FORM_INPUT_WEEK || mType == NS_FORM_INPUT_RANGE, "Check that kDefaultStepBase is correct for this new type"); Decimal stepBase; // Do NOT use GetMinimum here - the spec says to use "the min content // attribute", not "the minimum". nsAutoString minStr; if (GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr) && ConvertStringToNumber(minStr, stepBase)) { return stepBase; } // If @min is not a double, we should use @value. nsAutoString valueStr; if (GetAttr(kNameSpaceID_None, nsGkAtoms::value, valueStr) && ConvertStringToNumber(valueStr, stepBase)) { return stepBase; } if (mType == NS_FORM_INPUT_WEEK) { return kDefaultStepBaseWeek; } return kDefaultStepBase; } nsresult HTMLInputElement::GetValueIfStepped(int32_t aStep, StepCallerType aCallerType, Decimal* aNextStep) { if (!DoStepDownStepUpApply()) { return NS_ERROR_DOM_INVALID_STATE_ERR; } Decimal stepBase = GetStepBase(); Decimal step = GetStep(); if (step == kStepAny) { if (aCallerType != CALLED_FOR_USER_EVENT) { return NS_ERROR_DOM_INVALID_STATE_ERR; } // Allow the spin buttons and up/down arrow keys to do something sensible: step = GetDefaultStep(); } Decimal minimum = GetMinimum(); Decimal maximum = GetMaximum(); if (!maximum.isNaN()) { // "max - (max - stepBase) % step" is the nearest valid value to max. maximum = maximum - NS_floorModulo(maximum - stepBase, step); if (!minimum.isNaN()) { if (minimum > maximum) { // Either the minimum was greater than the maximum prior to our // adjustment to align maximum on a step, or else (if we adjusted // maximum) there is no valid step between minimum and the unadjusted // maximum. return NS_OK; } } } Decimal value = GetValueAsDecimal(); bool valueWasNaN = false; if (value.isNaN()) { value = Decimal(0); valueWasNaN = true; } Decimal valueBeforeStepping = value; Decimal deltaFromStep = NS_floorModulo(value - stepBase, step); if (deltaFromStep != Decimal(0)) { if (aStep > 0) { value += step - deltaFromStep; // partial step value += step * Decimal(aStep - 1); // then remaining steps } else if (aStep < 0) { value -= deltaFromStep; // partial step value += step * Decimal(aStep + 1); // then remaining steps } } else { value += step * Decimal(aStep); } if (value < minimum) { value = minimum; deltaFromStep = NS_floorModulo(value - stepBase, step); if (deltaFromStep != Decimal(0)) { value += step - deltaFromStep; } } if (value > maximum) { value = maximum; deltaFromStep = NS_floorModulo(value - stepBase, step); if (deltaFromStep != Decimal(0)) { value -= deltaFromStep; } } if (!valueWasNaN && // value="", resulting in us using "0" ((aStep > 0 && value < valueBeforeStepping) || (aStep < 0 && value > valueBeforeStepping))) { // We don't want step-up to effectively step down, or step-down to // effectively step up, so return; return NS_OK; } *aNextStep = value; return NS_OK; } nsresult HTMLInputElement::ApplyStep(int32_t aStep) { Decimal nextStep = Decimal::nan(); // unchanged if value will not change nsresult rv = GetValueIfStepped(aStep, CALLED_FOR_SCRIPT, &nextStep); if (NS_SUCCEEDED(rv) && nextStep.isFinite()) { SetValue(nextStep); } return rv; } /* static */ bool HTMLInputElement::IsExperimentalMobileType(uint8_t aType) { return (aType == NS_FORM_INPUT_DATE && !IsInputDateTimeEnabled() && !IsDatePickerEnabled()) || (aType == NS_FORM_INPUT_TIME && !IsInputDateTimeEnabled()); } bool HTMLInputElement::IsDateTimeInputType(uint8_t aType) { return aType == NS_FORM_INPUT_DATE || aType == NS_FORM_INPUT_TIME || aType == NS_FORM_INPUT_MONTH || aType == NS_FORM_INPUT_WEEK || aType == NS_FORM_INPUT_DATETIME_LOCAL; } NS_IMETHODIMP HTMLInputElement::StepDown(int32_t n, uint8_t optional_argc) { return ApplyStep(optional_argc ? -n : -1); } NS_IMETHODIMP HTMLInputElement::StepUp(int32_t n, uint8_t optional_argc) { return ApplyStep(optional_argc ? n : 1); } void HTMLInputElement::FlushFrames() { if (GetComposedDoc()) { GetComposedDoc()->FlushPendingNotifications(Flush_Frames); } } void HTMLInputElement::MozGetFileNameArray(nsTArray& aArray, ErrorResult& aRv) { for (uint32_t i = 0; i < mFilesOrDirectories.Length(); i++) { nsAutoString str; GetDOMFileOrDirectoryPath(mFilesOrDirectories[i], str, aRv); if (NS_WARN_IF(aRv.Failed())) { return; } aArray.AppendElement(str); } } NS_IMETHODIMP HTMLInputElement::MozGetFileNameArray(uint32_t* aLength, char16_t*** aFileNames) { if (!nsContentUtils::IsCallerChrome()) { // Since this function returns full paths it's important that normal pages // can't call it. return NS_ERROR_DOM_SECURITY_ERR; } ErrorResult rv; nsTArray array; MozGetFileNameArray(array, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } *aLength = array.Length(); char16_t** ret = static_cast(moz_xmalloc(*aLength * sizeof(char16_t*))); if (!ret) { return NS_ERROR_OUT_OF_MEMORY; } for (uint32_t i = 0; i < *aLength; ++i) { ret[i] = NS_strdup(array[i].get()); } *aFileNames = ret; return NS_OK; } void HTMLInputElement::MozSetFileArray(const Sequence>& aFiles) { nsCOMPtr global = OwnerDoc()->GetScopeObject(); MOZ_ASSERT(global); if (!global) { return; } nsTArray files; for (uint32_t i = 0; i < aFiles.Length(); ++i) { RefPtr file = File::Create(global, aFiles[i].get()->Impl()); MOZ_ASSERT(file); OwningFileOrDirectory* element = files.AppendElement(); element->SetAsFile() = file; } SetFilesOrDirectories(files, true); } void HTMLInputElement::MozSetFileNameArray(const Sequence& aFileNames, ErrorResult& aRv) { if (XRE_IsContentProcess()) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return; } nsTArray files; for (uint32_t i = 0; i < aFileNames.Length(); ++i) { nsCOMPtr file; if (StringBeginsWith(aFileNames[i], NS_LITERAL_STRING("file:"), nsASCIICaseInsensitiveStringComparator())) { // Converts the URL string into the corresponding nsIFile if possible // A local file will be created if the URL string begins with file:// NS_GetFileFromURLSpec(NS_ConvertUTF16toUTF8(aFileNames[i]), getter_AddRefs(file)); } if (!file) { // this is no "file://", try as local file NS_NewLocalFile(aFileNames[i], false, getter_AddRefs(file)); } if (!file) { continue; // Not much we can do if the file doesn't exist } nsCOMPtr global = OwnerDoc()->GetScopeObject(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return; } RefPtr domFile = File::CreateFromFile(global, file); OwningFileOrDirectory* element = files.AppendElement(); element->SetAsFile() = domFile; } SetFilesOrDirectories(files, true); } NS_IMETHODIMP HTMLInputElement::MozSetFileNameArray(const char16_t** aFileNames, uint32_t aLength) { if (!nsContentUtils::IsCallerChrome()) { // setting the value of a "FILE" input widget requires chrome privilege return NS_ERROR_DOM_SECURITY_ERR; } Sequence list; nsString* names = list.AppendElements(aLength, fallible); if (!names) { return NS_ERROR_OUT_OF_MEMORY; } for (uint32_t i = 0; i < aLength; ++i) { const char16_t* filename = aFileNames[i]; names[i].Rebind(filename, nsCharTraits::length(filename)); } ErrorResult rv; MozSetFileNameArray(list, rv); return rv.StealNSResult(); } void HTMLInputElement::MozSetDirectory(const nsAString& aDirectoryPath, ErrorResult& aRv) { nsCOMPtr file; aRv = NS_NewLocalFile(aDirectoryPath, true, getter_AddRefs(file)); if (NS_WARN_IF(aRv.Failed())) { return; } nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); if (NS_WARN_IF(!window)) { aRv.Throw(NS_ERROR_FAILURE); return; } RefPtr directory = Directory::Create(window, file); MOZ_ASSERT(directory); nsTArray array; OwningFileOrDirectory* element = array.AppendElement(); element->SetAsDirectory() = directory; SetFilesOrDirectories(array, true); } void HTMLInputElement::GetDateTimeInputBoxValue(DateTimeValue& aValue) { if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) { return; } aValue = *mDateTimeInputBoxValue; } void HTMLInputElement::UpdateDateTimeInputBox(const DateTimeValue& aValue) { if (NS_WARN_IF(!IsDateTimeInputType(mType))) { return; } nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->SetValueFromPicker(aValue); } } void HTMLInputElement::SetDateTimePickerState(bool aOpen) { if (NS_WARN_IF(!IsDateTimeInputType(mType))) { return; } nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->SetPickerState(aOpen); } } void HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue) { if (NS_WARN_IF(!IsDateTimeInputType(mType))) { return; } mDateTimeInputBoxValue = new DateTimeValue(aInitialValue); nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("MozOpenDateTimePicker"), true, true); } void HTMLInputElement::UpdateDateTimePicker(const DateTimeValue& aValue) { if (NS_WARN_IF(!IsDateTimeInputType(mType))) { return; } mDateTimeInputBoxValue = new DateTimeValue(aValue); nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("MozUpdateDateTimePicker"), true, true); } void HTMLInputElement::CloseDateTimePicker() { if (NS_WARN_IF(!IsDateTimeInputType(mType))) { return; } nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("MozCloseDateTimePicker"), true, true); } bool HTMLInputElement::MozIsTextField(bool aExcludePassword) { // TODO: temporary until bug 888320 is fixed. if (IsExperimentalMobileType(mType) || IsDateTimeInputType(mType)) { return false; } return IsSingleLineTextControl(aExcludePassword); } HTMLInputElement* HTMLInputElement::GetOwnerNumberControl() { if (IsInNativeAnonymousSubtree() && mType == NS_FORM_INPUT_TEXT && GetParent() && GetParent()->GetParent()) { HTMLInputElement* grandparent = HTMLInputElement::FromContentOrNull(GetParent()->GetParent()); if (grandparent && grandparent->mType == NS_FORM_INPUT_NUMBER) { return grandparent; } } return nullptr; } HTMLInputElement* HTMLInputElement::GetOwnerDateTimeControl() { if (IsInNativeAnonymousSubtree() && mType == NS_FORM_INPUT_TEXT && GetParent() && GetParent()->GetParent() && GetParent()->GetParent()->GetParent() && GetParent()->GetParent()->GetParent()->GetParent()) { // Yes, this is very very deep. HTMLInputElement* ownerDateTimeControl = HTMLInputElement::FromContentOrNull( GetParent()->GetParent()->GetParent()->GetParent()); if (ownerDateTimeControl && (ownerDateTimeControl->mType == NS_FORM_INPUT_TIME || ownerDateTimeControl->mType == NS_FORM_INPUT_DATE)) { return ownerDateTimeControl; } } return nullptr; } NS_IMETHODIMP HTMLInputElement::MozIsTextField(bool aExcludePassword, bool* aResult) { *aResult = MozIsTextField(aExcludePassword); return NS_OK; } void HTMLInputElement::SetUserInput(const nsAString& aInput, nsIPrincipal& aSubjectPrincipal) { if (mType == NS_FORM_INPUT_FILE && !nsContentUtils::IsSystemPrincipal(&aSubjectPrincipal)) { return; } SetUserInput(aInput); } NS_IMETHODIMP HTMLInputElement::SetUserInput(const nsAString& aValue) { if (mType == NS_FORM_INPUT_FILE) { Sequence list; if (!list.AppendElement(aValue, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } ErrorResult rv; MozSetFileNameArray(list, rv); return rv.StealNSResult(); } else { nsresult rv = SetValueInternal(aValue, nsTextEditorState::eSetValue_BySetUserInput | nsTextEditorState::eSetValue_Notify); NS_ENSURE_SUCCESS(rv, rv); } nsContentUtils::DispatchTrustedEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("input"), true, true); // If this element is not currently focused, it won't receive a change event for this // update through the normal channels. So fire a change event immediately, instead. if (!ShouldBlur(this)) { FireChangeEventIfNeeded(); } return NS_OK; } nsIEditor* HTMLInputElement::GetEditor() { nsTextEditorState* state = GetEditorState(); if (state) { return state->GetEditor(); } return nullptr; } NS_IMETHODIMP_(nsIEditor*) HTMLInputElement::GetTextEditor() { return GetEditor(); } NS_IMETHODIMP_(nsISelectionController*) HTMLInputElement::GetSelectionController() { nsTextEditorState* state = GetEditorState(); if (state) { return state->GetSelectionController(); } return nullptr; } nsFrameSelection* HTMLInputElement::GetConstFrameSelection() { nsTextEditorState* state = GetEditorState(); if (state) { return state->GetConstFrameSelection(); } return nullptr; } NS_IMETHODIMP HTMLInputElement::BindToFrame(nsTextControlFrame* aFrame) { nsTextEditorState* state = GetEditorState(); if (state) { return state->BindToFrame(aFrame); } return NS_ERROR_FAILURE; } NS_IMETHODIMP_(void) HTMLInputElement::UnbindFromFrame(nsTextControlFrame* aFrame) { nsTextEditorState* state = GetEditorState(); if (state && aFrame) { state->UnbindFromFrame(aFrame); } } NS_IMETHODIMP HTMLInputElement::CreateEditor() { nsTextEditorState* state = GetEditorState(); if (state) { return state->PrepareEditor(); } return NS_ERROR_FAILURE; } NS_IMETHODIMP_(nsIContent*) HTMLInputElement::GetRootEditorNode() { nsTextEditorState* state = GetEditorState(); if (state) { return state->GetRootNode(); } return nullptr; } NS_IMETHODIMP_(Element*) HTMLInputElement::CreatePlaceholderNode() { nsTextEditorState* state = GetEditorState(); if (state) { NS_ENSURE_SUCCESS(state->CreatePlaceholderNode(), nullptr); return state->GetPlaceholderNode(); } return nullptr; } NS_IMETHODIMP_(Element*) HTMLInputElement::GetPlaceholderNode() { nsTextEditorState* state = GetEditorState(); if (state) { return state->GetPlaceholderNode(); } return nullptr; } NS_IMETHODIMP_(void) HTMLInputElement::UpdatePlaceholderVisibility(bool aNotify) { nsTextEditorState* state = GetEditorState(); if (state) { state->UpdatePlaceholderVisibility(aNotify); } } NS_IMETHODIMP_(bool) HTMLInputElement::GetPlaceholderVisibility() { nsTextEditorState* state = GetEditorState(); if (!state) { return false; } return state->GetPlaceholderVisibility(); } void HTMLInputElement::GetDisplayFileName(nsAString& aValue) const { if (OwnerDoc()->IsStaticDocument()) { aValue = mStaticDocFileList; return; } if (mFilesOrDirectories.Length() == 1) { GetDOMFileOrDirectoryName(mFilesOrDirectories[0], aValue); return; } nsXPIDLString value; if (mFilesOrDirectories.IsEmpty()) { if ((IsDirPickerEnabled() && Allowdirs()) || (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "NoDirSelected", value); } else if (HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)) { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "NoFilesSelected", value); } else { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "NoFileSelected", value); } } else { nsString count; count.AppendInt(int(mFilesOrDirectories.Length())); const char16_t* params[] = { count.get() }; nsContentUtils::FormatLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "XFilesSelected", params, value); } aValue = value; } void HTMLInputElement::SetFilesOrDirectories(const nsTArray& aFilesOrDirectories, bool aSetValueChanged) { ClearGetFilesHelpers(); if (IsWebkitFileSystemEnabled()) { HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this); mEntries.Clear(); } mFilesOrDirectories.Clear(); mFilesOrDirectories.AppendElements(aFilesOrDirectories); AfterSetFilesOrDirectories(aSetValueChanged); } void HTMLInputElement::SetFiles(nsIDOMFileList* aFiles, bool aSetValueChanged) { RefPtr files = static_cast(aFiles); mFilesOrDirectories.Clear(); ClearGetFilesHelpers(); if (IsWebkitFileSystemEnabled()) { HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this); mEntries.Clear(); } if (aFiles) { uint32_t listLength; aFiles->GetLength(&listLength); for (uint32_t i = 0; i < listLength; i++) { OwningFileOrDirectory* element = mFilesOrDirectories.AppendElement(); element->SetAsFile() = files->Item(i); } } AfterSetFilesOrDirectories(aSetValueChanged); } // This method is used for testing only. void HTMLInputElement::MozSetDndFilesAndDirectories(const nsTArray& aFilesOrDirectories) { SetFilesOrDirectories(aFilesOrDirectories, true); if (IsWebkitFileSystemEnabled()) { UpdateEntries(aFilesOrDirectories); } RefPtr dispatchChangeEventCallback = new DispatchChangeEventCallback(this); if (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) { ErrorResult rv; GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */, rv); if (NS_WARN_IF(rv.Failed())) { rv.SuppressException(); return; } helper->AddCallback(dispatchChangeEventCallback); } else { dispatchChangeEventCallback->DispatchEvents(); } } void HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged) { // No need to flush here, if there's no frame at this point we // don't need to force creation of one just to tell it about this // new value. We just want the display to update as needed. nsIFormControlFrame* formControlFrame = GetFormControlFrame(false); if (formControlFrame) { nsAutoString readableValue; GetDisplayFileName(readableValue); formControlFrame->SetFormProperty(nsGkAtoms::value, readableValue); } // Grab the full path here for any chrome callers who access our .value via a // CPOW. This path won't be called from a CPOW meaning the potential sync IPC // call under GetMozFullPath won't be rejected for not being urgent. // XXX Protected by the ifndef because the blob code doesn't allow us to send // this message in b2g. if (mFilesOrDirectories.IsEmpty()) { mFirstFilePath.Truncate(); } else { ErrorResult rv; GetDOMFileOrDirectoryPath(mFilesOrDirectories[0], mFirstFilePath, rv); if (NS_WARN_IF(rv.Failed())) { rv.SuppressException(); } } UpdateFileList(); if (aSetValueChanged) { SetValueChanged(true); } UpdateAllValidityStates(true); } void HTMLInputElement::FireChangeEventIfNeeded() { nsAutoString value; GetValue(value); if (!MayFireChangeOnBlur() || mFocusedValue.Equals(value)) { return; } // Dispatch the change event. mFocusedValue = value; nsContentUtils::DispatchTrustedEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("change"), true, false); } FileList* HTMLInputElement::GetFiles() { if (mType != NS_FORM_INPUT_FILE) { return nullptr; } if (IsDirPickerEnabled() && Allowdirs() && (!IsWebkitDirPickerEnabled() || !HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) { return nullptr; } if (!mFileList) { mFileList = new FileList(static_cast(this)); UpdateFileList(); } return mFileList; } /* static */ void HTMLInputElement::HandleNumberControlSpin(void* aData) { HTMLInputElement* input = static_cast(aData); NS_ASSERTION(input->mNumberControlSpinnerIsSpinning, "Should have called nsRepeatService::Stop()"); nsNumberControlFrame* numberControlFrame = do_QueryFrame(input->GetPrimaryFrame()); if (input->mType != NS_FORM_INPUT_NUMBER || !numberControlFrame) { // Type has changed (and possibly our frame type hasn't been updated yet) // or else we've lost our frame. Either way, stop the timer and don't do // anything else. input->StopNumberControlSpinnerSpin(); } else { input->StepNumberControlForUserEvent(input->mNumberControlSpinnerSpinsUp ? 1 : -1); } } void HTMLInputElement::UpdateFileList() { if (mFileList) { mFileList->Clear(); const nsTArray& array = GetFilesOrDirectoriesInternal(); for (uint32_t i = 0; i < array.Length(); ++i) { if (array[i].IsFile()) { mFileList->Append(array[i].GetAsFile()); } } } } nsresult HTMLInputElement::SetValueInternal(const nsAString& aValue, uint32_t aFlags) { NS_PRECONDITION(GetValueMode() != VALUE_MODE_FILENAME, "Don't call SetValueInternal for file inputs"); switch (GetValueMode()) { case VALUE_MODE_VALUE: { // At the moment, only single line text control have to sanitize their value // Because we have to create a new string for that, we should prevent doing // it if it's useless. nsAutoString value(aValue); if (mDoneCreating) { SanitizeValue(value); } // else DoneCreatingElement calls us again once mDoneCreating is true bool setValueChanged = !!(aFlags & nsTextEditorState::eSetValue_Notify); if (setValueChanged) { SetValueChanged(true); } if (IsSingleLineTextControl(false)) { if (!mInputData.mState->SetValue(value, aFlags)) { return NS_ERROR_OUT_OF_MEMORY; } if (mType == NS_FORM_INPUT_EMAIL) { UpdateAllValidityStates(!mDoneCreating); } } else { free(mInputData.mValue); mInputData.mValue = ToNewUnicode(value); if (setValueChanged) { SetValueChanged(true); } if (mType == NS_FORM_INPUT_NUMBER) { // This has to happen before OnValueChanged is called because that // method needs the new value of our frame's anon text control. nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { numberControlFrame->SetValueOfAnonTextControl(value); } } else if (mType == NS_FORM_INPUT_RANGE) { nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->UpdateForValueChange(); } } else if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->UpdateInputBoxValue(); } } if (mDoneCreating) { OnValueChanged(/* aNotify = */ true, /* aWasInteractiveUserChange = */ false); } // else DoneCreatingElement calls us again once mDoneCreating is true } if (mType == NS_FORM_INPUT_COLOR) { // Update color frame, to reflect color changes nsColorControlFrame* colorControlFrame = do_QueryFrame(GetPrimaryFrame()); if (colorControlFrame) { colorControlFrame->UpdateColor(); } } // This call might be useless in some situations because if the element is // a single line text control, nsTextEditorState::SetValue will call // nsHTMLInputElement::OnValueChanged which is going to call UpdateState() // if the element is focused. This bug 665547. if (PlaceholderApplies() && HasAttr(kNameSpaceID_None, nsGkAtoms::placeholder)) { UpdateState(true); } return NS_OK; } case VALUE_MODE_DEFAULT: case VALUE_MODE_DEFAULT_ON: // If the value of a hidden input was changed, we mark it changed so that we // will know we need to save / restore the value. Yes, we are overloading // the meaning of ValueChanged just a teensy bit to save a measly byte of // storage space in HTMLInputElement. Yes, you are free to make a new flag, // NEED_TO_SAVE_VALUE, at such time as mBitField becomes a 16-bit value. if (mType == NS_FORM_INPUT_HIDDEN) { SetValueChanged(true); } // Treat value == defaultValue for other input elements. return nsGenericHTMLFormElementWithState::SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true); case VALUE_MODE_FILENAME: return NS_ERROR_UNEXPECTED; } // This return statement is required for some compilers. return NS_OK; } NS_IMETHODIMP HTMLInputElement::SetValueChanged(bool aValueChanged) { bool valueChangedBefore = mValueChanged; mValueChanged = aValueChanged; if (valueChangedBefore != aValueChanged) { UpdateState(true); } return NS_OK; } NS_IMETHODIMP HTMLInputElement::GetChecked(bool* aChecked) { *aChecked = Checked(); return NS_OK; } void HTMLInputElement::SetCheckedChanged(bool aCheckedChanged) { DoSetCheckedChanged(aCheckedChanged, true); } void HTMLInputElement::DoSetCheckedChanged(bool aCheckedChanged, bool aNotify) { if (mType == NS_FORM_INPUT_RADIO) { if (mCheckedChanged != aCheckedChanged) { nsCOMPtr visitor = new nsRadioSetCheckedChangedVisitor(aCheckedChanged); VisitGroup(visitor, aNotify); } } else { SetCheckedChangedInternal(aCheckedChanged); } } void HTMLInputElement::SetCheckedChangedInternal(bool aCheckedChanged) { bool checkedChangedBefore = mCheckedChanged; mCheckedChanged = aCheckedChanged; // This method can't be called when we are not authorized to notify // so we do not need a aNotify parameter. if (checkedChangedBefore != aCheckedChanged) { UpdateState(true); } } NS_IMETHODIMP HTMLInputElement::SetChecked(bool aChecked) { DoSetChecked(aChecked, true, true); return NS_OK; } void HTMLInputElement::DoSetChecked(bool aChecked, bool aNotify, bool aSetValueChanged) { // If the user or JS attempts to set checked, whether it actually changes the // value or not, we say the value was changed so that defaultValue don't // affect it no more. if (aSetValueChanged) { DoSetCheckedChanged(true, aNotify); } // Don't do anything if we're not changing whether it's checked (it would // screw up state actually, especially when you are setting radio button to // false) if (mChecked == aChecked) { return; } // Set checked if (mType != NS_FORM_INPUT_RADIO) { SetCheckedInternal(aChecked, aNotify); return; } // For radio button, we need to do some extra fun stuff if (aChecked) { RadioSetChecked(aNotify); return; } nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (container) { nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); container->SetCurrentRadioButton(name, nullptr); } // SetCheckedInternal is going to ask all radios to update their // validity state. We have to be sure the radio group container knows // the currently selected radio. SetCheckedInternal(false, aNotify); } void HTMLInputElement::RadioSetChecked(bool aNotify) { // Find the selected radio button so we can deselect it nsCOMPtr currentlySelected = GetSelectedRadioButton(); // Deselect the currently selected radio button if (currentlySelected) { // Pass true for the aNotify parameter since the currently selected // button is already in the document. static_cast(currentlySelected.get()) ->SetCheckedInternal(false, true); } // Let the group know that we are now the One True Radio Button nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (container) { nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); container->SetCurrentRadioButton(name, this); } // SetCheckedInternal is going to ask all radios to update their // validity state. SetCheckedInternal(true, aNotify); } nsIRadioGroupContainer* HTMLInputElement::GetRadioGroupContainer() const { NS_ASSERTION(mType == NS_FORM_INPUT_RADIO, "GetRadioGroupContainer should only be called when type='radio'"); nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); if (name.IsEmpty()) { return nullptr; } if (mForm) { return mForm; } //XXXsmaug It isn't clear how this should work in Shadow DOM. return static_cast(GetUncomposedDoc()); } already_AddRefed HTMLInputElement::GetSelectedRadioButton() const { nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (!container) { return nullptr; } nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); nsCOMPtr selected = container->GetCurrentRadioButton(name); return selected.forget(); } nsresult HTMLInputElement::MaybeSubmitForm(nsPresContext* aPresContext) { if (!mForm) { // Nothing to do here. return NS_OK; } nsCOMPtr shell = aPresContext->GetPresShell(); if (!shell) { return NS_OK; } // Get the default submit element nsIFormControl* submitControl = mForm->GetDefaultSubmitElement(); if (submitControl) { nsCOMPtr submitContent = do_QueryInterface(submitControl); NS_ASSERTION(submitContent, "Form control not implementing nsIContent?!"); // Fire the button's onclick handler and let the button handle // submitting the form. WidgetMouseEvent event(true, eMouseClick, nullptr, WidgetMouseEvent::eReal); nsEventStatus status = nsEventStatus_eIgnore; shell->HandleDOMEventWithTarget(submitContent, &event, &status); } else if (!mForm->ImplicitSubmissionIsDisabled() && mForm->SubmissionCanProceed(nullptr)) { // TODO: removing this code and have the submit event sent by the form, // bug 592124. // If there's only one text control, just submit the form // Hold strong ref across the event RefPtr form = mForm; InternalFormEvent event(true, eFormSubmit); nsEventStatus status = nsEventStatus_eIgnore; shell->HandleDOMEventWithTarget(form, &event, &status); } return NS_OK; } void HTMLInputElement::SetCheckedInternal(bool aChecked, bool aNotify) { // Set the value mChecked = aChecked; // Notify the frame if (mType == NS_FORM_INPUT_CHECKBOX || mType == NS_FORM_INPUT_RADIO) { nsIFrame* frame = GetPrimaryFrame(); if (frame) { frame->InvalidateFrameSubtree(); } } UpdateAllValidityStates(aNotify); // Notify the document that the CSS :checked pseudoclass for this element // has changed state. UpdateState(aNotify); // Notify all radios in the group that value has changed, this is to let // radios to have the chance to update its states, e.g., :indeterminate. if (mType == NS_FORM_INPUT_RADIO) { nsCOMPtr visitor = new nsRadioUpdateStateVisitor(this); VisitGroup(visitor, aNotify); } } void HTMLInputElement::Blur(ErrorResult& aError) { if (mType == NS_FORM_INPUT_NUMBER) { // Blur our anonymous text control, if we have one. (DOM 'change' event // firing and other things depend on this.) nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { HTMLInputElement* textControl = numberControlFrame->GetAnonTextControl(); if (textControl) { textControl->Blur(aError); return; } } } if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleBlurEvent(); return; } } nsGenericHTMLElement::Blur(aError); } void HTMLInputElement::Focus(ErrorResult& aError) { if (mType == NS_FORM_INPUT_NUMBER) { // Focus our anonymous text control, if we have one. nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { HTMLInputElement* textControl = numberControlFrame->GetAnonTextControl(); if (textControl) { textControl->Focus(aError); return; } } } if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleFocusEvent(); return; } } if (mType != NS_FORM_INPUT_FILE) { nsGenericHTMLElement::Focus(aError); return; } // For file inputs, focus the first button instead. In the case of there // being two buttons (when the picker is a directory picker) the user can // tab to the next one. nsIFrame* frame = GetPrimaryFrame(); if (frame) { for (nsIFrame* childFrame : frame->PrincipalChildList()) { // See if the child is a button control. nsCOMPtr formCtrl = do_QueryInterface(childFrame->GetContent()); if (formCtrl && formCtrl->GetType() == NS_FORM_BUTTON_BUTTON) { nsCOMPtr element = do_QueryInterface(formCtrl); nsIFocusManager* fm = nsFocusManager::GetFocusManager(); if (fm && element) { fm->SetFocus(element, 0); } break; } } } return; } #if !defined(ANDROID) && !defined(XP_MACOSX) bool HTMLInputElement::IsNodeApzAwareInternal() const { // Tell APZC we may handle mouse wheel event and do preventDefault when input // type is number. return (mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE) || nsINode::IsNodeApzAwareInternal(); } #endif bool HTMLInputElement::IsInteractiveHTMLContent(bool aIgnoreTabindex) const { return mType != NS_FORM_INPUT_HIDDEN || nsGenericHTMLFormElementWithState::IsInteractiveHTMLContent(aIgnoreTabindex); } void HTMLInputElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { nsImageLoadingContent::AsyncEventRunning(aEvent); } NS_IMETHODIMP HTMLInputElement::Select() { if (mType == NS_FORM_INPUT_NUMBER) { nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { return numberControlFrame->HandleSelectCall(); } return NS_OK; } if (!IsSingleLineTextControl(false)) { return NS_OK; } // XXX Bug? We have to give the input focus before contents can be // selected FocusTristate state = FocusState(); if (state == eUnfocusable) { return NS_OK; } nsTextEditorState* tes = GetEditorState(); if (tes) { RefPtr fs = tes->GetConstFrameSelection(); if (fs && fs->MouseDownRecorded()) { // This means that we're being called while the frame selection has a mouse // down event recorded to adjust the caret during the mouse up event. // We are probably called from the focus event handler. We should override // the delayed caret data in this case to ensure that this select() call // takes effect. fs->SetDelayedCaretData(nullptr); } } nsIFocusManager* fm = nsFocusManager::GetFocusManager(); RefPtr presContext = GetPresContext(eForComposedDoc); if (state == eInactiveWindow) { if (fm) fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL); SelectAll(presContext); return NS_OK; } if (DispatchSelectEvent(presContext) && fm) { fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL); // ensure that the element is actually focused nsCOMPtr focusedElement; fm->GetFocusedElement(getter_AddRefs(focusedElement)); if (SameCOMIdentity(static_cast(this), focusedElement)) { // Now Select all the text! SelectAll(presContext); } } return NS_OK; } bool HTMLInputElement::DispatchSelectEvent(nsPresContext* aPresContext) { nsEventStatus status = nsEventStatus_eIgnore; // If already handling select event, don't dispatch a second. if (!mHandlingSelectEvent) { WidgetEvent event(nsContentUtils::LegacyIsCallerChromeOrNativeCode(), eFormSelect); mHandlingSelectEvent = true; EventDispatcher::Dispatch(static_cast(this), aPresContext, &event, nullptr, &status); mHandlingSelectEvent = false; } // If the DOM event was not canceled (e.g. by a JS event handler // returning false) return (status == nsEventStatus_eIgnore); } void HTMLInputElement::SelectAll(nsPresContext* aPresContext) { nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); if (formControlFrame) { formControlFrame->SetFormProperty(nsGkAtoms::select, EmptyString()); } } bool HTMLInputElement::NeedToInitializeEditorForEvent( EventChainPreVisitor& aVisitor) const { // We only need to initialize the editor for single line input controls because they // are lazily initialized. We don't need to initialize the control for // certain types of events, because we know that those events are safe to be // handled without the editor being initialized. These events include: // mousein/move/out, overflow/underflow, and DOM mutation events. if (!IsSingleLineTextControl(false) || aVisitor.mEvent->mClass == eMutationEventClass) { return false; } switch (aVisitor.mEvent->mMessage) { case eMouseMove: case eMouseEnterIntoWidget: case eMouseExitFromWidget: case eMouseOver: case eMouseOut: case eScrollPortUnderflow: case eScrollPortOverflow: return false; default: return true; } } bool HTMLInputElement::IsDisabledForEvents(EventMessage aMessage) { return IsElementDisabledForEvents(aMessage, GetPrimaryFrame()); } nsresult HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor) { // Do not process any DOM events if the element is disabled aVisitor.mCanHandle = false; if (IsDisabledForEvents(aVisitor.mEvent->mMessage)) { return NS_OK; } // Initialize the editor if needed. if (NeedToInitializeEditorForEvent(aVisitor)) { nsITextControlFrame* textControlFrame = do_QueryFrame(GetPrimaryFrame()); if (textControlFrame) textControlFrame->EnsureEditorInitialized(); } //FIXME Allow submission etc. also when there is no prescontext, Bug 329509. if (!aVisitor.mPresContext) { return nsGenericHTMLElement::PreHandleEvent(aVisitor); } // // Web pages expect the value of a radio button or checkbox to be set // *before* onclick and DOMActivate fire, and they expect that if they set // the value explicitly during onclick or DOMActivate it will not be toggled // or any such nonsense. // In order to support that (bug 57137 and 58460 are examples) we toggle // the checked attribute *first*, and then fire onclick. If the user // returns false, we reset the control to the old checked value. Otherwise, // we dispatch DOMActivate. If DOMActivate is cancelled, we also reset // the control to the old checked value. We need to keep track of whether // we've already toggled the state from onclick since the user could // explicitly dispatch DOMActivate on the element. // // This is a compatibility hack. // // Track whether we're in the outermost Dispatch invocation that will // cause activation of the input. That is, if we're a click event, or a // DOMActivate that was dispatched directly, this will be set, but if we're // a DOMActivate dispatched from click handling, it will not be set. WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); bool outerActivateEvent = ((mouseEvent && mouseEvent->IsLeftClickEvent()) || (aVisitor.mEvent->mMessage == eLegacyDOMActivate && !mInInternalActivate)); if (outerActivateEvent) { aVisitor.mItemFlags |= NS_OUTER_ACTIVATE_EVENT; } bool originalCheckedValue = false; if (outerActivateEvent) { mCheckedIsToggled = false; switch(mType) { case NS_FORM_INPUT_CHECKBOX: { if (mIndeterminate) { // indeterminate is always set to FALSE when the checkbox is toggled SetIndeterminateInternal(false, false); aVisitor.mItemFlags |= NS_ORIGINAL_INDETERMINATE_VALUE; } GetChecked(&originalCheckedValue); DoSetChecked(!originalCheckedValue, true, true); mCheckedIsToggled = true; } break; case NS_FORM_INPUT_RADIO: { nsCOMPtr selectedRadioButton = GetSelectedRadioButton(); aVisitor.mItemData = selectedRadioButton; originalCheckedValue = mChecked; if (!originalCheckedValue) { DoSetChecked(true, true, true); mCheckedIsToggled = true; } } break; case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_IMAGE: if (mForm) { // tell the form that we are about to enter a click handler. // that means that if there are scripted submissions, the // latest one will be deferred until after the exit point of the handler. mForm->OnSubmitClickBegin(this); } break; default: break; } } if (originalCheckedValue) { aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE; } // If mNoContentDispatch is true we will not allow content to handle // this event. But to allow middle mouse button paste to work we must allow // middle clicks to go to text fields anyway. if (aVisitor.mEvent->mFlags.mNoContentDispatch) { aVisitor.mItemFlags |= NS_NO_CONTENT_DISPATCH; } if (IsSingleLineTextControl(false) && aVisitor.mEvent->mMessage == eMouseClick && aVisitor.mEvent->AsMouseEvent()->button == WidgetMouseEvent::eMiddleButton) { aVisitor.mEvent->mFlags.mNoContentDispatch = false; } // We must cache type because mType may change during JS event (bug 2369) aVisitor.mItemFlags |= mType; if (aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->IsTrusted() && MayFireChangeOnBlur() && // StartRangeThumbDrag already set mFocusedValue on 'mousedown' before // we get the 'focus' event. !mIsDraggingRange) { GetValue(mFocusedValue); } // Fire onchange (if necessary), before we do the blur, bug 357684. if (aVisitor.mEvent->mMessage == eBlur) { // Experimental mobile types rely on the system UI to prevent users to not // set invalid values but we have to be extra-careful. Especially if the // option has been enabled on desktop. if (IsExperimentalMobileType(mType)) { nsAutoString aValue; GetValueInternal(aValue); nsresult rv = SetValueInternal(aValue, nsTextEditorState::eSetValue_Internal); NS_ENSURE_SUCCESS(rv, rv); } FireChangeEventIfNeeded(); } if (mType == NS_FORM_INPUT_RANGE && (aVisitor.mEvent->mMessage == eFocus || aVisitor.mEvent->mMessage == eBlur)) { // Just as nsGenericHTMLFormElementWithState::PreHandleEvent calls // nsIFormControlFrame::SetFocus, we handle focus here. nsIFrame* frame = GetPrimaryFrame(); if (frame) { frame->InvalidateFrameSubtree(); } } if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType) && aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->mOriginalTarget == this) { // If original target is this and not the anonymous text control, we should // pass the focus to the anonymous text control. nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleFocusEvent(); } } if (mType == NS_FORM_INPUT_NUMBER && aVisitor.mEvent->IsTrusted()) { if (mNumberControlSpinnerIsSpinning) { // If the timer is running the user has depressed the mouse on one of the // spin buttons. If the mouse exits the button we either want to reverse // the direction of spin if it has moved over the other button, or else // we want to end the spin. We do this here (rather than in // PostHandleEvent) because we don't want to let content preventDefault() // the end of the spin. if (aVisitor.mEvent->mMessage == eMouseMove) { // Be aggressive about stopping the spin: bool stopSpin = true; nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { bool oldNumberControlSpinTimerSpinsUpValue = mNumberControlSpinnerSpinsUp; switch (numberControlFrame->GetSpinButtonForPointerEvent( aVisitor.mEvent->AsMouseEvent())) { case nsNumberControlFrame::eSpinButtonUp: mNumberControlSpinnerSpinsUp = true; stopSpin = false; break; case nsNumberControlFrame::eSpinButtonDown: mNumberControlSpinnerSpinsUp = false; stopSpin = false; break; } if (mNumberControlSpinnerSpinsUp != oldNumberControlSpinTimerSpinsUpValue) { nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { numberControlFrame->SpinnerStateChanged(); } } } if (stopSpin) { StopNumberControlSpinnerSpin(); } } else if (aVisitor.mEvent->mMessage == eMouseUp) { StopNumberControlSpinnerSpin(); } } if (aVisitor.mEvent->mMessage == eFocus || aVisitor.mEvent->mMessage == eBlur) { if (aVisitor.mEvent->mMessage == eFocus) { // Tell our frame it's getting focus so that it can make sure focus // is moved to our anonymous text control. nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { // This could kill the frame! numberControlFrame->HandleFocusEvent(aVisitor.mEvent); } } nsIFrame* frame = GetPrimaryFrame(); if (frame && frame->IsThemed()) { // Our frame's nested will be invalidated when it // loses focus, but since we are also native themed we need to make // sure that our entire area is repainted since any focus highlight // from the theme should be removed from us (the repainting of the // sub-area occupied by the anon text control is not enough to do // that). frame->InvalidateFrame(); } } } nsresult rv = nsGenericHTMLFormElementWithState::PreHandleEvent(aVisitor); // We do this after calling the base class' PreHandleEvent so that // nsIContent::PreHandleEvent doesn't reset any change we make to mCanHandle. if (mType == NS_FORM_INPUT_NUMBER && aVisitor.mEvent->IsTrusted() && aVisitor.mEvent->mOriginalTarget != this) { // has an anonymous descendant. If // 'input' or 'change' events are fired at that text control then we need // to do some special handling here. HTMLInputElement* textControl = nullptr; nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { textControl = numberControlFrame->GetAnonTextControl(); } if (textControl && aVisitor.mEvent->mOriginalTarget == textControl) { if (aVisitor.mEvent->mMessage == eEditorInput) { // Propogate the anon text control's new value to our HTMLInputElement: nsAutoString value; numberControlFrame->GetValueOfAnonTextControl(value); numberControlFrame->HandlingInputEvent(true); nsWeakFrame weakNumberControlFrame(numberControlFrame); rv = SetValueInternal(value, nsTextEditorState::eSetValue_BySetUserInput | nsTextEditorState::eSetValue_Notify); NS_ENSURE_SUCCESS(rv, rv); if (weakNumberControlFrame.IsAlive()) { numberControlFrame->HandlingInputEvent(false); } } else if (aVisitor.mEvent->mMessage == eFormChange) { // We cancel the DOM 'change' event that is fired for any change to our // anonymous text control since we fire our own 'change' events and // content shouldn't be seeing two 'change' events. Besides that we // (as a number) control have tighter restrictions on when our internal // value changes than our anon text control does, so in some cases // (if our text control's value doesn't parse as a number) we don't // want to fire a 'change' event at all. aVisitor.mCanHandle = false; } } } // Stop the event if the related target's first non-native ancestor is the // same as the original target's first non-native ancestor (we are moving // inside of the same element). if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType) && (aVisitor.mEvent->mMessage == eFocus || aVisitor.mEvent->mMessage == eFocusIn || aVisitor.mEvent->mMessage == eFocusOut || aVisitor.mEvent->mMessage == eBlur)) { nsCOMPtr originalTarget = do_QueryInterface(aVisitor.mEvent->AsFocusEvent()->mRelatedTarget); nsCOMPtr relatedTarget = do_QueryInterface(aVisitor.mEvent->AsFocusEvent()->mRelatedTarget); if (originalTarget && relatedTarget && originalTarget->FindFirstNonChromeOnlyAccessContent() == relatedTarget->FindFirstNonChromeOnlyAccessContent()) { aVisitor.mCanHandle = false; } } return rv; } void HTMLInputElement::StartRangeThumbDrag(WidgetGUIEvent* aEvent) { mIsDraggingRange = true; mRangeThumbDragStartValue = GetValueAsDecimal(); // Don't use CAPTURE_RETARGETTOELEMENT, as that breaks pseudo-class styling // of the thumb. nsIPresShell::SetCapturingContent(this, CAPTURE_IGNOREALLOWED); nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); // Before we change the value, record the current value so that we'll // correctly send a 'change' event if appropriate. We need to do this here // because the 'focus' event is handled after the 'mousedown' event that // we're being called for (i.e. too late to update mFocusedValue, since we'll // have changed it by then). GetValue(mFocusedValue); SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent)); } void HTMLInputElement::FinishRangeThumbDrag(WidgetGUIEvent* aEvent) { MOZ_ASSERT(mIsDraggingRange); if (nsIPresShell::GetCapturingContent() == this) { nsIPresShell::SetCapturingContent(nullptr, 0); // cancel capture } if (aEvent) { nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent)); } mIsDraggingRange = false; FireChangeEventIfNeeded(); } void HTMLInputElement::CancelRangeThumbDrag(bool aIsForUserEvent) { MOZ_ASSERT(mIsDraggingRange); mIsDraggingRange = false; if (nsIPresShell::GetCapturingContent() == this) { nsIPresShell::SetCapturingContent(nullptr, 0); // cancel capture } if (aIsForUserEvent) { SetValueOfRangeForUserEvent(mRangeThumbDragStartValue); } else { // Don't dispatch an 'input' event - at least not using // DispatchTrustedEvent. // TODO: decide what we should do here - bug 851782. nsAutoString val; ConvertNumberToString(mRangeThumbDragStartValue, val); // TODO: What should we do if SetValueInternal fails? (The allocation // is small, so we should be fine here.) SetValueInternal(val, nsTextEditorState::eSetValue_BySetUserInput | nsTextEditorState::eSetValue_Notify); nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->UpdateForValueChange(); } RefPtr asyncDispatcher = new AsyncEventDispatcher(this, NS_LITERAL_STRING("input"), true, false); asyncDispatcher->RunDOMEventWhenSafe(); } } void HTMLInputElement::SetValueOfRangeForUserEvent(Decimal aValue) { MOZ_ASSERT(aValue.isFinite()); Decimal oldValue = GetValueAsDecimal(); nsAutoString val; ConvertNumberToString(aValue, val); // TODO: What should we do if SetValueInternal fails? (The allocation // is small, so we should be fine here.) SetValueInternal(val, nsTextEditorState::eSetValue_BySetUserInput | nsTextEditorState::eSetValue_Notify); nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->UpdateForValueChange(); } if (GetValueAsDecimal() != oldValue) { nsContentUtils::DispatchTrustedEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("input"), true, false); } } void HTMLInputElement::StartNumberControlSpinnerSpin() { MOZ_ASSERT(!mNumberControlSpinnerIsSpinning); mNumberControlSpinnerIsSpinning = true; nsRepeatService::GetInstance()->Start(HandleNumberControlSpin, this); // Capture the mouse so that we can tell if the pointer moves from one // spin button to the other, or to some other element: nsIPresShell::SetCapturingContent(this, CAPTURE_IGNOREALLOWED); nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { numberControlFrame->SpinnerStateChanged(); } } void HTMLInputElement::StopNumberControlSpinnerSpin(SpinnerStopState aState) { if (mNumberControlSpinnerIsSpinning) { if (nsIPresShell::GetCapturingContent() == this) { nsIPresShell::SetCapturingContent(nullptr, 0); // cancel capture } nsRepeatService::GetInstance()->Stop(HandleNumberControlSpin, this); mNumberControlSpinnerIsSpinning = false; if (aState == eAllowDispatchingEvents) { FireChangeEventIfNeeded(); } nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { MOZ_ASSERT(aState == eAllowDispatchingEvents, "Shouldn't have primary frame for the element when we're not " "allowed to dispatch events to it anymore."); numberControlFrame->SpinnerStateChanged(); } } } void HTMLInputElement::StepNumberControlForUserEvent(int32_t aDirection) { // We can't use GetValidityState here because the validity state is not set // if the user hasn't previously taken an action to set or change the value, // according to the specs. if (HasBadInput()) { // If the user has typed a value into the control and inadvertently made a // mistake (e.g. put a thousand separator at the wrong point) we do not // want to wipe out what they typed if they try to increment/decrement the // value. Better is to highlight the value as being invalid so that they // can correct what they typed. // We only do this if there actually is a value typed in by/displayed to // the user. (IsValid() can return false if the 'required' attribute is // set and the value is the empty string.) nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame && !numberControlFrame->AnonTextControlIsEmpty()) { // We pass 'true' for UpdateValidityUIBits' aIsFocused argument // regardless because we need the UI to update _now_ or the user will // wonder why the step behavior isn't functioning. UpdateValidityUIBits(true); UpdateState(true); return; } } Decimal newValue = Decimal::nan(); // unchanged if value will not change nsresult rv = GetValueIfStepped(aDirection, CALLED_FOR_USER_EVENT, &newValue); if (NS_FAILED(rv) || !newValue.isFinite()) { return; // value should not or will not change } nsAutoString newVal; ConvertNumberToString(newValue, newVal); // TODO: What should we do if SetValueInternal fails? (The allocation // is small, so we should be fine here.) SetValueInternal(newVal, nsTextEditorState::eSetValue_BySetUserInput | nsTextEditorState::eSetValue_Notify); nsContentUtils::DispatchTrustedEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("input"), true, false); } static bool SelectTextFieldOnFocus() { if (!gSelectTextFieldOnFocus) { int32_t selectTextfieldsOnKeyFocus = -1; nsresult rv = LookAndFeel::GetInt(LookAndFeel::eIntID_SelectTextfieldsOnKeyFocus, &selectTextfieldsOnKeyFocus); if (NS_FAILED(rv)) { gSelectTextFieldOnFocus = -1; } else { gSelectTextFieldOnFocus = selectTextfieldsOnKeyFocus != 0 ? 1 : -1; } } return gSelectTextFieldOnFocus == 1; } bool HTMLInputElement::ShouldPreventDOMActivateDispatch(EventTarget* aOriginalTarget) { /* * For the moment, there is only one situation where we actually want to * prevent firing a DOMActivate event: * - we are a that just got a click event, * - the event was targeted to our button which should have sent a * DOMActivate event. */ if (mType != NS_FORM_INPUT_FILE) { return false; } nsCOMPtr target = do_QueryInterface(aOriginalTarget); if (!target) { return false; } return target->GetParent() == this && target->IsRootOfNativeAnonymousSubtree() && target->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::button, eCaseMatters); } nsresult HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) { // Open a file picker when we receive a click on a , or // open a color picker when we receive a click on a . // A click is handled in the following cases: // - preventDefault() has not been called (or something similar); // - it's the left mouse button. // We do not prevent non-trusted click because authors can already use // .click(). However, the pickers will follow the rules of popup-blocking. if (aVisitor.mEvent->DefaultPrevented()) { return NS_OK; } WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); if (!(mouseEvent && mouseEvent->IsLeftClickEvent())) { return NS_OK; } if (mType == NS_FORM_INPUT_FILE) { // If the user clicked on the "Choose folder..." button we open the // directory picker, else we open the file picker. FilePickerType type = FILE_PICKER_FILE; nsCOMPtr target = do_QueryInterface(aVisitor.mEvent->mOriginalTarget); if (target && target->FindFirstNonChromeOnlyAccessContent() == this && ((IsDirPickerEnabled() && Allowdirs()) || (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)))) { type = FILE_PICKER_DIRECTORY; } return InitFilePicker(type); } if (mType == NS_FORM_INPUT_COLOR) { return InitColorPicker(); } if (mType == NS_FORM_INPUT_DATE) { return InitDatePicker(); } return NS_OK; } /** * Return true if the input event should be ignore because of it's modifiers */ static bool IgnoreInputEventWithModifier(WidgetInputEvent* aEvent) { return aEvent->IsShift() || aEvent->IsControl() || aEvent->IsAlt() || aEvent->IsMeta() || aEvent->IsAltGraph() || aEvent->IsFn() || aEvent->IsOS(); } nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { if (!aVisitor.mPresContext) { // Hack alert! In order to open file picker even in case the element isn't // in document, try to init picker even without PresContext. return MaybeInitPickers(aVisitor); } if (aVisitor.mEvent->mMessage == eFocus || aVisitor.mEvent->mMessage == eBlur) { if (aVisitor.mEvent->mMessage == eBlur) { if (mIsDraggingRange) { FinishRangeThumbDrag(); } else if (mNumberControlSpinnerIsSpinning) { StopNumberControlSpinnerSpin(); } } UpdateValidityUIBits(aVisitor.mEvent->mMessage == eFocus); UpdateState(true); } nsresult rv = NS_OK; bool outerActivateEvent = !!(aVisitor.mItemFlags & NS_OUTER_ACTIVATE_EVENT); bool originalCheckedValue = !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE); bool noContentDispatch = !!(aVisitor.mItemFlags & NS_NO_CONTENT_DISPATCH); uint8_t oldType = NS_CONTROL_TYPE(aVisitor.mItemFlags); // Ideally we would make the default action for click and space just dispatch // DOMActivate, and the default action for DOMActivate flip the checkbox/ // radio state and fire onchange. However, for backwards compatibility, we // need to flip the state before firing click, and we need to fire click // when space is pressed. So, we just nest the firing of DOMActivate inside // the click event handling, and allow cancellation of DOMActivate to cancel // the click. if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault && !IsSingleLineTextControl(true) && mType != NS_FORM_INPUT_NUMBER) { WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); if (mouseEvent && mouseEvent->IsLeftClickEvent() && !ShouldPreventDOMActivateDispatch(aVisitor.mEvent->mOriginalTarget)) { // DOMActive event should be trusted since the activation is actually // occurred even if the cause is an untrusted click event. InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent); actEvent.mDetail = 1; nsCOMPtr shell = aVisitor.mPresContext->GetPresShell(); if (shell) { nsEventStatus status = nsEventStatus_eIgnore; mInInternalActivate = true; rv = shell->HandleDOMEventWithTarget(this, &actEvent, &status); mInInternalActivate = false; // If activate is cancelled, we must do the same as when click is // cancelled (revert the checkbox to its original value). if (status == nsEventStatus_eConsumeNoDefault) { aVisitor.mEventStatus = status; } } } } if (outerActivateEvent) { switch(oldType) { case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_IMAGE: if (mForm) { // tell the form that we are about to exit a click handler // so the form knows not to defer subsequent submissions // the pending ones that were created during the handler // will be flushed or forgoten. mForm->OnSubmitClickEnd(); } break; default: break; } } // Reset the flag for other content besides this text field aVisitor.mEvent->mFlags.mNoContentDispatch = noContentDispatch; // now check to see if the event was "cancelled" if (mCheckedIsToggled && outerActivateEvent) { if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { // if it was cancelled and a radio button, then set the old // selected btn to TRUE. if it is a checkbox then set it to its // original value if (oldType == NS_FORM_INPUT_RADIO) { nsCOMPtr selectedRadioButton = do_QueryInterface(aVisitor.mItemData); if (selectedRadioButton) { selectedRadioButton->SetChecked(true); } // If there was no checked radio button or this one is no longer a // radio button we must reset it back to false to cancel the action. // See how the web of hack grows? if (!selectedRadioButton || mType != NS_FORM_INPUT_RADIO) { DoSetChecked(false, true, true); } } else if (oldType == NS_FORM_INPUT_CHECKBOX) { bool originalIndeterminateValue = !!(aVisitor.mItemFlags & NS_ORIGINAL_INDETERMINATE_VALUE); SetIndeterminateInternal(originalIndeterminateValue, false); DoSetChecked(originalCheckedValue, true, true); } } else { // Fire input event and then change event. nsContentUtils::DispatchTrustedEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("input"), true, false); nsContentUtils::DispatchTrustedEvent(OwnerDoc(), static_cast(this), NS_LITERAL_STRING("change"), true, false); #ifdef ACCESSIBILITY // Fire an event to notify accessibility if (mType == NS_FORM_INPUT_CHECKBOX) { FireEventForAccessibility(this, aVisitor.mPresContext, NS_LITERAL_STRING("CheckboxStateChange")); } else { FireEventForAccessibility(this, aVisitor.mPresContext, NS_LITERAL_STRING("RadioStateChange")); // Fire event for the previous selected radio. nsCOMPtr previous = do_QueryInterface(aVisitor.mItemData); if (previous) { FireEventForAccessibility(previous, aVisitor.mPresContext, NS_LITERAL_STRING("RadioStateChange")); } } #endif } } if (NS_SUCCEEDED(rv)) { WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent(); if (mType == NS_FORM_INPUT_NUMBER && keyEvent && keyEvent->mMessage == eKeyPress && aVisitor.mEvent->IsTrusted() && (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN) && !IgnoreInputEventWithModifier(keyEvent)) { // We handle the up/down arrow keys specially for . // On some platforms the editor for the nested text control will // process these keys to send the cursor to the start/end of the text // control and as a result aVisitor.mEventStatus will already have been // set to nsEventStatus_eConsumeNoDefault. However, we know that // whenever the up/down arrow keys cause the value of the number // control to change the string in the text control will change, and // the cursor will be moved to the end of the text control, overwriting // the editor's handling of up/down keypress events. For that reason we // just ignore aVisitor.mEventStatus here and go ahead and handle the // event to increase/decrease the value of the number control. if (!aVisitor.mEvent->DefaultPreventedByContent() && IsMutable()) { StepNumberControlForUserEvent(keyEvent->mKeyCode == NS_VK_UP ? 1 : -1); FireChangeEventIfNeeded(); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } else if (nsEventStatus_eIgnore == aVisitor.mEventStatus) { switch (aVisitor.mEvent->mMessage) { case eFocus: { // see if we should select the contents of the textbox. This happens // for text and password fields when the field was focused by the // keyboard or a navigation, the platform allows it, and it wasn't // just because we raised a window. nsIFocusManager* fm = nsFocusManager::GetFocusManager(); if (fm && IsSingleLineTextControl(false) && !aVisitor.mEvent->AsFocusEvent()->mFromRaise && SelectTextFieldOnFocus()) { nsIDocument* document = GetComposedDoc(); if (document) { uint32_t lastFocusMethod; fm->GetLastFocusMethod(document->GetWindow(), &lastFocusMethod); if (lastFocusMethod & (nsIFocusManager::FLAG_BYKEY | nsIFocusManager::FLAG_BYMOVEFOCUS)) { RefPtr presContext = GetPresContext(eForComposedDoc); if (DispatchSelectEvent(presContext)) { SelectAll(presContext); } } } } break; } case eKeyPress: case eKeyUp: { // For backwards compat, trigger checks/radios/buttons with // space or enter (bug 25300) WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent(); if ((aVisitor.mEvent->mMessage == eKeyPress && keyEvent->mKeyCode == NS_VK_RETURN) || (aVisitor.mEvent->mMessage == eKeyUp && keyEvent->mKeyCode == NS_VK_SPACE)) { switch(mType) { case NS_FORM_INPUT_CHECKBOX: case NS_FORM_INPUT_RADIO: { // Checkbox and Radio try to submit on Enter press if (keyEvent->mKeyCode != NS_VK_SPACE) { MaybeSubmitForm(aVisitor.mPresContext); break; // If we are submitting, do not send click event } // else fall through and treat Space like click... MOZ_FALLTHROUGH; } case NS_FORM_INPUT_BUTTON: case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_IMAGE: // Bug 34418 case NS_FORM_INPUT_COLOR: { DispatchSimulatedClick(this, aVisitor.mEvent->IsTrusted(), aVisitor.mPresContext); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } // case } // switch } if (aVisitor.mEvent->mMessage == eKeyPress && mType == NS_FORM_INPUT_RADIO && !keyEvent->IsAlt() && !keyEvent->IsControl() && !keyEvent->IsMeta()) { bool isMovingBack = false; switch (keyEvent->mKeyCode) { case NS_VK_UP: case NS_VK_LEFT: isMovingBack = true; MOZ_FALLTHROUGH; case NS_VK_DOWN: case NS_VK_RIGHT: // Arrow key pressed, focus+select prev/next radio button nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (container) { nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); RefPtr selectedRadioButton; container->GetNextRadioButton(name, isMovingBack, this, getter_AddRefs(selectedRadioButton)); if (selectedRadioButton) { rv = selectedRadioButton->Focus(); if (NS_SUCCEEDED(rv)) { rv = DispatchSimulatedClick(selectedRadioButton, aVisitor.mEvent->IsTrusted(), aVisitor.mPresContext); if (NS_SUCCEEDED(rv)) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } } } } } /* * For some input types, if the user hits enter, the form is submitted. * * Bug 99920, bug 109463 and bug 147850: * (a) if there is a submit control in the form, click the first * submit control in the form. * (b) if there is just one text control in the form, submit by * sending a submit event directly to the form * (c) if there is more than one text input and no submit buttons, do * not submit, period. */ if (aVisitor.mEvent->mMessage == eKeyPress && keyEvent->mKeyCode == NS_VK_RETURN && (IsSingleLineTextControl(false, mType) || mType == NS_FORM_INPUT_NUMBER || IsExperimentalMobileType(mType) || IsDateTimeInputType(mType))) { FireChangeEventIfNeeded(); rv = MaybeSubmitForm(aVisitor.mPresContext); NS_ENSURE_SUCCESS(rv, rv); } if (aVisitor.mEvent->mMessage == eKeyPress && mType == NS_FORM_INPUT_RANGE && !keyEvent->IsAlt() && !keyEvent->IsControl() && !keyEvent->IsMeta() && (keyEvent->mKeyCode == NS_VK_LEFT || keyEvent->mKeyCode == NS_VK_RIGHT || keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN || keyEvent->mKeyCode == NS_VK_PAGE_UP || keyEvent->mKeyCode == NS_VK_PAGE_DOWN || keyEvent->mKeyCode == NS_VK_HOME || keyEvent->mKeyCode == NS_VK_END)) { Decimal minimum = GetMinimum(); Decimal maximum = GetMaximum(); MOZ_ASSERT(minimum.isFinite() && maximum.isFinite()); if (minimum < maximum) { // else the value is locked to the minimum Decimal value = GetValueAsDecimal(); Decimal step = GetStep(); if (step == kStepAny) { step = GetDefaultStep(); } MOZ_ASSERT(value.isFinite() && step.isFinite()); Decimal newValue; switch (keyEvent->mKeyCode) { case NS_VK_LEFT: newValue = value + (GetComputedDirectionality() == eDir_RTL ? step : -step); break; case NS_VK_RIGHT: newValue = value + (GetComputedDirectionality() == eDir_RTL ? -step : step); break; case NS_VK_UP: // Even for horizontal range, "up" means "increase" newValue = value + step; break; case NS_VK_DOWN: // Even for horizontal range, "down" means "decrease" newValue = value - step; break; case NS_VK_HOME: newValue = minimum; break; case NS_VK_END: newValue = maximum; break; case NS_VK_PAGE_UP: // For PgUp/PgDn we jump 10% of the total range, unless step // requires us to jump more. newValue = value + std::max(step, (maximum - minimum) / Decimal(10)); break; case NS_VK_PAGE_DOWN: newValue = value - std::max(step, (maximum - minimum) / Decimal(10)); break; } SetValueOfRangeForUserEvent(newValue); FireChangeEventIfNeeded(); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } } break; // eKeyPress || eKeyUp case eMouseDown: case eMouseUp: case eMouseDoubleClick: { // cancel all of these events for buttons //XXXsmaug Why? WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); if (mouseEvent->button == WidgetMouseEvent::eMiddleButton || mouseEvent->button == WidgetMouseEvent::eRightButton) { if (mType == NS_FORM_INPUT_BUTTON || mType == NS_FORM_INPUT_RESET || mType == NS_FORM_INPUT_SUBMIT) { if (aVisitor.mDOMEvent) { aVisitor.mDOMEvent->StopPropagation(); } else { rv = NS_ERROR_FAILURE; } } } if (mType == NS_FORM_INPUT_NUMBER && aVisitor.mEvent->IsTrusted()) { if (mouseEvent->button == WidgetMouseEvent::eLeftButton && !IgnoreInputEventWithModifier(mouseEvent)) { nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame) { if (aVisitor.mEvent->mMessage == eMouseDown && IsMutable()) { switch (numberControlFrame->GetSpinButtonForPointerEvent( aVisitor.mEvent->AsMouseEvent())) { case nsNumberControlFrame::eSpinButtonUp: StepNumberControlForUserEvent(1); mNumberControlSpinnerSpinsUp = true; StartNumberControlSpinnerSpin(); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; break; case nsNumberControlFrame::eSpinButtonDown: StepNumberControlForUserEvent(-1); mNumberControlSpinnerSpinsUp = false; StartNumberControlSpinnerSpin(); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; break; } } } } if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { // We didn't handle this to step up/down. Whatever this was, be // aggressive about stopping the spin. (And don't set // nsEventStatus_eConsumeNoDefault after doing so, since that // might prevent, say, the context menu from opening.) StopNumberControlSpinnerSpin(); } } break; } #if !defined(ANDROID) && !defined(XP_MACOSX) case eWheel: { // Handle wheel events as increasing / decreasing the input element's // value when it's focused and it's type is number or range. WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent(); if (!aVisitor.mEvent->DefaultPrevented() && aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent && wheelEvent->mDeltaY != 0 && wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL) { if (mType == NS_FORM_INPUT_NUMBER) { nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame && numberControlFrame->IsFocused()) { StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1); FireChangeEventIfNeeded(); aVisitor.mEvent->PreventDefault(); } } else if (mType == NS_FORM_INPUT_RANGE && nsContentUtils::IsFocusedContent(this) && GetMinimum() < GetMaximum()) { Decimal value = GetValueAsDecimal(); Decimal step = GetStep(); if (step == kStepAny) { step = GetDefaultStep(); } MOZ_ASSERT(value.isFinite() && step.isFinite()); SetValueOfRangeForUserEvent(wheelEvent->mDeltaY < 0 ? value + step : value - step); FireChangeEventIfNeeded(); aVisitor.mEvent->PreventDefault(); } } break; } #endif default: break; } if (outerActivateEvent) { if (mForm && (oldType == NS_FORM_INPUT_SUBMIT || oldType == NS_FORM_INPUT_IMAGE)) { if (mType != NS_FORM_INPUT_SUBMIT && mType != NS_FORM_INPUT_IMAGE) { // If the type has changed to a non-submit type, then we want to // flush the stored submission if there is one (as if the submit() // was allowed to succeed) mForm->FlushPendingSubmission(); } } switch(mType) { case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_IMAGE: if (mForm) { InternalFormEvent event(true, (mType == NS_FORM_INPUT_RESET) ? eFormReset : eFormSubmit); event.mOriginator = this; nsEventStatus status = nsEventStatus_eIgnore; nsCOMPtr presShell = aVisitor.mPresContext->GetPresShell(); // If |nsIPresShell::Destroy| has been called due to // handling the event the pres context will return a null // pres shell. See bug 125624. // TODO: removing this code and have the submit event sent by the // form, see bug 592124. if (presShell && (event.mMessage != eFormSubmit || mForm->SubmissionCanProceed(this))) { // Hold a strong ref while dispatching RefPtr form(mForm); presShell->HandleDOMEventWithTarget(form, &event, &status); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } break; default: break; } //switch } //click or outer activate event } else if (outerActivateEvent && (oldType == NS_FORM_INPUT_SUBMIT || oldType == NS_FORM_INPUT_IMAGE) && mForm) { // tell the form to flush a possible pending submission. // the reason is that the script returned false (the event was // not ignored) so if there is a stored submission, it needs to // be submitted immediately. mForm->FlushPendingSubmission(); } } // if if (NS_SUCCEEDED(rv) && mType == NS_FORM_INPUT_RANGE) { PostHandleEventForRangeThumb(aVisitor); } return MaybeInitPickers(aVisitor); } void HTMLInputElement::PostHandleEventForRangeThumb(EventChainPostVisitor& aVisitor) { MOZ_ASSERT(mType == NS_FORM_INPUT_RANGE); if (nsEventStatus_eConsumeNoDefault == aVisitor.mEventStatus || !(aVisitor.mEvent->mClass == eMouseEventClass || aVisitor.mEvent->mClass == eTouchEventClass || aVisitor.mEvent->mClass == eKeyboardEventClass)) { return; } nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); if (!rangeFrame && mIsDraggingRange) { CancelRangeThumbDrag(); return; } switch (aVisitor.mEvent->mMessage) { case eMouseDown: case eTouchStart: { if (mIsDraggingRange) { break; } if (nsIPresShell::GetCapturingContent()) { break; // don't start drag if someone else is already capturing } WidgetInputEvent* inputEvent = aVisitor.mEvent->AsInputEvent(); if (IgnoreInputEventWithModifier(inputEvent)) { break; // ignore } if (aVisitor.mEvent->mMessage == eMouseDown) { if (aVisitor.mEvent->AsMouseEvent()->buttons == WidgetMouseEvent::eLeftButtonFlag) { StartRangeThumbDrag(inputEvent); } else if (mIsDraggingRange) { CancelRangeThumbDrag(); } } else { if (aVisitor.mEvent->AsTouchEvent()->mTouches.Length() == 1) { StartRangeThumbDrag(inputEvent); } else if (mIsDraggingRange) { CancelRangeThumbDrag(); } } aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; } break; case eMouseMove: case eTouchMove: if (!mIsDraggingRange) { break; } if (nsIPresShell::GetCapturingContent() != this) { // Someone else grabbed capture. CancelRangeThumbDrag(); break; } SetValueOfRangeForUserEvent( rangeFrame->GetValueAtEventPoint(aVisitor.mEvent->AsInputEvent())); aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; break; case eMouseUp: case eTouchEnd: if (!mIsDraggingRange) { break; } // We don't check to see whether we are the capturing content here and // call CancelRangeThumbDrag() if that is the case. We just finish off // the drag and set our final value (unless someone has called // preventDefault() and prevents us getting here). FinishRangeThumbDrag(aVisitor.mEvent->AsInputEvent()); aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; break; case eKeyPress: if (mIsDraggingRange && aVisitor.mEvent->AsKeyboardEvent()->mKeyCode == NS_VK_ESCAPE) { CancelRangeThumbDrag(); } break; case eTouchCancel: if (mIsDraggingRange) { CancelRangeThumbDrag(); } break; default: break; } } void HTMLInputElement::MaybeLoadImage() { // Our base URI may have changed; claim that our URI changed, and the // nsImageLoadingContent will decide whether a new image load is warranted. nsAutoString uri; if (mType == NS_FORM_INPUT_IMAGE && GetAttr(kNameSpaceID_None, nsGkAtoms::src, uri) && (NS_FAILED(LoadImage(uri, false, true, eImageLoadType_Normal)) || !LoadingEnabled())) { CancelImageRequests(true); } } nsresult HTMLInputElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent, nsIContent* aBindingParent, bool aCompileEventHandlers) { nsresult rv = nsGenericHTMLFormElementWithState::BindToTree(aDocument, aParent, aBindingParent, aCompileEventHandlers); NS_ENSURE_SUCCESS(rv, rv); nsImageLoadingContent::BindToTree(aDocument, aParent, aBindingParent, aCompileEventHandlers); if (mType == NS_FORM_INPUT_IMAGE) { // Our base URI may have changed; claim that our URI changed, and the // nsImageLoadingContent will decide whether a new image load is warranted. if (HasAttr(kNameSpaceID_None, nsGkAtoms::src)) { // FIXME: Bug 660963 it would be nice if we could just have // ClearBrokenState update our state and do it fast... ClearBrokenState(); RemoveStatesSilently(NS_EVENT_STATE_BROKEN); nsContentUtils::AddScriptRunner( NewRunnableMethod(this, &HTMLInputElement::MaybeLoadImage)); } } // Add radio to document if we don't have a form already (if we do it's // already been added into that group) if (aDocument && !mForm && mType == NS_FORM_INPUT_RADIO) { AddedToRadioGroup(); } // Set direction based on value if dir=auto SetDirectionIfAuto(HasDirAuto(), false); // An element can't suffer from value missing if it is not in a document. // We have to check if we suffer from that as we are now in a document. UpdateValueMissingValidityState(); // If there is a disabled fieldset in the parent chain, the element is now // barred from constraint validation and can't suffer from value missing // (call done before). UpdateBarredFromConstraintValidation(); // And now make sure our state is up to date UpdateState(false); if (mType == NS_FORM_INPUT_PASSWORD) { if (IsInComposedDoc()) { AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(this, NS_LITERAL_STRING("DOMInputPasswordAdded"), true, true); dispatcher->PostDOMEvent(); } #ifdef EARLY_BETA_OR_EARLIER Telemetry::Accumulate(Telemetry::PWMGR_PASSWORD_INPUT_IN_FORM, !!mForm); #endif } return rv; } void HTMLInputElement::UnbindFromTree(bool aDeep, bool aNullParent) { // If we have a form and are unbound from it, // nsGenericHTMLFormElementWithState::UnbindFromTree() will unset the form and // that takes care of form's WillRemove so we just have to take care // of the case where we're removing from the document and we don't // have a form if (!mForm && mType == NS_FORM_INPUT_RADIO) { WillRemoveFromRadioGroup(); } nsImageLoadingContent::UnbindFromTree(aDeep, aNullParent); nsGenericHTMLFormElementWithState::UnbindFromTree(aDeep, aNullParent); // GetCurrentDoc is returning nullptr so we can update the value // missing validity state to reflect we are no longer into a doc. UpdateValueMissingValidityState(); // We might be no longer disabled because of parent chain changed. UpdateBarredFromConstraintValidation(); // And now make sure our state is up to date UpdateState(false); } void HTMLInputElement::HandleTypeChange(uint8_t aNewType) { if (mType == NS_FORM_INPUT_RANGE && mIsDraggingRange) { CancelRangeThumbDrag(false); } ValueModeType aOldValueMode = GetValueMode(); uint8_t oldType = mType; nsAutoString aOldValue; if (aOldValueMode == VALUE_MODE_VALUE) { GetValue(aOldValue); } nsTextEditorState::SelectionProperties sp; if (GetEditorState()) { sp = mInputData.mState->GetSelectionProperties(); } // We already have a copy of the value, lets free it and changes the type. FreeData(); mType = aNewType; if (IsSingleLineTextControl()) { mInputData.mState = new nsTextEditorState(this); if (!sp.IsDefault()) { mInputData.mState->SetSelectionProperties(sp); } } /** * The following code is trying to reproduce the algorithm described here: * http://www.whatwg.org/specs/web-apps/current-work/complete.html#input-type-change */ switch (GetValueMode()) { case VALUE_MODE_DEFAULT: case VALUE_MODE_DEFAULT_ON: // If the previous value mode was value, we need to set the value content // attribute to the previous value. // There is no value sanitizing algorithm for elements in this mode. if (aOldValueMode == VALUE_MODE_VALUE && !aOldValue.IsEmpty()) { SetAttr(kNameSpaceID_None, nsGkAtoms::value, aOldValue, true); } break; case VALUE_MODE_VALUE: // If the previous value mode wasn't value, we have to set the value to // the value content attribute. // SetValueInternal is going to sanitize the value. { nsAutoString value; if (aOldValueMode != VALUE_MODE_VALUE) { GetAttr(kNameSpaceID_None, nsGkAtoms::value, value); } else { value = aOldValue; } // TODO: What should we do if SetValueInternal fails? (The allocation // may potentially be big, but most likely we've failed to allocate // before the type change.) SetValueInternal(value, nsTextEditorState::eSetValue_Internal); } break; case VALUE_MODE_FILENAME: default: // We don't care about the value. // There is no value sanitizing algorithm for elements in this mode. break; } // Updating mFocusedValue in consequence: // If the new type fires a change event on blur, but the previous type // doesn't, we should set mFocusedValue to the current value. // Otherwise, if the new type doesn't fire a change event on blur, but the // previous type does, we should clear out mFocusedValue. if (MayFireChangeOnBlur(mType) && !MayFireChangeOnBlur(oldType)) { GetValue(mFocusedValue); } else if (!IsSingleLineTextControl(false, mType) && IsSingleLineTextControl(false, oldType)) { mFocusedValue.Truncate(); } UpdateHasRange(); // Do not notify, it will be done after if needed. UpdateAllValidityStates(false); UpdateApzAwareFlag(); } void HTMLInputElement::SanitizeValue(nsAString& aValue) { NS_ASSERTION(mDoneCreating, "The element creation should be finished!"); switch (mType) { case NS_FORM_INPUT_TEXT: case NS_FORM_INPUT_SEARCH: case NS_FORM_INPUT_TEL: case NS_FORM_INPUT_PASSWORD: { char16_t crlf[] = { char16_t('\r'), char16_t('\n'), 0 }; aValue.StripChars(crlf); } break; case NS_FORM_INPUT_EMAIL: case NS_FORM_INPUT_URL: { char16_t crlf[] = { char16_t('\r'), char16_t('\n'), 0 }; aValue.StripChars(crlf); aValue = nsContentUtils::TrimWhitespace(aValue); } break; case NS_FORM_INPUT_NUMBER: { Decimal value; bool ok = ConvertStringToNumber(aValue, value); if (!ok) { aValue.Truncate(); } } break; case NS_FORM_INPUT_RANGE: { Decimal minimum = GetMinimum(); Decimal maximum = GetMaximum(); MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), "type=range should have a default maximum/minimum"); // We use this to avoid modifying the string unnecessarily, since that // may introduce rounding. This is set to true only if the value we // parse out from aValue needs to be sanitized. bool needSanitization = false; Decimal value; bool ok = ConvertStringToNumber(aValue, value); if (!ok) { needSanitization = true; // Set value to midway between minimum and maximum. value = maximum <= minimum ? minimum : minimum + (maximum - minimum)/Decimal(2); } else if (value < minimum || maximum < minimum) { needSanitization = true; value = minimum; } else if (value > maximum) { needSanitization = true; value = maximum; } Decimal step = GetStep(); if (step != kStepAny) { Decimal stepBase = GetStepBase(); // There could be rounding issues below when dealing with fractional // numbers, but let's ignore that until ECMAScript supplies us with a // decimal number type. Decimal deltaToStep = NS_floorModulo(value - stepBase, step); if (deltaToStep != Decimal(0)) { // "suffering from a step mismatch" // Round the element's value to the nearest number for which the // element would not suffer from a step mismatch, and which is // greater than or equal to the minimum, and, if the maximum is not // less than the minimum, which is less than or equal to the // maximum, if there is a number that matches these constraints: MOZ_ASSERT(deltaToStep > Decimal(0), "stepBelow/stepAbove will be wrong"); Decimal stepBelow = value - deltaToStep; Decimal stepAbove = value - deltaToStep + step; Decimal halfStep = step / Decimal(2); bool stepAboveIsClosest = (stepAbove - value) <= halfStep; bool stepAboveInRange = stepAbove >= minimum && stepAbove <= maximum; bool stepBelowInRange = stepBelow >= minimum && stepBelow <= maximum; if ((stepAboveIsClosest || !stepBelowInRange) && stepAboveInRange) { needSanitization = true; value = stepAbove; } else if ((!stepAboveIsClosest || !stepAboveInRange) && stepBelowInRange) { needSanitization = true; value = stepBelow; } } } if (needSanitization) { char buf[32]; DebugOnly ok = value.toString(buf, ArrayLength(buf)); aValue.AssignASCII(buf); MOZ_ASSERT(ok, "buf not big enough"); } } break; case NS_FORM_INPUT_DATE: { if (!aValue.IsEmpty() && !IsValidDate(aValue)) { aValue.Truncate(); } } break; case NS_FORM_INPUT_TIME: { if (!aValue.IsEmpty() && !IsValidTime(aValue)) { aValue.Truncate(); } } break; case NS_FORM_INPUT_MONTH: { if (!aValue.IsEmpty() && !IsValidMonth(aValue)) { aValue.Truncate(); } } break; case NS_FORM_INPUT_WEEK: { if (!aValue.IsEmpty() && !IsValidWeek(aValue)) { aValue.Truncate(); } } break; case NS_FORM_INPUT_DATETIME_LOCAL: { if (!aValue.IsEmpty() && !IsValidDateTimeLocal(aValue)) { aValue.Truncate(); } else { NormalizeDateTimeLocal(aValue); } } break; case NS_FORM_INPUT_COLOR: { if (IsValidSimpleColor(aValue)) { ToLowerCase(aValue); } else { // Set default (black) color, if aValue wasn't parsed correctly. aValue.AssignLiteral("#000000"); } } break; } } bool HTMLInputElement::IsValidSimpleColor(const nsAString& aValue) const { if (aValue.Length() != 7 || aValue.First() != '#') { return false; } for (int i = 1; i < 7; ++i) { if (!nsCRT::IsAsciiDigit(aValue[i]) && !(aValue[i] >= 'a' && aValue[i] <= 'f') && !(aValue[i] >= 'A' && aValue[i] <= 'F')) { return false; } } return true; } bool HTMLInputElement::IsLeapYear(uint32_t aYear) const { if ((aYear % 4 == 0 && aYear % 100 != 0) || ( aYear % 400 == 0)) { return true; } return false; } uint32_t HTMLInputElement::DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay, bool isoWeek) const { // Tomohiko Sakamoto algorithm. int monthTable[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; aYear -= aMonth < 3; uint32_t day = (aYear + aYear / 4 - aYear / 100 + aYear / 400 + monthTable[aMonth - 1] + aDay) % 7; if (isoWeek) { return ((day + 6) % 7) + 1; } return day; } uint32_t HTMLInputElement::MaximumWeekInYear(uint32_t aYear) const { int day = DayOfWeek(aYear, 1, 1, true); // January 1. // A year starting on Thursday or a leap year starting on Wednesday has 53 // weeks. All other years have 52 weeks. return day == 4 || (day == 3 && IsLeapYear(aYear)) ? kMaximumWeekInYear : kMaximumWeekInYear - 1; } bool HTMLInputElement::GetTimeFromMs(double aValue, uint16_t* aHours, uint16_t* aMinutes, uint16_t* aSeconds, uint16_t* aMilliseconds) const { MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay, "aValue must be milliseconds within a day!"); uint32_t value = floor(aValue); *aMilliseconds = value % 1000; value /= 1000; *aSeconds = value % 60; value /= 60; *aMinutes = value % 60; value /= 60; *aHours = value; return true; } bool HTMLInputElement::IsValidWeek(const nsAString& aValue) const { uint32_t year, week; return ParseWeek(aValue, &year, &week); } bool HTMLInputElement::IsValidMonth(const nsAString& aValue) const { uint32_t year, month; return ParseMonth(aValue, &year, &month); } bool HTMLInputElement::IsValidDate(const nsAString& aValue) const { uint32_t year, month, day; return ParseDate(aValue, &year, &month, &day); } bool HTMLInputElement::IsValidDateTimeLocal(const nsAString& aValue) const { uint32_t year, month, day, time; return ParseDateTimeLocal(aValue, &year, &month, &day, &time); } bool HTMLInputElement::ParseYear(const nsAString& aValue, uint32_t* aYear) const { if (aValue.Length() < 4) { return false; } return DigitSubStringToNumber(aValue, 0, aValue.Length(), aYear) && *aYear > 0; } bool HTMLInputElement::ParseMonth(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth) const { // Parse the year, month values out a string formatted as 'yyyy-mm'. if (aValue.Length() < 7) { return false; } uint32_t endOfYearOffset = aValue.Length() - 3; if (aValue[endOfYearOffset] != '-') { return false; } const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset); if (!ParseYear(yearStr, aYear)) { return false; } return DigitSubStringToNumber(aValue, endOfYearOffset + 1, 2, aMonth) && *aMonth > 0 && *aMonth <= 12; } bool HTMLInputElement::ParseWeek(const nsAString& aValue, uint32_t* aYear, uint32_t* aWeek) const { // Parse the year, month values out a string formatted as 'yyyy-Www'. if (aValue.Length() < 8) { return false; } uint32_t endOfYearOffset = aValue.Length() - 4; if (aValue[endOfYearOffset] != '-') { return false; } if (aValue[endOfYearOffset + 1] != 'W') { return false; } const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset); if (!ParseYear(yearStr, aYear)) { return false; } return DigitSubStringToNumber(aValue, endOfYearOffset + 2, 2, aWeek) && *aWeek > 0 && *aWeek <= MaximumWeekInYear(*aYear); } bool HTMLInputElement::ParseDate(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth, uint32_t* aDay) const { /* * Parse the year, month, day values out a date string formatted as 'yyyy-mm-dd'. * -The year must be 4 or more digits long, and year > 0 * -The month must be exactly 2 digits long, and 01 <= month <= 12 * -The day must be exactly 2 digit long, and 01 <= day <= maxday * Where maxday is the number of days in the month 'month' and year 'year' */ if (aValue.Length() < 10) { return false; } uint32_t endOfMonthOffset = aValue.Length() - 3; if (aValue[endOfMonthOffset] != '-') { return false; } const nsAString& yearMonthStr = Substring(aValue, 0, endOfMonthOffset); if (!ParseMonth(yearMonthStr, aYear, aMonth)) { return false; } return DigitSubStringToNumber(aValue, endOfMonthOffset + 1, 2, aDay) && *aDay > 0 && *aDay <= NumberOfDaysInMonth(*aMonth, *aYear); } bool HTMLInputElement::ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth, uint32_t* aDay, uint32_t* aTime) const { // Parse the year, month, day and time values out a string formatted as // 'yyyy-mm-ddThh:mm[:ss.s] or 'yyyy-mm-dd hh:mm[:ss.s]', where fractions of // seconds can be 1 to 3 digits. // The minimum length allowed is 16, which is of the form 'yyyy-mm-ddThh:mm' // or 'yyyy-mm-dd hh:mm'. if (aValue.Length() < 16) { return false; } const uint32_t sepIndex = 10; if (aValue[sepIndex] != 'T' && aValue[sepIndex] != ' ') { return false; } const nsAString& dateStr = Substring(aValue, 0, sepIndex); if (!ParseDate(dateStr, aYear, aMonth, aDay)) { return false; } const nsAString& timeStr = Substring(aValue, sepIndex + 1, aValue.Length() - sepIndex + 1); if (!ParseTime(timeStr, aTime)) { return false; } return true; } void HTMLInputElement::NormalizeDateTimeLocal(nsAString& aValue) const { if (aValue.IsEmpty()) { return; } // Use 'T' as the separator between date string and time string. const uint32_t sepIndex = 10; if (aValue[sepIndex] == ' ') { aValue.Replace(sepIndex, 1, NS_LITERAL_STRING("T")); } // Time expressed as the shortest possible string. if (aValue.Length() == 16) { return; } // Fractions of seconds part is optional, ommit it if it's 0. if (aValue.Length() > 19) { uint32_t milliseconds; if (!DigitSubStringToNumber(aValue, 20, aValue.Length() - 20, &milliseconds)) { return; } if (milliseconds != 0) { return; } aValue.Cut(19, aValue.Length() - 19); } // Seconds part is optional, ommit it if it's 0. uint32_t seconds; if (!DigitSubStringToNumber(aValue, 17, aValue.Length() - 17, &seconds)) { return; } if (seconds != 0) { return; } aValue.Cut(16, aValue.Length() - 16); } double HTMLInputElement::DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const { double days = JS::DayFromYear(aYear) + (aWeek - 1) * 7; uint32_t dayOneIsoWeekday = DayOfWeek(aYear, 1, 1, true); // If day one of that year is on/before Thursday, we should subtract the // days that belong to last year in our first week, otherwise, our first // days belong to last year's last week, and we should add those days // back. if (dayOneIsoWeekday <= 4) { days -= (dayOneIsoWeekday - 1); } else { days += (7 - dayOneIsoWeekday + 1); } return days; } uint32_t HTMLInputElement::NumberOfDaysInMonth(uint32_t aMonth, uint32_t aYear) const { /* * Returns the number of days in a month. * Months that are |longMonths| always have 31 days. * Months that are not |longMonths| have 30 days except February (month 2). * February has 29 days during leap years which are years that are divisible by 400. * or divisible by 100 and 4. February has 28 days otherwise. */ static const bool longMonths[] = { true, false, true, false, true, false, true, true, false, true, false, true }; MOZ_ASSERT(aMonth <= 12 && aMonth > 0); if (longMonths[aMonth-1]) { return 31; } if (aMonth != 2) { return 30; } return IsLeapYear(aYear) ? 29 : 28; } /* static */ bool HTMLInputElement::DigitSubStringToNumber(const nsAString& aStr, uint32_t aStart, uint32_t aLen, uint32_t* aRetVal) { MOZ_ASSERT(aStr.Length() > (aStart + aLen - 1)); for (uint32_t offset = 0; offset < aLen; ++offset) { if (!NS_IsAsciiDigit(aStr[aStart + offset])) { return false; } } nsresult ec; *aRetVal = static_cast(PromiseFlatString(Substring(aStr, aStart, aLen)).ToInteger(&ec)); return NS_SUCCEEDED(ec); } bool HTMLInputElement::IsValidTime(const nsAString& aValue) const { return ParseTime(aValue, nullptr); } /* static */ bool HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult) { /* The string must have the following parts: * - HOURS: two digits, value being in [0, 23]; * - Colon (:); * - MINUTES: two digits, value being in [0, 59]; * - Optional: * - Colon (:); * - SECONDS: two digits, value being in [0, 59]; * - Optional: * - DOT (.); * - FRACTIONAL SECONDS: one to three digits, no value range. */ // The following format is the shorter one allowed: "HH:MM". if (aValue.Length() < 5) { return false; } uint32_t hours; if (!DigitSubStringToNumber(aValue, 0, 2, &hours) || hours > 23) { return false; } // Hours/minutes separator. if (aValue[2] != ':') { return false; } uint32_t minutes; if (!DigitSubStringToNumber(aValue, 3, 2, &minutes) || minutes > 59) { return false; } if (aValue.Length() == 5) { if (aResult) { *aResult = ((hours * 60) + minutes) * 60000; } return true; } // The following format is the next shorter one: "HH:MM:SS". if (aValue.Length() < 8 || aValue[5] != ':') { return false; } uint32_t seconds; if (!DigitSubStringToNumber(aValue, 6, 2, &seconds) || seconds > 59) { return false; } if (aValue.Length() == 8) { if (aResult) { *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000; } return true; } // The string must follow this format now: "HH:MM:SS.{s,ss,sss}". // There can be 1 to 3 digits for the fractions of seconds. if (aValue.Length() == 9 || aValue.Length() > 12 || aValue[8] != '.') { return false; } uint32_t fractionsSeconds; if (!DigitSubStringToNumber(aValue, 9, aValue.Length() - 9, &fractionsSeconds)) { return false; } if (aResult) { *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000 + // NOTE: there is 10.0 instead of 10 and static_cast because // some old [and stupid] compilers can't just do the right thing. fractionsSeconds * pow(10.0, static_cast(3 - (aValue.Length() - 9))); } return true; } /* static */ bool HTMLInputElement::IsDateTimeTypeSupported(uint8_t aDateTimeInputType) { return (aDateTimeInputType == NS_FORM_INPUT_DATE && (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled() || IsDatePickerEnabled())) || (aDateTimeInputType == NS_FORM_INPUT_TIME && (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled())) || ((aDateTimeInputType == NS_FORM_INPUT_MONTH || aDateTimeInputType == NS_FORM_INPUT_WEEK || aDateTimeInputType == NS_FORM_INPUT_DATETIME_LOCAL) && IsInputDateTimeEnabled()); } /* static */ bool HTMLInputElement::IsWebkitDirPickerEnabled() { static bool sWebkitDirPickerEnabled = false; static bool sWebkitDirPickerPrefCached = false; if (!sWebkitDirPickerPrefCached) { sWebkitDirPickerPrefCached = true; Preferences::AddBoolVarCache(&sWebkitDirPickerEnabled, "dom.webkitBlink.dirPicker.enabled", false); } return sWebkitDirPickerEnabled; } /* static */ bool HTMLInputElement::IsWebkitFileSystemEnabled() { static bool sWebkitFileSystemEnabled = false; static bool sWebkitFileSystemPrefCached = false; if (!sWebkitFileSystemPrefCached) { sWebkitFileSystemPrefCached = true; Preferences::AddBoolVarCache(&sWebkitFileSystemEnabled, "dom.webkitBlink.filesystem.enabled", false); } return sWebkitFileSystemEnabled; } /* static */ bool HTMLInputElement::IsDirPickerEnabled() { static bool sDirPickerEnabled = false; static bool sDirPickerPrefCached = false; if (!sDirPickerPrefCached) { sDirPickerPrefCached = true; Preferences::AddBoolVarCache(&sDirPickerEnabled, "dom.input.dirpicker", false); } return sDirPickerEnabled; } /* static */ bool HTMLInputElement::IsDatePickerEnabled() { static bool sDatePickerEnabled = false; static bool sDatePickerPrefCached = false; if (!sDatePickerPrefCached) { sDatePickerPrefCached = true; Preferences::AddBoolVarCache(&sDatePickerEnabled, "dom.forms.datepicker", false); } return sDatePickerEnabled; } /* static */ bool HTMLInputElement::IsExperimentalFormsEnabled() { static bool sExperimentalFormsEnabled = false; static bool sExperimentalFormsPrefCached = false; if (!sExperimentalFormsPrefCached) { sExperimentalFormsPrefCached = true; Preferences::AddBoolVarCache(&sExperimentalFormsEnabled, "dom.experimental_forms", false); } return sExperimentalFormsEnabled; } /* static */ bool HTMLInputElement::IsInputDateTimeEnabled() { static bool sDateTimeEnabled = false; static bool sDateTimePrefCached = false; if (!sDateTimePrefCached) { sDateTimePrefCached = true; Preferences::AddBoolVarCache(&sDateTimeEnabled, "dom.forms.datetime", false); } return sDateTimeEnabled; } /* static */ bool HTMLInputElement::IsInputNumberEnabled() { static bool sInputNumberEnabled = false; static bool sInputNumberPrefCached = false; if (!sInputNumberPrefCached) { sInputNumberPrefCached = true; Preferences::AddBoolVarCache(&sInputNumberEnabled, "dom.forms.number", false); } return sInputNumberEnabled; } /* static */ bool HTMLInputElement::IsInputColorEnabled() { static bool sInputColorEnabled = false; static bool sInputColorPrefCached = false; if (!sInputColorPrefCached) { sInputColorPrefCached = true; Preferences::AddBoolVarCache(&sInputColorEnabled, "dom.forms.color", false); } return sInputColorEnabled; } bool HTMLInputElement::ParseAttribute(int32_t aNamespaceID, nsIAtom* aAttribute, const nsAString& aValue, nsAttrValue& aResult) { if (aNamespaceID == kNameSpaceID_None) { if (aAttribute == nsGkAtoms::type) { // XXX ARG!! This is major evilness. ParseAttribute // shouldn't set members. Override SetAttr instead int32_t newType; bool success = aResult.ParseEnumValue(aValue, kInputTypeTable, false); if (success) { newType = aResult.GetEnumValue(); if ((IsExperimentalMobileType(newType) && !IsExperimentalFormsEnabled()) || (newType == NS_FORM_INPUT_NUMBER && !IsInputNumberEnabled()) || (newType == NS_FORM_INPUT_COLOR && !IsInputColorEnabled()) || (IsDateTimeInputType(newType) && !IsDateTimeTypeSupported(newType))) { newType = kInputDefaultType->value; aResult.SetTo(newType, &aValue); } } else { newType = kInputDefaultType->value; } if (newType != mType) { // Make sure to do the check for newType being NS_FORM_INPUT_FILE and // the corresponding SetValueInternal() call _before_ we set mType. // That way the logic in SetValueInternal() will work right (that logic // makes assumptions about our frame based on mType, but we won't have // had time to recreate frames yet -- that happens later in the // SetAttr() process). if (newType == NS_FORM_INPUT_FILE || mType == NS_FORM_INPUT_FILE) { // This call isn't strictly needed any more since we'll never // confuse values and filenames. However it's there for backwards // compat. ClearFiles(false); } HandleTypeChange(newType); } return success; } if (aAttribute == nsGkAtoms::width) { return aResult.ParseSpecialIntValue(aValue); } if (aAttribute == nsGkAtoms::height) { return aResult.ParseSpecialIntValue(aValue); } if (aAttribute == nsGkAtoms::maxlength) { return aResult.ParseNonNegativeIntValue(aValue); } if (aAttribute == nsGkAtoms::minlength) { return aResult.ParseNonNegativeIntValue(aValue); } if (aAttribute == nsGkAtoms::size) { return aResult.ParsePositiveIntValue(aValue); } if (aAttribute == nsGkAtoms::border) { return aResult.ParseIntWithBounds(aValue, 0); } if (aAttribute == nsGkAtoms::align) { return ParseAlignValue(aValue, aResult); } if (aAttribute == nsGkAtoms::formmethod) { return aResult.ParseEnumValue(aValue, kFormMethodTable, false); } if (aAttribute == nsGkAtoms::formenctype) { return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false); } if (aAttribute == nsGkAtoms::autocomplete) { aResult.ParseAtomArray(aValue); return true; } if (aAttribute == nsGkAtoms::inputmode) { return aResult.ParseEnumValue(aValue, kInputInputmodeTable, false); } if (ParseImageAttribute(aAttribute, aValue, aResult)) { // We have to call |ParseImageAttribute| unconditionally since we // don't know if we're going to have a type="image" attribute yet, // (or could have it set dynamically in the future). See bug // 214077. return true; } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, aResult); } void HTMLInputElement::MapAttributesIntoRule(const nsMappedAttributes* aAttributes, nsRuleData* aData) { const nsAttrValue* value = aAttributes->GetAttr(nsGkAtoms::type); if (value && value->Type() == nsAttrValue::eEnum && value->GetEnumValue() == NS_FORM_INPUT_IMAGE) { nsGenericHTMLFormElementWithState::MapImageBorderAttributeInto(aAttributes, aData); nsGenericHTMLFormElementWithState::MapImageMarginAttributeInto(aAttributes, aData); nsGenericHTMLFormElementWithState::MapImageSizeAttributesInto(aAttributes, aData); // Images treat align as "float" nsGenericHTMLFormElementWithState::MapImageAlignAttributeInto(aAttributes, aData); } nsGenericHTMLFormElementWithState::MapCommonAttributesInto(aAttributes, aData); } nsChangeHint HTMLInputElement::GetAttributeChangeHint(const nsIAtom* aAttribute, int32_t aModType) const { nsChangeHint retval = nsGenericHTMLFormElementWithState::GetAttributeChangeHint(aAttribute, aModType); if (aAttribute == nsGkAtoms::type || // The presence or absence of the 'directory' attribute determines what // buttons we show for type=file. aAttribute == nsGkAtoms::allowdirs || aAttribute == nsGkAtoms::webkitdirectory) { retval |= nsChangeHint_ReconstructFrame; } else if (mType == NS_FORM_INPUT_IMAGE && (aAttribute == nsGkAtoms::alt || aAttribute == nsGkAtoms::value)) { // We might need to rebuild our alt text. Just go ahead and // reconstruct our frame. This should be quite rare.. retval |= nsChangeHint_ReconstructFrame; } else if (aAttribute == nsGkAtoms::value) { retval |= NS_STYLE_HINT_REFLOW; } else if (aAttribute == nsGkAtoms::size && IsSingleLineTextControl(false)) { retval |= NS_STYLE_HINT_REFLOW; } else if (PlaceholderApplies() && aAttribute == nsGkAtoms::placeholder) { retval |= nsChangeHint_ReconstructFrame; } return retval; } NS_IMETHODIMP_(bool) HTMLInputElement::IsAttributeMapped(const nsIAtom* aAttribute) const { static const MappedAttributeEntry attributes[] = { { &nsGkAtoms::align }, { &nsGkAtoms::type }, { nullptr }, }; static const MappedAttributeEntry* const map[] = { attributes, sCommonAttributeMap, sImageMarginSizeAttributeMap, sImageBorderAttributeMap, }; return FindAttributeDependence(aAttribute, map); } nsMapRuleToAttributesFunc HTMLInputElement::GetAttributeMappingFunction() const { return &MapAttributesIntoRule; } // Directory picking methods: bool HTMLInputElement::IsFilesAndDirectoriesSupported() const { // This method is supposed to return true if a file and directory picker // supports the selection of both files and directories *at the same time*. // Only Mac currently supports that. We could implement it for Mac, but // currently we do not. return false; } void HTMLInputElement::ChooseDirectory(ErrorResult& aRv) { if (mType != NS_FORM_INPUT_FILE) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } // Script can call this method directly, so even though we don't show the // "Pick Folder..." button on platforms that don't have a directory picker // we have to redirect to the file picker here. InitFilePicker( #if defined(ANDROID) || defined(MOZ_B2G) // No native directory picker - redirect to plain file picker FILE_PICKER_FILE #else FILE_PICKER_DIRECTORY #endif ); } already_AddRefed HTMLInputElement::GetFilesAndDirectories(ErrorResult& aRv) { if (mType != NS_FORM_INPUT_FILE) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } nsCOMPtr global = OwnerDoc()->GetScopeObject(); MOZ_ASSERT(global); if (!global) { return nullptr; } RefPtr p = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } const nsTArray& filesAndDirs = GetFilesOrDirectoriesInternal(); Sequence filesAndDirsSeq; if (!filesAndDirsSeq.SetLength(filesAndDirs.Length(), mozilla::fallible_t())) { p->MaybeReject(NS_ERROR_OUT_OF_MEMORY); return p.forget(); } for (uint32_t i = 0; i < filesAndDirs.Length(); ++i) { if (filesAndDirs[i].IsDirectory()) { RefPtr directory = filesAndDirs[i].GetAsDirectory(); // In future we could refactor SetFilePickerFiltersFromAccept to return a // semicolon separated list of file extensions and include that in the // filter string passed here. directory->SetContentFilters(NS_LITERAL_STRING("filter-out-sensitive")); filesAndDirsSeq[i].SetAsDirectory() = directory; } else { MOZ_ASSERT(filesAndDirs[i].IsFile()); // This file was directly selected by the user, so don't filter it. filesAndDirsSeq[i].SetAsFile() = filesAndDirs[i].GetAsFile(); } } p->MaybeResolve(filesAndDirsSeq); return p.forget(); } already_AddRefed HTMLInputElement::GetFiles(bool aRecursiveFlag, ErrorResult& aRv) { if (mType != NS_FORM_INPUT_FILE) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } GetFilesHelper* helper = GetOrCreateGetFilesHelper(aRecursiveFlag, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } MOZ_ASSERT(helper); nsCOMPtr global = OwnerDoc()->GetScopeObject(); MOZ_ASSERT(global); if (!global) { return nullptr; } RefPtr p = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } helper->AddPromise(p); return p.forget(); } // Controllers Methods nsIControllers* HTMLInputElement::GetControllers(ErrorResult& aRv) { //XXX: what about type "file"? if (IsSingleLineTextControl(false)) { if (!mControllers) { nsresult rv; mControllers = do_CreateInstance(kXULControllersCID, &rv); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } nsCOMPtr controller(do_CreateInstance("@mozilla.org/editor/editorcontroller;1", &rv)); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } mControllers->AppendController(controller); controller = do_CreateInstance("@mozilla.org/editor/editingcontroller;1", &rv); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } mControllers->AppendController(controller); } } return mControllers; } NS_IMETHODIMP HTMLInputElement::GetControllers(nsIControllers** aResult) { NS_ENSURE_ARG_POINTER(aResult); ErrorResult rv; RefPtr controller = GetControllers(rv); controller.forget(aResult); return rv.StealNSResult(); } int32_t HTMLInputElement::GetTextLength(ErrorResult& aRv) { nsAutoString val; GetValue(val); return val.Length(); } NS_IMETHODIMP HTMLInputElement::GetTextLength(int32_t* aTextLength) { ErrorResult rv; *aTextLength = GetTextLength(rv); return rv.StealNSResult(); } void HTMLInputElement::SetSelectionRange(int32_t aSelectionStart, int32_t aSelectionEnd, const Optional& aDirection, ErrorResult& aRv) { if (!SupportsTextSelection()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } nsresult rv = SetSelectionRange(aSelectionStart, aSelectionEnd, aDirection.WasPassed() ? aDirection.Value() : NullString()); if (NS_FAILED(rv)) { aRv.Throw(rv); } } NS_IMETHODIMP HTMLInputElement::SetSelectionRange(int32_t aSelectionStart, int32_t aSelectionEnd, const nsAString& aDirection) { nsresult rv = NS_OK; nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); nsITextControlFrame* textControlFrame = do_QueryFrame(formControlFrame); if (textControlFrame) { // Default to forward, even if not specified. // Note that we don't currently support directionless selections, so // "none" is treated like "forward". nsITextControlFrame::SelectionDirection dir = nsITextControlFrame::eForward; if (!aDirection.IsEmpty() && aDirection.EqualsLiteral("backward")) { dir = nsITextControlFrame::eBackward; } rv = textControlFrame->SetSelectionRange(aSelectionStart, aSelectionEnd, dir); if (NS_SUCCEEDED(rv)) { rv = textControlFrame->ScrollSelectionIntoView(); RefPtr asyncDispatcher = new AsyncEventDispatcher(this, NS_LITERAL_STRING("select"), true, false); asyncDispatcher->PostDOMEvent(); } } return rv; } void HTMLInputElement::SetRangeText(const nsAString& aReplacement, ErrorResult& aRv) { if (!SupportsTextSelection()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } int32_t start, end; aRv = GetSelectionRange(&start, &end); if (aRv.Failed()) { nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { start = state->GetSelectionProperties().GetStart(); end = state->GetSelectionProperties().GetEnd(); aRv = NS_OK; } } SetRangeText(aReplacement, start, end, mozilla::dom::SelectionMode::Preserve, aRv, start, end); } void HTMLInputElement::SetRangeText(const nsAString& aReplacement, uint32_t aStart, uint32_t aEnd, const SelectionMode& aSelectMode, ErrorResult& aRv, int32_t aSelectionStart, int32_t aSelectionEnd) { if (!SupportsTextSelection()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (aStart > aEnd) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return; } nsAutoString value; GetValueInternal(value); uint32_t inputValueLength = value.Length(); if (aStart > inputValueLength) { aStart = inputValueLength; } if (aEnd > inputValueLength) { aEnd = inputValueLength; } if (aSelectionStart == -1 && aSelectionEnd == -1) { aRv = GetSelectionRange(&aSelectionStart, &aSelectionEnd); if (aRv.Failed()) { nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { aSelectionStart = state->GetSelectionProperties().GetStart(); aSelectionEnd = state->GetSelectionProperties().GetEnd(); aRv = NS_OK; } } } if (aStart <= aEnd) { value.Replace(aStart, aEnd - aStart, aReplacement); nsresult rv = SetValueInternal(value, nsTextEditorState::eSetValue_ByContent); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } } uint32_t newEnd = aStart + aReplacement.Length(); int32_t delta = aReplacement.Length() - (aEnd - aStart); switch (aSelectMode) { case mozilla::dom::SelectionMode::Select: { aSelectionStart = aStart; aSelectionEnd = newEnd; } break; case mozilla::dom::SelectionMode::Start: { aSelectionStart = aSelectionEnd = aStart; } break; case mozilla::dom::SelectionMode::End: { aSelectionStart = aSelectionEnd = newEnd; } break; case mozilla::dom::SelectionMode::Preserve: { if ((uint32_t)aSelectionStart > aEnd) { aSelectionStart += delta; } else if ((uint32_t)aSelectionStart > aStart) { aSelectionStart = aStart; } if ((uint32_t)aSelectionEnd > aEnd) { aSelectionEnd += delta; } else if ((uint32_t)aSelectionEnd > aStart) { aSelectionEnd = newEnd; } } break; default: MOZ_CRASH("Unknown mode!"); } Optional direction; SetSelectionRange(aSelectionStart, aSelectionEnd, direction, aRv); } Nullable HTMLInputElement::GetSelectionStart(ErrorResult& aRv) { if (!SupportsTextSelection()) { return Nullable(); } int32_t selStart; nsresult rv = GetSelectionStart(&selStart); if (NS_FAILED(rv)) { aRv.Throw(rv); } return Nullable(selStart); } NS_IMETHODIMP HTMLInputElement::GetSelectionStart(int32_t* aSelectionStart) { NS_ENSURE_ARG_POINTER(aSelectionStart); int32_t selEnd, selStart; nsresult rv = GetSelectionRange(&selStart, &selEnd); if (NS_FAILED(rv)) { nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { *aSelectionStart = state->GetSelectionProperties().GetStart(); return NS_OK; } return rv; } *aSelectionStart = selStart; return NS_OK; } void HTMLInputElement::SetSelectionStart(const Nullable& aSelectionStart, ErrorResult& aRv) { if (!SupportsTextSelection()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } int32_t selStart = 0; if (!aSelectionStart.IsNull()) { selStart = aSelectionStart.Value(); } nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { state->GetSelectionProperties().SetStart(selStart); return; } nsAutoString direction; aRv = GetSelectionDirection(direction); if (aRv.Failed()) { return; } int32_t start, end; aRv = GetSelectionRange(&start, &end); if (aRv.Failed()) { return; } start = selStart; if (end < start) { end = start; } aRv = SetSelectionRange(start, end, direction); } NS_IMETHODIMP HTMLInputElement::SetSelectionStart(int32_t aSelectionStart) { ErrorResult rv; Nullable selStart(aSelectionStart); SetSelectionStart(selStart, rv); return rv.StealNSResult(); } Nullable HTMLInputElement::GetSelectionEnd(ErrorResult& aRv) { if (!SupportsTextSelection()) { return Nullable(); } int32_t selEnd; nsresult rv = GetSelectionEnd(&selEnd); if (NS_FAILED(rv)) { aRv.Throw(rv); } return Nullable(selEnd); } NS_IMETHODIMP HTMLInputElement::GetSelectionEnd(int32_t* aSelectionEnd) { NS_ENSURE_ARG_POINTER(aSelectionEnd); int32_t selEnd, selStart; nsresult rv = GetSelectionRange(&selStart, &selEnd); if (NS_FAILED(rv)) { nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { *aSelectionEnd = state->GetSelectionProperties().GetEnd(); return NS_OK; } return rv; } *aSelectionEnd = selEnd; return NS_OK; } void HTMLInputElement::SetSelectionEnd(const Nullable& aSelectionEnd, ErrorResult& aRv) { if (!SupportsTextSelection()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } int32_t selEnd = 0; if (!aSelectionEnd.IsNull()) { selEnd = aSelectionEnd.Value(); } nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { state->GetSelectionProperties().SetEnd(selEnd); return; } nsAutoString direction; aRv = GetSelectionDirection(direction); if (aRv.Failed()) { return; } int32_t start, end; aRv = GetSelectionRange(&start, &end); if (aRv.Failed()) { return; } end = selEnd; if (start > end) { start = end; } aRv = SetSelectionRange(start, end, direction); } NS_IMETHODIMP HTMLInputElement::SetSelectionEnd(int32_t aSelectionEnd) { ErrorResult rv; Nullable selEnd(aSelectionEnd); SetSelectionEnd(selEnd, rv); return rv.StealNSResult(); } NS_IMETHODIMP HTMLInputElement::GetFiles(nsIDOMFileList** aFileList) { RefPtr list = GetFiles(); list.forget(aFileList); return NS_OK; } nsresult HTMLInputElement::GetSelectionRange(int32_t* aSelectionStart, int32_t* aSelectionEnd) { nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); nsITextControlFrame* textControlFrame = do_QueryFrame(formControlFrame); if (textControlFrame) { return textControlFrame->GetSelectionRange(aSelectionStart, aSelectionEnd); } return NS_ERROR_FAILURE; } static void DirectionToName(nsITextControlFrame::SelectionDirection dir, nsAString& aDirection) { if (dir == nsITextControlFrame::eNone) { aDirection.AssignLiteral("none"); } else if (dir == nsITextControlFrame::eForward) { aDirection.AssignLiteral("forward"); } else if (dir == nsITextControlFrame::eBackward) { aDirection.AssignLiteral("backward"); } else { NS_NOTREACHED("Invalid SelectionDirection value"); } } void HTMLInputElement::GetSelectionDirection(nsAString& aDirection, ErrorResult& aRv) { if (!SupportsTextSelection()) { aDirection.SetIsVoid(true); return; } nsresult rv = NS_ERROR_FAILURE; nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); nsITextControlFrame* textControlFrame = do_QueryFrame(formControlFrame); if (textControlFrame) { nsITextControlFrame::SelectionDirection dir; rv = textControlFrame->GetSelectionRange(nullptr, nullptr, &dir); if (NS_SUCCEEDED(rv)) { DirectionToName(dir, aDirection); } } if (NS_FAILED(rv)) { nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { DirectionToName(state->GetSelectionProperties().GetDirection(), aDirection); return; } } if (NS_FAILED(rv)) { aRv.Throw(rv); } } NS_IMETHODIMP HTMLInputElement::GetSelectionDirection(nsAString& aDirection) { ErrorResult rv; GetSelectionDirection(aDirection, rv); return rv.StealNSResult(); } void HTMLInputElement::SetSelectionDirection(const nsAString& aDirection, ErrorResult& aRv) { if (!SupportsTextSelection()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } nsTextEditorState* state = GetEditorState(); if (state && state->IsSelectionCached()) { nsITextControlFrame::SelectionDirection dir = nsITextControlFrame::eNone; if (aDirection.EqualsLiteral("forward")) { dir = nsITextControlFrame::eForward; } else if (aDirection.EqualsLiteral("backward")) { dir = nsITextControlFrame::eBackward; } state->GetSelectionProperties().SetDirection(dir); return; } int32_t start, end; aRv = GetSelectionRange(&start, &end); if (!aRv.Failed()) { aRv = SetSelectionRange(start, end, aDirection); } } NS_IMETHODIMP HTMLInputElement::SetSelectionDirection(const nsAString& aDirection) { ErrorResult rv; SetSelectionDirection(aDirection, rv); return rv.StealNSResult(); } NS_IMETHODIMP HTMLInputElement::GetPhonetic(nsAString& aPhonetic) { aPhonetic.Truncate(); nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); nsITextControlFrame* textControlFrame = do_QueryFrame(formControlFrame); if (textControlFrame) { textControlFrame->GetPhonetic(aPhonetic); } return NS_OK; } #ifdef ACCESSIBILITY /*static*/ nsresult FireEventForAccessibility(nsIDOMHTMLInputElement* aTarget, nsPresContext* aPresContext, const nsAString& aEventType) { nsCOMPtr element = do_QueryInterface(aTarget); RefPtr event = NS_NewDOMEvent(element, aPresContext, nullptr); event->InitEvent(aEventType, true, true); event->SetTrusted(true); EventDispatcher::DispatchDOMEvent(aTarget, nullptr, event, aPresContext, nullptr); return NS_OK; } #endif void HTMLInputElement::UpdateApzAwareFlag() { #if !defined(ANDROID) && !defined(XP_MACOSX) if ((mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE)) { SetMayBeApzAware(); } #endif } nsresult HTMLInputElement::SetDefaultValueAsValue() { NS_ASSERTION(GetValueMode() == VALUE_MODE_VALUE, "GetValueMode() should return VALUE_MODE_VALUE!"); // The element has a content attribute value different from it's value when // it's in the value mode value. nsAutoString resetVal; GetDefaultValue(resetVal); // SetValueInternal is going to sanitize the value. return SetValueInternal(resetVal, nsTextEditorState::eSetValue_Internal); } void HTMLInputElement::SetDirectionIfAuto(bool aAuto, bool aNotify) { if (aAuto) { SetHasDirAuto(); if (IsSingleLineTextControl(true)) { nsAutoString value; GetValue(value); SetDirectionalityFromValue(this, value, aNotify); } } else { ClearHasDirAuto(); } } NS_IMETHODIMP HTMLInputElement::Reset() { // We should be able to reset all dirty flags regardless of the type. SetCheckedChanged(false); SetValueChanged(false); mLastValueChangeWasInteractive = false; switch (GetValueMode()) { case VALUE_MODE_VALUE: return SetDefaultValueAsValue(); case VALUE_MODE_DEFAULT_ON: DoSetChecked(DefaultChecked(), true, false); return NS_OK; case VALUE_MODE_FILENAME: ClearFiles(false); return NS_OK; case VALUE_MODE_DEFAULT: default: return NS_OK; } } NS_IMETHODIMP HTMLInputElement::SubmitNamesValues(HTMLFormSubmission* aFormSubmission) { // Disabled elements don't submit // For type=reset, and type=button, we just never submit, period. // For type=image and type=button, we only submit if we were the button // pressed // For type=radio and type=checkbox, we only submit if checked=true if (IsDisabled() || mType == NS_FORM_INPUT_RESET || mType == NS_FORM_INPUT_BUTTON || ((mType == NS_FORM_INPUT_SUBMIT || mType == NS_FORM_INPUT_IMAGE) && aFormSubmission->GetOriginatingElement() != this) || ((mType == NS_FORM_INPUT_RADIO || mType == NS_FORM_INPUT_CHECKBOX) && !mChecked)) { return NS_OK; } // Get the name nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); // Submit .x, .y for input type=image if (mType == NS_FORM_INPUT_IMAGE) { // Get a property set by the frame to find out where it was clicked. nsIntPoint* lastClickedPoint = static_cast(GetProperty(nsGkAtoms::imageClickedPoint)); int32_t x, y; if (lastClickedPoint) { // Convert the values to strings for submission x = lastClickedPoint->x; y = lastClickedPoint->y; } else { x = y = 0; } nsAutoString xVal, yVal; xVal.AppendInt(x); yVal.AppendInt(y); if (!name.IsEmpty()) { aFormSubmission->AddNameValuePair(name + NS_LITERAL_STRING(".x"), xVal); aFormSubmission->AddNameValuePair(name + NS_LITERAL_STRING(".y"), yVal); } else { // If the Image Element has no name, simply return x and y // to Nav and IE compatibility. aFormSubmission->AddNameValuePair(NS_LITERAL_STRING("x"), xVal); aFormSubmission->AddNameValuePair(NS_LITERAL_STRING("y"), yVal); } return NS_OK; } // // Submit name=value // // If name not there, don't submit if (name.IsEmpty()) { return NS_OK; } // Get the value nsAutoString value; nsresult rv = GetValue(value); if (NS_FAILED(rv)) { return rv; } if (mType == NS_FORM_INPUT_SUBMIT && value.IsEmpty() && !HasAttr(kNameSpaceID_None, nsGkAtoms::value)) { // Get our default value, which is the same as our default label nsXPIDLString defaultValue; nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "Submit", defaultValue); value = defaultValue; } // // Submit file if its input type=file and this encoding method accepts files // if (mType == NS_FORM_INPUT_FILE) { // Submit files const nsTArray& files = GetFilesOrDirectoriesInternal(); if (files.IsEmpty()) { aFormSubmission->AddNameBlobOrNullPair(name, nullptr); return NS_OK; } for (uint32_t i = 0; i < files.Length(); ++i) { if (files[i].IsFile()) { aFormSubmission->AddNameBlobOrNullPair(name, files[i].GetAsFile()); } else { MOZ_ASSERT(files[i].IsDirectory()); aFormSubmission->AddNameDirectoryPair(name, files[i].GetAsDirectory()); } } return NS_OK; } if (mType == NS_FORM_INPUT_HIDDEN && name.EqualsLiteral("_charset_")) { nsCString charset; aFormSubmission->GetCharset(charset); return aFormSubmission->AddNameValuePair(name, NS_ConvertASCIItoUTF16(charset)); } if (IsSingleLineTextControl(true) && name.EqualsLiteral("isindex") && aFormSubmission->SupportsIsindexSubmission()) { return aFormSubmission->AddIsindex(value); } return aFormSubmission->AddNameValuePair(name, value); } NS_IMETHODIMP HTMLInputElement::SaveState() { RefPtr inputState; switch (GetValueMode()) { case VALUE_MODE_DEFAULT_ON: if (mCheckedChanged) { inputState = new HTMLInputElementState(); inputState->SetChecked(mChecked); } break; case VALUE_MODE_FILENAME: if (!mFilesOrDirectories.IsEmpty()) { inputState = new HTMLInputElementState(); inputState->SetFilesOrDirectories(mFilesOrDirectories); } break; case VALUE_MODE_VALUE: case VALUE_MODE_DEFAULT: // VALUE_MODE_DEFAULT shouldn't have their value saved except 'hidden', // mType shouldn't be NS_FORM_INPUT_PASSWORD and value should have changed. if ((GetValueMode() == VALUE_MODE_DEFAULT && mType != NS_FORM_INPUT_HIDDEN) || mType == NS_FORM_INPUT_PASSWORD || !mValueChanged) { break; } inputState = new HTMLInputElementState(); nsAutoString value; nsresult rv = GetValue(value); if (NS_FAILED(rv)) { return rv; } if (!IsSingleLineTextControl(false)) { rv = nsLinebreakConverter::ConvertStringLineBreaks( value, nsLinebreakConverter::eLinebreakPlatform, nsLinebreakConverter::eLinebreakContent); if (NS_FAILED(rv)) { NS_ERROR("Converting linebreaks failed!"); return rv; } } inputState->SetValue(value); break; } if (inputState) { nsPresState* state = GetPrimaryPresState(); if (state) { state->SetStateProperty(inputState); } } if (mDisabledChanged) { nsPresState* state = GetPrimaryPresState(); if (state) { // We do not want to save the real disabled state but the disabled // attribute. state->SetDisabled(HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)); } } return NS_OK; } void HTMLInputElement::DoneCreatingElement() { mDoneCreating = true; // // Restore state as needed. Note that disabled state applies to all control // types. // bool restoredCheckedState = !mInhibitRestoration && NS_SUCCEEDED(GenerateStateKey()) && RestoreFormControlState(); // // If restore does not occur, we initialize .checked using the CHECKED // property. // if (!restoredCheckedState && mShouldInitChecked) { DoSetChecked(DefaultChecked(), false, true); DoSetCheckedChanged(false, false); } // Sanitize the value. if (GetValueMode() == VALUE_MODE_VALUE) { nsAutoString aValue; GetValue(aValue); // TODO: What should we do if SetValueInternal fails? (The allocation // may potentially be big, but most likely we've failed to allocate // before the type change.) SetValueInternal(aValue, nsTextEditorState::eSetValue_Internal); } mShouldInitChecked = false; } EventStates HTMLInputElement::IntrinsicState() const { // If you add states here, and they're type-dependent, you need to add them // to the type case in AfterSetAttr. EventStates state = nsGenericHTMLFormElementWithState::IntrinsicState(); if (mType == NS_FORM_INPUT_CHECKBOX || mType == NS_FORM_INPUT_RADIO) { // Check current checked state (:checked) if (mChecked) { state |= NS_EVENT_STATE_CHECKED; } // Check current indeterminate state (:indeterminate) if (mType == NS_FORM_INPUT_CHECKBOX && mIndeterminate) { state |= NS_EVENT_STATE_INDETERMINATE; } if (mType == NS_FORM_INPUT_RADIO) { nsCOMPtr selected = GetSelectedRadioButton(); bool indeterminate = !selected && !mChecked; if (indeterminate) { state |= NS_EVENT_STATE_INDETERMINATE; } } // Check whether we are the default checked element (:default) if (DefaultChecked()) { state |= NS_EVENT_STATE_DEFAULT; } } else if (mType == NS_FORM_INPUT_IMAGE) { state |= nsImageLoadingContent::ImageState(); } if (DoesRequiredApply() && HasAttr(kNameSpaceID_None, nsGkAtoms::required)) { state |= NS_EVENT_STATE_REQUIRED; } else { state |= NS_EVENT_STATE_OPTIONAL; } if (IsCandidateForConstraintValidation()) { if (IsValid()) { state |= NS_EVENT_STATE_VALID; } else { state |= NS_EVENT_STATE_INVALID; if ((!mForm || !mForm->HasAttr(kNameSpaceID_None, nsGkAtoms::novalidate)) && (GetValidityState(VALIDITY_STATE_CUSTOM_ERROR) || (mCanShowInvalidUI && ShouldShowValidityUI()))) { state |= NS_EVENT_STATE_MOZ_UI_INVALID; } } // :-moz-ui-valid applies if all of the following conditions are true: // 1. The element is not focused, or had either :-moz-ui-valid or // :-moz-ui-invalid applying before it was focused ; // 2. The element is either valid or isn't allowed to have // :-moz-ui-invalid applying ; // 3. The element has no form owner or its form owner doesn't have the // novalidate attribute set ; // 4. The element has already been modified or the user tried to submit the // form owner while invalid. if ((!mForm || !mForm->HasAttr(kNameSpaceID_None, nsGkAtoms::novalidate)) && (mCanShowValidUI && ShouldShowValidityUI() && (IsValid() || (!state.HasState(NS_EVENT_STATE_MOZ_UI_INVALID) && !mCanShowInvalidUI)))) { state |= NS_EVENT_STATE_MOZ_UI_VALID; } // :in-range and :out-of-range only apply if the element currently has a range if (mHasRange) { state |= (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) || GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW)) ? NS_EVENT_STATE_OUTOFRANGE : NS_EVENT_STATE_INRANGE; } } if (PlaceholderApplies() && HasAttr(kNameSpaceID_None, nsGkAtoms::placeholder) && IsValueEmpty()) { state |= NS_EVENT_STATE_PLACEHOLDERSHOWN; } if (mForm && !mForm->GetValidity() && IsSubmitControl()) { state |= NS_EVENT_STATE_MOZ_SUBMITINVALID; } return state; } void HTMLInputElement::AddStates(EventStates aStates) { if (mType == NS_FORM_INPUT_TEXT) { EventStates focusStates(aStates & (NS_EVENT_STATE_FOCUS | NS_EVENT_STATE_FOCUSRING)); if (!focusStates.IsEmpty()) { HTMLInputElement* ownerNumberControl = GetOwnerNumberControl(); if (ownerNumberControl) { ownerNumberControl->AddStates(focusStates); } else { HTMLInputElement* ownerDateTimeControl = GetOwnerDateTimeControl(); if (ownerDateTimeControl) { ownerDateTimeControl->AddStates(focusStates); } } } } nsGenericHTMLFormElementWithState::AddStates(aStates); } void HTMLInputElement::RemoveStates(EventStates aStates) { if (mType == NS_FORM_INPUT_TEXT) { EventStates focusStates(aStates & (NS_EVENT_STATE_FOCUS | NS_EVENT_STATE_FOCUSRING)); if (!focusStates.IsEmpty()) { HTMLInputElement* ownerNumberControl = GetOwnerNumberControl(); if (ownerNumberControl) { ownerNumberControl->RemoveStates(focusStates); } else { HTMLInputElement* ownerDateTimeControl = GetOwnerDateTimeControl(); if (ownerDateTimeControl) { ownerDateTimeControl->RemoveStates(focusStates); } } } } nsGenericHTMLFormElementWithState::RemoveStates(aStates); } bool HTMLInputElement::RestoreState(nsPresState* aState) { bool restoredCheckedState = false; nsCOMPtr inputState (do_QueryInterface(aState->GetStateProperty())); if (inputState) { switch (GetValueMode()) { case VALUE_MODE_DEFAULT_ON: if (inputState->IsCheckedSet()) { restoredCheckedState = true; DoSetChecked(inputState->GetChecked(), true, true); } break; case VALUE_MODE_FILENAME: { nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); if (window) { nsTArray array; inputState->GetFilesOrDirectories(window, array); SetFilesOrDirectories(array, true); } } break; case VALUE_MODE_VALUE: case VALUE_MODE_DEFAULT: if (GetValueMode() == VALUE_MODE_DEFAULT && mType != NS_FORM_INPUT_HIDDEN) { break; } // TODO: What should we do if SetValueInternal fails? (The allocation // may potentially be big, but most likely we've failed to allocate // before the type change.) SetValueInternal(inputState->GetValue(), nsTextEditorState::eSetValue_Notify); break; } } if (aState->IsDisabledSet()) { SetDisabled(aState->GetDisabled()); } return restoredCheckedState; } bool HTMLInputElement::AllowDrop() { // Allow drop on anything other than file inputs. return mType != NS_FORM_INPUT_FILE; } /* * Radio group stuff */ void HTMLInputElement::AddedToRadioGroup() { // If the element is neither in a form nor a document, there is no group so we // should just stop here. if (!mForm && !IsInUncomposedDoc()) { return; } // Make sure not to notify if we're still being created bool notify = mDoneCreating; // // If the input element is checked, and we add it to the group, it will // deselect whatever is currently selected in that group // if (mChecked) { // // If it is checked, call "RadioSetChecked" to perform the selection/ // deselection ritual. This has the side effect of repainting the // radio button, but as adding a checked radio button into the group // should not be that common an occurrence, I think we can live with // that. // RadioSetChecked(notify); } // // For integrity purposes, we have to ensure that "checkedChanged" is // the same for this new element as for all the others in the group // bool checkedChanged = mCheckedChanged; nsCOMPtr visitor = new nsRadioGetCheckedChangedVisitor(&checkedChanged, this); VisitGroup(visitor, notify); SetCheckedChangedInternal(checkedChanged); // // Add the radio to the radio group container. // nsCOMPtr container = GetRadioGroupContainer(); if (container) { nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); container->AddToRadioGroup(name, static_cast(this)); // We initialize the validity of the element to the validity of the group // because we assume UpdateValueMissingState() will be called after. SetValidityState(VALIDITY_STATE_VALUE_MISSING, container->GetValueMissingState(name)); } } void HTMLInputElement::WillRemoveFromRadioGroup() { nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (!container) { return; } nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); // If this button was checked, we need to notify the group that there is no // longer a selected radio button if (mChecked) { container->SetCurrentRadioButton(name, nullptr); nsCOMPtr visitor = new nsRadioUpdateStateVisitor(this); VisitGroup(visitor, true); } // Remove this radio from its group in the container. // We need to call UpdateValueMissingValidityStateForRadio before to make sure // the group validity is updated (with this element being ignored). UpdateValueMissingValidityStateForRadio(true); container->RemoveFromRadioGroup(name, static_cast(this)); } bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, int32_t* aTabIndex) { if (nsGenericHTMLFormElementWithState::IsHTMLFocusable(aWithMouse, aIsFocusable, aTabIndex)) { return true; } if (IsDisabled()) { *aIsFocusable = false; return true; } if (IsSingleLineTextControl(false) || mType == NS_FORM_INPUT_RANGE) { *aIsFocusable = true; return false; } #ifdef XP_MACOSX const bool defaultFocusable = !aWithMouse || nsFocusManager::sMouseFocusesFormControl; #else const bool defaultFocusable = true; #endif if (mType == NS_FORM_INPUT_FILE || mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) { if (aTabIndex) { // We only want our native anonymous child to be tabable to, not ourself. *aTabIndex = -1; } if (mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) { *aIsFocusable = true; } else { *aIsFocusable = defaultFocusable; } return true; } if (mType == NS_FORM_INPUT_HIDDEN) { if (aTabIndex) { *aTabIndex = -1; } *aIsFocusable = false; return false; } if (!aTabIndex) { // The other controls are all focusable *aIsFocusable = defaultFocusable; return false; } if (mType != NS_FORM_INPUT_RADIO) { *aIsFocusable = defaultFocusable; return false; } if (mChecked) { // Selected radio buttons are tabbable *aIsFocusable = defaultFocusable; return false; } // Current radio button is not selected. // But make it tabbable if nothing in group is selected. nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (!container) { *aIsFocusable = defaultFocusable; return false; } nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); if (container->GetCurrentRadioButton(name)) { *aTabIndex = -1; } *aIsFocusable = defaultFocusable; return false; } nsresult HTMLInputElement::VisitGroup(nsIRadioVisitor* aVisitor, bool aFlushContent) { nsIRadioGroupContainer* container = GetRadioGroupContainer(); if (container) { nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); return container->WalkRadioGroup(name, aVisitor, aFlushContent); } aVisitor->Visit(this); return NS_OK; } HTMLInputElement::ValueModeType HTMLInputElement::GetValueMode() const { switch (mType) { case NS_FORM_INPUT_HIDDEN: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_BUTTON: case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_IMAGE: return VALUE_MODE_DEFAULT; case NS_FORM_INPUT_CHECKBOX: case NS_FORM_INPUT_RADIO: return VALUE_MODE_DEFAULT_ON; case NS_FORM_INPUT_FILE: return VALUE_MODE_FILENAME; #ifdef DEBUG case NS_FORM_INPUT_TEXT: case NS_FORM_INPUT_PASSWORD: case NS_FORM_INPUT_SEARCH: case NS_FORM_INPUT_TEL: case NS_FORM_INPUT_EMAIL: case NS_FORM_INPUT_URL: case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: case NS_FORM_INPUT_DATE: case NS_FORM_INPUT_TIME: case NS_FORM_INPUT_COLOR: case NS_FORM_INPUT_MONTH: case NS_FORM_INPUT_WEEK: case NS_FORM_INPUT_DATETIME_LOCAL: return VALUE_MODE_VALUE; default: NS_NOTYETIMPLEMENTED("Unexpected input type in GetValueMode()"); return VALUE_MODE_VALUE; #else // DEBUG default: return VALUE_MODE_VALUE; #endif // DEBUG } } bool HTMLInputElement::IsMutable() const { return !IsDisabled() && !(DoesReadOnlyApply() && HasAttr(kNameSpaceID_None, nsGkAtoms::readonly)); } bool HTMLInputElement::DoesReadOnlyApply() const { switch (mType) { case NS_FORM_INPUT_HIDDEN: case NS_FORM_INPUT_BUTTON: case NS_FORM_INPUT_IMAGE: case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_RADIO: case NS_FORM_INPUT_FILE: case NS_FORM_INPUT_CHECKBOX: case NS_FORM_INPUT_RANGE: case NS_FORM_INPUT_COLOR: return false; #ifdef DEBUG case NS_FORM_INPUT_TEXT: case NS_FORM_INPUT_PASSWORD: case NS_FORM_INPUT_SEARCH: case NS_FORM_INPUT_TEL: case NS_FORM_INPUT_EMAIL: case NS_FORM_INPUT_URL: case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_DATE: case NS_FORM_INPUT_TIME: case NS_FORM_INPUT_MONTH: case NS_FORM_INPUT_WEEK: case NS_FORM_INPUT_DATETIME_LOCAL: return true; default: NS_NOTYETIMPLEMENTED("Unexpected input type in DoesReadOnlyApply()"); return true; #else // DEBUG default: return true; #endif // DEBUG } } bool HTMLInputElement::DoesRequiredApply() const { switch (mType) { case NS_FORM_INPUT_HIDDEN: case NS_FORM_INPUT_BUTTON: case NS_FORM_INPUT_IMAGE: case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_RANGE: case NS_FORM_INPUT_COLOR: return false; #ifdef DEBUG case NS_FORM_INPUT_RADIO: case NS_FORM_INPUT_CHECKBOX: case NS_FORM_INPUT_FILE: case NS_FORM_INPUT_TEXT: case NS_FORM_INPUT_PASSWORD: case NS_FORM_INPUT_SEARCH: case NS_FORM_INPUT_TEL: case NS_FORM_INPUT_EMAIL: case NS_FORM_INPUT_URL: case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_DATE: case NS_FORM_INPUT_TIME: case NS_FORM_INPUT_MONTH: case NS_FORM_INPUT_WEEK: case NS_FORM_INPUT_DATETIME_LOCAL: return true; default: NS_NOTYETIMPLEMENTED("Unexpected input type in DoesRequiredApply()"); return true; #else // DEBUG default: return true; #endif // DEBUG } } bool HTMLInputElement::PlaceholderApplies() const { if (IsDateTimeInputType(mType)) { return false; } return IsSingleLineTextControl(false); } bool HTMLInputElement::DoesPatternApply() const { // TODO: temporary until bug 773205 is fixed. if (IsExperimentalMobileType(mType) || IsDateTimeInputType(mType)) { return false; } return IsSingleLineTextControl(false); } bool HTMLInputElement::DoesMinMaxApply() const { switch (mType) { case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_DATE: case NS_FORM_INPUT_TIME: case NS_FORM_INPUT_RANGE: case NS_FORM_INPUT_MONTH: case NS_FORM_INPUT_WEEK: case NS_FORM_INPUT_DATETIME_LOCAL: return true; #ifdef DEBUG case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_IMAGE: case NS_FORM_INPUT_BUTTON: case NS_FORM_INPUT_HIDDEN: case NS_FORM_INPUT_RADIO: case NS_FORM_INPUT_CHECKBOX: case NS_FORM_INPUT_FILE: case NS_FORM_INPUT_TEXT: case NS_FORM_INPUT_PASSWORD: case NS_FORM_INPUT_SEARCH: case NS_FORM_INPUT_TEL: case NS_FORM_INPUT_EMAIL: case NS_FORM_INPUT_URL: case NS_FORM_INPUT_COLOR: return false; default: NS_NOTYETIMPLEMENTED("Unexpected input type in DoesRequiredApply()"); return false; #else // DEBUG default: return false; #endif // DEBUG } } bool HTMLInputElement::DoesAutocompleteApply() const { switch (mType) { case NS_FORM_INPUT_HIDDEN: case NS_FORM_INPUT_TEXT: case NS_FORM_INPUT_SEARCH: case NS_FORM_INPUT_URL: case NS_FORM_INPUT_TEL: case NS_FORM_INPUT_EMAIL: case NS_FORM_INPUT_PASSWORD: case NS_FORM_INPUT_DATE: case NS_FORM_INPUT_TIME: case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: case NS_FORM_INPUT_COLOR: case NS_FORM_INPUT_MONTH: case NS_FORM_INPUT_WEEK: case NS_FORM_INPUT_DATETIME_LOCAL: return true; #ifdef DEBUG case NS_FORM_INPUT_RESET: case NS_FORM_INPUT_SUBMIT: case NS_FORM_INPUT_IMAGE: case NS_FORM_INPUT_BUTTON: case NS_FORM_INPUT_RADIO: case NS_FORM_INPUT_CHECKBOX: case NS_FORM_INPUT_FILE: return false; default: NS_NOTYETIMPLEMENTED("Unexpected input type in DoesAutocompleteApply()"); return false; #else // DEBUG default: return false; #endif // DEBUG } } Decimal HTMLInputElement::GetStep() const { MOZ_ASSERT(DoesStepApply(), "GetStep() can only be called if @step applies"); if (!HasAttr(kNameSpaceID_None, nsGkAtoms::step)) { return GetDefaultStep() * GetStepScaleFactor(); } nsAutoString stepStr; GetAttr(kNameSpaceID_None, nsGkAtoms::step, stepStr); if (stepStr.LowerCaseEqualsLiteral("any")) { // The element can't suffer from step mismatch if there is no step. return kStepAny; } Decimal step = StringToDecimal(stepStr); if (!step.isFinite() || step <= Decimal(0)) { step = GetDefaultStep(); } // For input type=date, we round the step value to have a rounded day. if (mType == NS_FORM_INPUT_DATE || mType == NS_FORM_INPUT_MONTH || mType == NS_FORM_INPUT_WEEK) { step = std::max(step.round(), Decimal(1)); } return step * GetStepScaleFactor(); } // nsIConstraintValidation NS_IMETHODIMP HTMLInputElement::SetCustomValidity(const nsAString& aError) { nsIConstraintValidation::SetCustomValidity(aError); UpdateState(true); return NS_OK; } bool HTMLInputElement::IsTooLong() { if (!mValueChanged || !mLastValueChangeWasInteractive || !MinOrMaxLengthApplies() || !HasAttr(kNameSpaceID_None, nsGkAtoms::maxlength)) { return false; } int32_t maxLength = MaxLength(); // Maxlength of -1 means parsing error. if (maxLength == -1) { return false; } int32_t textLength = -1; GetTextLength(&textLength); return textLength > maxLength; } bool HTMLInputElement::IsTooShort() { if (!mValueChanged || !mLastValueChangeWasInteractive || !MinOrMaxLengthApplies() || !HasAttr(kNameSpaceID_None, nsGkAtoms::minlength)) { return false; } int32_t minLength = MinLength(); // Minlength of -1 means parsing error. if (minLength == -1) { return false; } int32_t textLength = -1; GetTextLength(&textLength); return textLength && textLength < minLength; } bool HTMLInputElement::IsValueMissing() const { // Should use UpdateValueMissingValidityStateForRadio() for type radio. MOZ_ASSERT(mType != NS_FORM_INPUT_RADIO); if (!HasAttr(kNameSpaceID_None, nsGkAtoms::required) || !DoesRequiredApply()) { return false; } if (!IsMutable()) { return false; } switch (GetValueMode()) { case VALUE_MODE_VALUE: return IsValueEmpty(); case VALUE_MODE_FILENAME: return GetFilesOrDirectoriesInternal().IsEmpty(); case VALUE_MODE_DEFAULT_ON: // This should not be used for type radio. // See the MOZ_ASSERT at the beginning of the method. return !mChecked; case VALUE_MODE_DEFAULT: default: return false; } } bool HTMLInputElement::HasTypeMismatch() const { if (mType != NS_FORM_INPUT_EMAIL && mType != NS_FORM_INPUT_URL) { return false; } nsAutoString value; NS_ENSURE_SUCCESS(GetValueInternal(value), false); if (value.IsEmpty()) { return false; } if (mType == NS_FORM_INPUT_EMAIL) { return HasAttr(kNameSpaceID_None, nsGkAtoms::multiple) ? !IsValidEmailAddressList(value) : !IsValidEmailAddress(value); } else if (mType == NS_FORM_INPUT_URL) { /** * TODO: * The URL is not checked as the HTML5 specifications want it to be because * there is no code to check for a valid URI/IRI according to 3986 and 3987 * RFC's at the moment, see bug 561586. * * RFC 3987 (IRI) implementation: bug 42899 * * HTML5 specifications: * http://dev.w3.org/html5/spec/infrastructure.html#valid-url */ nsCOMPtr ioService = do_GetIOService(); nsCOMPtr uri; return !NS_SUCCEEDED(ioService->NewURI(NS_ConvertUTF16toUTF8(value), nullptr, nullptr, getter_AddRefs(uri))); } return false; } bool HTMLInputElement::HasPatternMismatch() const { if (!DoesPatternApply() || !HasAttr(kNameSpaceID_None, nsGkAtoms::pattern)) { return false; } nsAutoString pattern; GetAttr(kNameSpaceID_None, nsGkAtoms::pattern, pattern); nsAutoString value; NS_ENSURE_SUCCESS(GetValueInternal(value), false); if (value.IsEmpty()) { return false; } nsIDocument* doc = OwnerDoc(); return !nsContentUtils::IsPatternMatching(value, pattern, doc); } bool HTMLInputElement::IsRangeOverflow() const { // TODO: this is temporary until bug 888331 is fixed. if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { return false; } Decimal maximum = GetMaximum(); if (maximum.isNaN()) { return false; } Decimal value = GetValueAsDecimal(); if (value.isNaN()) { return false; } return value > maximum; } bool HTMLInputElement::IsRangeUnderflow() const { // TODO: this is temporary until bug 888331 is fixed. if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { return false; } Decimal minimum = GetMinimum(); if (minimum.isNaN()) { return false; } Decimal value = GetValueAsDecimal(); if (value.isNaN()) { return false; } return value < minimum; } bool HTMLInputElement::HasStepMismatch(bool aUseZeroIfValueNaN) const { if (!DoesStepApply()) { return false; } Decimal value = GetValueAsDecimal(); if (value.isNaN()) { if (aUseZeroIfValueNaN) { value = Decimal(0); } else { // The element can't suffer from step mismatch if it's value isn't a number. return false; } } Decimal step = GetStep(); if (step == kStepAny) { return false; } // Value has to be an integral multiple of step. return NS_floorModulo(value - GetStepBase(), step) != Decimal(0); } /** * Takes aEmail and attempts to convert everything after the first "@" * character (if anything) to punycode before returning the complete result via * the aEncodedEmail out-param. The aIndexOfAt out-param is set to the index of * the "@" character. * * If no "@" is found in aEmail, aEncodedEmail is simply set to aEmail and * the aIndexOfAt out-param is set to kNotFound. * * Returns true in all cases unless an attempt to punycode encode fails. If * false is returned, aEncodedEmail has not been set. * * This function exists because ConvertUTF8toACE() splits on ".", meaning that * for 'user.name@sld.tld' it would treat "name@sld" as a label. We want to * encode the domain part only. */ static bool PunycodeEncodeEmailAddress(const nsAString& aEmail, nsAutoCString& aEncodedEmail, uint32_t* aIndexOfAt) { nsAutoCString value = NS_ConvertUTF16toUTF8(aEmail); *aIndexOfAt = (uint32_t)value.FindChar('@'); if (*aIndexOfAt == (uint32_t)kNotFound || *aIndexOfAt == value.Length() - 1) { aEncodedEmail = value; return true; } nsCOMPtr idnSrv = do_GetService(NS_IDNSERVICE_CONTRACTID); if (!idnSrv) { NS_ERROR("nsIIDNService isn't present!"); return false; } uint32_t indexOfDomain = *aIndexOfAt + 1; const nsDependentCSubstring domain = Substring(value, indexOfDomain); bool ace; if (NS_SUCCEEDED(idnSrv->IsACE(domain, &ace)) && !ace) { nsAutoCString domainACE; if (NS_FAILED(idnSrv->ConvertUTF8toACE(domain, domainACE))) { return false; } value.Replace(indexOfDomain, domain.Length(), domainACE); } aEncodedEmail = value; return true; } bool HTMLInputElement::HasBadInput() const { if (mType == NS_FORM_INPUT_NUMBER) { nsAutoString value; GetValueInternal(value); if (!value.IsEmpty()) { // The input can't be bad, otherwise it would have been sanitized to the // empty string. NS_ASSERTION(!GetValueAsDecimal().isNaN(), "Should have sanitized"); return false; } nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); if (numberControlFrame && !numberControlFrame->AnonTextControlIsEmpty()) { // The input the user entered failed to parse as a number. return true; } return false; } if (mType == NS_FORM_INPUT_EMAIL) { // With regards to suffering from bad input the spec says that only the // punycode conversion works, so we don't care whether the email address is // valid or not here. (If the email address is invalid then we will be // suffering from a type mismatch.) nsAutoString value; nsAutoCString unused; uint32_t unused2; NS_ENSURE_SUCCESS(GetValueInternal(value), false); HTMLSplitOnSpacesTokenizer tokenizer(value, ','); while (tokenizer.hasMoreTokens()) { if (!PunycodeEncodeEmailAddress(tokenizer.nextToken(), unused, &unused2)) { return true; } } return false; } return false; } void HTMLInputElement::UpdateTooLongValidityState() { SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong()); } void HTMLInputElement::UpdateTooShortValidityState() { SetValidityState(VALIDITY_STATE_TOO_SHORT, IsTooShort()); } void HTMLInputElement::UpdateValueMissingValidityStateForRadio(bool aIgnoreSelf) { bool notify = mDoneCreating; nsCOMPtr selection = GetSelectedRadioButton(); aIgnoreSelf = aIgnoreSelf || !IsMutable(); // If there is no selection, that might mean the radio is not in a group. // In that case, we can look for the checked state of the radio. bool selected = selection || (!aIgnoreSelf && mChecked); bool required = !aIgnoreSelf && HasAttr(kNameSpaceID_None, nsGkAtoms::required); bool valueMissing = false; nsCOMPtr container = GetRadioGroupContainer(); if (!container) { SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsMutable() && required && !selected); return; } nsAutoString name; GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); // If the current radio is required and not ignored, we can assume the entire // group is required. if (!required) { required = (aIgnoreSelf && HasAttr(kNameSpaceID_None, nsGkAtoms::required)) ? container->GetRequiredRadioCount(name) - 1 : container->GetRequiredRadioCount(name); } valueMissing = required && !selected; if (container->GetValueMissingState(name) != valueMissing) { container->SetValueMissingState(name, valueMissing); SetValidityState(VALIDITY_STATE_VALUE_MISSING, valueMissing); // nsRadioSetValueMissingState will call ContentStateChanged while visiting. nsAutoScriptBlocker scriptBlocker; nsCOMPtr visitor = new nsRadioSetValueMissingState(this, valueMissing, notify); VisitGroup(visitor, notify); } } void HTMLInputElement::UpdateValueMissingValidityState() { if (mType == NS_FORM_INPUT_RADIO) { UpdateValueMissingValidityStateForRadio(false); return; } SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing()); } void HTMLInputElement::UpdateTypeMismatchValidityState() { SetValidityState(VALIDITY_STATE_TYPE_MISMATCH, HasTypeMismatch()); } void HTMLInputElement::UpdatePatternMismatchValidityState() { SetValidityState(VALIDITY_STATE_PATTERN_MISMATCH, HasPatternMismatch()); } void HTMLInputElement::UpdateRangeOverflowValidityState() { SetValidityState(VALIDITY_STATE_RANGE_OVERFLOW, IsRangeOverflow()); } void HTMLInputElement::UpdateRangeUnderflowValidityState() { SetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW, IsRangeUnderflow()); } void HTMLInputElement::UpdateStepMismatchValidityState() { SetValidityState(VALIDITY_STATE_STEP_MISMATCH, HasStepMismatch()); } void HTMLInputElement::UpdateBadInputValidityState() { SetValidityState(VALIDITY_STATE_BAD_INPUT, HasBadInput()); } void HTMLInputElement::UpdateAllValidityStates(bool aNotify) { bool validBefore = IsValid(); UpdateTooLongValidityState(); UpdateTooShortValidityState(); UpdateValueMissingValidityState(); UpdateTypeMismatchValidityState(); UpdatePatternMismatchValidityState(); UpdateRangeOverflowValidityState(); UpdateRangeUnderflowValidityState(); UpdateStepMismatchValidityState(); UpdateBadInputValidityState(); if (validBefore != IsValid()) { UpdateState(aNotify); } } void HTMLInputElement::UpdateBarredFromConstraintValidation() { SetBarredFromConstraintValidation(mType == NS_FORM_INPUT_HIDDEN || mType == NS_FORM_INPUT_BUTTON || mType == NS_FORM_INPUT_RESET || HasAttr(kNameSpaceID_None, nsGkAtoms::readonly) || IsDisabled()); } void HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, ErrorResult& aRv) { aRv = GetValidationMessage(aValidationMessage); } nsresult HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, ValidityStateType aType) { nsresult rv = NS_OK; switch (aType) { case VALIDITY_STATE_TOO_LONG: { nsXPIDLString message; int32_t maxLength = MaxLength(); int32_t textLength = -1; nsAutoString strMaxLength; nsAutoString strTextLength; GetTextLength(&textLength); strMaxLength.AppendInt(maxLength); strTextLength.AppendInt(textLength); const char16_t* params[] = { strMaxLength.get(), strTextLength.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationTextTooLong", params, message); aValidationMessage = message; break; } case VALIDITY_STATE_TOO_SHORT: { nsXPIDLString message; int32_t minLength = MinLength(); int32_t textLength = -1; nsAutoString strMinLength; nsAutoString strTextLength; GetTextLength(&textLength); strMinLength.AppendInt(minLength); strTextLength.AppendInt(textLength); const char16_t* params[] = { strMinLength.get(), strTextLength.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationTextTooShort", params, message); aValidationMessage = message; break; } case VALIDITY_STATE_VALUE_MISSING: { nsXPIDLString message; nsAutoCString key; switch (mType) { case NS_FORM_INPUT_FILE: key.AssignLiteral("FormValidationFileMissing"); break; case NS_FORM_INPUT_CHECKBOX: key.AssignLiteral("FormValidationCheckboxMissing"); break; case NS_FORM_INPUT_RADIO: key.AssignLiteral("FormValidationRadioMissing"); break; case NS_FORM_INPUT_NUMBER: key.AssignLiteral("FormValidationBadInputNumber"); break; default: key.AssignLiteral("FormValidationValueMissing"); } rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, key.get(), message); aValidationMessage = message; break; } case VALIDITY_STATE_TYPE_MISMATCH: { nsXPIDLString message; nsAutoCString key; if (mType == NS_FORM_INPUT_EMAIL) { key.AssignLiteral("FormValidationInvalidEmail"); } else if (mType == NS_FORM_INPUT_URL) { key.AssignLiteral("FormValidationInvalidURL"); } else { return NS_ERROR_UNEXPECTED; } rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, key.get(), message); aValidationMessage = message; break; } case VALIDITY_STATE_PATTERN_MISMATCH: { nsXPIDLString message; nsAutoString title; GetAttr(kNameSpaceID_None, nsGkAtoms::title, title); if (title.IsEmpty()) { rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationPatternMismatch", message); } else { if (title.Length() > nsIConstraintValidation::sContentSpecifiedMaxLengthMessage) { title.Truncate(nsIConstraintValidation::sContentSpecifiedMaxLengthMessage); } const char16_t* params[] = { title.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationPatternMismatchWithTitle", params, message); } aValidationMessage = message; break; } case VALIDITY_STATE_RANGE_OVERFLOW: { static const char kNumberOverTemplate[] = "FormValidationNumberRangeOverflow"; static const char kDateOverTemplate[] = "FormValidationDateRangeOverflow"; static const char kTimeOverTemplate[] = "FormValidationTimeRangeOverflow"; const char* msgTemplate; nsXPIDLString message; nsAutoString maxStr; if (mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_RANGE) { msgTemplate = kNumberOverTemplate; //We want to show the value as parsed when it's a number Decimal maximum = GetMaximum(); MOZ_ASSERT(!maximum.isNaN()); char buf[32]; DebugOnly ok = maximum.toString(buf, ArrayLength(buf)); maxStr.AssignASCII(buf); MOZ_ASSERT(ok, "buf not big enough"); } else if (IsDateTimeInputType(mType)) { msgTemplate = mType == NS_FORM_INPUT_TIME ? kTimeOverTemplate : kDateOverTemplate; GetAttr(kNameSpaceID_None, nsGkAtoms::max, maxStr); } else { msgTemplate = kNumberOverTemplate; NS_NOTREACHED("Unexpected input type"); } const char16_t* params[] = { maxStr.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, msgTemplate, params, message); aValidationMessage = message; break; } case VALIDITY_STATE_RANGE_UNDERFLOW: { static const char kNumberUnderTemplate[] = "FormValidationNumberRangeUnderflow"; static const char kDateUnderTemplate[] = "FormValidationDateRangeUnderflow"; static const char kTimeUnderTemplate[] = "FormValidationTimeRangeUnderflow"; const char* msgTemplate; nsXPIDLString message; nsAutoString minStr; if (mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_RANGE) { msgTemplate = kNumberUnderTemplate; Decimal minimum = GetMinimum(); MOZ_ASSERT(!minimum.isNaN()); char buf[32]; DebugOnly ok = minimum.toString(buf, ArrayLength(buf)); minStr.AssignASCII(buf); MOZ_ASSERT(ok, "buf not big enough"); } else if (IsDateTimeInputType(mType)) { msgTemplate = mType == NS_FORM_INPUT_TIME ? kTimeUnderTemplate : kDateUnderTemplate; GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr); } else { msgTemplate = kNumberUnderTemplate; NS_NOTREACHED("Unexpected input type"); } const char16_t* params[] = { minStr.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, msgTemplate, params, message); aValidationMessage = message; break; } case VALIDITY_STATE_STEP_MISMATCH: { nsXPIDLString message; Decimal value = GetValueAsDecimal(); MOZ_ASSERT(!value.isNaN()); Decimal step = GetStep(); MOZ_ASSERT(step != kStepAny && step > Decimal(0)); Decimal stepBase = GetStepBase(); Decimal valueLow = value - NS_floorModulo(value - stepBase, step); Decimal valueHigh = value + step - NS_floorModulo(value - stepBase, step); Decimal maximum = GetMaximum(); if (maximum.isNaN() || valueHigh <= maximum) { nsAutoString valueLowStr, valueHighStr; ConvertNumberToString(valueLow, valueLowStr); ConvertNumberToString(valueHigh, valueHighStr); if (valueLowStr.Equals(valueHighStr)) { const char16_t* params[] = { valueLowStr.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationStepMismatchOneValue", params, message); } else { const char16_t* params[] = { valueLowStr.get(), valueHighStr.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationStepMismatch", params, message); } } else { nsAutoString valueLowStr; ConvertNumberToString(valueLow, valueLowStr); const char16_t* params[] = { valueLowStr.get() }; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, "FormValidationStepMismatchOneValue", params, message); } aValidationMessage = message; break; } case VALIDITY_STATE_BAD_INPUT: { nsXPIDLString message; nsAutoCString key; if (mType == NS_FORM_INPUT_NUMBER) { key.AssignLiteral("FormValidationBadInputNumber"); } else if (mType == NS_FORM_INPUT_EMAIL) { key.AssignLiteral("FormValidationInvalidEmail"); } else { return NS_ERROR_UNEXPECTED; } rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, key.get(), message); aValidationMessage = message; break; } default: rv = nsIConstraintValidation::GetValidationMessage(aValidationMessage, aType); } return rv; } //static bool HTMLInputElement::IsValidEmailAddressList(const nsAString& aValue) { HTMLSplitOnSpacesTokenizer tokenizer(aValue, ','); while (tokenizer.hasMoreTokens()) { if (!IsValidEmailAddress(tokenizer.nextToken())) { return false; } } return !tokenizer.separatorAfterCurrentToken(); } //static bool HTMLInputElement::IsValidEmailAddress(const nsAString& aValue) { // Email addresses can't be empty and can't end with a '.' or '-'. if (aValue.IsEmpty() || aValue.Last() == '.' || aValue.Last() == '-') { return false; } uint32_t atPos; nsAutoCString value; if (!PunycodeEncodeEmailAddress(aValue, value, &atPos) || atPos == (uint32_t)kNotFound || atPos == 0 || atPos == value.Length() - 1) { // Could not encode, or "@" was not found, or it was at the start or end // of the input - in all cases, not a valid email address. return false; } uint32_t length = value.Length(); uint32_t i = 0; // Parsing the username. for (; i < atPos; ++i) { char16_t c = value[i]; // The username characters have to be in this list to be valid. if (!(nsCRT::IsAsciiAlpha(c) || nsCRT::IsAsciiDigit(c) || c == '.' || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\''|| c == '*' || c == '+' || c == '-' || c == '/' || c == '=' || c == '?' || c == '^' || c == '_' || c == '`' || c == '{' || c == '|' || c == '}' || c == '~' )) { return false; } } // Skip the '@'. ++i; // The domain name can't begin with a dot or a dash. if (value[i] == '.' || value[i] == '-') { return false; } // Parsing the domain name. for (; i < length; ++i) { char16_t c = value[i]; if (c == '.') { // A dot can't follow a dot or a dash. if (value[i-1] == '.' || value[i-1] == '-') { return false; } } else if (c == '-'){ // A dash can't follow a dot. if (value[i-1] == '.') { return false; } } else if (!(nsCRT::IsAsciiAlpha(c) || nsCRT::IsAsciiDigit(c) || c == '-')) { // The domain characters have to be in this list to be valid. return false; } } return true; } NS_IMETHODIMP_(bool) HTMLInputElement::IsSingleLineTextControl() const { return IsSingleLineTextControl(false); } NS_IMETHODIMP_(bool) HTMLInputElement::IsTextArea() const { return false; } NS_IMETHODIMP_(bool) HTMLInputElement::IsPlainTextControl() const { // need to check our HTML attribute and/or CSS. return true; } NS_IMETHODIMP_(bool) HTMLInputElement::IsPasswordTextControl() const { return mType == NS_FORM_INPUT_PASSWORD; } NS_IMETHODIMP_(int32_t) HTMLInputElement::GetCols() { // Else we know (assume) it is an input with size attr const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::size); if (attr && attr->Type() == nsAttrValue::eInteger) { int32_t cols = attr->GetIntegerValue(); if (cols > 0) { return cols; } } return DEFAULT_COLS; } NS_IMETHODIMP_(int32_t) HTMLInputElement::GetWrapCols() { return 0; // only textarea's can have wrap cols } NS_IMETHODIMP_(int32_t) HTMLInputElement::GetRows() { return DEFAULT_ROWS; } NS_IMETHODIMP_(void) HTMLInputElement::GetDefaultValueFromContent(nsAString& aValue) { nsTextEditorState *state = GetEditorState(); if (state) { GetDefaultValue(aValue); // This is called by the frame to show the value. // We have to sanitize it when needed. if (mDoneCreating) { SanitizeValue(aValue); } } } NS_IMETHODIMP_(bool) HTMLInputElement::ValueChanged() const { return mValueChanged; } NS_IMETHODIMP_(void) HTMLInputElement::GetTextEditorValue(nsAString& aValue, bool aIgnoreWrap) const { nsTextEditorState* state = GetEditorState(); if (state) { state->GetValue(aValue, aIgnoreWrap); } } NS_IMETHODIMP_(void) HTMLInputElement::InitializeKeyboardEventListeners() { nsTextEditorState* state = GetEditorState(); if (state) { state->InitializeKeyboardEventListeners(); } } NS_IMETHODIMP_(void) HTMLInputElement::OnValueChanged(bool aNotify, bool aWasInteractiveUserChange) { mLastValueChangeWasInteractive = aWasInteractiveUserChange; UpdateAllValidityStates(aNotify); if (HasDirAuto()) { SetDirectionIfAuto(true, aNotify); } // :placeholder-shown pseudo-class may change when the value changes. // However, we don't want to waste cycles if the state doesn't apply. if (PlaceholderApplies() && HasAttr(kNameSpaceID_None, nsGkAtoms::placeholder)) { UpdateState(aNotify); } } NS_IMETHODIMP_(bool) HTMLInputElement::HasCachedSelection() { bool isCached = false; nsTextEditorState* state = GetEditorState(); if (state) { isCached = state->IsSelectionCached() && state->HasNeverInitializedBefore() && !state->GetSelectionProperties().IsDefault(); if (isCached) { state->WillInitEagerly(); } } return isCached; } void HTMLInputElement::FieldSetDisabledChanged(bool aNotify) { UpdateValueMissingValidityState(); UpdateBarredFromConstraintValidation(); nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify); } void HTMLInputElement::SetFilePickerFiltersFromAccept(nsIFilePicker* filePicker) { // We always add |filterAll| filePicker->AppendFilters(nsIFilePicker::filterAll); NS_ASSERTION(HasAttr(kNameSpaceID_None, nsGkAtoms::accept), "You should not call SetFilePickerFiltersFromAccept if the" " element has no accept attribute!"); // Services to retrieve image/*, audio/*, video/* filters nsCOMPtr stringService = mozilla::services::GetStringBundleService(); if (!stringService) { return; } nsCOMPtr filterBundle; if (NS_FAILED(stringService->CreateBundle("chrome://global/content/filepicker.properties", getter_AddRefs(filterBundle)))) { return; } // Service to retrieve mime type information for mime types filters nsCOMPtr mimeService = do_GetService("@mozilla.org/mime;1"); if (!mimeService) { return; } nsAutoString accept; GetAttr(kNameSpaceID_None, nsGkAtoms::accept, accept); HTMLSplitOnSpacesTokenizer tokenizer(accept, ','); nsTArray filters; nsString allExtensionsList; bool allMimeTypeFiltersAreValid = true; bool atLeastOneFileExtensionFilter = false; // Retrieve all filters while (tokenizer.hasMoreTokens()) { const nsDependentSubstring& token = tokenizer.nextToken(); if (token.IsEmpty()) { continue; } int32_t filterMask = 0; nsString filterName; nsString extensionListStr; // First, check for image/audio/video filters... if (token.EqualsLiteral("image/*")) { filterMask = nsIFilePicker::filterImages; filterBundle->GetStringFromName(u"imageFilter", getter_Copies(extensionListStr)); } else if (token.EqualsLiteral("audio/*")) { filterMask = nsIFilePicker::filterAudio; filterBundle->GetStringFromName(u"audioFilter", getter_Copies(extensionListStr)); } else if (token.EqualsLiteral("video/*")) { filterMask = nsIFilePicker::filterVideo; filterBundle->GetStringFromName(u"videoFilter", getter_Copies(extensionListStr)); } else if (token.First() == '.') { if (token.Contains(';') || token.Contains('*')) { // Ignore this filter as it contains reserved characters continue; } extensionListStr = NS_LITERAL_STRING("*") + token; filterName = extensionListStr; atLeastOneFileExtensionFilter = true; } else { //... if no image/audio/video filter is found, check mime types filters nsCOMPtr mimeInfo; if (NS_FAILED(mimeService->GetFromTypeAndExtension( NS_ConvertUTF16toUTF8(token), EmptyCString(), // No extension getter_AddRefs(mimeInfo))) || !mimeInfo) { allMimeTypeFiltersAreValid = false; continue; } // Get a name for the filter: first try the description, then the mime type // name if there is no description mimeInfo->GetDescription(filterName); if (filterName.IsEmpty()) { nsCString mimeTypeName; mimeInfo->GetType(mimeTypeName); CopyUTF8toUTF16(mimeTypeName, filterName); } // Get extension list nsCOMPtr extensions; mimeInfo->GetFileExtensions(getter_AddRefs(extensions)); bool hasMore; while (NS_SUCCEEDED(extensions->HasMore(&hasMore)) && hasMore) { nsCString extension; if (NS_FAILED(extensions->GetNext(extension))) { continue; } if (!extensionListStr.IsEmpty()) { extensionListStr.AppendLiteral("; "); } extensionListStr += NS_LITERAL_STRING("*.") + NS_ConvertUTF8toUTF16(extension); } } if (!filterMask && (extensionListStr.IsEmpty() || filterName.IsEmpty())) { // No valid filter found allMimeTypeFiltersAreValid = false; continue; } // If we arrived here, that means we have a valid filter: let's create it // and add it to our list, if no similar filter is already present nsFilePickerFilter filter; if (filterMask) { filter = nsFilePickerFilter(filterMask); } else { filter = nsFilePickerFilter(filterName, extensionListStr); } if (!filters.Contains(filter)) { if (!allExtensionsList.IsEmpty()) { allExtensionsList.AppendLiteral("; "); } allExtensionsList += extensionListStr; filters.AppendElement(filter); } } // Remove similar filters // Iterate over a copy, as we might modify the original filters list nsTArray filtersCopy; filtersCopy = filters; for (uint32_t i = 0; i < filtersCopy.Length(); ++i) { const nsFilePickerFilter& filterToCheck = filtersCopy[i]; if (filterToCheck.mFilterMask) { continue; } for (uint32_t j = 0; j < filtersCopy.Length(); ++j) { if (i == j) { continue; } // Check if this filter's extension list is a substring of the other one. // e.g. if filters are "*.jpeg" and "*.jpeg; *.jpg" the first one should // be removed. // Add an extra "; " to be sure the check will work and avoid cases like // "*.xls" being a subtring of "*.xslx" while those are two differents // filters and none should be removed. if (FindInReadable(filterToCheck.mFilter + NS_LITERAL_STRING(";"), filtersCopy[j].mFilter + NS_LITERAL_STRING(";"))) { // We already have a similar, less restrictive filter (i.e. // filterToCheck extensionList is just a subset of another filter // extension list): remove this one filters.RemoveElement(filterToCheck); } } } // Add "All Supported Types" filter if (filters.Length() > 1) { nsXPIDLString title; nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "AllSupportedTypes", title); filePicker->AppendFilter(title, allExtensionsList); } // Add each filter for (uint32_t i = 0; i < filters.Length(); ++i) { const nsFilePickerFilter& filter = filters[i]; if (filter.mFilterMask) { filePicker->AppendFilters(filter.mFilterMask); } else { filePicker->AppendFilter(filter.mTitle, filter.mFilter); } } if (filters.Length() >= 1 && (allMimeTypeFiltersAreValid || atLeastOneFileExtensionFilter)) { // |filterAll| will always use index=0 so we need to set index=1 as the // current filter. filePicker->SetFilterIndex(1); } } Decimal HTMLInputElement::GetStepScaleFactor() const { MOZ_ASSERT(DoesStepApply()); switch (mType) { case NS_FORM_INPUT_DATE: return kStepScaleFactorDate; case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: return kStepScaleFactorNumberRange; case NS_FORM_INPUT_TIME: return kStepScaleFactorTime; case NS_FORM_INPUT_MONTH: return kStepScaleFactorMonth; case NS_FORM_INPUT_WEEK: return kStepScaleFactorWeek; default: MOZ_ASSERT(false, "Unrecognized input type"); return Decimal::nan(); } } Decimal HTMLInputElement::GetDefaultStep() const { MOZ_ASSERT(DoesStepApply()); switch (mType) { case NS_FORM_INPUT_DATE: case NS_FORM_INPUT_MONTH: case NS_FORM_INPUT_WEEK: case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: return kDefaultStep; case NS_FORM_INPUT_TIME: return kDefaultStepTime; default: MOZ_ASSERT(false, "Unrecognized input type"); return Decimal::nan(); } } void HTMLInputElement::UpdateValidityUIBits(bool aIsFocused) { if (aIsFocused) { // If the invalid UI is shown, we should show it while focusing (and // update). Otherwise, we should not. mCanShowInvalidUI = !IsValid() && ShouldShowValidityUI(); // If neither invalid UI nor valid UI is shown, we shouldn't show the valid // UI while typing. mCanShowValidUI = ShouldShowValidityUI(); } else { mCanShowInvalidUI = true; mCanShowValidUI = true; } } void HTMLInputElement::UpdateHasRange() { /* * There is a range if min/max applies for the type and if the element * currently have a valid min or max. */ mHasRange = false; // TODO: this is temporary until bug 888331 is fixed. if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { return; } Decimal minimum = GetMinimum(); if (!minimum.isNaN()) { mHasRange = true; return; } Decimal maximum = GetMaximum(); if (!maximum.isNaN()) { mHasRange = true; return; } } void HTMLInputElement::PickerClosed() { mPickerRunning = false; } JSObject* HTMLInputElement::WrapNode(JSContext* aCx, JS::Handle aGivenProto) { return HTMLInputElementBinding::Wrap(aCx, this, aGivenProto); } void HTMLInputElement::ClearGetFilesHelpers() { if (mGetFilesRecursiveHelper) { mGetFilesRecursiveHelper->Unlink(); mGetFilesRecursiveHelper = nullptr; } if (mGetFilesNonRecursiveHelper) { mGetFilesNonRecursiveHelper->Unlink(); mGetFilesNonRecursiveHelper = nullptr; } } GetFilesHelper* HTMLInputElement::GetOrCreateGetFilesHelper(bool aRecursiveFlag, ErrorResult& aRv) { nsCOMPtr global = OwnerDoc()->GetScopeObject(); MOZ_ASSERT(global); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } if (aRecursiveFlag) { if (!mGetFilesRecursiveHelper) { mGetFilesRecursiveHelper = GetFilesHelper::Create(global, GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } } return mGetFilesRecursiveHelper; } if (!mGetFilesNonRecursiveHelper) { mGetFilesNonRecursiveHelper = GetFilesHelper::Create(global, GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } } return mGetFilesNonRecursiveHelper; } void HTMLInputElement::UpdateEntries(const nsTArray& aFilesOrDirectories) { MOZ_ASSERT(mEntries.IsEmpty()); nsCOMPtr global = OwnerDoc()->GetScopeObject(); MOZ_ASSERT(global); RefPtr fs = FileSystem::Create(global); if (NS_WARN_IF(!fs)) { return; } Sequence> entries; for (uint32_t i = 0; i < aFilesOrDirectories.Length(); ++i) { RefPtr entry = FileSystemEntry::Create(global, aFilesOrDirectories[i], fs); MOZ_ASSERT(entry); if (!entries.AppendElement(entry, fallible)) { return; } } // The root fileSystem is a DirectoryEntry object that contains only the // dropped fileEntry and directoryEntry objects. fs->CreateRoot(entries); mEntries.SwapElements(entries); } void HTMLInputElement::GetWebkitEntries(nsTArray>& aSequence) { Telemetry::Accumulate(Telemetry::BLINK_FILESYSTEM_USED, true); aSequence.AppendElements(mEntries); } already_AddRefed HTMLInputElement::GetLabels() { if (!IsLabelable()) { return nullptr; } return nsGenericHTMLElement::Labels(); } } // namespace dom } // namespace mozilla #undef NS_ORIGINAL_CHECKED_VALUE