pics/models/Form.php

346 lines
8.6 KiB
PHP

<?php
/*****************************************************************************
* Form.php
* Contains key class Form.
*
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
*****************************************************************************/
class Form
{
public $request_method;
public $request_url;
public $content_above;
public $content_below;
private $fields = [];
private $data = [];
private $missing = [];
private $submit_caption;
private $trim_inputs;
// NOTE: this class does not verify the completeness of form options.
public function __construct($options)
{
$this->request_method = !empty($options['request_method']) ? $options['request_method'] : 'POST';
$this->request_url = !empty($options['request_url']) ? $options['request_url'] : BASEURL;
$this->fields = !empty($options['fields']) ? $options['fields'] : [];
$this->content_below = !empty($options['content_below']) ? $options['content_below'] : null;
$this->content_above = !empty($options['content_above']) ? $options['content_above'] : null;
$this->submit_caption = !empty($options['submit_caption']) ? $options['submit_caption'] : 'Save information';
$this->trim_inputs = !empty($options['trim_inputs']);
}
public function getFields()
{
return $this->fields;
}
public function getData()
{
return $this->data;
}
public function getSubmitButtonCaption()
{
return $this->submit_caption;
}
public function getMissing()
{
return $this->missing;
}
public function setData($data)
{
$this->verify($data, true);
$this->missing = [];
}
public function setFieldAsMissing($field)
{
$this->missing[] = $field;
}
public function verify($post, $initalisation = false)
{
$this->data = [];
$this->missing = [];
foreach ($this->fields as $field_id => $field)
{
// Field disabled?
if (!empty($field['disabled']))
{
$this->data[$field_id] = '';
continue;
}
// No data present at all for this field?
if ((!isset($post[$field_id]) || $post[$field_id] == '') &&
$field['type'] !== 'captcha')
{
if (empty($field['is_optional']))
$this->missing[] = $field_id;
if ($field['type'] === 'select' && !empty($field['multiple']))
$this->data[$field_id] = [];
else
$this->data[$field_id] = '';
continue;
}
// Should we trim this?
if ($this->trim_inputs && $field['type'] !== 'captcha' && empty($field['multiple']))
$post[$field_id] = trim($post[$field_id]);
// Using a custom validation function?
if (isset($field['validate']) && is_callable($field['validate']))
{
// Validation functions can clean up the data if passed by reference
$this->data[$field_id] = $post[$field_id];
// Evaluate validation functions as boolean to see if data is missing
if (!$field['validate']($post[$field_id]))
$this->missing[] = $field_id;
continue;
}
// Verify data by field type
switch ($field['type'])
{
case 'select':
case 'radio':
$this->validateSelect($field_id, $field, $post);
break;
case 'checkbox':
// Just give us a 'boolean' int for this one
$this->data[$field_id] = empty($post[$field_id]) ? 0 : 1;
break;
case 'color':
$this->validateColor($field_id, $field, $post);
break;
case 'file':
// Asset needs to be processed out of POST! This is just a filename.
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
break;
case 'numeric':
$this->validateNumeric($field_id, $field, $post);
break;
case 'captcha':
if (isset($_POST['g-recaptcha-response']) && !$initalisation)
$this->validateCaptcha($field_id);
elseif (!$initalisation)
{
$this->missing[] = $field_id;
$this->data[$field_id] = 0;
}
break;
case 'text':
case 'textarea':
default:
$this->validateText($field_id, $field, $post);
}
}
}
private function validateCaptcha($field_id)
{
$postdata = http_build_query([
'secret' => RECAPTCHA_API_SECRET,
'response' => $_POST['g-recaptcha-response'],
]);
$opts = [
'http' => [
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => $postdata,
]
];
$context = stream_context_create($opts);
$result = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
$check = json_decode($result);
if ($check->success)
{
$this->data[$field_id] = 1;
}
else
{
$this->data[$field_id] = 0;
$this->missing[] = $field_id;
}
}
private function validateColor($field_id, array $field, array $post)
{
// Colors are stored as a string of length 3 or 6 (hex)
if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6))
{
$this->missing[] = $field_id;
$this->data[$field_id] = '';
}
else
$this->data[$field_id] = $post[$field_id];
}
private function validateNumeric($field_id, array $field, array $post)
{
$data = isset($post[$field_id]) ? $post[$field_id] : '';
// Sanity check: does this even look numeric?
if (!is_numeric($data))
{
$this->missing[] = $field_id;
$this->data[$field_id] = 0;
return;
}
// Do we need to a minimum bound?
if (isset($field['min_value']))
{
if (is_float($field['min_value']) && (float) $data < $field['min_value'])
{
$this->missing[] = $field_id;
$this->data[$field_id] = 0.0;
}
elseif (is_int($field['min_value']) && (int) $data < $field['min_value'])
{
$this->missing[] = $field_id;
$this->data[$field_id] = 0;
}
}
// What about a maximum bound?
if (isset($field['max_value']))
{
if (is_float($field['max_value']) && (float) $data > $field['max_value'])
{
$this->missing[] = $field_id;
$this->data[$field_id] = 0.0;
}
elseif (is_int($field['max_value']) && (int) $data > $field['max_value'])
{
$this->missing[] = $field_id;
$this->data[$field_id] = 0;
}
}
$this->data[$field_id] = $data;
}
private function validateSelect($field_id, array $field, array $post)
{
// Skip validation? Dangerous territory!
if (isset($field['verify_options']) && $field['verify_options'] === false)
{
$this->data[$field_id] = $post[$field_id];
return;
}
// Check whether selected option is valid.
if (($field['type'] !== 'select' || empty($field['multiple'])) && empty($field['has_groups']))
{
if (isset($post[$field_id]) && !isset($field['options'][$post[$field_id]]))
{
$this->missing[] = $field_id;
$this->data[$field_id] = '';
return;
}
else
$this->data[$field_id] = $post[$field_id];
}
// Multiple selections involve a bit more work.
elseif (!empty($field['multiple']) && empty($field['has_groups']))
{
$this->data[$field_id] = [];
if (!is_array($post[$field_id]))
{
if (isset($field['options'][$post[$field_id]]))
$this->data[$field_id][] = $post[$field_id];
else
$this->missing[] = $field_id;
return;
}
foreach ($post[$field_id] as $option)
{
if (isset($field['options'][$option]))
$this->data[$field_id][] = $option;
}
if (empty($this->data[$field_id]))
$this->missing[] = $field_id;
}
// Any optgroups involved?
elseif (!empty($field['has_groups']))
{
if (!isset($post[$field_id]))
{
$this->missing[] = $field_id;
$this->data[$field_id] = '';
return;
}
// Expensive: iterate over all groups until the value selected has been found.
foreach ($field['options'] as $label => $options)
{
if (is_array($options))
{
// Consider each of the options as a valid a value.
foreach ($options as $value => $label)
{
if ($post[$field_id] === $value)
{
$this->data[$field_id] = $options;
return;
}
}
}
else
{
// This is an ungrouped value in disguise! Treat it as such.
if ($post[$field_id] === $options)
{
$this->data[$field_id] = $options;
return;
}
else
continue;
}
}
// If we've reached this point, we'll consider the data invalid.
$this->missing[] = $field_id;
$this->data[$field_id] = '';
}
else
{
throw new UnexpectedValueException('Unexpected field configuration in validateSelect!');
}
}
private function validateText($field_id, array $field, array $post)
{
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
// Trim leading and trailing whitespace?
if (!empty($field['trim']))
$this->data[$field_id] = trim($this->data[$field_id]);
// Is there a length limit to enforce?
if (isset($field['maxlength']) && strlen($post[$field_id]) > $field['maxlength']) {
$post[$field_id] = substr($post[$field_id], 0, $field['maxlength']);
}
}
}