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

  1. Create a Views exposed filter on a date field ordinarily rendered via the 'datetime' element (in our case, a smart_date field filtered via Drupal\smart_date\Plugin\views\filter\Date, which extends core's Drupal\views\Plugin\views\filter\Date).
  2. Enable Date Popup. Its buildExposedForm() hook (called from the filter's buildExposedForm()) unconditionally calls DatePopupHelper::applyDatePopup(), which forces the exposed element to #type = 'date' via DatePopupTrait::applyDatePopupToForm().
  3. Visit the view and submit the exposed filter with an out-of-range year, e.g. append ?field_date_time_value=99999-01-01 to 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).
  4. 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-01 to 2009-01-01 internally.)

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-31 by default, matching Datetime's default #date_year_range), giving the browser's native <input type="date"> its own client-side constraint.
  • Registers a new #element_validate callback, validateYearRange(), mirroring Datetime::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 min and max sub-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.

CommentFileSizeAuthor
#2 date_popup-3609564-2.patch4.55 KBckng

Comments

ckng created an issue. See original summary.

ckng’s picture

Status: Active » Needs review
StatusFileSize
new4.55 KB