Problem/Motivation
When Date Popup converts a Views exposed date filter to a 'date' form element (DatePopupTrait::applyDatePopupToForm()), it sets #type = 'date' but adds no year-range constraint and no server-side validation. Core's own 'datetime' element (\Drupal\Core\Datetime\Element\Datetime) enforces a #date_year_range (default 1900:2050) via validateDatetime() — see #2840220: 'Date and time' Form API element allows entry beyond min/max values. The plain 'date' element (\Drupal\Core\Render\Element\Date) has no equivalent: no #element_validate callback, no min/max concept at all. Because applyDatePopupToForm() forces every date-typed Views exposed filter to this unconstrained element, any exposed date filter using Date Popup silently accepts a year of arbitrary length.
The practical impact is worse than "out of range accepted": PHP's loose date parser (used by \DateTime/DrupalDateTime without an explicit format) does not error on an oversized year, it silently reinterprets it as a different, smaller year. For example new DrupalDateTime('99999-01-01') does not throw and does not set hasErrors() — it resolves to 2009-01-01. So a user (or a scripted/malicious request) submitting a 5-digit year gets no error and the filter query silently runs against a year the user never entered.
Steps to reproduce
- Create a Views exposed filter on a date field ordinarily rendered via the
'datetime'element (in our case, asmart_datefield filtered viaDrupal\smart_date\Plugin\views\filter\Date, which extends core'sDrupal\views\Plugin\views\filter\Date). - Enable Date Popup. Its
buildExposedForm()hook (called from the filter'sbuildExposedForm()) unconditionally callsDatePopupHelper::applyDatePopup(), which forces the exposed element to#type = 'date'viaDatePopupTrait::applyDatePopupToForm(). - Visit the view and submit the exposed filter with an out-of-range year, e.g. append
?field_date_time_value=99999-01-01to the view's URL (or type an oversized year directly into the native date input's year sub-field, bypassing the browser picker's own normalization). - Observe: no validation error, and the view executes as if a valid date were submitted. (In our reproduction the year is additionally reinterpreted rather than rejected: PHP resolves
99999-01-01to2009-01-01internally.)
Confirmed via \Drupal\views\Views::getView() / setExposedInput() / execute() directly against a real view+filter combination using Date Popup, and via a full HTTP GET request against the live exposed form.
Proposed resolution
Patch attached. It adds a shared applyYearRangeValidation() helper to DatePopupTrait, called for every branch of applyDatePopupToForm() (single value, min, max), which:
- Sets
#attributes['min']/#attributes['max']on the element (1900-01-01/2050-12-31by default, matchingDatetime's default#date_year_range), giving the browser's native<input type="date">its own client-side constraint. - Registers a new
#element_validatecallback,validateYearRange(), mirroringDatetime::validateDatetime()'s year-range check but adapted for the flat string value a'date'element receives (rather than the structured['date' => ..., 'object' => DrupalDateTime]array a'datetime'element receives).
The validator deliberately does not reuse DrupalDateTime's loose parsing to check the year, for the reason described above: the loose parser will happily reinterpret an out-of-range year into an in-range one, defeating the check. Instead it first matches the submitted string against a strict ^\d{4}-\d{2}-\d{2}$ shape, then confirms component validity with \DateTime::createFromFormat('Y-m-d', $value) plus \DateTime::getLastErrors(), and only then checks the captured 4-digit year against the configured range. Any string that doesn't match that literal shape (including inputs a loose parser would otherwise "fix," such as single-digit month/day, or years reinterpreted to a different year entirely) is rejected outright.
The year range is exposed as two class constants (YEAR_RANGE_MIN / YEAR_RANGE_MAX) rather than hardcoded inline, so a future iteration could make it configurable per-filter (matching how #date_year_range is configurable on the 'datetime' element) without changing the validation shape.
Remaining tasks
- Review/test the patch against Date Popup's existing test coverage, if any (module has no automated tests currently — see #3297117).
- Confirm behavior for the "between"/min-max exposed operator (two-input range filters), not just the single-value case — the patch adds validation to both
minandmaxsub-elements, but this hasn't been exercised against a live "between" filter, only the single-value>=/<=case. - Decide whether the year range should become a configurable option (e.g. via the Views UI filter settings) rather than a fixed 1900–2050 constant, to match
#date_year_range's flexibility on core's'datetime'element.
User interface changes
The exposed date input gains HTML min/max attributes (1900-01-01 / 2050-12-31), which most browsers surface as a native inline validation hint/tooltip on their own date-picker widget. Submitting an out-of-range or malformed date now produces a standard Drupal form validation error ("The %field date is invalid. Date should be in the %min-%max year range.") instead of silently succeeding.
API changes
DatePopupTrait gains two new class constants (YEAR_RANGE_MIN, YEAR_RANGE_MAX) and two new protected/public static methods (applyYearRangeValidation(), validateYearRange()). Both are additive; no existing public API is changed or removed.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #2 | date_popup-3609564-2.patch | 4.55 KB | ckng |
Comments
Comment #2
ckng