<?php
/*
* Copyright (C) 2017 Karmabunny Pty Ltd.
*
* This file is a part of SproutCMS.
*
* SproutCMS is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 2 of the License, or (at your option) any later version.
*
* For more information, visit <http://getsproutcms.com>.
*/
namespace Sprout\Helpers;
use InvalidArgumentException;
use Sprout\Exceptions\ValidationException;
/**
* New validation class for Sprout 3.
* Used with the {@see Validity} class.
*
* @example
* // Plain example
*
* $valid = new Validator($_POST);
*
* $valid->required(['name', 'email']);
*
* $valid->check('name', 'Validity::length', 1, 100);
* $valid->check('email', 'Validity::email');
*
* if ($valid->hasErrors()) {
* $_SESSION['register']['field_errors'] = $valid->getFieldErrors();
* $valid->createNotifications();
* Url::redirect('user/register');
* }
* @example
* // Multiedit example for a course with students
*
* $has_error = false;
*
* $valid = new Validator($_POST);
* $valid->required(['name']);
* $valid->check('name', 'Validity::length', 1, 100);
*
* if ($valid->hasErrors()) {
* $_SESSION['course_edit']['field_errors'] = $valid->getFieldErrors();
* $valid->createNotifications();
* $has_error = true;
* }
*
* if (empty($_POST['multiedit_students'])) {
* $_POST['multiedit_students'] = [];
* }
*
* $record_num = 0;
* foreach ($_POST['multiedit_students'] as $idx => $data) {
* if (MultiEdit::recordEmpty($data)) continue;
*
* ++$record_num;
*
* $multi_valid = new Validator($data);
* $multi_valid->setLabels([
* 'name' => 'Name for student ' . $record_num,
* 'email' => 'Email address for student ' . $record_num,
* ]);
*
* $multi_valid->required(['name', 'email']);
* $multi_valid->check('name', 'Validity::length', 1, 100);
* $multi_valid->check('email', 'Validity::email');
*
* if ($multi_valid->hasErrors()) {
* $_SESSION['course_edit']['field_errors']['multiedit_students'][$idx] = $multi_valid->getFieldErrors();
* $multi_valid->createNotifications();
* $has_error = true;
* }
* }
*
* if ($has_error) {
* Url::redirect('course/edit');
* }
*/
class Validator
{
protected $labels;
protected $data;
protected $field_errors;
protected $general_errors;
/**
* Recursive trim data
*
* Alters in-place AND returns the array
* This allows for use such as:
*
* $_SESSION['register']['field_values'] = Validator::trim($_POST);
*
* When used like this, the session gets set and the POST data is also trimmed,
* so can be used directly for database inserts.
*
* @param array $data Data to trim. Passed by-reference.
* @return array Trimmed data
*/
{
foreach ($data as $key => $val) {
$data[$key] = trim($val); }
}
return $data;
}
/**
* @param array $data Data to validate
*/
public function __construct
(array $data) {
$this->labels = null;
$this->data = $data;
$this->field_errors = array(); $this->general_errors = array(); }
/**
* Field labels make error messages a little friendlier
*
* @param array $labels Field labels
*/
public function setLabels
(array $labels) {
$this->labels = $labels;
}
/**
* Update the data to validate
*
* @param array $data Data to validate
*/
public function setData
(array $data) {
$this->data = $data;
}
/**
* Set the value for a single data field
*
* @param string $field The field to set
* @param mixed $value The value to set on the field
*/
public function setFieldValue($field, $value)
{
$this->data[$field] = $value;
}
/**
* Set the label for a single field
*
* @param string $field The field to set
* @param string $label The label to set on the field
*/
public function setFieldLabel($field, $label)
{
$this->labels[$field] = $label;
}
/**
* For a given function, expand the namespace for Sprout helpers
*
* @param callable|string $func The function to expand with the Sprout\Helpers namespace.
* Strings representing function names are affected; closures are not.
* @return callable $func
*/
protected function expandNs($func)
{
$class = Sprout::nsClass($class, ['Sprout\Helpers']);
$func = $class . '::' . $func;
}
return $func;
}
/**
* Check the value of a field against a validation method, storing any error messages received
* Additional arguments are passed to the underlying method
*
* Methods which are on classes within the Sprout\Helpers namespace do not need the namespace
* specified on the function name
*
* If a field has already been checked with {@see Validator::required} and the field was empty,
* this function will not report errors (but will still return an appropriate value)
*
* If a empty value is provided, it is not validated - returns true
*
* @param string $field_name The field to check
* @param callable $func The function or method to call.
* @return bool True if validation was successful, false if it failed
*/
public function check($field_name, $func)
{
if (!isset($this->data[$field_name]) or
self::isEmpty($this->data[$field_name])) { return true;
}
$func = self::expandNs($func);
try {
return true;
} catch (ValidationException $ex) {
$this->addFieldError($field_name, $ex->getMessage());
return false;
}
}
/**
* Run a validation check against each value in an array.
* Behaviour is very similar to the {@see Validator::check} method.
*
* Only supports single-depth arrays
*
* Errors are put into the field_errors array under a subkey matching the array key
*
* Return value is an array of key => boolean with the validation result for each key
*
* @example
* $data = ['vals' => [1, 2, 'A', 'B', 5]];
* $validator = new Validator($data);
* $result = $validator->arrayCheck('vals', 'Validity::positiveInt');
* // $result now contains [true, true, false, false, true]
* $errs = $validator->getFieldErrors();
* // $errs now contains [ 'vals' => [2 => [...], 3 => [...]] ]
*
* @param string $field_name The field to check
* @param callable $func The function or method to call.
* @return array Key => Boolean True if validation was successful, false if it failed
*/
public function arrayCheck($field_name, $func)
{
if (!isset($this->data[$field_name]) or
self::isEmpty($this->data[$field_name])) { return true;
}
if (!is_array($this->data[$field_name])) { throw new InvalidArgumentException("Field <{$field_name}> is not an array");
}
$func = self::expandNs($func);
$results = [];
foreach ($this->data[$field_name] as $index => $value) {
$args[0] = $value;
try {
$results[$index] = true;
} catch (ValidationException $ex) {
$this->addArrayFieldError($field_name, $index, $ex->getMessage());
$results[$index] = false;
}
}
return $results;
}
/**
* Check multiple fields against a validation method
*
* This is similar to {@see Validator::check} but it's designed for different validation
* methods, which work on a set of fields instead of a single field (e.g. Validity::oneRequired)
*
* Additional arguments are passed to the underlying method
* Methods which are on classes within the Sprout\Helpers namespace do not need the namespace
* specified on the function name
*
* @param array $fields The fields to check
* @param callable $func The function or method to call.
* @return bool True if validation was successful, false if it failed
*/
public function multipleCheck
(array $fields, $func) {
$func = self::expandNs($func);
foreach ($fields as $field_name) {
$vals[] = @$this->data[$field_name];
}
try {
return true;
} catch (ValidationException $ex) {
$this->addMultipleFieldError($fields, $ex->getMessage());
return false;
}
}
/**
* Sadly, the PHP builtin empty() considers '0' to be empty, but it actually isn't
*
* @param mixed $val
* @return bool True if empty, false if not.
*/
public static function isEmpty($val)
{
return true;
} else if ($val == '') {
return true;
}
return false;
}
/**
* Checks various fields are required
* If a field is required and no value is provided, no other validation will be proessed for that field.
*
* @param array $fields Fields to check
*/
public function required
(array $fields) {
foreach ($fields as $field_name) {
if (!isset($this->data[$field_name])) { $this->field_errors[$field_name] = ['required' => 'This field is required'];
} elseif (self::isEmpty($this->data[$field_name])) {
$this->field_errors[$field_name] = ['required' => 'This field is required'];
}
}
}
/**
* Add an error message for a given field to the field errors list
*
* @param string $field_name The field to add the error message for
* @param string $message The message text
*/
public function addFieldError($field_name, $message)
{
if (!isset($this->field_errors[$field_name])) { $this->field_errors[$field_name] = [$message];
} else {
$this->field_errors[$field_name][] = $message;
}
}
/**
* Add an error message for a given field to the field errors list
* This variation is for array validation, e.g. an array of integers
*
* @param string $field_name The field to add the error message for
* @param int $index The array index of the field to report error for
* @param string $message The message text
*/
public function addArrayFieldError($field_name, $index, $message)
{
if (!isset($this->field_errors[$field_name])) { $this->field_errors[$field_name] = [];
}
if (!isset($this->field_errors[$field_name][$index])) { $this->field_errors[$field_name][$index] = [$message];
} else {
$this->field_errors[$field_name][$index][] = $message;
}
}
/**
* Add an error message from a multiple-field validation (e.g. checking at least one is set)
*
* @param array $fields The fields to add the error message for
* @param string $message The message text
*/
public function addMultipleFieldError
(array $fields, $message) {
foreach ($fields as $f) {
$this->addFieldError($f, $message);
}
}
/**
* Get an array of all field errors, indexed by field name
* Fields may have multiple errors defined
*
* @return array
*/
public function getFieldErrors()
{
return $this->field_errors;
}
/**
* Add a general error message, e.g. for errors affecting many fields
*
* @param string $message The message text
*/
public function addGeneralError($message)
{
$this->general_errors[] = $message;
}
/**
* Get an array of all general errors
*
* @return array
*/
public function getGeneralErrors()
{
return $this->general_errors;
}
/**
* @return bool True if there were any validation errors, false if there wasn't
*/
public function hasErrors()
{
if (count($this->field_errors)) return true; if (count($this->general_errors)) return true; return false;
}
/**
* Create notification error messages for each error
*
* @param string $scope Set the scope for the notifications
*/
public function createNotifications($scope = 'default')
{
foreach ($this->general_errors as $msg) {
Notification::error($msg, 'plain', $scope);
}
foreach ($this->field_errors as $field => $msg) {
if (isset($this->labels[$field])) { Notification
::error($this->labels[$field] . ' -- ' . implode(', ', $msg), 'plain', $scope); } else {
Notification
::error($label . ' -- ' . implode(', ', $msg), 'plain', $scope); }
}
}
}