Files
api/services/nginx/app/traits/form_t.php
T
Jeppe B 2a6a86c9c3 Resolve backend Qodana critical and high findings (#314)
Resolve recommended-profile Critical and High findings, retain narrow analyzer exceptions, and update the edge-broker WebSocket dependency to a non-vulnerable release.
2026-07-17 05:44:16 +02:00

870 lines
32 KiB
PHP

<?php
namespace traits;
use classes\response;
use Exception;
use forms\objects\book_wash_f;
use objects\department_variables_o;
use objects\departments_o;
use objects\form_submissions_o;
use objects\users_o;
use routes\formRoute;
trait form_t
{
/**
* The form identifier
* @var string $form_identifier The form identifier
*/
public string $form_identifier;
/**
* The form name
* @var string $form_name The form name
*/
public string $form_name;
/**
* The form description
* @var string $form_description The form description
*/
public string $form_description;
/**
* The form sanitized data
* @var array $form_sanitized_data The form sanitized data
*/
public array $form_sanitized_data;
/**
* The department id, if applicable
* @notation This should be used to store the department id, if applicable
* @see setDepartmentId()
* @see getDepartmentId()
* @var int|null $department_id The department id
*/
public int|null $department_id = null;
/**
* The user id, if applicable
* @notation This should be used to store the user id, if applicable
* @see setUserId()
* @see getUserId()
* @var int|null $user_id The user id
*/
public int|null $user_id = null;
/**
* The customer number, if applicable
* @notation This should be used to store the customer number, if applicable
* @see setCustomerNumber()
* @see getCustomerNumber()
* @var int|null $customer_number The customer number
*/
public int|null $customer_number = null;
/**
* The form fields
* @notation This should be used to store the form fields and their validation methods
* @var array $form_fields The form fields and their validation methods (E.g. ['name' => 'self::className()::validateString', 'email' => 'self::className()::validateEmail'])
* @see defineInputFields()
* @see requireInputFields()
* @see validateInput()
*/
public array $form_fields_validations = [];
/**
* The form fields options
* @notation This should be used to store the form fields options (e.g. ['field1' => ['option1' => 'Option 1', 'option2' => 'Option 2']])
* @var array $form_fields_options The form fields options
*/
public array $form_fields_options = [];
/**
* The form raw data / unsanitized input values
* @notation This should be used to store the form raw data (Unsanitized)
* @var array $form_unsanitized_data
*/
public array $form_unsanitized_data = [];
/**
* The form submissions object (When the form is submitted)
* @var form_submissions_o $form_submissions_o The form submissions object
* @see form_submissions_o
* @see submit()
*/
public form_submissions_o $form_submissions_o;
/**
* The form fields descriptions
* @var array $form_fields_metas The form fields descriptions
* @notation This should be used to store the form fields descriptions (e.g. ['field1' => 'Field 1', 'field2' => 'Field 2'])
* @see getFormFields()
* @see defineInputFields()
*/
public array $form_fields_metas = [];
/**
* The text displayed on the submit button
* @var string $submit_button_text The text displayed on the submit button
* @notation This should be used to store the text displayed on the submit button
* @see submit()
* @see setSubmitButtonText()
* @see getSubmitButtonText()
*/
public string $submit_button_text = 'Submit';
/**
* The form constructor (Runs the setup method)
* @notation This should be used to set the form identifier, name and description
* @throws Exception If the form identifier, name or description is empty
*/
public function __construct()
{
parent::__construct();
self::setup();
}
/**
* The form setup method, this is run when the form is created
* @throws Exception If the form identifier, name or description is empty
* @throws Exception If the form identifier is not a valid format (e.g. "FORM_IDENTIFIER")
*/
abstract public function setup();
/**
* The form run method, this is run when the form is submitted
* @throws Exception If the form input is not valid
* @throws Exception If the form was not submitted successfully
*/
public function submit(): void
{
// Validate the form identifier, name and description
self::validate();
// Require the input fields to be set
self::requireInputFields();
// Validate the form input
self::validateInput();
// Sanitize the form input
self::sanitize();
// Before save function
self::beforeSave();
// Save the form input to the database
self::save();
// Run the form after successful submission
self::afterSubmit();
}
/**
* Validate the form identifier, name and description
* @throws Exception If the form identifier, name or description is empty
*/
public function validate(): void
{
if (empty($this->form_identifier)) {
throw new Exception('The form identifier is empty');
}
if (empty($this->form_name)) {
throw new Exception('The form name is empty');
}
if (empty($this->form_description)) {
throw new Exception('The form description is empty');
}
}
/**
* Require input fields to be set
* @notation This should be used to check if the fields are set and if the input is valid
* @throws Exception If the input fields callable is not callable, or if the input fields are not set
* @see defineInputFields()
* @see validateInput()
*/
public function requireInputFields(): void
{
global /** @var response $response */
$response;
$errors = [];
$missing_fields = [];
$DATA = json_decode(file_get_contents('php://input'), true);
foreach ( $this->form_fields_validations as $field => $validation_method ) {
// Check if the post field is set
if (!isset($DATA[self::getFormFieldPrefix() . $field])) {
// Check if the field is required
if (self::isFieldRequired($field)) {
// If the field is required, add it to the missing fields
$missing_fields[] = self::getFormFieldPrefix() . $field;
}
} else {
// Check if the field is valid
if (is_callable($validation_method) || method_exists($this, $validation_method)) {
$callable = is_callable($validation_method) ? $validation_method : [$this, $validation_method];
try {
// Check if the field is multiple ( e.g. ['field1', 'field2'])
if (is_array($DATA[self::getFormFieldPrefix() . $field])) {
// If there's no value, check if the field is required
if (count($DATA[self::getFormFieldPrefix() . $field]) === 0) {
// If the field is required, add it to the missing fields
if (self::isFieldRequired($field)) {
$missing_fields[] = self::getFormFieldPrefix() . $field;
}
}
// Loop through the array and check if each value is valid
foreach ( $DATA[self::getFormFieldPrefix() . $field] as $value ) {
// Call the validation method
$is_valid = call_user_func($callable, $value);
if (!$is_valid) {
// If the value is not valid, add it to the errors
$errors[self::getFormFieldPrefix() . $field] = [
'error' => 'The field is invalid, please check the format',
'field' => self::getFormFieldPrefix() . $field,
];
}
}
} else {
// Call the validation method
$is_valid = call_user_func($callable, $DATA[self::getFormFieldPrefix() . $field]);
}
} catch (Exception $e) {
// If the validation method throws an exception, add it to the errors
$errors[self::getFormFieldPrefix() . $field] = [
'error' => 'The field is invalid, please check the format',
'field' => self::getFormFieldPrefix() . $field,
];
}
if (!$is_valid) {
// Check if the value is empty, and the field isn't required
// If the field is required, we don't need to add it to the errors
// If the field is not required, we need to add it to the errors
if (self::isFieldRequired($field) || !empty($DATA[self::getFormFieldPrefix() . $field])) {
// If the value is not valid, add it to the errors
$errors[self::getFormFieldPrefix() . $field] = [
'error' => 'The field is invalid, please check the format',
'field' => self::getFormFieldPrefix() . $field,
];
}
}
} else {
// Throw an error if the validation method is not callable
throw new Exception('The validation method is not callable for field: ' . self::getFormFieldPrefix() . $field . ' - ' . $validation_method);
}
}
// Add the field to the form unsanitized data
$this->form_unsanitized_data[self::getFormFieldPrefix() . $field] = $DATA[self::getFormFieldPrefix() . $field];
}
// Check if any fields are missing
if (!empty($missing_fields)) {
foreach ( $missing_fields as $field ) {
$errors[$field] = [
'error' => 'The field is required',
'field' => $field,
];
}
}
// Check if there are any errors
if (empty($errors)) {
// All fields are valid
return;
}
// Return an error
$response->error(
[
'message' => 'Input validation failed',
'errors' => $errors,
],
400,
);
}
/**
* Get the form field prefix
* @return string The form field prefix
*/
public function getFormFieldPrefix(): string
{
return $this->form_identifier . '_field_';
}
/**
* Check if the field is required
* @param string $field The field to check
* @return bool True if the field is required, false otherwise
*/
public function isFieldRequired(string $field): bool
{
// Check if the field is required
if (isset($this->form_fields_metas[$field]['required'])) {
return $this->form_fields_metas[$field]['required'];
}
return false;
}
/**
* The form input validation
* @notation This should be used to check if the fields are set and if the input is valid
* @throws Exception If the input is not valid
*/
abstract public function validateInput(): void;
/**
* The form input sanitization
* @throws Exception If the input is not valid
*/
abstract public function sanitize(): void;
/**
* The form before save method, this is run before the form is saved
* @throws Exception If the form was not submitted successfully
*/
abstract public function beforeSave(): void;
/**
* Save the form input to the database
* @throws Exception If the form was not submitted successfully
*/
public function save(): void
{
// Save the form input to the database
$form_submissions_o = new form_submissions_o;
$form_submissions_o->add(
$this->form_identifier,
$this->form_sanitized_data,
self::getUserId(),
self::getDepartmentId(),
self::getCustomerNumber(),
);
// Set the form submissions object
$this->form_submissions_o = $form_submissions_o;
}
/**
* Get the user id
* @return int|null The user id
*/
public function getUserId(): int|null
{
return $this->user_id;
}
/**
* Set the user id
* @param int|null $user_id The user id
* @return form_t|book_wash_f
*/
public function setUserId(int|null $user_id): self
{
$this->user_id = $user_id;
return $this;
}
/**
* Get the department id
* @return int|null The department id
*/
public function getDepartmentId(): int|null
{
return $this->department_id;
}
/**
* Set the department id
* @param int|null $department_id The department id
* @return form_t|book_wash_f
* @throws Exception If the department does not exist
*/
public function setDepartmentId(int|null $department_id): self
{
// If the department id is not set, set it to null
if (empty($department_id)) {
$department_id = null;
}
// Check if the department exists
$department = (new departments_o())->selectId($department_id);
if (!$department->exists()) {
throw new Exception('The department does not exist');
}
// Set the department id
$this->department_id = $department_id;
return $this;
}
/**
* Get the customer number
* @return int|null The customer number
*/
public function getCustomerNumber(): int|null
{
return $this->customer_number;
}
/**
* Set the customer number
* @param int|null $customer_number The customer number
* @return form_t|book_wash_f
* @throws Exception
*/
public function setCustomerNumber(int|null $customer_number): self
{
// If the customer number is not set, set it to null
if (empty($customer_number)) {
$customer_number = null;
}
// Check if the customer number is valid
if (!is_numeric($customer_number)) {
throw new Exception('The customer number is not valid');
}
// Check if the customer number exists
$customer = (new users_o())->getUserByCustomerNumber($customer_number);
if (!$customer->exists()) {
throw new Exception('The customer number does not exist');
}
// Set the customer number
$this->customer_number = $customer_number;
return $this;
}
/**
* The form after successful submission
*/
abstract public function afterSubmit(): void;
/**
* Get the sanitized data key
* @param string $key The key to get
* @return mixed The sanitized data
* @throws Exception If the key is not valid
*/
public function getSanitizedData(string $key): mixed
{
// Check if the key is valid
if (!isset($this->form_sanitized_data[self::getFormFieldPrefix() . $key])) {
throw new Exception('The key is not valid: ' . $key);
}
// Return the sanitized data
return $this->form_sanitized_data[self::getFormFieldPrefix() . $key];
}
/**
* Define the form input fields
* @notation E.g. ['name' => 'self::className()::validateString', 'email' => 'self::className()::validateEmail']
* @throws Exception If the input fields are not valid
* @var array $input_fields The form input fields
*/
public function defineInputFields(array $input_fields): void
{
// Set the form input fields
$fields = [];
foreach ( $input_fields as $field => $validation_method ) {
// Check if the field is valid
if (is_callable($validation_method)) {
$fields[$field] = $validation_method;
} else {
// Throw an error if the validation method is not callable
throw new Exception('The validation method is not callable for field: ' . $field . ' - ' . $validation_method);
}
}
// Set the form input fields
$this->form_fields_validations = $fields;
}
/**
* Define the form fields with validation methods, descriptions and options
* @param array $input_fields The form input fields (e.g. ['name' => ['validation_method' => 'validateString', 'description' => 'Name', 'options' => ['option1' => 'Option 1', 'option2' => 'Option 2']]])
* @throws Exception If the input fields are not valid
* @throws Exception If the input fields options are not valid
* @throws Exception If the input fields metadata are not valid
*/
public function defineInputFieldsAdvanced(array $input_fields): void
{
// Set the form input fields
$tmp_validators = [];
$tmp_metas = [];
$tmp_options = [];
foreach ( $input_fields as $field => $input_field ) {
// Check if the field is valid
if (isset($input_field['validation_method'])) {
$tmp_validators[$field] = self::className() . '::' . $input_field['validation_method'];
// We need to remove the validation method from the input field, to avoid adding it to the form fields metas
unset($input_field['validation_method']);
} else {
throw new Exception('The validation method is not valid for field: ' . $field);
}
// Check if the field has options
if (isset($input_field['options'])) {
$tmp_options[$field] = $input_field['options'];
// We need to remove the options from the input field, to avoid adding it to the form fields metas
unset($input_field['options']);
}
// Add the prefix to the display condition
if (isset($input_field['display_if'])) {
$input_field['display_if'] = self::getFormFieldPrefix() . $input_field['display_if'];
}
// Loop through the input fields data
foreach ( $input_field as $key => $value ) {
// Add the field key to the form fields metas
$tmp_metas[$field][$key] = $value;
}
// Check if the field has a display condition
}
// Set the form input fields
$this->form_fields_validations = $tmp_validators;
$this->form_fields_metas = $tmp_metas;
$this->form_fields_options = $tmp_options;
}
/**
* Get the class name
* @return string The class name
*/
public function className(): string
{
return get_class($this);
}
/**
* Add options to the form input fields
* @param string $field The field to add options to
* @param array $input_fields_options The input fields options (e.g. ['option1' => 'Option 1', 'option2' => 'Option 2'])
* @throws Exception If the field is not valid
* @throws Exception If the input fields options are not valid
* @notation E.g. ['field1' => ['option1' => 'Option 1', 'option2' => 'Option 2']]
* @throws Exception
*/
public function addInputFieldOptions(string $field, array $input_fields_options): void
{
// Check if the field is valid
if (!isset($this->form_fields_validations[$field])) {
throw new Exception('The field is not valid: ' . $field);
}
// Check if the input fields options are valid
if (empty($input_fields_options)) {
throw new Exception('The input fields options are not valid: ' . $field);
}
// Check if the input fields options are valid
foreach ( $input_fields_options as $option => $value ) {
// Check if the option is valid
if (!is_string($value)) {
throw new Exception('The input fields options are not valid: ' . $field);
}
}
// Check if the field is valid
if (!isset($this->form_fields_validations[$field])) {
throw new Exception('The field is not valid: ' . $field);
}
// Set the form input fields options
$this->form_fields_options[$field] = $input_fields_options;
}
/**
* Validation method for emails
* @param string $email The email to validate
* @return bool True if the email is valid, false otherwise
*/
public function validateEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Validation method for phone numbers
* @param string $phone The phone number to validate
* @return bool True if the phone number is valid, false otherwise
*/
public function validatePhone(string $phone): bool
{
// Check if the phone number is valid
// This regex allows for optional '+' at the start, followed by 7 to 15 digits
return preg_match('/^\+?[0-9]{7,15}$/', $phone) === 1;
}
/**
* Validation method for customer numbers
* @param int $customer_number The customer number to validate
* @return bool True if the customer number is valid, false otherwise
*/
public function validateCustomerNumber(int|string $customer_number): bool
{
// Check if the customer number is valid
// This regex allows for 1 to 10 digits
return preg_match('/^[0-9]{1,10}$/', $customer_number) === 1;
}
/**
* Validation method for integers
* @param int $int The integer to validate
* @return bool True if the integer is valid, false otherwise
*/
public function validateInt(int $int): bool
{
// Check if the integer is valid
// This regex allows for optional '+' or '-' at the start, followed by 1 to 10 digits
return preg_match('/^[+-]?[0-9]{1,10}$/', $int) === 1;
}
/**
* Validation method for booking ids
* @param int $int The integer to validate
* @return bool True if the integer is valid, false otherwise
*/
public function validateBookingId(int $int): bool
{
// Check if the booking id is valid (A positive integer)
return preg_match('/^[1-9][0-9]*$/', $int) === 1;
}
/**
* Validation method for department ids
* @param int $int The integer to validate
* @return bool True if the integer is valid, false otherwise
*/
public function validateDepartmentId(int $int): bool
{
// Check if the department id is valid (A positive integer)
return preg_match('/^[1-9][0-9]*$/', $int) === 1;
}
/**
* Validation method for strings
* @param string $string The string to validate
* @return bool True if the string is valid, false otherwise
*/
public function validateString(string $string): bool
{
// Check if the string is valid
// This regex allows for any character, including spaces, and has a length of 1 to 255 characters
return preg_match('/^.{1,255}$/', $string) === 1;
}
/**
* Validation method for booleans
* @param bool $boolean The boolean to validate
* @return bool True if the boolean is valid, false otherwise
*/
public function validateBoolean(bool $boolean): bool
{
// Check if the boolean is valid
return true;
}
/**
* Validation method for dates
* @param string $date The date to validate
* @return bool True if the date is valid, false otherwise
*/
public function validateDate(string $date): bool
{
// Check if the date is valid
// This regex allows for YYYY-MM-DD format
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1;
}
/**
* Validation method for registration numbers
* @param string $registration_number The registration number to validate
* @return bool True if the registration number is valid, false otherwise
*/
public function validateRegistrationNumber(string $registration_number): bool
{
// Check if the registration number is valid
// This regex allows for 1 to 20 alphanumeric characters
return preg_match('/^[a-zA-Z0-9]{1,20}$/', $registration_number) === 1;
}
/**
* Get the form identifier
* @return string The form identifier
*/
public function getFormIdentifier(): string
{
return $this->form_identifier;
}
/**
* Set the form identifier (e.g. "FORM_IDENTIFIER")
* @notation This should be used to set the form identifier (e.g. "FORM_IDENTIFIER"), this is used to store the form data in the database.
* @param string $form_identifier The form identifier
* @throws Exception If the form identifier is not a valid format (e.g. "FORM_IDENTIFIER")
*/
public function setFormIdentifier(string $form_identifier): void
{
// Check if the form identifier is valid (e.g. "FORM_IDENTIFIER")
if (!preg_match('/^[A-Z_]+$/', $form_identifier)) {
throw new Exception('The form identifier is not a valid format (e.g. "FORM_IDENTIFIER")');
}
$this->form_identifier = $form_identifier;
}
/**
* Get the form name
* @return string The form name
*/
public function getFormName(): string
{
return $this->form_name;
}
/**
* Set the form name
* @param string $form_name The form name
*/
public function setFormName(string $form_name): void
{
$this->form_name = $form_name;
}
/**
* Get the form description
* @return string The form description
*/
public function getFormDescription(): string
{
return $this->form_description;
}
/**
* Set the form description
* @param string $form_description The form description
*/
public function setFormDescription(string $form_description): void
{
$this->form_description = $form_description;
}
/**
* Get the form as an array
* @notation This returns the form as an array, including the form identifier, name, description and fields
* @return array The form as an array
*/
public function asArray(): array
{
return [
'metadata' => [
'identifier' => $this->form_identifier,
'name' => $this->form_name,
'description' => $this->form_description,
'submit_button_text' => self::getSubmitButtonText(),
],
'fields' => self::getFormFields(),
];
}
/**
* Get the submit button text
* @return string The submit button text (e.g. "Submit form")
*/
public function getSubmitButtonText(): string
{
return $this->submit_button_text ?? 'Submit form';
}
/**
* Set the submit button text
* @param string $text The submit button text (e.g. "Submit form")
*/
public function setSubmitButtonText(string $text): void
{
$this->submit_button_text = $text;
}
/**
* Get the form fields
* @notation This should be used to get the form fields
* @return array The form fields and their validation methods (E.g. ['name' => 'self::className()::validateString', 'email' => 'self::className()::validateEmail'])
* @see formRoute - This is used to get the form fields, to dynamically generate the form.
* @see defineInputFields() - This is used to define the form fields and their validation methods.
* @see requireInputFields() - This is used to require the input fields to be set.
*/
public function getFormFields(): array
{
function removeMethodPrefix($method): array|string|null
{
// Remove the class name and the '::' prefix from the method name
return preg_replace('/^.*?::/', '', $method);
}
// Format the form fields with types, names and values
$formatted_fields = [];
foreach ( $this->form_fields_validations as $field => $validation_method ) {
// Check if the field is valid
$formatted_fields[$field] = [
'name' => $field,
'id' => self::getFormFieldPrefix() . $field,
'validation' => removeMethodPrefix($validation_method),
'metadata' => $this->form_fields_metas[$field] ?? null,
];
// Check if the field has options
if (count(self::getFormFieldOptions($field)) > 0) {
$formatted_fields[$field]['options'] = self::getFormFieldOptions($field);
}
}
return $formatted_fields;
}
/**
* Get the form field options (if applicable)
* @notation This should be used to get the form field options (e.g. ['option1' => 'Option 1', 'option2' => 'Option 2'])
* @param string $field The field to get the options for
* @return array The form field options
*/
public function getFormFieldOptions(string $field): array
{
// Check if the field is valid
if (!isset($this->form_fields_options[$field])) {
return [];
}
// Return the form field options
return $this->form_fields_options[$field];
}
/**
* Set the sanitized data to the form
* @param array $data The sanitized data
* @throws Exception If the data is not valid
*/
public function setSanitizedData(array $data): void
{
// Check if the data is valid
if (empty($data)) {
throw new Exception('The data is empty');
}
// Set the form sanitized data
$this->form_sanitized_data = $data;
}
/**
* Get department options
* @return array The department options
* @notation This should be used to get the department options (e.g. [1 => 'Department 1', 2 => 'Department 2'])
* @see formRoute - This is used to get the department options, to dynamically generate the form.
*/
public function getDepartmentOptions(array|null $onlyWithVariableTrue = null): array
{
$departments = (new departments_o())->list(false) ?? [];
$options = [];
/** @var array $department */
foreach ( $departments as $department ) {
// If the department is not visible, skip it
if (isset($department['visible']) && !$department['visible']) {
continue;
}
// Check if the department has a variable (if the $onlyWithVariableTrue parameter is set)
if ($onlyWithVariableTrue) {
$department_variables = (new department_variables_o())->selectDepartment((int)$department['id']);
// Check if the department has the variables set to true
foreach ( $onlyWithVariableTrue as $variable ) {
//echo 'Checking variable: ' . $variable . ' for department: ' . $department['name'] . PHP_EOL;
if (!self::hasPositiveVariable($variable, $department_variables)) {
//echo 'Variable: ' . $variable . ' is not set to true for department: ' . $department['name'] . PHP_EOL;
continue 2;
}
}
}
$options[$department['id']] = $department['name'];
}
return $options;
}
public function hasPositiveVariable($variable, $department_variables_object): bool
{
$positive_values = [
true,
'true',
1,
'1',
];
return in_array(
$department_variables_object->getVariable($variable),
$positive_values);
}
}