Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Upgrading from v4.3.3
- `Prado\Web\TUri` is now a PSR-7 `UriInterface`: scheme/host are lower-cased, path/query/fragment are
percent-encoded, and `getPort()` returns `?int` (`null` when absent or scheme default).
- TDatePicker::InputMode changed to TDatePicker::DateInputMode - due to conflict with TWebControl::InputMode
- TActiveFileUpload now validates by default: attached validators run client side before the upload starts, and the page
validates the ValidationGroup during the upload callback before OnFileUpload is raised. Set CausesValidation=false to
restore the previous behavior.

Upgrading from v4.3.2
---------------------
Expand Down
14 changes: 14 additions & 0 deletions agents/framework/Web/UI/ActiveControls/TActiveFileUpload.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,22 @@ Async file upload using hidden iframe. Does postback in hidden iframe followed b
- `onFileUpload($param)` - Event raised when file upload completes
- `getFiles()` - Gets uploaded file items
- `getBusyImage()`, `getSuccessImage()`, `getErrorImage()` - Status indicator images
- `getCausesValidation()` / `setCausesValidation($value)` - Validation integration on/off, default true (@since 4.4.0)
- `getValidationGroup()` / `setValidationGroup($value)` - Group the page validates during the upload callback (@since 4.4.0)
- `getClientClassName()` - Returns `Prado.WebUI.TActiveFileUpload`

## Validation Integration (@since 4.4.0)

With `CausesValidation` (default true) and a [TFileValidator](../WebControls/TFileValidator.md)/[TImageValidator](../WebControls/TImageValidator.md) attached to the control:

