<?php
namespace Lia\Addon;
/**
* When activated, it unsets `$_POST` and then only re-sets it after validation
*/
class POSTValidator extends \Lia\Addon {
public string $fqn = 'lia:server.postvalidator';
public readonly \Lia\Utility\InputValidator $validator;
/**
* @key the POST key. i.e. `$_POST['key']`
* @value the list of rules that failed. See InputValidator::$failed_rules
*/
public array $failures = [];
/**
* `$_POST` is copied to here
*/
protected array $post;
/**
* Failure messages retrieved from the session.
*/
protected ?array $session_failure_messages;
/** true if any validation has failed */
protected bool $validation_failed = false;
/**
* Enable for the given instance of liaison. Sets `$_POST = []`. Re-populates `$_POST` when validations are successful.
*/
static public function activate(\Lia $lia){
$instance = static::from($lia);
$instance->post = $_POST;
$_POST = [];
}
/**
* Hooks into \Lia\Hooks::ROUTES_FILTERED, to activate only if the given package is requested by the route.
*/
static public function activateForPackage(\Lia\Package $package){
\Lia\Addon\Hook::from($package->lia)
->add(\Lia\Hooks::ROUTES_FILTERED,
function(\Lia\Obj\Route $route) use ($package): void {
if ($route->package() != $package)return;
static::activate($package->lia);
}
);
}
static public function from(\Lia $lia): self|static {
return parent::from($lia);
}
public function __construct(?\Lia\Package $package=null){
parent::__construct($package);
$this->validator = new \Lia\Utility\InputValidator();
}
/** return true if any validation failed on the current request */
public function validation_failed(): bool {
return $this->validation_failed;
}
/**
* Print all $_POST values and `exit`
*/
public function dump(){
print_r($this->post);
exit;
}
/**
* Check if the given entry matches expectations. You may pass BOTH `$rules` AND `$validation_function`, or you may pass just one (*your choice*). If you pass both, then both `$rules` AND `$validation_function` MUST return `TRUE` for validation to pass. If you pass only `$key`, then it is considered valid as long as `$key` is set in `$_POST`. When validation passes, `$_POST[$key]` is populated with the validated value.
*
* @param string $key the `$_POST['key']`
* @param array $rules Rules to validate using the built-in validator class.
* @param callable $validation_function a user-defined `function(string $key, mixed $value): bool` that returns TRUE (*valid*) or FALSE (*invalid*)
* @return bool TRUE (valid) or FALSE (invalid)
*/
public function validate(string $key, array|string|null $rules = null, ?callable $validation_function = null): bool {
if (!isset($this->post)){
throw new \Lia\Exception(\Lia\Exception::VALIDATOR_NOT_ACTIVE);
}
if (!isset($this->post[$key])){
// TODO: Report that value was not submitted
// NOTE: Perhaps we need a 'required' rule
return false;
}
$is_valid = true;
if ($rules != null && $rules != []){
$is_valid = $this->validator->validate($rules, $this->post[$key], null, $this->post[$key]);
}
if ($is_valid && $validation_function != null){
$is_valid = call_user_func($validation_function, $key, $this->post[$key]);
}
if (!$is_valid){
$this->failures[$key] = $this->validator->failed_rules;
$this->validation_failed = true;
return false;
}
$_POST[$key] = $this->post[$key];
return true;
}
/**
* Get array of failure messages determined during this request
*
* @return array<string POST_key, array failmsgs> where failmsgs is array<int index, string message>
*/
public function get_failure_messages(): array {
if (isset($this->session_failure_messages)){
return $this->session_failure_messages;
}
// TODO: we can only get fail messages of rules that are built-in to InputValidator. Non-builtins will cause an exception to be thrown by get_failure_message(). We need ... some kind of solution.
$msgs = [];
foreach ($this->failures as $post_key => $rules_failed){
$msgs[$post_key] = [];
foreach ($rules_failed as $index => $rule){
$rule_name = $rule[0];
$rule_options = $rule[1];
$rule_msg = $this->validator->get_failure_message($rule_name, $rule_options);
$msgs[$post_key][] = $rule_msg;
}
}
return $msgs;
}
/**
* Store failure messages in `$_SESSION` to be retrieved by and displayed on a form after redirect. Silently fails if sessions are disabled.
*/
public function store_failure_messages(){
if (session_status() == PHP_SESSION_NONE){
session_start();
}
$msgs = $this->get_failure_messages();
$_SESSION['postvalidator-failure-messages'] = $msgs;
}
/**
* If there are failure messages stored in the session, they will be retrieved, and then removed from the session. If none are stored, nothing happens (*no errors*)
*
*/
public function load_failure_messages_from_session(){
if (session_status() == PHP_SESSION_NONE){
session_start();
}
if (!isset($_SESSION['postvalidator-failure-messages'])){
return;
}
$this->session_failure_messages = $_SESSION['postvalidator-failure-messages'];
unset($_SESSION['postvalidator-failure-messages']);
return $this->session_failure_messages;
}
/**
* Get the first failure message for the given POST key
*
* @return empty string if there is no failure. Return generic string if there was a failure but no defined message.
*/
public function get_failure_message(string $for_key): string {
if (!isset($this->failures[$for_key])
&& !isset($this->session_failure_messages[$for_key])
)return '';
$messages = $this->get_failure_messages();
if (!isset($messages[$for_key])
||count($messages[$for_key]) == 0) return 'Invalid input';
return $messages[$for_key][0];
}
}