- **Client:** `fileChanged()` runs `manager.validateControl(inputID)` before the iframe submit; an invalid selection skips the upload and the validators display their messages. Guarded — pages without the validator script or a validation manager upload as before.
- **Server:** `raiseCallbackEvent()` calls `$page->validate(ValidationGroup)` after `loadPostData()` and before raising `OnFileUpload`. The handler checks `$this->getPage()->getIsValid()` (or the upload's `IsValid`) before `saveAs()`. The temp files persist in `TempPath` during the callback, so `finfo`/`getimagesize` sniffing works.
- **Timing trap:** validate during the upload callback, never on a later postback — a successful upload clears the input and `onUnload` deletes the temp files, so later validators see an empty selection and pass vacuously.
- Give the upload and its validators a dedicated `ValidationGroup` so the upload callback does not validate unrelated page controls.
- The status icons reflect transfer status only; server-side validation feedback is the `OnFileUpload` handler's responsibility (e.g. via an active label).

Functional test: `tests/playwright/active-controls/ActiveFileUploadValidatorTestCase.spec.js` + `TActiveFileUploadValidatorTest.page`.

## See Also

- `TFileUpload`, [ICallbackEventHandler](./ICallbackEventHandler.md), [TActiveFileUploadItem](./TActiveFileUploadItem.md)
2 changes: 2 additions & 0 deletions agents/framework/Web/UI/WebControls/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ All validators extend `TBaseValidator`. Shared properties: `ControlToValidate`,
| `TCustomValidator` | Custom server-side (and optional client-side) logic |
| `TEmailAddressValidator` | Valid email format |
| `TDataTypeValidator` | Value is correct data type (integer, date, etc.) |
| `TFileValidator` | Files selected in a `TFileUpload`: size, count, extension, MIME type (@since 4.4.0) |
| `TImageValidator` | Image files selected in a `TFileUpload`: TFileValidator checks plus pixel dimensions and a readable-image check (@since 4.4.0) |
| `TValidationSummary` | Displays all errors in a group; `DisplayMode` (List/BulletList/SingleParagraph) |

## Semantic HTML5 Controls (@since 4.3.3)
Expand Down
4 changes: 4 additions & 0 deletions agents/framework/Web/UI/WebControls/TFileUpload.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Since Prado 4.0, TFileUpload supports multiple-file uploads via the `Multiple` p

TFileUpload automatically sets `enctype="multipart/form-data"` on the page form during `onPreRender` (via `TForm::setEnctype` or, in callback mode, via `TCallbackClient::setAttribute`).

Since Prado 4.4.0, the `Accept` property renders the HTML5 `accept` attribute to filter the file types offered in the browser file picker, and [TFileValidator](./TFileValidator.md) validates the selected files (size, count, extension, MIME type) on both client and server.

## Inheritance

`TFileUpload` → `TWebControl` → `TControl` → `TComponent`
Expand All @@ -32,6 +34,8 @@ Implements: `IPostBackDataHandler`, `IValidatable`
|---|---|---|---|
| `Multiple` | bool | `false` | Enables multi-file upload. Adds `multiple` attribute and `[]` to the `name`. |
| `MaxFileSize` | int | `1048576` | Advisory maximum size (bytes). Written to a hidden field; enforced by PHP, not the browser. |
| `Accept` | string | `''` | Comma-separated file type specifiers (`.jpg`, `image/png`, `image/*`) rendered as the `accept` attribute. Advisory; pair with `TFileValidator`. (@since 4.4.0) |
| `Capture` | string | `''` | `user` or `environment`, rendered as the `capture` attribute so mobile browsers capture new media with that device. Desktop browsers ignore it. (@since 4.4.0) |
| `IsValid` | bool | `true` | Writeable by validators. |

## Key Methods
Expand Down
71 changes: 71 additions & 0 deletions agents/framework/Web/UI/WebControls/TFileValidator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Web/UI/WebControls/TFileValidator

### Directories
[framework](../../../INDEX.md) / [Web](../../INDEX.md) / [UI](../INDEX.md) / [WebControls](./INDEX.md) / **`TFileValidator`**

## Class Info
**Location:** `framework/Web/UI/WebControls/TFileValidator.php`
**Namespace:** `Prado\Web\UI\WebControls`
**Since:** 4.4.0 (issue #636)

## Overview
TFileValidator validates the files selected in a [TFileUpload](./TFileUpload.md) (or `TActiveFileUpload`) control. Each file is checked against size, extension, and MIME type restrictions; the file count is checked against min/max limits. With `EnableClientScript` (default true) the same checks run in the browser through the HTML5 File API before the files upload, avoiding the transfer of files the server would reject.

Validation succeeds when no file is selected — pair with `TRequiredFieldValidator` to require a selection. A file with a PHP upload error code (`UPLOAD_ERR_FORM_SIZE`, `UPLOAD_ERR_PARTIAL`, …) fails validation.

## Inheritance

`TFileValidator` → `TBaseValidator` → `TLabel` → `TWebControl` → `TControl` → `TComponent`

Client-side class: `Prado.WebUI.TFileValidator` (`framework/Web/Javascripts/source/prado/validator/validation3.js`).

## Key Properties

| Property | Type | Default | Description |
|---|---|---|---|
| `MaxFileSize` | int | `0` | Maximum bytes per file. `0` → falls back to the target's `TFileUpload::MaxFileSize` (1 MB default). |
| `MinFileSize` | int | `0` | Minimum bytes per file. `0` disables the check. |
| `TotalMaxFileSize` | int | `0` | Maximum combined bytes of all selected files. `0` disables the check. Helps a `Multiple` selection stay under `post_max_size`. (@since 4.4.0) |
| `MaxFileCount` | int | `0` | Maximum number of files. `0` disables the check. |
| `MinFileCount` | int | `0` | Minimum number of files when at least one is selected. `0` disables the check. |
| `AllowedFileExtensions` | string | `''` | Comma/space separated extensions (`"jpg, png"` or `".jpg"`), case-insensitive. |
| `AllowedFileTypes` | string | `''` | Comma/space separated MIME types; `image/*` matches every subtype. |
| `CheckExtensionMimeType` | bool | `false` | Server-only. The `fileinfo`-sniffed content type must correspond to the file name extension (map in `static::$extensionMimeTypes`, extendable by subclasses). Detects renamed files. Unknown extensions, extensionless names, and unavailable sniffing pass. |
| `InvalidFileNames` | string[] | `[]` | Read-only. Names of the files that failed the last validation. |

## Matching Semantics

| Configuration | Rule |
|---|---|
| `AllowedFileExtensions` and/or `AllowedFileTypes` set | The file must match every non-empty list (AND). |
| Both empty, target has `Accept` property or `accept` attribute | Restrictions derive from the Accept tokens; the file must match any token (OR), mirroring the HTML file picker. `.jpg` tokens match the extension, MIME tokens match the type. |
| Both empty, no Accept | Only size, count, and error-code checks apply. |

## Server-side MIME Detection

`getFileMimeType()` sniffs the uploaded file content with the `fileinfo` extension when available (`finfo_file` on `LocalName`), falling back to the untrusted browser-supplied `FileType`. The client side can only check the browser-reported `file.type`.

## `{files}` ErrorMessage Token

`ErrorMessage="Wrong type: {files}"` — both sides replace `{files}` with the comma-separated invalid file names (HTML-encoded server-side; `textContent` client-side). The client options keep the raw token (`getClientScriptOptions()` resets `ErrorMessage` to the unsubstituted value).

## Patterns & Gotchas

- **Target must be a TFileUpload** — `evaluateIsValid()` throws `TConfigurationException` (`filevalidator_fileupload_required`) otherwise.
- **The client JS does not use `getValidationValue()`** — it reads `this.control.files` directly, so `TBaseValidator::$_clientClass` and the `getRawValidationValue()` switches stay untouched and `TRequiredFieldValidator` on file inputs keeps its `control.value` fakepath behavior.
- **`MaxFileSize=0` still enforces a limit** — the target's `MaxFileSize` (default 1 MB) is the fallback; PHP enforces the same value through the `MAX_FILE_SIZE` hidden field anyway.
- **Client `file.type` can be empty** for unknown types — a type-restricted validator then rejects the file client-side; the server re-checks with `fileinfo`.

## Subclasses

[TImageValidator](./TImageValidator.md) adds pixel dimension restrictions and a readable-image check.

## TActiveFileUpload Integration (@since 4.4.0)

`TActiveFileUpload` with `CausesValidation` (default true) runs this control's validators client-side before its auto-upload starts and validates the page server-side during the upload callback — see [TActiveFileUpload](../ActiveControls/TActiveFileUpload.md) for the timing trap (validate during the callback, not a later postback).

## Tests

- Unit: `tests/unit/Web/UI/WebControls/TFileValidatorTest.php`, `TFileUploadTest.php`
- JS (vitest): `tests/js/validator/filevalidator.test.js`
- Functional (Playwright): `tests/playwright/validators/FileValidatorTestCase.spec.js` + `tests/harness/validators/protected/pages/FileValidator.page`
46 changes: 46 additions & 0 deletions agents/framework/Web/UI/WebControls/TImageValidator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Web/UI/WebControls/TImageValidator

### Directories
[framework](../../../INDEX.md) / [Web](../../INDEX.md) / [UI](../INDEX.md) / [WebControls](./INDEX.md) / **`TImageValidator`**

## Class Info
**Location:** `framework/Web/UI/WebControls/TImageValidator.php`
**Namespace:** `Prado\Web\UI\WebControls`
**Since:** 4.4.0

## Overview
TImageValidator extends [TFileValidator](./TFileValidator.md): every file restriction applies, and each file must additionally be a readable image whose pixel dimensions satisfy the bounds. Server side uses `getimagesize()`; a file that cannot be read as an image (or has no local temp file) fails closed.

## Inheritance

`TImageValidator` → `TFileValidator` → `TBaseValidator` → `TLabel` → …

Client-side class: `Prado.WebUI.TImageValidator` (validation3.js), extends `Prado.WebUI.TFileValidator` via `$super` injection.

## Key Properties

| Property | Type | Default | Description |
|---|---|---|---|
| `MinImageWidth` / `MaxImageWidth` | int | `0` | Pixel width bounds. `0` disables either check. |
| `MinImageHeight` / `MaxImageHeight` | int | `0` | Pixel height bounds. `0` disables either check. |

## Client-side Asynchronous Decode

Prado client validation is synchronous, so the JS class decodes dimensions out of band:

- `onInit()` observes the input's `change` event; each selection change drops the cache (`_imageInfo`, keyed by `name|size|lastModified`) and decodes every file via `URL.createObjectURL` + `Image`.
- `evaluateIsValid()` → `isValidFile()`: an undecoded (pending or uncached) file **passes** client-side; when a decode completes, `revalidate()` re-runs `validate()` + `updateSummary()` if results are already displayed (`this.visible`).
- A submit that outruns the decode therefore posts back and the authoritative server-side validation catches the file. The Playwright tests accept either path.
- Browsers without `URL.createObjectURL` skip the client dimension checks entirely (`canReadImages()` guard); jsdom tests seed `_imageInfo` directly.

## Patterns & Gotchas

- **Fails closed server-side** — a missing/unreadable temp file or a `getimagesize()` failure is invalid, unlike TFileValidator checks that pass when unverifiable.
- **notImage detection** — client: `Image.onerror`; server: `getimagesize() === false`. Both fail the file.
- Dimension property names carry the `Image` infix (`MinImageWidth`, not `MinWidth`) to avoid confusion with the validator control's own `Width`/`Height` style properties.

## Tests

- Unit: `tests/unit/Web/UI/WebControls/TImageValidatorTest.php` (crafts minimal GIF headers — `'GIF89a' . pack('v',$w) . pack('v',$h) . "\x00\x00\x00"` — which `getimagesize()` reads)
- JS (vitest): `tests/js/validator/imagevalidator.test.js` (seeds the dimension cache; jsdom cannot decode)
- Functional (Playwright): `tests/playwright/validators/ImageValidatorTestCase.spec.js` + `ImageValidator.page`, real PNGs built by `tests/playwright/validators/png.js`
2 changes: 2 additions & 0 deletions framework/Exceptions/messages/messages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ basevalidator_forcontrol_unsupported = {0}.ForControl is not supported.

comparevalidator_controltocompare_invalid = TCompareValidator.ControlToCompare contains an invalid control ID path.

filevalidator_fileupload_required = {0}.ControlToValidate must point to a TFileUpload control.

listcontrolvalidator_invalid_control = {0}.ControlToValidate contains an invalid TListControl ID path, "{1}" is a {2}.

repeater_template_required = TRepeater.{0} requires a template instance implementing ITemplate interface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Prado.WebUI.TActiveFileUpload = Prado.Class(Prado.WebUI.Control,
fileChanged() {
// ie11 fix
if(this.input.value=='') return;
// let the validators of the file input block an invalid selection
if(!this.validateFiles()) return;
// show the upload indicator, and hide the complete and error indicators (if they areSn't already).
this.flag.value = '1';
this.complete.style.display = 'none';
Expand All @@ -47,6 +49,21 @@ Prado.WebUI.TActiveFileUpload = Prado.Class(Prado.WebUI.Control,
this.form.enctype = this.oldFormEnctype;
},

/**
* Run the validators attached to the file input before the upload starts.
* Passes when causesValidation is off, the validation script is not loaded,
* or the form has no validation manager.
* @return {boolean} true when the selected files may upload.
*/
validateFiles() {
if(!this.options.causesValidation || typeof(Prado.Validation) == "undefined")
return true;
const manager = Prado.Validation.managers[this.options.formID];
if(!manager)
return true;
return manager.validateControl(this.options.inputID);
},

finishUpload(options) {

if (this.options.targetID == options.targetID)
Expand Down
Loading
Loading