$value) { if ($value !== NULL) { // Allow numerical value 0 and empty string "" $name = (is_int($name) ? $value : $name); if ($name == 'placeholder') { $html .= ' ' . html($name) . '="' . preg_replace('/\r?\n/', ' ', html($value)) . '"'; // Support multi-line placeholder on textarea (not all attributes, as it's a slow RegExp, and Safari 11.1 does not support this). } else { $html .= ' ' . html($name) . '="' . html($value) . '"'; } } } return new html_safe_value($html . ($tag == 'input' || $tag == 'link' ? ' />' : '>')); // Not ideal, as some attributes are not safe (e.g. ) } function human_to_ref($text) { $text = strtolower($text); $text = preg_replace('/[^a-z0-9]/i', ' ', $text); // TODO: Allow hyphens, so the radio field "-1" does not get changed to "1", and the URL "/a/sub-page/" gets the ID "#p_a_sub-page". $text = preg_replace('/ +/', '_', trim($text)); return $text; } function format_bytes($size, $precision = 0, $units = NULL) { // like format_currency(), format_postcode(), format_telephone_number() ... and number_format(), date_format(), money_format() $separator = ''; if (!is_array($units)) { if (is_string($units)) { $separator = $units; } $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; } $last_unit = end($units); foreach ($units as $unit) { if ($size >= 1024 && $unit != $last_unit) { $size = ($size / 1024); } else { return round($size, $precision) . $separator . $unit; } } } function parse_bytes($size) { // Like parse_number() ... and parse_url(), parse_str(), date_parse(), xml_parse() - inspired by the function get_real_size(), from Moodle (http://moodle.org) by Martin Dougiamas $size = trim($size); if (strtoupper(substr($size, -1)) == 'B') { $size = substr($size, 0, -1); // Drop the B, as in 10B or 10KB } $units = array( 'P' => 1125899906842624, 'T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024, ); $unit = strtoupper(substr($size, -1)); if (isset($units[$unit])) { $size = (substr($size, 0, -1) * $units[$unit]); } return intval($size); } function is_email($email, $domain_check = true) { $format_valid = preg_match('/^(?:[a-z0-9\.!#$%&\'\*\+\/=?^_`{|}~-]+|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(\w[-._\w]*\.[a-zA-Z]{2,})$/i', $email, $matches); // The RegExp used to: // - End '{2,}.*)$', not sure why it had '.*' at the end, as it allowed 'example@example.com extra' // - Start '\w[-=.+\'\w]*@', but got too restrictive (missing #), so now following RFC 5322 (emailregex.com), but keeping the simplistic domain matching bit (don't want IP addresses). // // Also vaguely following: // https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email) // /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ if ($format_valid) { if ($domain_check === false || config::get('email.check_domain', true) === false || !function_exists('checkdnsrr')) { return true; } foreach (array('MX', 'A') as $type) { $start = hrtime(true); $valid = checkdnsrr($matches[1] . '.', $type); if (function_exists('debug_log_time')) { debug_log_time('DNS' . $type, round(hrtime_diff($start), 3)); } if ($valid) { function is_assoc($array) { return (count(array_filter(array_keys($array), 'is_string')) > 0); // https://stackoverflow.com/q/173400 } function format_postcode($postcode, $country = 'UK') { if ($country == 'UK') { // UK: https://en.wikipedia.org/wiki/UK_postcodes // A9 9AA | A99 9AA | AA9 9AA | AA99 9AA | A9A 9AA | AA9A 9AA | BFPO 99 $postcode = preg_replace('/[^A-Z0-9]/', '', strtoupper(strval($postcode))); if (preg_match('/^([A-Z](?:\d[A-Z\d]?|[A-Z]\d[A-Z\d]?))(\d[A-Z]{2})$/', $postcode, $matches)) { return $matches[1] . ' ' . $matches[2]; } else if (preg_match('/^(BFPO) *([0-9]+)$/', $postcode, $matches)) { // British forces post office return $matches[1] . ' ' . $matches[2]; } else { return NULL; } } else { return $postcode; // Unknown country, don't change } } function format_currency($value, $currency_char = NULL, $decimal_places = 2, $zero_to_blank = false) { if ($currency_char === NULL) { $currency_char = config::get('output.currency_char', '£'); } if ($value === NULL) { return NULL; } if ($decimal_places === 'auto') { $decimal_places = (fmod($value, 1) == 0 ? 0 : 2); } $value = floatval($value); $value = (round($value, $decimal_places) == 0 ? 0 : $value); // Stop negative -£0 if ($value == 0 && $zero_to_blank) { return ''; } else if ($value < 0) { return '-' . $currency_char . number_format((0 - $value), $decimal_places); } else { return $currency_char . number_format($value, $decimal_places); } } function parse_number($value) { if (!is_float($value) && !is_int($value) && $value !== NULL) { $value = preg_replace('/^[^0-9\.\-]*(-?)[^0-9\.]*(.*?)[^0-9\.]*$/', '$1$2', $value); // Strip prefix/suffix invalid characters (e.g. currency symbol) $pos = strrpos($value, ','); if ($pos !== false && (strlen($value) - $pos) > 3) { // Strip the thousand separators, but don't convert the European "5,00" to "500" $value = str_replace(',', '', $value); } if (!preg_match('/^\-?[0-9]*(\.[0-9]{0,})?$/', $value)) { // Also allowing '.3' to become 0.3 return NULL; // Invalid number } else { $value = floatval($value); } } return $value; } function prefix_match($prefix, $string) { if (PHP_VERSION_ID >= 80000) { trigger_error('Please replace prefix_match(), as PHP 8 provides str_starts_with(); but please note, the arguments are reversed.', E_USER_NOTICE); } return (strncmp($string, $prefix, strlen($prefix)) === 0); } //-------------------------------------------------- // Config object class config { //-------------------------------------------------- // Variables private $store = []; private $encrypted = []; //-------------------------------------------------- // Set and get public static function set($variable, $value = NULL, $encrypted = false) { $obj = config::instance_get(); if (is_array($variable) && $value === NULL) { $obj->store = array_merge($obj->store, $variable); $obj->encrypted = array_merge($obj->encrypted, array_fill_keys(array_keys($obj->encrypted), false)); } else { $obj->store[$variable] = $value; $obj->encrypted[$variable] = $encrypted; } } public static function set_default($variable, $value, $encrypted = false) { $obj = config::instance_get(); if (!array_key_exists($variable, $obj->store)) { // Can be set to NULL $obj->store[$variable] = $value; $obj->encrypted[$variable] = $encrypted; } } public static function set_all($variables) { // Only really used once, during setup $obj = config::instance_get(); $obj->store = $variables; $obj->encrypted = array_fill_keys(array_keys($variables), false); } public static function get($variable, $default = NULL) { $obj = config::instance_get(); if (array_key_exists($variable, $obj->store)) { return $obj->store[$variable]; } else { return $default; } } public static function get_all($prefix = '', $encrypted_mask = NULL) { $obj = config::instance_get(); $prefix .= '.'; $prefix_length = strlen($prefix); if ($prefix_length <= 1) { $data = $obj->store; if ($encrypted_mask) { $data = array_merge($data, array_fill_keys(array_keys($obj->encrypted, true), $encrypted_mask)); } return $data; } else { $data = []; foreach ($obj->store as $k => $v) { if (substr($k, 0, $prefix_length) == $prefix) { $data[substr($k, $prefix_length)] = $v; } } return $data; } } public static function get_encrypted($value) { // TODO [secrets-cleanup] - Use secrets::get() or secrets::key_get() instead $key = getenv('PRIME_CONFIG_KEY'); if (!$key) { throw new error_exception('Missing environment variable "PRIME_CONFIG_KEY"'); } return encryption::encode($value, $key); } public static function get_decrypted($variable, $default = NULL) { // TODO [secrets-cleanup] - Use secrets::get() or secrets::key_get() instead $obj = config::instance_get(); if (array_key_exists($variable, $obj->store)) { if (isset($obj->encrypted[$variable]) && $obj->encrypted[$variable]) { return config::value_decrypt($obj->store[$variable]); } else { return $obj->store[$variable]; } } else { return $default; } } public static function value_decrypt($value) { // TODO [secrets-cleanup] - Use secrets::get() or secrets::key_get() instead $key = getenv('PRIME_CONFIG_KEY'); if (!$key) { throw new error_exception('Missing environment variable "PRIME_CONFIG_KEY"'); } return encryption::decode($value, $key); } //-------------------------------------------------- // Array support public static function array_push($variable, $value) { $obj = config::instance_get(); if (!isset($obj->store[$variable]) || !is_array($obj->store[$variable])) { $obj->store[$variable] = []; } $obj->store[$variable][] = $value; } public static function array_set($variable, $key, $value) { // e.g. // config::array_set('example', 'A', 1); // config::array_set('example', 'B', 2); // config::array_set('example', 'C', 'D', 3); <-- Walking further into the array $obj = config::instance_get(); $args = func_get_args(); $count = (count($args) - 1); // Last arg is the $value $ref = &$obj->store; for ($k = 0; $k < $count; $k++) { $key = $args[$k]; if (!isset($ref[$key]) || !is_array($ref[$key])) { $ref[$key] = []; } $ref = &$ref[$key]; } $ref = $args[$count]; } public static function array_get($variable, $key, $default = NULL) { $obj = config::instance_get(); if (isset($obj->store[$variable][$key])) { return $obj->store[$variable][$key]; } else { return $default; } } public static function array_search($variable, $value) { $obj = config::instance_get(); if (isset($obj->store[$variable]) && is_array($obj->store[$variable])) { return array_search($value, $obj->store[$variable]); } return false; } //-------------------------------------------------- // Singleton private static function instance_get() { static $instance = NULL; if (!$instance) { $instance = new config(); } return $instance; } final private function __construct() { // Being private prevents direct creation of object, which also prevents use of clone. } } config::set('output.charset', $GLOBALS['pageCharset']); config::set('request.uri', $GLOBALS['tplPageUrl']); config::set('request.url', $GLOBALS['tplHttpsUrl']); config::set('request.method', (isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET')); config::set('request.referrer', str_replace($GLOBALS['webDomainSSL'], '', (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''))); //-------------------------------------------------- // Database object class db extends database { public function __construct() { $this->link = $GLOBALS['db']->link; } public function fetch_row($result = null) { return $this->fetchAssoc($result); } public function insert_id() { return $this->insertId(); } public function affected_rows() { return $this->affectedRows(); } public function enum_values($table_sql, $field) { return $this->enumValues($table_sql, $field); } } //-------------------------------------------------- // Form objects //-------------------------------------------------- // http://www.phpprime.com/doc/helpers/form/ //-------------------------------------------------- class form { //-------------------------------------------------- // Variables private $form_id = NULL; private $form_action = './'; private $form_method = 'POST'; private $form_class = ''; private $form_button = 'Save'; private $form_button_name = 'button'; private $form_attributes = []; private $form_passive = false; private $form_submitted = false; private $autocomplete = NULL; private $autocomplete_default = NULL; private $disabled = false; private $readonly = false; private $autofocus = false; private $autofocus_submit = false; private $wrapper_tag = 'fieldset'; private $print_page_setup = NULL; // Current page being setup in code. private $print_page_submit = NULL; // Current page the user submitted. private $print_page_skipped = false; private $print_page_valid = true; private $print_group = NULL; private $print_group_tag = 'h2'; private $print_group_class = NULL; private $hidden_values = []; private $fields = []; private $field_refs = []; private $field_count = 0; private $field_tag_id = 0; private $file_setup_complete = false; private $required_mark_html = NULL; private $required_mark_position = 'left'; private $label_prefix_html = ''; private $label_suffix_html = ':'; private $label_override_function = NULL; private $errors_html = []; private $error_override_function = NULL; private $post_validation_done = false; private $db_link = NULL; private $db_record = NULL; private $db_table_name_sql = NULL; private $db_table_alias_sql = NULL; private $db_where_sql = NULL; private $db_where_parameters = NULL; private $db_log_table = NULL; private $db_log_values = []; private $db_fields = NULL; private $db_values = []; private $db_save_disabled = false; private $dedupe_user_id = NULL; // Protects against the form being submitted multiple times (typically on a slow internet connection) private $dedupe_ref = NULL; private $dedupe_data = NULL; private $saved_values_data = NULL; private $saved_values_used = NULL; private $saved_values_ignore = NULL; private $saved_message_html = 'Please submit this form again.'; private $csrf_token = NULL; private $csrf_error_html = 'The request did not appear to come from a trusted source, please try again.'; private $fetch_allowed = [ 'dest' => ['document'], 'mode' => ['navigate'], 'site' => ['same-origin', 'none'], // 'none' because a user can POST the form, see errors, and refresh the page. 'user' => ['?1', '?T'], ]; private $fetch_known = [ 'dest' => ['audio', 'audioworklet', 'document', 'embed', 'empty', 'font', 'image', 'manifest', 'object', 'paintworklet', 'report', 'script', 'serviceworker', 'sharedworker', 'style', 'track', 'video', 'worker', 'xslt', 'nested-document'], 'mode' => ['cors', 'navigate', 'nested-navigate', 'no-cors', 'same-origin', 'websocket'], 'site' => ['cross-site', 'same-origin', 'same-site', 'none'], 'user' => ['?0', '?1', '?F', '?T'], // In HTTP/1 headers, a boolean is indicated with a leading "?" ]; //-------------------------------------------------- // Setup public function __construct() { // Do not have any arguments for init, as objects like order_form() need to set their own. $this->setup(); } final protected function __clone() { trigger_error('Clone of form object is not allowed.', E_USER_ERROR); // You would need a different form_id (to distinguish which form is submitted), but now fields defined on form 1 won't won't collect their POST values (form 1 was "not submitted"), and errors generated on form 1 will be shown on both forms (irrespective of which form was submitted). } protected function setup() { //-------------------------------------------------- // Site config $site_config = config::get_all('form'); //-------------------------------------------------- // Defaults $this->form_action = config::get('request.url'); // Use the full domain name so a malicious "base" meta tag does not send data to a different website (so not request.uri). if (isset($site_config['disabled'])) $this->disabled = ($site_config['disabled'] == true); if (isset($site_config['readonly'])) $this->readonly = ($site_config['readonly'] == true); if (isset($site_config['dedupe_user_id'])) $this->dedupe_user_id = $site_config['dedupe_user_id']; if (isset($site_config['label_override_function'])) $this->label_override_function = $site_config['label_override_function']; if (isset($site_config['error_override_function'])) $this->error_override_function = $site_config['error_override_function']; //-------------------------------------------------- // Internal form ID $form_id = (isset($site_config['count']) ? $site_config['count'] : 1); config::set('form.count', ($form_id + 1)); $this->form_id_set('form_' . $form_id); //-------------------------------------------------- // CSRF setup (set cookie) $this->csrf_token = csrf_token_get(); //-------------------------------------------------- // Fetch limits $fetch_allowed = config::get('form.fetch_allowed', NULL); if (is_array($fetch_allowed)) { if (in_array('nested', $fetch_allowed)) $this->fetch_allowed_nested(); if (in_array('fetch', $fetch_allowed)) $this->fetch_allowed_fetch(); if (in_array('cors', $fetch_allowed)) $this->fetch_allowed_cors(); // Deprecated } else if ($fetch_allowed === false) { $this->fetch_allowed = false; // Disabled, not a good idea. } //-------------------------------------------------- // Dest support if ($this->form_submitted) { $this->hidden_value('dest'); } else { $dest = request('dest'); // Can be GET or POST if ($dest == 'referrer') { $referrer = config::get('request.referrer'); if (substr($referrer, 0, 1) == '/') { // Must have a value, and must be for this site. Where a scheme-relative URL "//example.com" won't work, as the domain would be prefixed. $dest = url($referrer, array('dest' => NULL)); // If the previous page also had a "dest" value, drop it (stop loop) } else { $dest = NULL; // Not provided, e.g. user re-loaded page } } else if (substr(strval($dest), 0, 3) == '%2F') { $dest = urldecode($dest); // In a 'passive' form, the form isn't 'submitted', so hidden_value() isn't used, and the URL-encoded value (needed to overcome limits on hidden input fields), is not decoded. } $this->hidden_value_set('dest', $dest); } } public function form_id_set($form_id) { $this->form_id = $form_id; $this->_is_submitted(); } public function form_id_get() { return $this->form_id; } public function form_action_set($form_action) { if ($form_action instanceof url) { $form_action->format_set('full'); // We use full URL's, see comment about 'request.url' above. } $this->form_action = $form_action; } public function form_action_get() { return $this->form_action; } public function form_method_set($form_method) { $this->form_method = strtoupper($form_method); $this->_is_submitted(); } public function form_method_get() { return $this->form_method; } public function form_class_set($form_class) { $this->form_class = $form_class; } public function form_class_add($class) { $this->form_class .= ($this->form_class == '' ? '' : ' ') . $class; } public function form_class_get() { return $this->form_class; } public function form_button_set($text = NULL) { $this->form_button = $text; } public function form_button_get() { return $this->form_button; } public function form_attribute_set($attribute, $value) { if ($value == '') { unset($this->form_attributes[$attribute]); } else { $this->form_attributes[$attribute] = $value; } } public function form_button_name($name) { $this->form_button_name = $name; } public function form_passive_set($passive, $method = NULL) { // Always considered as "submitted" and uses "form.csrf_passive_checks"... good for a search form. $this->form_passive = ($passive == true); $this->form_button_name = ($this->form_passive ? NULL : 'button'); // As passive we don't need to know which button is pressed (just adds cruft to url) if ($method !== NULL) { $this->form_method_set($method); } else { $this->_is_submitted(); } } public function form_passive_get() { return $this->form_passive; } public function form_autocomplete_set($autocomplete) { $this->autocomplete = $autocomplete; } public function form_autocomplete_get() { return $this->autocomplete; } public function autocomplete_default_set($autocomplete) { $this->autocomplete_default = $autocomplete; } public function autocomplete_default_get() { return $this->autocomplete_default; } public function disabled_set($disabled) { $this->disabled = ($disabled == true); } public function disabled_get() { return $this->disabled; } public function readonly_set($readonly) { $this->readonly = ($readonly == true); } public function readonly_get() { return $this->readonly; } public function autofocus_set($autofocus) { $this->autofocus = ($autofocus == true); $this->autofocus_submit = $this->autofocus; } public function autofocus_get() { return $this->autofocus; } public function wrapper_tag_set($tag) { $this->wrapper_tag = $tag; } public function print_page_start($page) { $page = intval($page); if (($this->print_page_setup + 1) != $page) { // also blocks adding fields to page 1 after starting page 2. exit_with_error('Missing call to form->print_page_start(' . ($this->print_page_setup + 1) . ') - must be sequential'); } if ($this->print_page_valid !== true) { exit_with_error('Cannot call form->print_page_start(' . $page . ') without first checking form->valid()'); } if ($this->print_page_setup === NULL) { if (count($this->fields) != 0) { exit_with_error('You must call form->print_page_start(1) before adding any fields.'); } $this->print_page_submit = intval($this->hidden_value_get('page')); if ($this->print_page_submit == 0) { $this->print_page_submit = 1; } } else { foreach ($this->fields as $field) { $field->print_hidden_set(true); } } if ($page != 1 && !$this->submitted($page - 1)) { exit_with_error('Cannot call form->print_page_start(' . $page . ') without first checking form->submitted(' . ($page - 1) . ')'); } $this->hidden_value_set('page', $page); $this->print_page_setup = $page; } public function print_page_skip($page) { // If there is an optional page, this implies that it has been submitted, before calling the next print_page_start() $this->print_page_skipped = true; if ($page == 1) { $this->form_submitted = true; } if ($this->print_page_submit >= $page) { return; // Already on or after this page. } if (($this->print_page_submit + 1) != $page) { exit_with_error('You must call form->print_page_skip(' . ($this->print_page_submit + 1) . ') before you can skip to page "' . $page . '"'); } $this->print_page_submit = $page; } public function print_page_get() { return $this->print_page_setup; } public function print_group_start($print_group) { $this->print_group = $print_group; } public function print_group_get() { return $this->print_group; } public function print_group_tag_set($tag) { return $this->print_group_tag = $tag; } public function print_group_class_set($tag) { return $this->print_group_class = $tag; } public function hidden_value($name) { // You should call form->hidden_value() first to initialise - get/set may not be called when form is submitted with errors. if ($this->form_submitted) { $value = request($name, $this->form_method); $value = ($value === NULL ? NULL : urldecode($value)); } else { $value = ''; } $this->hidden_value_set($name, $value); } public function hidden_value_set($name, $value = NULL) { if (str_starts_with($name, 'h-')) { exit_with_error('Cannot set the hidden value "' . $name . '", as it begins with a "h-" (used by hidden fields).'); } if ($value === NULL) { unset($this->hidden_values[$name]); } else { $this->hidden_values[$name] = $value; } } public function hidden_value_get($name) { if (isset($this->hidden_values[$name])) { return $this->hidden_values[$name]; } else { if ($this->saved_values_available()) { $value = $this->saved_value_get($name); } else { $value = request($name, $this->form_method); } return ($value === NULL ? NULL : urldecode($value)); } } public function dest_url_get() { return $this->hidden_value_get('dest'); } public function dest_url_set($url) { return $this->hidden_value_set('dest', $url); } public function dest_redirect($default_url, $config = []) { $url = strval($this->dest_url_get()); if (substr($url, 0, 1) != '/') { // Must have a value, and must be for this site. Where a scheme-relative URL "//example.com" won't work, as the domain would be prefixed. $url = $default_url; } $this->redirect($url, $config); } public function redirect($url, $config = []) { if ($this->dedupe_ref) { $now = new timestamp(); $db = $this->db_get(); $db->insert(DB_PREFIX . 'system_form_dedupe', [ 'user_id' => $this->dedupe_user_id, 'ref' => $this->dedupe_ref, 'data' => $this->dedupe_data, 'created' => $now, 'redirect_url' => $url, 'redirect_config' => json_encode($config), ]); $sql = 'DELETE FROM ' . DB_PREFIX . 'system_form_dedupe WHERE created < ?'; $parameters = []; $parameters[] = $now->clone('-1 hour'); $db->query($sql, $parameters); } redirect($url, $config); } public function human_check($label, $error, $config = []) { $field_human = NULL; $is_human = NULL; if ($this->form_submitted) { $config = array_merge([ 'content_values' => [], 'db_field' => NULL, 'db_insert' => false, // Still insert when found to be spam (for logging purposes); can also be an array e.g. ['deleted' => $now] 'time_min' => 5, 'time_max' => (60*60*3), 'force_check' => false, ], $config); $content_values = $config['content_values']; foreach ($content_values as $key => $value) { if ($value instanceof form_field) { $content_values[$key] = $value->value_get(); } } if (count($content_values) > 0) { $content_spam = is_spam_like(implode(' ', $content_values)); } else { $content_spam = false; } $timestamp_original = request('o'); if (preg_match('/^[0-9]{10,}$/', $timestamp_original)) { $timestamp_diff = (time() - $timestamp_original); $timestamp_spam = ($timestamp_diff < $config['time_min'] || $timestamp_diff > $config['time_max']); } else { $timestamp_spam = true; // Missing or invalid value } if ($content_spam || $timestamp_spam || $config['force_check']) { list($field_human, $is_human) = $this->human_check_field_get($label, $error); if ($config['db_field']) { $field_human->db_field_set($config['db_field']); // Set the field to enum('', 'true', 'false') where a blank value shows they did not see this field. } if (!$is_human && $config['db_field']) { if (is_array($config['db_insert'])) { foreach ($config['db_insert'] as $field => $value) { $this->db_value_set($field, $value); } $config['db_insert'] = true; } if ($config['db_insert'] === true) { $this->db_insert(); } } } } return [$field_human, $is_human]; } public function human_check_field_get($label, $error) { $day = floor(time() / (60*60*24)); $field_human = new form_field_checkbox($this, $label, 'human_' . $day); $field_human->text_values_set('true', 'false'); $field_human->required_error_set($error); $is_human = ($field_human->value_get() == 'true'); return [$field_human, $is_human]; } public function required_mark_set($required_mark) { $this->required_mark_set_html(to_safe_html($required_mark)); } public function required_mark_set_html($required_mark_html) { $this->required_mark_html = $required_mark_html; } public function required_mark_get_html($required_mark_position = 'left') { if ($this->required_mark_html !== NULL) { return $this->required_mark_html; } else if (($required_mark_position === 'right') || ($required_mark_position === NULL && $this->required_mark_position === 'right')) { return ' *'; } else { return '* '; } } public function required_mark_position_set($value) { if ($value == 'left' || $value == 'right' || $value == 'none') { $this->required_mark_position = $value; } else { exit_with_error('Invalid required mark position specified (left/right/none)'); } } public function required_mark_position_get() { return $this->required_mark_position; } public function label_prefix_set($prefix) { $this->label_prefix_set_html(to_safe_html($prefix)); } public function label_prefix_set_html($prefix_html) { $this->label_prefix_html = $prefix_html; } public function label_prefix_get_html() { return $this->label_prefix_html; } public function label_suffix_set($suffix) { $this->label_suffix_set_html(to_safe_html($suffix)); } public function label_suffix_set_html($suffix_html) { $this->label_suffix_html = $suffix_html; } public function label_suffix_get_html() { return $this->label_suffix_html; } public function label_override_set_function($function) { $this->label_override_function = $function; } public function label_override_get_function() { return $this->label_override_function; } public function error_override_set_function($function) { $this->error_override_function = $function; } public function error_override_get_function() { return $this->error_override_function; } public function db_set($db_link) { $this->db_link = $db_link; } public function db_get() { if ($this->db_link === NULL) { $this->db_link = db_get(); } return $this->db_link; } public function db_record_set($record) { $this->db_record = $record; } public function db_record_get() { if ($this->db_record === NULL) { if ($this->db_table_name_sql !== NULL) { $this->db_record = record_get([ 'table_sql' => $this->db_table_name_sql, 'table_alias' => $this->db_table_alias_sql, 'where_sql' => $this->db_where_sql, 'where_parameters' => $this->db_where_parameters, 'log_table' => $this->db_log_table, 'log_values' => $this->db_log_values, ]); } else { $this->db_record = false; } } return $this->db_record; } public function db_table_set_sql($table_sql, $alias_sql = NULL) { $this->db_table_name_sql = $table_sql; $this->db_table_alias_sql = $alias_sql; // To deprecate: // grep -r "db_table_set_sql(" */app/ // grep -r "db_where_set_sql(" */app/ // function db_field_set { grep "db_field_set" $1 | sed -E "s/.*\('([^']+)'.*/'\1',/"; } } public function db_where_set_sql($where_sql, $parameters = []) { $this->db_where_sql = $where_sql; $this->db_where_parameters = $parameters; } public function db_log_set($table, $values = []) { $this->db_log_table = $table; $this->db_log_values = $values; } public function db_save_disable() { $this->db_save_disabled = true; } public function db_value_set($name, $value) { // TODO: Look at using $record->value_set(); $this->db_values[$name] = $value; } public function saved_message_set_html($message_html) { $this->saved_message_html = $message_html; } public function saved_values_ignore() { // The login form will skip them (should not be used, but also preserve them for the login redirect). $this->saved_values_ignore = true; } public function saved_values_available() { return false; if ($this->form_passive || $this->saved_values_ignore === true) { return false; } if ($this->saved_values_used === NULL) { $this->saved_values_used = false; if (session::open() && session::get('save_request_url') == config::get('request.uri') && config::get('request.method') == 'GET' && $this->form_method == 'POST') { $data = session::get('save_request_data'); if (isset($data['act']) && $data['act'] == $this->form_id) { $this->saved_values_data = $data; } } } return ($this->saved_values_data !== NULL); } private function saved_values_used() { if ($this->saved_values_used === false && session::open()) { $this->saved_values_used = true; save_request_reset(); } } public function saved_value_get($name) { $this->saved_values_used(); if (isset($this->saved_values_data[$name])) { return $this->saved_values_data[$name]; } else { return NULL; } } //-------------------------------------------------- // Status public function submitted($page = NULL) { if ($this->form_submitted === true && $this->disabled === false && $this->readonly === false) { $this->saved_values_used(); // Just incase there are no fields on the page calling saved_value_get() if ($this->print_page_setup === NULL) { if ($page !== NULL) { exit_with_error('Cannot call form->submitted(' . $page . ') without form->print_page_start(X)'); } return true; } else { if ($page === NULL) { $page = $this->print_page_setup; } return ($page <= $this->print_page_submit); } } return false; } private function _is_submitted() { $this->form_submitted = ($this->form_passive || (request('act', $this->form_method) == $this->form_id && config::get('request.method') == $this->form_method)); if ($this->dedupe_user_id > 0 && $this->form_submitted && $this->form_method == 'POST') { // GET requests should not change state $this->dedupe_ref = request('r'); $this->dedupe_data = hash('sha256', json_encode($_REQUEST)); if ($this->dedupe_ref) { $db = $this->db_get(); $sql = 'SELECT sfd.redirect_url, sfd.redirect_config FROM ' . DB_PREFIX . 'system_form_dedupe AS sfd WHERE sfd.user_id = ? AND sfd.ref = ? AND sfd.data = ? LIMIT 1'; $parameters = []; $parameters[] = intval($this->dedupe_user_id); $parameters[] = $this->dedupe_ref; $parameters[] = $this->dedupe_data; if ($row = $db->fetch_row($sql, $parameters)) { config::set('debug.response_code_extra', 'deduped'); redirect($row['redirect_url'], json_decode($row['redirect_config'])); exit(); // Protect against the config containing ['exit' => false] } } } } public function initial($page = NULL) { // Because you cant have a function called "default", and "defaults" implies an array of default values. return (!$this->submitted($page) && !$this->saved_values_available()); } public function valid() { $this->_post_validation(); if (count($this->errors_html) > 0) { if (function_exists('response_get')) { $response = response_get(); $response->error_set(true); // Changes the page title } return false; } else { $this->print_page_valid = true; return true; } } //-------------------------------------------------- // CSRF public function csrf_error_set($error) { $this->csrf_error_set_html(to_safe_html($error)); } public function csrf_error_set_html($error_html) { $this->csrf_error_html = $error_html; } public function csrf_token_get() { if ($this->form_method == 'POST') { return csrf_challenge_hash($this->form_action, $this->csrf_token); } else { return $this->csrf_token; } } //-------------------------------------------------- // Fetch limits public function fetch_allowed_nested() { // e.g. in an iframe. $this->fetch_allowed_add('dest', 'nested-document'); $this->fetch_allowed_add('mode', 'nested-navigate'); } public function fetch_allowed_fetch() { // e.g. XMLHttpRequest, fetch(), navigator.sendBeacon(), , , $this->fetch_allowed_add('dest', 'empty'); $this->fetch_allowed_add('mode', 'cors'); $this->fetch_allowed_add('mode', 'no-cors'); // navigator.sendBeacon } public function fetch_allowed_cors() { report_add('Deprecated: $form->fetch_allowed_cors(), either set manually, or use $form->fetch_allowed_fetch() which is more likely what you need.', 'notice'); $this->fetch_allowed_add('mode', 'cors'); } public function fetch_allowed_add($field, $value) { $this->fetch_allowed_set($field, array_merge($this->fetch_allowed[$field], [$value])); } public function fetch_allowed_set($field, $values) { $values = array_unique($values); if (!isset($this->fetch_known[$field])) { exit_with_error('Unknown fetch field "' . $field . '"'); } else { $unknown = array_diff($values, $this->fetch_known[$field]); if (count($unknown) > 0) { exit_with_error('Unknown fetch value "' . reset($unknown) . '"'); } $this->fetch_allowed[$field] = $values; } } //-------------------------------------------------- // Error support public function error_reset() { $this->errors_html = []; } public function error_add($error, $hidden_info = NULL) { $this->error_add_html(to_safe_html($error), $hidden_info); } public function error_add_html($error_html, $hidden_info = NULL) { $this->_field_error_add_html(-1, $error_html, $hidden_info); // -1 is for general errors, not really linked to a field } public function errors_get() { $errors = []; foreach ($this->errors_get_html() as $error_html) { $errors[] = html_decode(strip_tags($error_html)); } return $errors; } public function errors_get_html() { $this->_post_validation(); $errors_flat_html = []; $error_links = config::get('form.error_links', false); ksort($this->errors_html); // Match order of fields foreach ($this->errors_html as $ref => $errors_html) { foreach ($errors_html as $error_html) { if ($error_links && isset($this->fields[$ref]) && stripos($error_html, 'fields[$ref]->input_first_id_get(); if ($field_id) { $error_html = '' . $error_html . ''; } } $errors_flat_html[] = $error_html; } } return $errors_flat_html; } private function _post_validation() { //-------------------------------------------------- // Already done if ($this->post_validation_done) { return true; } //-------------------------------------------------- // CSRF checks $csrf_errors = []; $csrf_report = false; $csrf_block = (SERVER == 'stage'); // TODO: Remove, so all CSRF checks block (added 2019-05-27) if ($this->form_submitted && $this->csrf_error_html != NULL) { // Cant type check, as html() will convert NULL to string if ($this->print_page_skipped === true && config::get('request.method') == 'GET' && $this->form_method == 'POST') { // This multi-page form isn't complete yet, so don't show a CSRF error. // e.g. A GET request has been made, for a form that uses POST, where the request skips to a later 'page' in the process. } else { if ($this->form_passive) { $checks = config::get('form.csrf_passive_checks', []); } else { $checks = ['token', 'fetch']; } if (in_array('token', $checks)) { $csrf_token = strval(request('csrf', $this->form_method)); if (!csrf_challenge_check($csrf_token, $this->form_action, $this->csrf_token)) { cookie::require_support(); $csrf_errors[] = 'Token-[' . $this->csrf_token . ']-[' . $this->form_method . ']-[' . $csrf_token . ']'; $csrf_block = true; } } if (in_array('cookie', $checks) && trim(strval(cookie::get('f'))) == '') { // The cookie just needs to exist, where it's marked SameSite=Strict $csrf_errors[] = 'MissingCookie = ' . implode('/', array_keys($_COOKIE)); $csrf_report = true; } if (in_array('fetch', $checks) && $this->fetch_allowed !== false) { $fetch_values = config::get('request.fetch'); if ($this->form_passive && $fetch_values['dest'] == 'document' && $fetch_values['mode'] == 'navigate' && $fetch_values['site'] == 'cross-site') { // For a passive form, request from another website (maybe email link), top level navigation... probably fine, as a timing attack shouldn't be possible. } else { foreach ($this->fetch_allowed as $field => $allowed) { if ($fetch_values[$field] != NULL && !in_array($fetch_values[$field], $allowed)) { $csrf_errors[] = 'Sec-Fetch-' . ucfirst($field) . ' = "' . $fetch_values[$field] . '" (' . (in_array($fetch_values[$field], $this->fetch_known[$field]) ? 'known' : 'unknown') . ')'; $csrf_report = true; } } } } else { $fetch_values = NULL; } if ($csrf_errors) { if ($csrf_report) { report_add('CSRF error via SecFetch/SameSite checks (' . ($csrf_block ? 'user asked to re-submit' : 'not blocked') . ').' . "\n\n" . 'Errors = ' . debug_dump($csrf_errors) . "\n\n" . 'Passive = ' . ($this->form_passive ? 'True' : 'False') . "\n\n" . 'Sec-Fetch Provided Values = ' . debug_dump($fetch_values) . "\n\n" . ' Sec-Fetch Allowed Values = ' . debug_dump($this->fetch_allowed), 'error'); } if ($csrf_block) { $this->_field_error_add_html(-1, $this->csrf_error_html, implode('/', $csrf_errors)); } } } } //-------------------------------------------------- // Max input variables $input_vars_max = intval(ini_get('max_input_vars')); if ($input_vars_max > 0 && count($_REQUEST) >= $input_vars_max) { exit_with_error('The form submitted too many values for this server.', 'Maximum input variables: ' . $input_vars_max . ' (max_input_vars)'); } //-------------------------------------------------- // Max file uploads $file_uploads_max = intval(ini_get('max_file_uploads')); $file_upload_count = 0; foreach ($_FILES as $file) { $file_upload_count += (is_array($file['tmp_name']) ? count($file['tmp_name']) : 1); } if ($file_uploads_max > 0 && $file_upload_count >= $file_uploads_max && (PHP_INIT_ERROR['type'] ?? 0) === E_WARNING && strpos((PHP_INIT_ERROR['message'] ?? ''), 'Maximum number of allowable file uploads has been exceeded') !== NULL) { exit_with_error('The form submitted too many files for this server.', 'File uploads: ' . $file_upload_count . ', max_file_uploads = ' . $file_uploads_max . "\n\n" . debug_dump($_FILES)); } //-------------------------------------------------- // Saved value if (!$this->form_submitted && $this->saved_values_available() && $this->saved_message_html !== NULL) { $this->_field_error_add_html(-1, $this->saved_message_html); } //-------------------------------------------------- // Fields foreach ($this->fields as $field) { $field->_post_validation(); } //-------------------------------------------------- // Remember this has been done $this->post_validation_done = true; } //-------------------------------------------------- // Data output public function data_array_get() { $values = []; foreach ($this->fields as $field) { $field_name = $field->label_get_text(); $field_type = $field->type_get(); if ($field_type == 'date') { $value = $field->value_date_get(); if ($value == '0000-00-00') { $value = ''; // Not provided } } else if ($field_type == 'file' || $field_type == 'image') { if ($field->uploaded()) { $value = $field->file_name_get() . ' (' . format_bytes($field->file_size_get()) . ')'; } else { $value = 'N/A'; } } else { $value = $field->value_get(); } $values[$field->input_name_get()] = array($field_name, $value); // Input name should be unique } return $values; } public function data_db_get() { $values = []; foreach ($this->fields as $field) { $value_new = $field->_db_field_value_new_get(); if ($value_new) { $values[$value_new[0]] = $value_new[1]; } } foreach ($this->db_values as $name => $value) { // More reliable than array_merge at keeping keys $values[$name] = $value; } return $values; } //-------------------------------------------------- // Database saving public function db_save() { //-------------------------------------------------- // Validation if ($this->disabled) exit_with_error('This form is disabled, so you cannot call "db_save".'); if ($this->readonly) exit_with_error('This form is readonly, so you cannot call "db_save".'); if ($this->db_save_disabled) { exit_with_error('The "db_save" method has been disabled, you should probably be using an intermediate support object.'); } //-------------------------------------------------- // Record get $record = $this->db_record_get(); if ($record === false) { exit_with_error('You need to call "db_record_set" or "db_table_set_sql" on the form object'); } foreach ($this->fields as $field) { $field->_db_field_value_update(); } if (is_array($record)) { if (count($this->db_values) > 0) { exit_with_error('Cannot use "db_value_set" with multiple record helpers, instead call "value_set" on the record itself.'); } $records = $record; } else { foreach ($this->db_values as $name => $value) { // Values from db_value_set() replace those from $this->fields. $record->value_set($name, $value); } $records = array($record); } $changed = false; foreach ($records as $record) { if ($record->save() === true) { $changed = true; } } return $changed; } public function db_insert() { //-------------------------------------------------- // Cannot use a WHERE clause if ($this->db_where_sql !== NULL) { exit_with_error('The "db_insert" method does not work with a "db_where_sql" set.'); } //-------------------------------------------------- // Save and return the ID $this->db_save(); $db = $this->db_get(); return $db->insert_id(); } //-------------------------------------------------- // Field support public function field_get($ref, $config = []) { if (is_numeric($ref)) { if (isset($this->fields[$ref])) { return $this->fields[$ref]; } else { exit_with_error('Cannot return the field "' . $ref . '", on "' . get_class($this) . '".'); } } else { if (isset($this->field_refs[$ref])) { return $this->field_refs[$ref]; } else { $method = 'field_' . $ref . '_get'; if (method_exists($this, $method)) { $field = $this->$method($config); } else { $field = $this->_field_create($ref, $config); } $this->field_refs[$ref] = $field; return $field; } } } protected function _field_create($ref, $config) { exit_with_error('Cannot create the "' . $ref . '" field, missing the "field_' . $ref . '_get" method on "' . get_class($this) . '".'); } public function field_exists($ref) { return (isset($this->fields[$ref]) || isset($this->field_refs[$ref])); } public function fields_get($group = NULL) { if ($group === NULL) { return $this->fields; } else { $fields = []; foreach ($this->fields as $field) { if ($field->print_include_get() && !$field->print_hidden_get() && $field->print_group_get() == $group) { $fields[] = $field; } } return $fields; } } public function field_groups_get() { $field_groups = []; foreach ($this->fields as $field) { if ($field->print_include_get() && !$field->print_hidden_get()) { $field_group = $field->print_group_get(); if ($field_group !== NULL) { $field_groups[] = $field_group; } } } return array_unique($field_groups); } public function _field_add($field_obj) { // Public for form_field to call while (isset($this->fields[$this->field_count])) { $this->field_count++; } $this->fields[$this->field_count] = $field_obj; $this->print_page_valid = false; return $this->field_count; } public function _field_setup_file() { // Public for form_field to call, will only action once (for this form) if ($this->file_setup_complete !== true) { $this->file_setup_complete = true; $this->form_attribute_set('enctype', 'multipart/form-data'); if (session::open()) { session::regenerate_delay(60*20); // 20 minutes for the user to select the file(s), submit the form, and for the upload to complete (try to avoid issue with them using a second tab, and getting a new session key, while uploading). } } } public function _field_tag_id_get() { // Public for form_field to call return $this->form_id . '_tag_' . ++$this->field_tag_id; } public function _field_error_add_html($field_uid, $error_html, $hidden_info = NULL) { if ($this->error_override_function !== NULL) { $function = $this->error_override_function; if ($field_uid == -1) { $error_html = call_user_func($function, $error_html, $this, NULL); } else { $error_html = call_user_func($function, $error_html, $this, $this->fields[$field_uid]); } } if (!isset($this->errors_html[$field_uid])) { $this->errors_html[$field_uid] = []; } if ($hidden_info !== NULL) { $error_html .= ' '; } $this->errors_html[$field_uid][] = $error_html; $this->print_page_valid = false; } public function _field_error_set_html($field_uid, $error_html, $hidden_info = NULL) { $this->errors_html[$field_uid] = []; $this->_field_error_add_html($field_uid, $error_html, $hidden_info); } public function _field_errors_get_html($field_uid) { if (isset($this->errors_html[$field_uid])) { return $this->errors_html[$field_uid]; } else { return []; } } public function _field_valid($field_uid) { return (!isset($this->errors_html[$field_uid])); } //-------------------------------------------------- // HTML public function html_start($config = NULL) { //-------------------------------------------------- // Config if (!is_array($config)) { $config = []; } //-------------------------------------------------- // Remove query string if in GET mode $form_action = $this->form_action; if ($this->form_method == 'GET') { $pos = strpos($form_action, '?'); if ($pos !== false) { $form_action = substr($form_action, 0, $pos); $pos = strrpos($this->form_action, '#'); if ($pos !== false) { $form_action .= substr($this->form_action, $pos); } } } //-------------------------------------------------- // Hidden fields if (!isset($config['hidden']) || $config['hidden'] !== true) { $hidden_fields_html = $this->html_hidden(); } else { $hidden_fields_html = ''; } unset($config['hidden']); //-------------------------------------------------- // Attributes $attributes = array( 'id' => $this->form_id, 'class' => ($this->form_class == '' ? NULL : $this->form_class), 'action' => $form_action, 'method' => strtolower($this->form_method), // Lowercase for the HTML5 checker on totalvalidator.com 'accept-charset' => config::get('output.charset'), // When text from MS Word is pasted in an IE6 input field, it does not translate to UTF-8 ); if ($this->autocomplete !== NULL) { $attributes['autocomplete'] = ($this->autocomplete && $this->autocomplete !== 'off' ? 'on' : 'off'); // Can only be on/off, unlike a field } $attributes = array_merge($attributes, $this->form_attributes); $attributes = array_merge($attributes, $config); //-------------------------------------------------- // Return HTML return html_tag('form', $attributes) . $hidden_fields_html; } public function html_hidden($config = NULL) { //-------------------------------------------------- // Config if (!is_array($config)) { $config = []; } if (!isset($config['wrapper'])) $config['wrapper'] = 'div'; if (!isset($config['class'])) $config['class'] = 'form_hidden_fields'; $field_names = []; foreach ($this->fields as $field) { if ($field->type_get() != 'info') { $field_name = $field->input_name_get(); $field_names[] = $field_name; $field_value = $field->value_hidden_get(); // File field may always return a value, irrespective of $field->print_hidden if ($field_value !== NULL) { $this->hidden_values['h-' . $field_name] = $field_value; } } } if ($this->form_button_name !== NULL) { $field_names[] = $this->form_button_name; } //-------------------------------------------------- // Input fields - use array to keep unique keys $input_fields = []; if (!$this->form_passive) { $input_fields['act'] = ['value' => $this->form_id]; $original_request = intval(request('o')); if ($original_request == 0) { $original_request = time(); } $input_fields['o'] = ['value' => $original_request]; if ($this->dedupe_user_id > 0) { $input_fields['r'] = ['value' => random_key(15)]; // Request identifier } if ($this->csrf_error_html != NULL) { $input_fields['csrf'] = ['value' => $this->csrf_token_get()]; } } foreach ($this->hidden_values as $name => $field) { if (isset($input_fields[$name])) { exit_with_error('The hidden field "' . $name . '" already exists.'); } if (!is_array($field)) { $field = ['value' => $field]; } if (!isset($field['urlencode']) || $field['urlencode'] !== false) { $field['value'] = rawurlencode($field['value']); // URL encode allows newline characters to exist in hidden (one line) input fields. } $input_fields[$name] = $field; } if ($this->form_method == 'GET') { $form_action_query = @parse_url($this->form_action, PHP_URL_QUERY); if ($form_action_query) { parse_str($form_action_query, $form_action_query); foreach ($form_action_query as $name => $value) { if (!isset($input_fields[$name]) && !in_array($name, $field_names) && !is_array($value)) { // Currently does not support ./?a[]=1&a[]=2&a[]=3 $input_fields[$name] = ['value' => $value]; } } } } //-------------------------------------------------- // HTML $html = ''; if ($config['wrapper'] !== NULL) { $html .= '<' . html($config['wrapper']) . ' class="' . html($config['class']) . '">'; } foreach ($input_fields as $name => $field) { $attributes = ['type' => 'hidden', 'name' => $name, 'value' => $field['value']]; if (isset($field['attributes'])) { $attributes = array_merge($attributes, $field['attributes']); } $html .= html_tag('input', $attributes); } if ($config['wrapper'] !== NULL) { $html .= '' . "\n"; } return $html; } public function html_error_list($config = NULL) { $errors_flat_html = $this->errors_get_html(); $html = ''; if (count($errors_flat_html) > 0) { $html = config::get('form.error_prefix_html', ''); $html .= ''; $html .= config::get('form.error_suffix_html', ''); } return $html; } public function html_fields($group = NULL) { //-------------------------------------------------- // Start $k = 0; $html = ''; //-------------------------------------------------- // Auto focus if ($this->autofocus) { foreach ($this->fields as $field_uid => $field) { if ($field->autofocus_auto_set()) { $this->autofocus_submit = false; break; } } } //-------------------------------------------------- // Field groups $field_groups = []; if ($group !== NULL) { $field_groups = array($group); } else { foreach ($this->fields as $field) { if ($field->print_include_get() && !$field->print_hidden_get()) { $field_group = $field->print_group_get(); if ($field_group === NULL) { $field_groups = array(NULL); break; } else if ($field_group !== false) { // When using $form->print_group_start(false); $field_groups[] = $field_group; } } } } $field_groups = array_values(array_unique($field_groups)); // And re-index $group_headings = (count($field_groups) > 1); //-------------------------------------------------- // Fields HTML $html = ''; foreach ($field_groups as $k => $group) { if ($this->print_group_tag == 'fieldset') { if ($k > 0) { $html .= "\n\t\t\t\t" . '' . "\n"; } $html .= "\n\t\t\t\t" . 'print_group_class ? ' class="' . html($this->print_group_class) . '"' : '') . '>' . "\n"; if ($group !== '') { $html .= "\n\t\t\t\t\t" . '' . html($group) . '' . "\n"; } } else if ($group_headings) { if ($group !== '') { $html .= "\n\t\t\t\t" . '<' . html($this->print_group_tag) . '' . ($this->print_group_class ? ' class="' . html($this->print_group_class) . '"' : '') . '>' . html($group) . 'print_group_tag) . '>' . "\n"; } } foreach ($this->fields as $field) { if ($field->print_include_get() && !$field->print_hidden_get()) { $field_group = $field->print_group_get(); if (($group === NULL && $field_group === NULL) || ($group !== NULL && $group == $field_group)) { $k++; if ($k == 1) { $field->wrapper_class_add('first odd'); } else if ($k % 2) { $field->wrapper_class_add('odd'); } else { $field->wrapper_class_add('even'); } $html .= $field->html(); } } } } if ($this->print_group_tag == 'fieldset' && $html != '') { $html .= "\n\t\t\t\t" . '' . "\n"; } //-------------------------------------------------- // Return return $html; } public function html_submit($buttons = NULL) { if ($this->disabled === false && $this->readonly === false) { if ($buttons === NULL) { $buttons = $this->form_button; } if ($buttons === NULL) { return; } if (!is_array($buttons) || is_assoc($buttons)) { $buttons = array($buttons); } $html = '
'; $k = 0; foreach ($buttons as $attributes) { $k++; if (!is_array($attributes)) { if ($attributes instanceof html_template || $attributes instanceof html_safe_value) { $attributes = ['html' => $attributes]; } else { $attributes = ['value' => $attributes]; } } if (isset($attributes['text'])) { $attributes['html'] = html($attributes['text']); if (isset($attributes['url'])) { $attributes['html'] = '' . $attributes['html'] . ''; } } if (isset($attributes['html'])) { $html .= ' ' . $attributes['html']; } else { if (!isset($attributes['value'])) { $attributes['value'] = 'Save'; } if ($this->autofocus_submit && $k == 1) { $attributes['autofocus'] = 'autofocus'; } $html .= ' ' . html_tag('input', array_merge(array('type' => 'submit', 'name' => $this->form_button_name), $attributes)); } } return $html . '
'; } else { return ''; } } public function html_end() { return '' . "\n"; } public function html() { return ' ' . rtrim($this->html_start()) . ' <' . html($this->wrapper_tag) . '> ' . $this->html_error_list() . ' ' . $this->html_fields() . ' ' . $this->html_submit() . ' wrapper_tag) . '> ' . $this->html_end() . "\n"; } //-------------------------------------------------- // Shorter representation in debug_dump() public function _debug_dump() { return 'form()'; } } class form_field { //-------------------------------------------------- // Variables protected $form = NULL; protected $form_field_uid = NULL; protected $form_submitted = false; protected $id = NULL; protected $name = NULL; protected $type = 'unknown'; protected $wrapper_tag = 'div'; protected $wrapper_id = NULL; protected $wrapper_class = ''; protected $wrapper_data = []; protected $label_html = ''; protected $label_aria = NULL; protected $label_prefix_html = ''; protected $label_suffix_html = ''; protected $label_class = NULL; protected $label_wrapper_tag = 'span'; protected $label_wrapper_class = 'label'; protected $input_first = false; protected $input_class = NULL; protected $input_data = []; protected $input_wrapper_tag = 'span'; protected $input_wrapper_class = 'input'; protected $input_described_by = []; protected $format_class = 'format'; protected $format_tag = 'span'; protected $info_html = NULL; protected $info_class = 'info'; protected $info_tag = 'span'; protected $required = false; protected $required_mark_html = NULL; protected $required_mark_position = NULL; protected $autofocus = NULL; protected $autocorrect = NULL; protected $autocomplete = NULL; protected $autocapitalize = NULL; protected $disabled = false; protected $readonly = false; protected $print_group = NULL; protected $print_include = true; protected $print_hidden = false; protected $db_record = NULL; protected $db_field_name = NULL; protected $db_field_key = false; protected $db_field_info = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { $this->setup($form, $label, $name, 'unknown'); } protected function setup($form, $label, $name, $type) { //-------------------------------------------------- // Add this field to the form, and return a "UID" // NOTE: The "ID" is a string for the HTML $form_field_uid = $form->_field_add($this); //-------------------------------------------------- // Label $function = $form->label_override_get_function(); if ($function !== NULL) { $label = call_user_func($function, $label, $form, $this); } $label_html = to_safe_html($label); //-------------------------------------------------- // Name if ($name == '') { // Auto generate a name $name = substr(human_to_ref($label), 0, 30); // Trim really long labels if ($name == '' && !in_array($type, ['info', 'html'])) { exit_with_error('Cannot have a field with no name.', $type); } $k = 1; $name_original = $name; while (config::array_search('form.fields', $name) !== false) { // Ensure it's unique - provided names don't use this check, e.g. "names[]" $name = $name_original . '_' . ++$k; } } $this->input_name_set($name); //-------------------------------------------------- // Field configuration $this->form = $form; $this->form_field_uid = $form_field_uid; $this->form_submitted = $form->submitted(); $this->id = 'fld_' . human_to_ref($this->name); $this->type = $type; $this->label_html = $label_html; $this->label_prefix_html = $form->label_prefix_get_html(); $this->label_suffix_html = $form->label_suffix_get_html(); $this->autocomplete = $form->autocomplete_default_get(); $this->disabled = $form->disabled_get(); $this->readonly = $form->readonly_get(); $this->print_group = $form->print_group_get(); } public function form_get() { return $this->form; } public function uid_get() { return $this->form_field_uid; } public function type_get() { return $this->type; } public function wrapper_tag_set($tag) { $this->wrapper_tag = $tag; } public function wrapper_id_set($id) { $this->wrapper_id = $id; } public function wrapper_class_set($class) { $this->wrapper_class = $class; } public function wrapper_class_add($class) { $this->wrapper_class .= ($this->wrapper_class == '' ? '' : ' ') . $class; } public function wrapper_class_get() { $class = array('row', $this->type, $this->wrapper_class, $this->name); if (!$this->valid()) { $class[] = 'error'; } return implode(' ', array_filter($class)); } public function wrapper_data_set($field, $value) { $this->wrapper_data[$field] = $value; } public function label_set($label) { $this->label_html = to_safe_html($label); } public function label_set_html($label_html) { $this->label_html = $label_html; } public function label_get_html() { return $this->label_html; } public function label_get_text() { // Text suffix used as it's processed data return html_decode(strip_tags($this->label_html)); } public function label_aria_set($label) { $this->label_aria = $label; } public function label_prefix_set($prefix) { $this->label_prefix_set_html(to_safe_html($prefix)); } public function label_prefix_set_html($prefix_html) { $this->label_prefix_html = $prefix_html; } public function label_suffix_set($suffix) { $this->label_suffix_set_html(to_safe_html($suffix)); } public function label_suffix_set_html($suffix_html) { $this->label_suffix_html = $suffix_html; } public function label_class_set($class) { $this->label_class = $class; } public function label_wrapper_tag_set($tag) { $this->label_wrapper_tag = $tag; } public function label_wrapper_class_set($class) { $this->label_wrapper_class = $class; } public function input_id_set($id) { $this->id = $id; } public function input_id_get() { return $this->id; } public function input_first_id_get() { return $this->id; } public function input_name_set($name) { // name usually set on init, use this function ONLY if you really need to change it afterwards. if ($this->name !== NULL) { // Remove the old name from list of used names $fields = config::get('form.fields'); if (is_array($fields)) { $key = array_search($this->name, $fields); if ($key !== false) { unset($fields[$key]); config::set('form.fields', $fields); } } } $this->name = $name; config::array_push('form.fields', $this->name); } public function input_name_get() { return $this->name; } public function input_class_set($class) { $this->input_class = $class; } public function input_data_set($field, $value) { $this->input_data[$field] = $value; } public function input_first_set($first = NULL) { $this->input_first = ($first == true); $this->label_prefix_html = ($first ? '' : $this->form->label_prefix_get_html()); $this->label_suffix_html = ($first ? '' : $this->form->label_suffix_get_html()); if ($this->required_mark_position === NULL) { // Ignore if already set $this->required_mark_position_set($first ? 'right' : 'left'); } } public function input_first_get() { return $this->input_first; } public function input_wrapper_tag_set($tag) { $this->input_wrapper_tag = $tag; } public function input_wrapper_class_set($class) { $this->input_wrapper_class = $class; } public function format_default_get_html() { return ''; } public function format_class_set($class) { $this->format_class = $class; } public function format_tag_set($tag) { $this->format_tag = $tag; } public function info_set($info) { $this->info_set_html($info === NULL ? NULL : to_safe_html($info)); // An empty string can be used for the element to exist (e.g. for JS to populate) } public function info_set_html($info_html) { $this->info_html = $info_html; } public function info_get_html() { return $this->info_html; } public function info_class_set($class) { $this->info_class = $class; } public function info_tag_set($tag) { $this->info_tag = $tag; } public function required_mark_set($required_mark) { $this->required_mark_set_html($required_mark === true ? true : to_safe_html($required_mark)); } public function required_mark_set_html($required_mark_html) { $this->required_mark_html = $required_mark_html; } public function required_mark_get_html($required_mark_position = NULL) { if ($this->required || $this->required_mark_html !== NULL) { if ($this->required_mark_html !== NULL && $this->required_mark_html !== true) { return $this->required_mark_html; } else { return $this->form->required_mark_get_html($required_mark_position); } } else { return ''; } } public function required_mark_position_set($position) { if ($position == 'left' || $position == 'right' || $position == 'none') { $this->required_mark_position = $position; } else { exit_with_error('Invalid required mark position specified (left/right/none)'); } } public function autofocus_set($autofocus) { $this->autofocus = ($autofocus == true); } public function autofocus_auto_set() { if ($this->autofocus === NULL) { // Has been set manually if (!$this->valid()) { $this->autofocus = true; } else if (method_exists($this, '_value_print_get')) { $value = $this->_value_print_get(); if (is_array($value)) { $this->autofocus = (count(array_filter($value)) == 0); // Where $value may be [0,0,0] on a date field (when the form is submitted). } else { $this->autofocus = (strval($value) == ''); } } } return $this->autofocus; } public function autofocus_get() { return $this->autofocus; } public function autocorrect_set($autocorrect) { $this->autocorrect = ($autocorrect == true); } public function autocorrect_get() { return $this->autocorrect; } public function autocomplete_set($autocomplete) { $this->autocomplete = $autocomplete; } public function autocomplete_get() { return $this->autocomplete; } public function autocapitalize_set($autocapitalize) { $this->autocapitalize = $autocapitalize; } public function autocapitalize_get() { return $this->autocapitalize; } public function disabled_set($disabled) { $this->disabled = ($disabled == true); } public function disabled_get() { return $this->disabled; } public function readonly_set($readonly) { $this->readonly = ($readonly == true); } public function readonly_get() { return $this->readonly; } public function print_include_set($include) { // Print on main form automatically $this->print_include = ($include == true); } public function print_include_get() { return $this->print_include; } public function print_hidden_set($hidden) { // Won't print on main form automatically, but will preserve value in a hidden field $this->print_hidden = ($hidden == true); } public function print_hidden_get() { return $this->print_hidden; } public function print_group_set($group) { $this->print_group = $group; } public function print_group_get() { return $this->print_group; } protected function _db_field_set($a, $b = NULL, $c = NULL) { $form_record = $this->form->db_record_get(); if ($a instanceof record) { $record = $a; $field_name = $b; $field_type = $c; if (!in_array($record, (is_array($form_record) ? $form_record : array($form_record)))) { exit_with_error('The form helper needs to be told about the record for "' . $field_name . '" by using $form->db_record_set(array($record1, $record2, ...))'); } } else { $record = $form_record; if (!($record instanceof record)) { exit_with_error('Please specify a record to use when setting the db field for "' . $this->name . '"'); } $field_name = $a; $field_type = $b; } if ($this->db_field_name !== NULL && $this->db_field_name != $field_name) { if (SERVER == 'stage') { exit_with_error('Changing the "' . $this->label_get_text() . '" db_field from "' . $this->db_field_name . '" to "' . $field_name . '"'); } else { report_add('Changing the "' . $this->label_get_text() . '" db_field from "' . $this->db_field_name . '" to "' . $field_name . '"', 'error'); // TODO: Change to an exit_with_error } } $this->db_record = $record; $this->db_field_name = $field_name; $this->db_field_key = ($field_type == 'key'); $this->db_field_info = $record->field_get($field_name); // Will exit_with_error if invalid. $record->field_name_add($field_name); // Temp (only until all projects use the record helper) } public function db_field_set($a, $b = NULL) { $this->_db_field_set($a, $b); } public function db_field_name_get() { return $this->db_field_name; } public function db_field_key_get() { return $this->db_field_key; } public function db_field_info_get($key = NULL) { if ($key) { if (isset($this->db_field_info[$key])) { return $this->db_field_info[$key]; } } else { if (isset($this->db_field_info)) { return $this->db_field_info; } } return NULL; } public function db_field_value_get() { return $this->db_record->value_get($this->db_field_name); } public function _db_field_value_new_get() { if ($this->db_field_name !== NULL && !$this->disabled && !$this->readonly) { if ($this->db_field_key) { $field_value = $this->value_key_get(); } else if ($this->type == 'date') { $field_value = $this->value_date_get(); } else { $field_value = $this->value_get(); } if ($this->db_field_info['null']) { if ($field_value === '' && in_array($this->db_field_info['type'], ['int', 'decimal'])) { $field_value = NULL; // e.g. number field setting an empty string (not 0). } } else { if ($field_value === NULL) { $field_value = ''; // e.g. enum with "not null" and select field with selected label. } } return array($this->db_field_name, $field_value); } else { return NULL; // Not setting the field to NULL } } public function _db_field_value_update() { $value_new = $this->_db_field_value_new_get(); if ($value_new) { $this->db_record->value_set($value_new[0], $value_new[1]); } } //-------------------------------------------------- // Errors public function error_set($error) { $this->error_set_html(to_safe_html($error)); } public function error_set_html($error_html) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } public function error_add($error, $hidden_info = NULL) { $this->error_add_html(to_safe_html($error), $hidden_info); } public function error_add_html($error_html, $hidden_info = NULL) { $this->form->_field_error_add_html($this->form_field_uid, $error_html, $hidden_info); } public function error_count() { return count($this->errors_get_html()); } public function errors_get_html() { return $this->form->_field_errors_get_html($this->form_field_uid); } //-------------------------------------------------- // Value public function value_hidden_get() { if ($this->print_hidden) { return ''; } else { return NULL; } } //-------------------------------------------------- // Status public function valid() { return $this->form->_field_valid($this->form_field_uid); } //-------------------------------------------------- // Validation public function _post_validation() { } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = array( 'name' => $this->name, 'id' => $this->id, ); foreach ($this->input_data as $field => $value) { $attributes['data-' . $field] = $value; } if ($this->input_class !== NULL) { $attributes['class'] = $this->input_class; } if ($this->required) { $attributes['required'] = 'required'; } if ($this->autofocus) { $attributes['autofocus'] = 'autofocus'; } if ($this->autocorrect !== NULL) { $attributes['autocorrect'] = ($this->autocorrect ? 'on' : 'off'); } if ($this->autocomplete !== NULL) { $attributes['autocomplete'] = (is_string($this->autocomplete) ? $this->autocomplete : ($this->autocomplete ? 'on' : 'off')); } if ($this->autocapitalize !== NULL) { $attributes['autocapitalize'] = (is_string($this->autocapitalize) ? $this->autocapitalize : ($this->autocapitalize ? 'sentences' : 'none')); } if ($this->disabled) { $attributes['disabled'] = 'disabled'; } if ($this->readonly) { $attributes['readonly'] = 'readonly'; } if ($this->label_aria) { $attributes['aria-label'] = $this->label_aria; } if (!$this->valid()) { $attributes['aria-invalid'] = 'true'; } if ($this->input_described_by !== NULL && count($this->input_described_by) > 0) { $attributes['aria-describedby'] = implode(' ', $this->input_described_by); } return $attributes; } //-------------------------------------------------- // HTML public function html_label($label_html = NULL) { //-------------------------------------------------- // Required mark $required_mark_position = $this->required_mark_position; if ($required_mark_position === NULL) { $required_mark_position = $this->form->required_mark_position_get(); } $required_mark_html = $this->required_mark_get_html($required_mark_position); //-------------------------------------------------- // Return the HTML for the label if ($label_html === NULL) { $label_html = $this->label_html; } if ($label_html != '') { $label_html = $this->label_prefix_html . '' . $this->label_suffix_html; } return new html_safe_value($label_html); } protected function _html_input($attributes_custom = []) { return html_tag('input', array_merge($this->_input_attributes(), $attributes_custom)); } public function html_input() { return 'ERROR'; } public function html_format($indent = 0) { $format_html = $this->format_default_get_html(); if ($format_html == '') { return ''; } else { return ($indent > 0 ? "\n" : '') . str_repeat("\t", $indent) . '<' . html($this->format_tag) . ' class="' . html($this->format_class) . '">' . $format_html . 'format_tag) . '>'; } } public function html_info($indent = 0) { if ($this->info_html === NULL) { return ''; } else { $tag_id = $this->form->_field_tag_id_get(); if ($this->input_described_by !== NULL) { $this->input_described_by[] = $tag_id; } return ($indent > 0 ? "\n" : '') . str_repeat("\t", $indent) . '<' . html($this->info_tag) . ' class="' . html($this->info_class) . '" id="' . html($tag_id) . '">' . $this->info_html . 'info_tag) . '>'; } } public function html() { $info_html = $this->html_info(8); // Adds to input_described_by, so the input field can include "aria-describedby" $format_html = $this->html_format(8); $label_html = $this->html_label(); if ($label_html != '') { // Info fields might not specify a label $label_html = '<' . html($this->label_wrapper_tag) . ' class="' . html($this->label_wrapper_class) . '">' . $label_html . 'label_wrapper_tag) . '>'; } if (method_exists($this, 'html_input_by_key')) { $html = ' ' . $label_html . $this->html_input() . $format_html . $info_html; } else { $input_html = '<' . html($this->input_wrapper_tag) . ' class="' . html($this->input_wrapper_class) . '">' . $this->html_input() . 'input_wrapper_tag) . '>'; if ($this->input_first) { $html = ' ' . $input_html . ' ' . $label_html . $format_html . $info_html; } else { $html = ' ' . $label_html . ' ' . $input_html . $format_html . $info_html; } } $wrapper_attributes = array( 'id' => $this->wrapper_id, 'class' => $this->wrapper_class_get() . ($this->input_first ? ' input_first' : ''), ); foreach ($this->wrapper_data as $field => $value) { $wrapper_attributes['data-' . $field] = $value; } return ' ' . html_tag($this->wrapper_tag, $wrapper_attributes) . $html . ' wrapper_tag) . '>' . "\n"; } //-------------------------------------------------- // Shorter representation in debug_dump() public function _debug_dump() { if (isset($this->value)) { $value = '"' . $this->value . '"'; } else if (isset($this->values)) { $value = debug_dump($this->values, 2); } else if (method_exists($this, 'file_name_get')) { $value = $this->file_name_get(); } else { $value = 'NULL'; } return get_class($this) . ' = ' . $value; } } class form_field_text extends form_field { //-------------------------------------------------- // Variables protected $value; protected $min_length = NULL; protected $max_length = NULL; protected $placeholder = NULL; protected $input_type = 'text'; protected $input_mode = NULL; protected $input_size = NULL; protected $input_list_id = NULL; protected $input_list_options = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { $this->setup_text($form, $label, $name, 'text'); } protected function setup_text($form, $label, $name, $type) { //-------------------------------------------------- // Perform the standard field setup $this->setup($form, $label, $name, $type); //-------------------------------------------------- // Value $this->value = NULL; if ($this->form_submitted || $this->form->saved_values_available()) { if ($this->form_submitted) { $this->value = request($this->name, $this->form->form_method_get()); } else { $this->value = $this->form->saved_value_get($this->name); } if ($this->value === NULL) { $this->value = $this->form->hidden_value_get('h-' . $this->name); } if ($this->value !== NULL) { if (config::get('form.auto_clean_whitespace', false)) { // Before auto_trim, e.g. non-breaking-space is trimmed. $this->value = clean_whitespace($this->value); } if (config::get('form.auto_trim', true)) { $this->value = trim($this->value); } } } //-------------------------------------------------- // Additional field configuration $this->input_type = 'text'; } public function input_type_set($input_type) { $this->input_type = $input_type; // e.g. "tel" } public function input_size_set($input_size) { $this->input_size = $input_size; } public function input_mode_set($input_mode) { $this->input_mode = $input_mode; } public function input_list_set($options, $id = NULL) { if (count($options) > 0) { if ($id === NULL) { $id = $this->input_id_get() . '_list'; } $this->input_list_id = $id; $this->input_list_options = $options; } else { $this->input_list_id = NULL; $this->input_list_options = NULL; } } public function placeholder_set($placeholder) { $this->placeholder = $placeholder; } //-------------------------------------------------- // Errors public function min_length_set($error, $size = 1) { // Default is "required" $this->min_length_set_html(to_safe_html($error), $size); } public function min_length_set_html($error_html, $size = 1) { $error_html = str_replace('XXX', $size, $error_html); if ($this->form_submitted && strlen(trim(strval($this->value))) < $size) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->min_length = $size; $this->required = ($size > 0); } public function max_length_set($error, $size = NULL) { $this->max_length_set_html(to_safe_html($error), $size); } public function max_length_set_html($error_html, $size = NULL) { if ($size === NULL) { if ($this->db_field_name === NULL) { exit('

You need to call "db_field_set", on the field "' . $this->label_html . '"

'); } $size = intval($this->db_field_info_get('length')); // Convert NULL to 0 explicitly, always triggers error. } $error_html = str_replace('XXX', $size, $error_html); if ($this->form_submitted && strlen(strval($this->value)) > $size) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->max_length = $size; } //-------------------------------------------------- // Value public function value_set($value) { $this->value = $value; } public function value_get() { return $this->value; } protected function _value_print_get() { if ($this->value === NULL) { if ($this->db_field_name !== NULL) { $db_value = $this->db_field_value_get(); } else { $db_value = NULL; } return $db_value; } return $this->value; // Don't use $this->value_get(), as fields such as currency/postcode use that function to return the clean version. } public function value_hidden_get() { if ($this->print_hidden) { return strval($this->_value_print_get()); // Cannot be NULL, as a field with print_hidden_set(true) will not get a hidden field. } else { return NULL; } } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->max_length === NULL) { exit('

You need to call "max_length_set", on the field "' . $this->label_html . '"

'); } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if ($this->input_type !== NULL) { $attributes['type'] = $this->input_type; } if ($this->input_mode !== NULL) { $attributes['inputmode'] = $this->input_mode; // https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute } if ($this->input_size !== NULL) { $attributes['size'] = intval($this->input_size); } if ($this->input_list_id !== NULL) { $attributes['list'] = $this->input_list_id; } if ($this->min_length !== NULL && $this->min_length > 1) { // Value of 1 (default) is basically required $attributes['minlength'] = intval($this->min_length); } if ($this->max_length !== NULL && $this->max_length > 0) { $attributes['maxlength'] = intval($this->max_length); } if ($this->placeholder !== NULL) { $attributes['placeholder'] = $this->placeholder; } return $attributes; } //-------------------------------------------------- // HTML public function html_input() { $html = $this->_html_input(['value' => strval($this->_value_print_get())]); if ($this->input_list_id !== NULL) { $html .= ''; foreach ($this->input_list_options as $id => $value) { $html .= ''; } return $html; } } class form_field_textarea extends form_field_text { //-------------------------------------------------- // Variables protected $textarea_rows = 5; protected $textarea_cols = 40; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'textarea'); } public function rows_set($rows) { $this->textarea_rows = $rows; } public function cols_set($cols) { $this->textarea_cols = $cols; } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); unset($attributes['type']); unset($attributes['value']); unset($attributes['size']); $attributes['rows'] = intval($this->textarea_rows); $attributes['cols'] = intval($this->textarea_cols); return $attributes; } //-------------------------------------------------- // HTML public function html_input() { return html_tag('textarea', $this->_input_attributes()) . html(strval($this->_value_print_get())) . ''; } } class form_field_url extends form_field_text { //-------------------------------------------------- // Variables protected $format_error_set = false; protected $format_error_found = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'url'); //-------------------------------------------------- // Additional field configuration $this->input_type = 'text'; // Not "url", as it requires a "https?://" prefix, which most people don't bother with. $this->input_mode = 'url'; } //-------------------------------------------------- // Errors public function format_error_set($error) { $this->format_error_set_html(to_safe_html($error)); } public function format_error_set_html($error_html) { if ($this->form_submitted && $this->value != '') { $url_parts = @parse_url($this->value); if ($url_parts === false || !isset($url_parts['scheme']) || !isset($url_parts['host'])) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->format_error_found = true; } } $this->format_error_set = true; } public function scheme_default_set($scheme) { if ($this->form_submitted && $this->value != '' && !preg_match('/^[a-z]+:/i', $this->value)) { $this->value = $scheme . '://' . $this->value; } } public function scheme_allowed_set($error, $schemes) { $this->scheme_allowed_set_html(to_safe_html($error), $schemes); } public function scheme_allowed_set_html($error_html, $schemes) { if ($this->form_submitted && $this->value != '') { $url_parts = @parse_url($this->value); if (isset($url_parts['scheme']) && !in_array($url_parts['scheme'], $schemes)) { $this->form->_field_error_set_html($this->form_field_uid, $error_html, 'Scheme: ' . $url_parts['scheme']); } } } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->format_error_set == false) { exit('

You need to call "format_error_set", on the field "' . $this->label_html . '"

'); } } } class form_field_email extends form_field_text { //-------------------------------------------------- // Variables protected $multiple = false; protected $domain_check = true; protected $domain_error_html = NULL; protected $domain_error_skip_value = ''; protected $domain_error_skip_html = NULL; protected $domain_error_skip_show = false; protected $format_error_html = false; protected $format_error_set = false; protected $format_error_found = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'email'); //-------------------------------------------------- // Additional field configuration $this->input_type = 'email'; $this->input_mode = 'email'; $this->autocapitalize = false; } public function check_domain_set($check_domain) { report_add('Deprecated: The email field check_domain_set() is being re-named to domain_check_set()', 'notice'); $this->domain_check_set($check_domain); } public function multiple_set($multiple) { $this->multiple = $multiple; } public function multiple_get() { return $this->multiple; } //-------------------------------------------------- // Errors public function domain_check_set($domain_check) { $this->domain_check = $domain_check; } public function domain_error_set($error, $skip_label = NULL) { // If a domain error is not set, the format error will be used (assuming the domain is checked). $this->domain_error_set_html(to_safe_html($error), html($skip_label)); } public function domain_error_set_html($error_html, $skip_label_html = NULL) { $this->domain_error_html = $error_html; if ($skip_label_html) { $name = $this->name . '-DW'; $id = $this->id . '-DW'; $this->domain_error_skip_value = request($name, $this->form->form_method_get()); $this->domain_error_skip_html = ' domain_error_skip_value == $this->value ? ' checked="checked"' : '') . ' /> '; } else { $this->domain_error_skip_value = ''; $this->domain_error_skip_html = NULL; } } public function format_error_set($error) { // To provide an override to the domain_check, try using $field->domain_error_set('The email address does not end with a valid domain (the bit after the @ sign).', 'Skip Check?'); $this->format_error_set_html(to_safe_html($error)); } public function format_error_set_html($error_html) { $this->format_error_html = $error_html; $this->format_error_set = true; } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->format_error_set == false) { exit('

You need to call "format_error_set", on the field "' . $this->label_html . '"

'); } if ($this->form_submitted && $this->value != '') { if ($this->multiple) { $emails = array_filter(array_map('trim', explode(',', $this->value))); $this->value = implode(',', $emails); // Cleanup any whitespace characters (browsers generally do this automatically). } else { $emails = [$this->value]; } foreach ($emails as $email) { $valid = is_email($email, ($this->domain_check ? -1 : false)); // -1 to return the type of failure (-1 for format, -2 for domain check) if ($valid !== true) { if ($this->domain_error_html && $valid === -2) { $this->domain_error_skip_show = true; if (!$this->domain_error_skip_html || $this->domain_error_skip_value != $email) { $this->form->_field_error_set_html($this->form_field_uid, $this->domain_error_html); } } else { $this->form->_field_error_set_html($this->form_field_uid, $this->format_error_html); } } } } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if ($this->multiple) { $attributes['multiple'] = 'multiple'; } return $attributes; } //-------------------------------------------------- // HTML public function html_input() { $html = parent::html_input(); if ($this->domain_error_skip_show) { $html .= $this->domain_error_skip_html; } return $html; } } class form_field_number extends form_field_text { //-------------------------------------------------- // Variables protected $value_clean = NULL; protected $format_error_set = false; protected $format_error_found = false; protected $zero_to_blank = false; protected $min_value = NULL; protected $max_value = NULL; protected $step_value = 'any'; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { $this->setup_number($form, $label, $name, 'number'); } protected function setup_number($form, $label, $name, $type) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, $type); //-------------------------------------------------- // Clean input value if ($this->form_submitted) { $this->value_set($this->value); } //-------------------------------------------------- // Additional field configuration $this->input_type = 'number'; $this->input_mode = 'decimal'; // Can be 'numeric' when using step_value_set(). } public function zero_to_blank_set($blank) { $this->zero_to_blank = ($blank == true); } //-------------------------------------------------- // Errors public function format_error_set($error) { $this->format_error_set_html(to_safe_html($error)); } public function format_error_set_html($error_html) { if ($this->form_submitted && $this->value !== '' && $this->value_clean === NULL) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->format_error_found = true; } $this->format_error_set = true; } public function required_error_set($error) { $this->min_length_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { $this->min_length_set_html($error_html); } public function min_value_set($error, $value) { $this->min_value_set_html(to_safe_html($error), $value); } public function min_value_set_html($error_html, $value) { if ($this->form_submitted && !$this->format_error_found && $this->value !== '' && $this->value_clean < $value) { $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $value, $error_html)); } $this->min_value = $value; } public function max_value_set($error, $value) { $this->max_value_set_html(to_safe_html($error), $value); } public function max_value_set_html($error_html, $value) { if ($this->form_submitted && !$this->format_error_found && $this->value !== '' && $this->value_clean > $value) { $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $value, $error_html)); } $this->max_value = $value; $this->max_length = (strlen($value) + 6); // Allow for a decimal place, plus an arbitrary 5 digits. if ($this->input_size === NULL && $this->max_length < 20) { $this->input_size = $this->max_length; } } public function step_value_set($error, $step = 1) { $this->step_value_set_html(to_safe_html($error), $step); } public function step_value_set_html($error_html, $step = 1) { if ($this->form_submitted && !$this->format_error_found && $this->value !== '') { $value = $this->value_clean; if ($this->min_value !== NULL) { $value += $this->min_value; // HTML step starts at the min value } if (abs((round($value / $step) * $step) - $value) > 0.00001) { // ref 'epsilon' on https://php.net/manual/en/language.types.float.php $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $step, $error_html)); } } $this->step_value = $step; $this->input_mode = (floor($step) != $step ? 'decimal' : 'numeric'); } //-------------------------------------------------- // Value public function value_set($value) { if ($value === NULL) { // A disabled input field won't be submitted (NULL) $value = ''; } if ($value === '') { $this->value_clean = 0; } else { $this->value_clean = parse_number($value); } $this->value = $value; } public function value_get() { if ($this->value === '') { // Allow caller to differentiate between '' and '0', so it can store no value as NULL in database. return ''; } else { return $this->value_clean; } } protected function _value_print_get() { $value = parent::_value_print_get(); // Value from $this->value (request, saved_value, or hidden_value); or database. $value_clean = parse_number($value); if ($value_clean !== NULL) { if ($value_clean == 0 && $this->zero_to_blank && $this->type != 'currency') { return ''; } else { return $value_clean; } } return $value; } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->format_error_set == false) { exit('

You need to call "format_error_set", on the field "' . $this->label_html . '"

'); } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if ($this->min_value !== NULL) { $attributes['min'] = $this->min_value; } if ($this->max_value !== NULL) { $attributes['max'] = $this->max_value; } if ($this->step_value !== NULL && $this->input_type != 'text') { // Text is used for currency fields $attributes['step'] = $this->step_value; } if (isset($attributes['value']) && $attributes['value'] === '') { unset($attributes['value']); // HTML5 validation requires a valid floating point number, so can't be an empty string } if ($this->input_type == 'number') { unset($attributes['size']); // Invalid HTML5 attribute, but currency field is still text. } unset($attributes['minlength']); // Invalid HTML5 attributes unset($attributes['maxlength']); return $attributes; } } class form_field_password extends form_field_text { //-------------------------------------------------- // Variables protected $passwordrules = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'password'); //-------------------------------------------------- // Additional field configuration $this->input_type = 'password'; } public function passwordrules_set($passwordrules) { $this->passwordrules = $passwordrules; // https://github.com/whatwg/html/issues/3518 // https://developer.apple.com/password-rules/ // https://github.com/mozilla/standards-positions/issues/61 } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if ($this->passwordrules !== NULL) { $attributes['passwordrules'] = $this->passwordrules; } return $attributes; } } class form_field_postcode extends form_field_text { //-------------------------------------------------- // Variables protected $format_country = 'UK'; protected $format_error_set = false; protected $format_error_found = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'postcode'); //-------------------------------------------------- // Additional field configuration $this->max_length = 8; // Bypass required "max_length_set" call, and to set the } //-------------------------------------------------- // Errors public function format_country_set($country) { $this->format_country = $country; } public function format_error_set($error) { $this->format_error_set_html(to_safe_html($error)); } public function format_error_set_html($error_html) { if ($this->form_submitted && $this->value != '') { $postcode_clean = format_postcode($this->value, $this->format_country); if ($postcode_clean === NULL) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->format_error_found = true; } } $this->format_error_set = true; } public function required_error_set($error) { $this->required_error_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { if ($this->form_submitted && $this->value == '') { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->required = ($error_html !== NULL); } //-------------------------------------------------- // Value public function value_get() { $value = format_postcode($this->value, $this->format_country); return ($value === NULL ? '' : $value); // If the value is an empty string (or error), it should return an empty string, so changes can be detected with new_value !== old_value } public function value_raw_get() { return $this->value; } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->format_error_set == false) { exit('

You need to call "format_error_set", on the field "' . $this->label_html . '"

'); } } } class form_field_telephone extends form_field_text { //-------------------------------------------------- // Variables protected $format_error_set = false; protected $format_error_found = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'telephone'); //-------------------------------------------------- // Additional field configuration $this->input_type = 'tel'; // Still allow characters (e.g. "0000 000000 Ext 00") $this->input_mode = 'tel'; } //-------------------------------------------------- // Errors public function format_error_set($error) { $this->format_error_set_html(to_safe_html($error)); } public function format_error_set_html($error_html) { if ($this->form_submitted && $this->value != '') { $telephone_clean = format_telephone_number($this->value); if ($telephone_clean === NULL) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->format_error_found = true; } } $this->format_error_set = true; } public function required_error_set($error) { $this->required_error_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { if ($this->form_submitted && $this->value == '') { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->required = ($error_html !== NULL); } //-------------------------------------------------- // Value public function value_get() { $value = format_telephone_number($this->value); return ($value === NULL ? '' : $value); // If the value is an empty string (or error), it should return an empty string, so changes can be detected with new_value !== old_value } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->format_error_set == false) { exit('

You need to call "format_error_set", on the field "' . $this->label_html . '"

'); } } } class form_field_currency extends form_field_number { //-------------------------------------------------- // Variables protected $currency_char = '£'; protected $trim_decimal = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_number($form, $label, $name, 'currency'); //-------------------------------------------------- // Additional field configuration $this->step_value = NULL; $this->input_type = 'text'; // Not type="number", from number field } public function currency_char_set($char) { $this->currency_char = $char; } public function trim_decimal_set($trim) { $this->trim_decimal = $trim; } //-------------------------------------------------- // Errors public function min_value_set_html($error_html, $value) { $value = floatval($value); if ($this->form_submitted && !$this->format_error_found && $this->value !== '' && $this->value_clean < $value) { if ($value < 0) { $value_text = '-' . $this->currency_char . number_format((0 - $value), 2); } else { $value_text = $this->currency_char . number_format($value, 2); } $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $value_text, $error_html)); } $this->min_length = $value; } public function max_value_set_html($error_html, $value) { $value = floatval($value); if ($this->form_submitted && !$this->format_error_found && $this->value !== '' && $this->value_clean > $value) { if ($value < 0) { $value_text = '-' . $this->currency_char . number_format((0 - $value), 2); } else { $value_text = $this->currency_char . number_format($value, 2); } $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $value_text, $error_html)); } $this->max_length = strlen(floor($value)); $this->max_length += (intval($this->min_length) < 0 ? 1 : 0); // Negative numbers $this->max_length += (function_exists('mb_strlen') ? mb_strlen($this->currency_char, config::get('output.charset')) : strlen($this->currency_char)); $this->max_length += (floor((strlen(floor($value)) - 1) / 3)); // Thousand separators $this->max_length += 3; // Decimal place char, and 2 digits if ($this->input_size === NULL && $this->max_length < 20) { $this->input_size = $this->max_length; } } //-------------------------------------------------- // Value protected function _value_print_get() { $value = parent::_value_print_get(); // form_field_number will try to use parse_number() to return an int or float. if ($this->trim_decimal && fmod($value, 1) == 0) { $decimal_places = 0; } else { $decimal_places = ($this->step_value == 1 ? 0 : 2); } if (is_int($value) || is_float($value)) { return format_currency($value, $this->currency_char, $decimal_places, $this->zero_to_blank); } else { return $value; } } } class form_field_checkbox extends form_field_text { //-------------------------------------------------- // Variables protected $text_value_true = NULL; protected $text_value_false = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'check'); //-------------------------------------------------- // Value if ($this->form_submitted) { $this->value = ($this->value == 'true'); } //-------------------------------------------------- // Additional field configuration $this->max_length = -1; // Bypass the _post_validation on the text field (not used) $this->input_type = 'checkbox'; } public function text_values_set($true, $false) { $this->text_value_true = $true; $this->text_value_false = $false; } //-------------------------------------------------- // Errors public function required_error_set($error) { $this->required_error_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { if ($this->form_submitted && $this->value !== true) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->required = ($error_html !== NULL); } //-------------------------------------------------- // Value public function value_set($value) { if ($this->text_value_true !== NULL) { $this->value = ($value == $this->text_value_true); } else { $this->value = ($value == true); } } public function value_get() { if ($this->text_value_true !== NULL) { return ($this->value ? $this->text_value_true : $this->text_value_false); } else { return $this->value; } } protected function _value_print_get() { if ($this->value === NULL) { $true_value = ($this->text_value_true !== NULL ? $this->text_value_true : true); if ($this->db_field_name !== NULL) { $db_value = $this->db_field_value_get(); } else { $db_value = NULL; } return ($true_value == $db_value); } return $this->value; } public function value_hidden_get() { if ($this->print_hidden) { return ($this->value ? 'true' : 'false'); } else { return NULL; } } //-------------------------------------------------- // HTML public function html_input() { $attributes = array( 'value' => 'true', ); if ($this->_value_print_get()) { $attributes['checked'] = 'checked'; } return $this->_html_input($attributes); } } class form_field_checkboxes extends form_field_select { //-------------------------------------------------- // Variables protected $value_print_cache = NULL; protected $option_values_html = []; protected $options_group_id = []; protected $options_info_id = []; protected $options_info_html = NULL; protected $options_disabled = NULL; protected $options_suffix_html = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_select($form, $label, $name, 'checkboxes'); //-------------------------------------------------- // Additional field configuration $this->multiple = true; // So functions like value_get will return all items } public function options_set_html($options_html) { // If you are adding links, consider options_info_set() $this->options_set(array_map('html_decode', array_map('strip_tags', $options_html))); $this->option_values_html = $options_html; } public function option_set_html($ref, $value_html) { $this->option_values_html[$ref] = $value_html; } public function options_info_set($options_info) { $this->options_info_set_html(array_map('to_safe_html', $options_info)); } public function options_info_set_html($options_info_html) { $this->options_info_html = $options_info_html; } public function options_suffix_set($options_suffix) { $this->options_suffix_set_html(array_map('to_safe_html', $options_suffix)); } public function options_suffix_set_html($options_suffix_html) { $this->options_suffix_html = $options_suffix_html; } public function options_disabled_set($options_disabled) { $this->options_disabled = $options_disabled; } //-------------------------------------------------- // Field ID public function field_id_by_value_get($value) { $key = array_search($value, $this->option_values); if ($key !== false && $key !== NULL) { return $this->field_id_by_key_get($key); } else { return 'Unknown value "' . html($value) . '"'; } } public function field_id_by_key_get($key) { if ($key === NULL) { return $this->id; // Label } else { return $this->id . '_' . human_to_ref($key); } } public function input_first_id_get() { reset($this->option_values); return $this->field_id_by_key_get(key($this->option_values)); } //-------------------------------------------------- // HTML label public function html_label($label_html = NULL) { if ($label_html === NULL) { $label_html = parent::html_label(); $label_html = preg_replace('/]+>(.*)<\/label>/', '$1', $label_html); // Ugly, but better than duplication } $tag_id = $this->form->_field_tag_id_get(); // For "aria-describedby" array_unshift($this->input_described_by, $tag_id); // Label comes first return '' . $label_html . ''; // No, you still can't have multiple labels for an input } public function html_label_by_value($value, $label_html = NULL) { $key = array_search($value, $this->option_values); if ($key !== false && $key !== NULL) { return $this->html_label_by_key($key, $label_html); } else { return 'Unknown value "' . html($value) . '"'; } } public function html_label_by_key($key, $label_html = NULL, $label_tag = NULL) { if ($key !== NULL && !isset($this->option_values[$key])) { return 'Unknown key "' . html($key) . '"'; } $input_id = $this->field_id_by_key_get($key); if ($label_html === NULL) { if ($key === NULL) { $label_html = to_safe_html($this->label_option); } else if (isset($this->option_values_html[$key])) { $label_html = $this->option_values_html[$key]; } else { $label_html = to_safe_html($this->option_values[$key]); } $function = $this->form->label_override_get_function(); if ($function !== NULL) { $label_html = call_user_func($function, $label_html, $this->form, $this); } } if ($label_tag == 'span') { return 'label_class === NULL ? '' : ' class="' . html($this->label_class) . '"') . '>' . $label_html . ''; } else { return ''; } } //-------------------------------------------------- // HTML input public function html_input() { $label_tag = ($this->input_wrapper_tag == 'label' ? 'span' : NULL); if ($this->label_option != '') { // Could be NULL or '' $label_html = ' <' . html($this->input_wrapper_tag) . ' class="' . html($this->input_wrapper_class) . ' input_label"> ' . $this->html_input_by_key(NULL) . ' ' . $this->html_label_by_key(NULL, NULL, $label_tag) . ' input_wrapper_tag) . '>'; } else { $label_html = ''; } if ($this->options_group !== NULL) { foreach (array_unique($this->options_group) as $opt_group) { $this->options_group_id[$opt_group] = $this->form->_field_tag_id_get(); } } $option_html = []; foreach ($this->option_values as $key => $value) { $option_info_html = $this->html_info_by_key($key); // Allow the ID to be added to "aria-describedby" $option_disabled = (isset($this->options_disabled[$key]) && $this->options_disabled[$key] === true); $option_html[$key] = ' <' . html($this->input_wrapper_tag) . ' class="' . html($this->input_wrapper_class) . ' ' . html('key_' . human_to_ref($key)) . ' ' . html('value_' . human_to_ref($value)) . ($option_disabled ? ' option_disabled' : '') . '"> ' . $this->html_input_by_key($key) . ' ' . $this->html_label_by_key($key, NULL, $label_tag) . $option_info_html . ' input_wrapper_tag) . '>'; if (isset($this->options_suffix_html[$key])) { $option_html[$key] .= $this->options_suffix_html[$key]; } } $used_keys = []; $group_html = ''; if ($this->options_group !== NULL) { foreach (array_unique($this->options_group) as $opt_group) { if ($opt_group !== NULL) { $group_html .= '
' . html($opt_group) . ''; } foreach (array_keys($this->options_group, $opt_group) as $key) { if ($key === '') { $group_html .= $label_html; $label_html = ''; } else if (isset($option_html[$key])) { $used_keys[] = $key; $group_html .= $option_html[$key]; } } if ($opt_group !== NULL) { $group_html .= '
'; } } } $html = $label_html; foreach ($this->option_values as $key => $value) { if (!in_array($key, $used_keys)) { $html .= $option_html[$key]; } } $html .= $group_html; return $html; } public function html_input_by_value($value) { $key = array_search($value, $this->option_values); if ($key !== false && $key !== NULL) { return $this->html_input_by_key($key); } else { return 'Unknown value "' . html($value) . '"'; } } public function html_input_by_key($key) { if ($key !== NULL && !isset($this->option_values[$key])) { return 'Unknown key "' . html($key) . '"'; } $attributes = $this->_input_by_key_attributes($key); if (isset($this->options_info_id[$key])) { $attributes['aria-describedby'] .= ' ' . $this->options_info_id[$key]; } $group_ref = ($this->options_group[$key] ?? NULL); if ($group_ref && isset($this->options_group_id[$group_ref])) { $attributes['aria-describedby'] .= ' ' . $this->options_group_id[$group_ref]; // Should already be set, so append is ok. } return html_tag('input', $attributes); } public function _input_by_key_attributes($key) { if ($this->value_print_cache === NULL) { $this->value_print_cache = $this->_value_print_get(); // form_field_select always returns an array (not NULL) } $attributes = ($this->options_attributes[$key] ?? []); // Takes least precedence $attributes = array_merge($attributes, parent::_input_attributes()); $attributes['type'] = 'checkbox'; $attributes['id'] = $this->field_id_by_key_get($key); $attributes['name'] = $this->name . '[]'; $attributes['value'] = ($key === NULL ? '' : $key); $attributes['required'] = NULL; // Can't set to required, as otherwise you have to tick all of them. if (isset($this->options_disabled[$key]) && $this->options_disabled[$key] === true) { $attributes['disabled'] = 'disabled'; } if (isset($this->options_class[$key])) { $attributes['class'] = $this->options_class[$key]; } if (in_array($attributes['value'], $this->value_print_cache)) { $attributes['checked'] = 'checked'; } reset($this->option_values); if (key($this->option_values) != $key) { unset($attributes['autofocus']); } return $attributes; } public function html_info_by_key($key) { if (isset($this->options_info_html[$key])) { $id = $this->form->_field_tag_id_get(); $this->options_info_id[$key] = $id; return ' ' . $this->options_info_html[$key] . ''; } else { return ''; } } } class form_field_radios extends form_field_checkboxes { //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the select field setup $this->setup_select($form, $label, $name, 'radios'); } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->required_error_set == false && $this->label_option === NULL) { exit('

You need to call "required_error_set" or "label_option_set", on the field "' . $this->label_html . '"

'); } } //-------------------------------------------------- // HTML input public function _input_by_key_attributes($key) { if ($this->value_print_cache === NULL) { $this->value_print_cache = $this->_value_print_get(); // form_field_select always returns an array (not NULL) } $attributes = ($this->options_attributes[$key] ?? []); // Takes least precedence $attributes = array_merge($attributes, parent::_input_attributes()); $attributes['type'] = 'radio'; $attributes['id'] = $this->field_id_by_key_get($key); $attributes['value'] = ($key === NULL ? '' : $key); if (isset($this->options_disabled[$key]) && $this->options_disabled[$key] === true) { $attributes['disabled'] = 'disabled'; } if (isset($this->options_class[$key])) { $attributes['class'] = $this->options_class[$key]; } $checked = in_array($attributes['value'], $this->value_print_cache); // Cannot be a strict check, as an ID from the db may be a string or int. if ($key === NULL && count($this->value_print_cache) == 0) { $checked = true; } if ($checked) { $attributes['checked'] = 'checked'; } return $attributes; } } class form_field_select extends form_field { //-------------------------------------------------- // Variables protected $values = NULL; protected $multiple = false; protected $label_option = NULL; protected $option_values = []; protected $options_group = NULL; protected $options_class = NULL; protected $options_attributes = NULL; protected $db_field_options = NULL; protected $select_size = 1; protected $required_error_set = false; protected $invalid_error_set = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { $this->setup_select($form, $label, $name, 'select'); } protected function setup_select($form, $label, $name, $type) { //-------------------------------------------------- // Perform the standard field setup $this->setup($form, $label, $name, $type); //-------------------------------------------------- // Value $this->values = NULL; // Array of selected key(s), or NULL when not set if ($this->form_submitted || $this->form->saved_values_available()) { if ($this->form_submitted) { $this->values = request($this->name, $this->form->form_method_get()); } else { $this->values = $this->form->saved_value_get($this->name); } if ($this->values === NULL) { $this->values = $this->form->hidden_value_get('h-' . $this->name); if ($this->values !== NULL) { $this->values = json_decode($this->values, true); // associative array } else if ($this->form_submitted) { $this->values = []; // Form submitted, but no checkboxes ticked, so REQUEST data is NULL. } } if ($this->values !== NULL) { if (!is_array($this->values)) { $this->values = array($this->values); // Normal (non-multiple) select field } while (($key = array_search('', $this->values, true)) !== false) { // Remove the label, which is an empty string (strict type check required) unset($this->values[$key]); } } } } public function db_field_set($a, $b = NULL, $c = NULL) { //-------------------------------------------------- // Checks if ($this->invalid_error_set) { exit_with_error('Cannot call db_field_set() after invalid_error_set()'); } //-------------------------------------------------- // Set field $this->_db_field_set($a, $b, $c); //-------------------------------------------------- // Options $config = $this->db_field_info_get(); if ($config && ($config['type'] == 'enum' || $config['type'] == 'set')) { $options = $config['options']; while (($key = array_search('', $options)) !== false) { // If you want a blank option, use label_option_set, and remove the required_error. unset($options[$key]); } $this->db_field_options = $options; $this->option_values = array_combine($options, $options); } } public function db_field_options_get() { return $this->db_field_options; } public function multiple_set($multiple) { $this->multiple = $multiple; } public function multiple_get() { return $this->multiple; } public function label_option_set($text = NULL) { $this->label_option = $text; } public function options_set($options) { if ($this->invalid_error_set) { exit_with_error('Cannot call options_set() after invalid_error_set()'); } if (in_array('', array_keys($options), true)) { // Performs a strict check (allowing id 0) exit_with_error('Cannot have an option with a blank key, use label_option_set() instead.', debug_dump($options)); } if ($this->db_field_options !== NULL) { $diff = array_diff(($this->db_field_key ? array_keys($options) : $options), $this->db_field_options); if (count($diff) > 0) { exit_with_error('Invalid option ' . ($this->db_field_key ? 'key' : 'value') . (count($diff) == 1 ? '' : 's') . ' "' . implode('", "', $diff) . '" (not allowed in the database).', debug_dump($this->db_field_options)); } } $this->option_values = $options; } public function option_values_set($values) { $this->options_set(array_combine($values, $values)); } public function options_get() { return $this->option_values; } public function options_group_set($options_group) { $this->options_group = $options_group; } public function options_class_set($options_class) { $this->options_class = $options_class; } public function options_attributes_set($options_attributes) { $this->options_attributes = $options_attributes; } public function select_size_set($size) { $this->select_size = $size; } //-------------------------------------------------- // Legacy public function option_groups_set($options_group) { // Use options_group_set(), to be consistent with options_class_set() $this->options_group_set($options_group); } //-------------------------------------------------- // Errors public function required_error_set($error) { $this->required_error_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { if ($this->form_submitted && ($this->values == NULL || count($this->values) == 0)) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->required = ($error_html !== NULL); $this->required_error_set = true; // So the radios field can complain if not set. } public function invalid_error_set($error) { $this->invalid_error_set_html(to_safe_html($error)); } public function invalid_error_set_html($error_html) { if ($this->form_submitted && $this->values !== NULL) { foreach ($this->values as $key) { if (!isset($this->option_values[$key])) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); break; } } } $this->invalid_error_set = true; } //-------------------------------------------------- // Value set public function value_set($value) { $this->values_set(array($value)); } public function values_set($values) { $this->values = []; foreach ($values as $value) { $key = array_search($value, $this->option_values); if ($key !== false && $key !== NULL) { $this->values[] = $key; } } } public function value_key_set($key) { $this->value_keys_set(array($key)); } public function value_keys_set($keys) { $this->values = []; foreach ($keys as $key) { if ($key !== '' && isset($this->option_values[$key])) { $this->values[] = $key; } } } //-------------------------------------------------- // Value get public function value_get() { $values = $this->values_get(); if ($this->multiple) { return implode(',', $values); // Match value_key_get behaviour } else { return array_pop($values); // Returns NULL if label is selected } } public function values_get() { $return = []; foreach ($this->value_keys_get() as $key) { $return[$key] = $this->option_values[$key]; } return $return; } public function value_key_get() { $keys = $this->value_keys_get(); if ($this->multiple) { return implode(',', $keys); // Behaviour expected for a MySQL 'set' field, where a comma is not a valid character. } else { return array_pop($keys); // Returns NULL if label is selected } } public function value_keys_get() { $return = []; if ($this->values !== NULL) { foreach ($this->values as $key) { if ($key !== '' && isset($this->option_values[$key])) { $return[] = $key; // Can't preserve type from option_values (using array_search), as an array with id 0 and string values (e.g. "X"), it would match the first one, as ["X" == 0] } } } return $return; } protected function _value_print_get() { if ($this->values !== NULL) { $values = $this->values; } else if ($this->db_field_name !== NULL) { $db_values = $this->db_field_value_get(); if ($this->multiple) { $db_values = explode(',', strval($db_values)); // Commas are not valid characters in enum/set fields. } else { $db_values = array($db_values); } $values = []; if ($this->db_field_key) { foreach ($db_values as $key) { if (isset($this->option_values[$key])) { $values[] = $key; } } } else { foreach ($db_values as $value) { $key = array_search($value, $this->option_values); if ($key !== false && $key !== NULL) { $values[] = $key; } } } } else { $values = []; } return $values; } public function value_hidden_get() { if ($this->print_hidden) { $values = $this->_value_print_get(); if ($values === NULL && $this->label_option === NULL && !$this->multiple && count($this->option_values) > 0) { $values = array(key($this->option_values)); // Don't have a value or label, match browser behaviour of automatically selecting first item. } return json_encode($values); } else { return NULL; } } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->invalid_error_set == false) { $this->invalid_error_set('An invalid option has been selected for "' . strtolower($this->label_html) . '"'); } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if ($this->type == 'select') { if ($this->select_size > 1) { $attributes['size'] = intval($this->select_size); } if ($this->multiple) { $attributes['name'] .= '[]'; $attributes['multiple'] = 'multiple'; } } return $attributes; } //-------------------------------------------------- // HTML public function html_input() { //-------------------------------------------------- // Values $print_values = $this->_value_print_get(); if (!$this->multiple && count($print_values) > 1) { // Don't have multiple selected options, when not a multiple field $print_values = array_slice($print_values, 0, 1); } if ($this->label_option !== NULL && $this->select_size == 1 && !$this->multiple) { $label_class = ($this->options_class[''] ?? ''); $label_html = ' '; // Value must be blank for HTML5 } else { $label_html = ''; } //-------------------------------------------------- // Group HTML $used_keys = []; $group_html = ''; if ($this->options_group !== NULL) { foreach (array_unique($this->options_group) as $opt_group) { if ($opt_group !== NULL) { $group_html .= ' '; } foreach (array_keys($this->options_group, $opt_group) as $key) { if ($key === '') { $group_html .= $label_html; $label_html = ''; } else if (isset($this->option_values[$key])) { $used_keys[] = $key; $value = $this->option_values[$key]; $attributes = ($this->options_attributes[$key] ?? []); $attributes['value'] = $key; if (in_array($key, $print_values)) { $attributes['selected'] = 'selected'; } if (isset($this->options_class[$key])) { $attributes['class'] = $this->options_class[$key]; } $group_html .= ' ' . html_tag('option', $attributes) . ($value === '' ? ' ' : html($value)) . ''; } } if ($opt_group !== NULL) { $group_html .= ' '; } } } //-------------------------------------------------- // Main HTML $html = ' ' . html_tag('select', $this->_input_attributes()) . $label_html; foreach ($this->option_values as $key => $value) { if (!in_array($key, $used_keys)) { // Cannot do strict check with in_array() as an ID from the db may be a string or int. $attributes = ($this->options_attributes[$key] ?? []); $attributes['value'] = $key; if (in_array($key, $print_values)) { $attributes['selected'] = 'selected'; } if (isset($this->options_class[$key])) { $attributes['class'] = $this->options_class[$key]; } $html .= ' ' . html_tag('option', $attributes) . ($value === '' ? ' ' : html($value)) . ''; } } $html .= $group_html . ' ' . "\n\t\t\t\t\t\t\t\t"; //-------------------------------------------------- // Return return $html; } } class form_field_file extends form_field { //-------------------------------------------------- // Variables protected $multiple = false; protected $max_size = 0; protected $empty_file_error_set = false; protected $partial_file_error_set = false; protected $blank_name_error_set = false; protected $long_name_error_set = false; protected $files = []; protected $file_current = 0; protected $file_accept_mime = NULL; protected $file_accept_ext = NULL; protected $uploaded = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { $this->setup_file($form, $label, $name, 'file'); } protected function setup_file($form, $label, $name, $type) { //-------------------------------------------------- // Perform the standard field setup $this->setup($form, $label, $name, $type); //-------------------------------------------------- // First file field $this->form->_field_setup_file(); //-------------------------------------------------- // Newly uploaded files $this->files = []; if ($this->form_submitted && isset($_FILES[$this->name]['error']) && is_array($_FILES[$this->name]['error'])) { foreach ($_FILES[$this->name]['error'] as $key => $error) { $file_info = $this->file_store($this->name, $key); if ($file_info) { $file_info['preserve'] = ($file_info['hash'] !== NULL); $this->files[] = $file_info; } } } //-------------------------------------------------- // Hidden files if (count($this->files) == 0) { // If user uploaded one or more files, assume it is to replace. if ($this->form_submitted) { $hidden_files = $this->form->hidden_value_get('h-' . $this->name); } else if ($this->form->saved_values_available()) { $hidden_files = $this->form->saved_value_get('h-' . $this->name); // Looks for hidden field, saved_values does not include $_FILES if ($hidden_files !== NULL) { $hidden_files = urldecode($hidden_files); } } else { $hidden_files = NULL; } if ($hidden_files !== NULL) { foreach (explode('-', $hidden_files) as $file_hash) { $file_info = $this->file_info($file_hash); if ($file_info) { $file_info['preserve'] = true; $this->files[] = $file_info; } } } } //-------------------------------------------------- // Config $this->uploaded = (count($this->files) > 0); // Shortcut $this->file_current = 0; } public function multiple_set($multiple) { $this->multiple = $multiple; } public function multiple_get() { return $this->multiple; } public function placeholder_set($placeholder) { $this->placeholder = $placeholder; } //-------------------------------------------------- // Storing files support public static function file_store($file_name, $file_offset = NULL) { //-------------------------------------------------- // Return details if (!isset($_FILES[$file_name]['error'])) { return NULL; } else if (!is_array($_FILES[$file_name]['error'])) { $file_info = $_FILES[$file_name]; $file_offset = NULL; } else { if ($file_offset === NULL) { $file_offset = key($_FILES[$file_name]['error']); } else if (!isset($_FILES[$file_name]['error'][$file_offset])) { return NULL; } $file_info = []; foreach ($_FILES[$file_name] as $key => $value) { $file_info[$key] = $value[$file_offset]; } } //-------------------------------------------------- // File not uploaded if ($file_info['error'] == 4) { // 4 = No file was uploaded (UPLOAD_ERR_NO_FILE) return NULL; } //-------------------------------------------------- // Extension $file_ext = pathinfo($file_info['name'], PATHINFO_EXTENSION); if ($file_ext) { $file_info['ext'] = strtolower($file_ext); } else { $file_info['ext'] = ''; // File name missing value, which is not the same as NULL (useful distinction when sending to DB) } //-------------------------------------------------- // Mime - backwards computability $file_info['mime'] = $file_info['type']; unset($file_info['type']); //-------------------------------------------------- // Store if ($file_info['error'] == 0) { if (!is_uploaded_file($file_info['tmp_name'])) { exit_with_error('Only "uploaded" files can be processed with form_field_file', 'Path: ' . $file_info['tmp_name']); } $file_hash = hash('sha256', $file_info['tmp_name']); // Temp name should be unique, and faster than hasing the contents of the whole file. $file_path = form_field_file::_file_tmp_folder() . '/' . $file_hash; move_uploaded_file($file_info['tmp_name'], $file_path); unset($file_info['tmp_name']); $file_info['hash'] = $file_hash; $file_info['path'] = $file_path; file_put_contents($file_path . '.json', json_encode($file_info)); $config_name = $file_name . ($file_offset !== NULL ? '[' . $file_offset . ']' : ''); config::array_set('request.file_paths', $config_name, $file_path); } else { $file_info['hash'] = NULL; $file_info['path'] = NULL; } //-------------------------------------------------- // Return return $file_info; } public static function file_info($file_hash) { $info_path = form_field_file::_file_tmp_folder() . '/' . $file_hash . '.json'; if (is_file($info_path)) { return json_decode(file_get_contents($info_path), true); // As an array } else { return NULL; } } public static function _file_tmp_folder() { $tmp_folder = config::get('form.file_tmp_folder'); // Cached value, so old file check is only done once. if ($tmp_folder === NULL) { $tmp_folder = tmp_folder('form-file'); unlink_old_files($tmp_folder, strtotime('-1 hour')); config::set('form.file_tmp_folder', $tmp_folder); } return $tmp_folder; } //-------------------------------------------------- // Errors public function required_error_set($error) { $this->required_error_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { if ($this->form_submitted && !$this->uploaded) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->required = ($error_html !== NULL); } public function max_size_set($error, $size = NULL) { $this->max_size_set_html(to_safe_html($error), $size); } public function max_size_set_html($error_html, $size = NULL) { //-------------------------------------------------- // Max size server will allow $this->max_size = 0; $server_max = $this->max_size_get(); //-------------------------------------------------- // Set if ($size === NULL) { if ($server_max > 0) { $this->max_size = intval($server_max); } else { exit_with_error('Cannot determine max file upload size for this server.'); } } else { if ($server_max == 0 || $size <= $server_max) { $this->max_size = intval($size); } else { exit_with_error('The maximum file size the server accepts is "' . format_bytes($server_max) . '" (' . $server_max . '), not "' . format_bytes($size) . '" (' . $size . ')', 'upload_max_filesize = ' . ini_get('upload_max_filesize') . "\n" . 'post_max_size = ' . ini_get('post_max_size')); } } //-------------------------------------------------- // Validation on upload if ($this->uploaded) { $error_html = str_replace('XXX', format_bytes($this->max_size), $error_html); foreach ($this->files as $id => $file) { if ($file['error'] == 1) { $this->form->_field_error_set_html($this->form_field_uid, $error_html, 'ERROR: Exceeds "upload_max_filesize" ' . ini_get('upload_max_filesize')); $this->files[$id]['preserve'] = false; } if ($file['error'] == 2) { $this->form->_field_error_set_html($this->form_field_uid, $error_html, 'ERROR: Exceeds "MAX_FILE_SIZE" specified in the html form'); $this->files[$id]['preserve'] = false; } if ($file['size'] >= $this->max_size) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->files[$id]['preserve'] = false; } } } } public function max_size_get() { $size = $this->max_size; if ($size == 0) { foreach (array('upload_max_filesize', 'post_max_size') as $ini) { if ($limit = ini_get($ini)) { $limit = parse_bytes($limit); if (($limit > 0) && ($size == 0 || $size > $limit)) { $size = $limit; } } } } return $size; } public function partial_file_error_set($error) { $this->partial_file_error_set_html(to_safe_html($error)); } public function partial_file_error_set_html($error_html) { if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['error'] == 3) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->files[$id]['preserve'] = false; } } } $this->partial_file_error_set = true; } public function allowed_file_types_mime_set($error, $types) { $this->allowed_file_types_mime_set_html(to_safe_html($error), $types); } public function allowed_file_types_mime_set_html($error_html, $types) { if ($this->uploaded) { foreach ($this->files as $id => $file) { if (!in_array($file['mime'], $types)) { $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $file['mime'], $error_html), 'MIME: ' . $file['mime']); $this->files[$id]['preserve'] = false; } } } $this->file_accept_mime = $types; } public function allowed_file_types_ext_set($error, $types) { $this->allowed_file_types_ext_set_html(to_safe_html($error), $types); } public function allowed_file_types_ext_set_html($error_html, $types) { if ($this->uploaded) { foreach ($this->files as $id => $file) { if (!in_array($file['ext'], $types)) { $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $file['ext'], $error_html), 'EXT: ' . $file['ext']); $this->files[$id]['preserve'] = false; } } } $this->file_accept_ext = $types; } public function empty_file_error_set($error) { $this->empty_file_error_set_html(to_safe_html($error)); } public function empty_file_error_set_html($error_html) { if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['size'] == 0) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->files[$id]['preserve'] = false; } } } $this->empty_file_error_set = true; } public function blank_name_error_set($error) { $this->blank_name_error_set_html(to_safe_html($error)); } public function blank_name_error_set_html($error_html) { if ($this->uploaded) { foreach ($this->files as $id => $file) { $name = pathinfo($file['name'], PATHINFO_FILENAME); // Exclude extension - Don't want ".jpg" passing PHP "jpg" check, but being seen as a hidden file with no extension by the web server. if ($name == '') { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->files[$id]['preserve'] = false; } } } $this->blank_name_error_set = true; } public function long_name_error_set($error, $length = 100) { $this->long_name_error_set_html(to_safe_html($error), $length); } public function long_name_error_set_html($error_html, $length = 100) { $error_html = str_replace('XXX', $length, $error_html); if ($this->uploaded) { foreach ($this->files as $id => $file) { if (strlen($file['name']) > $length) { $html = str_replace('[FILE_NAME]', $file['name'], $error_html); $this->form->_field_error_set_html($this->form_field_uid, $html); $this->files[$id]['preserve'] = false; } } } $this->long_name_error_set = true; } //-------------------------------------------------- // Status public function uploaded() { return (isset($this->files[$this->file_current])); } public function upload_next() { // For multi-file uploads $this->file_current++; } public function upload_reset() { $this->file_current = 0; } //-------------------------------------------------- // Errors public function error_file_add($id, $error, $hidden_info = NULL) { if (isset($this->files[$id])) { $this->files[$id]['preserve'] = false; } else { exit_with_error('Unknown file id "' . $id . '"'); } $this->error_add($error, $hidden_info); } //-------------------------------------------------- // Value public function value_get() { exit('

Do you mean file_path_get?

'); } public function value_file_names_get() { $file_names = []; foreach ($this->files as $file) { if ($file['preserve']) { $file_names[] = $file['name']; } } return $file_names; } public function value_hashes_get() { $hashes = []; foreach ($this->files as $file) { if ($file['preserve']) { $hashes[] = $file['hash']; } } return $hashes; } public function value_hidden_get() { $hashes = $this->value_hashes_get(); if (count($hashes) > 0) { return implode('-', $hashes); } else { return NULL; } } public function files_get() { $return = []; foreach ($this->files as $file) { if ($file['error'] == 0) { $return[] = array( 'path' => $file['path'], 'ext' => $file['ext'], 'name' => $file['name'], 'size' => $file['size'], 'mime' => $file['mime'], ); // Don't expose preserve/error/hash keys } } return $return; } public function file_id_get() { return $this->file_current; } public function file_path_get() { return $this->_file_info_get('path'); } public function file_ext_get() { return $this->_file_info_get('ext'); } public function file_name_get() { return $this->_file_info_get('name'); } public function file_size_get() { return $this->_file_info_get('size'); } public function file_mime_get() { return $this->_file_info_get('mime'); } public function file_save_to($path_dst) { $path_src = $this->file_path_get(); if ($path_src) { $folder = dirname($path_dst); if (!is_dir($folder)) { @mkdir($folder, 0777, true); } if (is_file($path_dst) && !is_writable($path_dst)) { exit_with_error('Cannot save file "' . $this->label_html . '", check destination file permissions.', $path_dst); } else if (!is_writable(dirname($path_dst))) { exit_with_error('Cannot save file "' . $this->label_html . '", check destination folder permissions.', dirname($path_dst)); } $return = copy($path_src, $path_dst); // Don't unlink/rename, as the same file may have been uploaded multiple times. @chmod($path_dst, octdec(config::get('file.default_permission', 666))); return $return; } return false; } protected function _file_info_get($field) { if (isset($this->files[$this->file_current][$field])) { return $this->files[$this->file_current][$field]; } else { return NULL; } } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->max_size == 0) { exit('

You need to call "max_size_set", on the field "' . $this->label_html . '"

'); } if ($this->form_submitted && isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] == 'application/x-www-form-urlencoded') { // If not set, assume its correct exit('

The form needs the attribute: enctype="multipart/form-data"

'); } if ($this->empty_file_error_set == false) { // Provide default $this->empty_file_error_set('The uploaded file for "' . strtolower($this->label_html) . '" is empty.'); } if ($this->partial_file_error_set == false) { // Provide default $this->partial_file_error_set('The uploaded file for "' . strtolower($this->label_html) . '" was only partially uploaded.'); } if ($this->blank_name_error_set == false) { // Provide default $this->blank_name_error_set('The uploaded file for "' . strtolower($this->label_html) . '" does not have a filename.'); } if ($this->long_name_error_set == false) { // Provide default $this->long_name_error_set('The uploaded file "[FILE_NAME]", has a filename that is too long (max XXX characters).'); // for "' . strtolower($this->label_html) . '" } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if (isset($attributes['required']) && count($this->value_hashes_get()) > 0) { unset($attributes['required']); // Preserved files though hidden field } $attributes['type'] = 'file'; $attributes['name'] .= '[]'; // Treat all uploads as supporting multiple files, we will still just use the first one. if ($this->multiple) { $attributes['multiple'] = 'multiple'; } $accept = []; if ($this->file_accept_mime !== NULL) { $accept = $this->file_accept_mime; } if ($this->file_accept_ext !== NULL) { foreach ($this->file_accept_ext as $ext) { $accept[] = '.' . $ext; } } if (count($accept) > 0) { $attributes['accept'] = implode(',', $accept); } return $attributes; } //-------------------------------------------------- // HTML public function html_input() { return $this->_html_input(); } public function html_info($indent = 0) { $file_names = $this->value_file_names_get(); if (count($file_names) > 0) { if ($this->multiple) { $file_names = implode(', ', $file_names); } else { $file_names = array_shift($file_names); } $this->info_html = html($file_names) . ($this->info_html ? ' | ' . $this->info_html : ''); } return parent::html_info($indent); } } class form_field_image extends form_field_file { //-------------------------------------------------- // Variables protected $min_width_size = 0; protected $max_width_size = 0; protected $min_height_size = 0; protected $max_height_size = 0; protected $file_type_error_set = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_file($form, $label, $name, 'image'); //-------------------------------------------------- // File values foreach ($this->files as $id => $file) { $dimensions = ($file['path'] == '' ? false : getimagesize($file['path'])); if ($dimensions !== false) { if ($dimensions[2] == IMAGETYPE_JPEG) { $image_type = 'jpg'; } else if ($dimensions[2] == IMAGETYPE_GIF) { $image_type = 'gif'; } else if ($dimensions[2] == IMAGETYPE_PNG) { $image_type = 'png'; } else { $image_type = NULL; } $this->files[$id]['image_width'] = $dimensions[0]; $this->files[$id]['image_height'] = $dimensions[1]; $this->files[$id]['image_type'] = $image_type; } else { $this->files[$id]['image_width'] = NULL; $this->files[$id]['image_height'] = NULL; $this->files[$id]['image_type'] = NULL; } } } //-------------------------------------------------- // Errors public function min_width_set($error, $size) { $this->min_width_set_html(to_safe_html($error), $size); } public function min_width_set_html($error_html, $size) { $size = intval($size); if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_width'] !== NULL && $file['image_width'] < $size) { $this->form->_field_error_add_html($this->form_field_uid, str_replace('XXX', $size . 'px', $error_html), $file['image_width'] . 'px'); $this->files[$id]['preserve'] = false; } } } $this->min_width_size = $size; } public function max_width_set($error, $size) { $this->max_width_set_html(to_safe_html($error), $size); } public function max_width_set_html($error_html, $size) { $size = intval($size); if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_width'] !== NULL && $file['image_width'] > $size) { $this->form->_field_error_add_html($this->form_field_uid, str_replace('XXX', $size . 'px', $error_html), $file['image_width'] . 'px'); $this->files[$id]['preserve'] = false; } } } $this->max_width_size = $size; } public function required_width_set($error, $size) { $this->required_width_set_html(to_safe_html($error), $size); } public function required_width_set_html($error_html, $size) { $size = intval($size); if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_width'] !== NULL && $file['image_width'] != $size) { $this->form->_field_error_add_html($this->form_field_uid, str_replace('XXX', $size . 'px', $error_html), $file['image_width'] . 'px'); $this->files[$id]['preserve'] = false; } } } $this->min_width_size = $size; $this->max_width_size = $size; } public function required_width_min_get() { return $this->min_width_size; } public function required_width_max_get() { return $this->max_width_size; } public function required_width_get() { return $this->min_width_size; } public function min_height_set($error, $size) { $this->min_height_set_html(to_safe_html($error), $size); } public function min_height_set_html($error_html, $size) { $size = intval($size); if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_height'] !== NULL && $file['image_height'] < $size) { $this->form->_field_error_add_html($this->form_field_uid, str_replace('XXX', $size . 'px', $error_html), $file['image_height'] . 'px'); $this->files[$id]['preserve'] = false; } } } $this->min_height_size = $size; } public function max_height_set($error, $size) { $this->max_height_set_html(to_safe_html($error), $size); } public function max_height_set_html($error_html, $size) { $size = intval($size); if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_height'] !== NULL && $file['image_height'] > $size) { $this->form->_field_error_add_html($this->form_field_uid, str_replace('XXX', $size . 'px', $error_html), $file['image_height'] . 'px'); $this->files[$id]['preserve'] = false; } } } $this->max_height_size = $size; } public function required_height_set($error, $size) { $this->required_height_set_html(to_safe_html($error), $size); } public function required_height_set_html($error_html, $size) { $size = intval($size); if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_height'] !== NULL && $file['image_height'] != $size) { $this->form->_field_error_add_html($this->form_field_uid, str_replace('XXX', $size . 'px', $error_html), $file['image_height'] . 'px'); $this->files[$id]['preserve'] = false; } } } $this->min_height_size = $size; $this->max_height_size = $size; } public function required_height_min_get() { return $this->min_height_size; } public function required_height_max_get() { return $this->max_height_size; } public function required_height_get() { return $this->min_height_size; } public function file_type_error_set($error, $types = NULL) { $this->file_type_error_set_html(to_safe_html($error), $types); } public function file_type_error_set_html($error_html, $types = NULL) { //-------------------------------------------------- // Types if ($types === NULL) { $types = array('gif', 'jpg', 'png'); } //-------------------------------------------------- // Validate the mime type $mime_types = []; if (in_array('gif', $types)) { $mime_types[] = 'image/gif'; } if (in_array('jpg', $types)) { $mime_types[] = 'image/jpeg'; $mime_types[] = 'image/pjpeg'; // The wonderful world of IE } if (in_array('png', $types)) { $mime_types[] = 'image/png'; $mime_types[] = 'image/x-png'; // The wonderful world of IE } parent::allowed_file_types_mime_set_html($error_html, $mime_types); //-------------------------------------------------- // Could not use getimagesize if ($this->uploaded) { foreach ($this->files as $id => $file) { if ($file['image_type'] == NULL) { $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', 'invalid image', $error_html), 'ERROR: Failed getimagesize'); } else { if (!in_array($file['image_type'], $types)) { $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $file['image_type'], $error_html), 'ERROR: Non valid type (' . implode(', ', $types) . ')'); } } } } //-------------------------------------------------- // Done $this->file_type_error_set = true; } //-------------------------------------------------- // Value public function image_width_get() { return $this->_file_info_get('image_width'); } public function image_height_get() { return $this->_file_info_get('image_height'); } public function image_type_get() { return $this->_file_info_get('image_type'); } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->file_type_error_set == false) { exit('

You need to call "file_type_error_set", on the field "' . $this->label_html . '"

'); } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); if (!isset($attributes['accept'])) { $attributes['accept'] = 'image/*'; } return $attributes; } } class form_field_fields extends form_field { //-------------------------------------------------- // Variables protected $value = NULL; protected $value_default = NULL; protected $value_provided = NULL; protected $fields = []; protected $placeholders = []; protected $format_html = array('separator' => ' '); protected $invalid_error_set = false; protected $invalid_error_found = false; protected $input_order = []; protected $input_separator = ' '; protected $input_config = []; protected $input_described_by = NULL; // Disabled, as these fields use aria-label //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { $this->setup_fields($form, $label, $name, 'fields'); } protected function setup_fields($form, $label, $name, $type) { //-------------------------------------------------- // General setup $this->setup($form, $label, $name, $type); //-------------------------------------------------- // Value $this->value = NULL; if ($this->form_submitted || $this->form->saved_values_available()) { $hidden_value = $this->form->hidden_value_get('h-' . $this->name); if ($hidden_value !== NULL) { $this->value_set($hidden_value); } else { if ($this->form_submitted) { $request_value = request($this->name, $this->form->form_method_get()); } else { $request_value = $this->form->saved_value_get($this->name); } if ($request_value !== NULL) { $this->value_set($request_value); } } } $this->value_provided = NULL; // Reset after initial value_set (calling again will then be 'provided')... but date/time fields might change (via setup_fields), or NULL will worked out during required_error_set_html (after input_add). } public function input_first_id_get() { return $this->id . '_' . reset($this->input_order); } public function placeholder_set($placeholder) { $placeholder = $this->_value_parse($placeholder); if ($placeholder) { $this->placeholders_set($placeholder); } } public function placeholders_set($placeholders) { $this->placeholders = $placeholders; } //-------------------------------------------------- // Format public function format_set($format) { $this->format_set_html(is_array($format) ? array_map('to_safe_html', $format) : to_safe_html($format)); } public function format_set_html($format_html) { if (is_array($format_html)) { $this->format_html = array_merge($this->format_html, $format_html); } else { $this->format_html = $format_html; } } public function format_default_get_html() { if (!is_array($this->format_html)) { return $this->format_html; } else { $format_html = []; foreach ($this->input_order as $field) { if (isset($this->format_html[$field])) { $label_html = $this->format_html[$field]; } else if (!is_array($this->input_config[$field]['options'])) { // Not using a '; } else { $value = (isset($input_value[$field]) ? $input_value[$field] : NULL); if ($input_config['pad_length'] > 0) { if ($value !== NULL && $value !== '') { $value = str_pad($value, $input_config['pad_length'], $input_config['pad_char'], STR_PAD_LEFT); } } else if ($value == 0) { $value = ''; } $attributes['value'] = strval($value); // Ensure the attribute is still present for NULL values - e.g. for JS query input[value!=""] $attributes['type'] = 'text'; if ($input_config['size']) { $attributes['maxlength'] = $input_config['size']; $attributes['size'] = $input_config['size']; } return $this->_html_input($attributes); } } public function html_input() { $input_value = $this->_value_print_get(); $input_html = []; foreach ($this->input_order as $html) { if (in_array($html, $this->fields)) { $input_html[] = $this->html_input_field($html, $input_value); } else { $input_html[] = $html; } } return "\n\t\t\t\t\t\t\t\t\t" . implode($this->input_separator, $input_html) . "\n\t\t\t\t\t\t\t\t"; } } class form_field_date extends form_field_fields { //-------------------------------------------------- // Variables protected $input_day = NULL; protected $input_single = false; protected $input_partial_allowed = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Fields setup $this->fields = array('D', 'M', 'Y'); $this->format_html = array_merge(array('separator' => '/', 'D' => 'DD', 'M' => 'MM', 'Y' => 'YYYY'), config::get('form.date_format_html', [])); $this->value_default = '0000-00-00'; $this->input_separator = "\n\t\t\t\t\t\t\t\t\t"; $this->input_config = array( 'D' => array( 'size' => 2, 'pad_length' => 0, 'pad_char' => '0', 'label' => '', 'label_aria' => 'Day', 'input_required' => true, 'options' => NULL, ), 'M' => array( 'size' => 2, 'pad_length' => 0, 'pad_char' => '0', 'label' => '', 'label_aria' => 'Month', 'input_required' => true, 'options' => NULL, ), 'Y' => array( 'size' => 4, 'pad_length' => 0, 'pad_char' => '0', 'label' => '', 'label_aria' => 'Year', 'input_required' => true, 'options' => NULL, )); $this->setup_fields($form, $label, $name, 'date'); $this->input_single = config::get('form.date_input_single', false); // Use the standard 3 input fields by default, as most browsers still cannot do HTML5 type="date" fields. $this->input_order_set(config::get('form.date_input_order', array('D', 'M', 'Y'))); //-------------------------------------------------- // Guess correct year if only 2 digits provided if ($this->value !== NULL && $this->value['Y'] > 0 && $this->value['Y'] < 100) { $this->value['Y'] = DateTime::createFromFormat('y', $this->value['Y'])->format('Y'); } //-------------------------------------------------- // Value provided $this->value_provided = (is_array($this->value) && ($this->value['D'] != 0 || $this->value['M'] != 0 || $this->value['Y'] != 0)); } public function input_order_set($order) { parent::input_order_set($order); $this->input_day = in_array('D', $this->input_order); } public function input_options_text_set($field, $options, $label = '') { if ($field == 'M' && !is_array($options)) { $months = []; for ($k = 1; $k <= 12; $k++) { $months[$k] = date($options, mktime(0, 0, 0, $k, 1)); // Must specify day, as on the 31st this will push other month 2 is pushed to March } $options = $months; } parent::input_options_text_set($field, $options, $label); } public function input_partial_allowed_set($input_partial_allowed) { $this->input_partial_allowed = $input_partial_allowed; } //-------------------------------------------------- // Format public function format_default_get_html() { if ($this->input_single === true && is_array($this->format_html)) { return ''; } else { return parent::format_default_get_html(); } } //-------------------------------------------------- // Errors public function invalid_error_set_html($error_html) { if ($this->form_submitted && $this->value_provided) { $valid = true; $time_stamp_value = $this->value_time_stamp_get(); // Check upper bound to time-stamp, 2037 on 32bit systems $partial_value = $this->value; if (!$this->input_day) { $partial_value['D'] = 1; } if ($this->input_partial_allowed) { if ($partial_value['D'] == 0) $partial_value['D'] = 1; if ($partial_value['M'] == 0) $partial_value['M'] = 1; if ($partial_value['Y'] == 0) $partial_value['Y'] = 2000; } if (!checkdate($partial_value['M'], $partial_value['D'], $partial_value['Y']) || $time_stamp_value === false) { $valid = false; } foreach ($this->fields as $field) { if (is_array($this->input_config[$field]['options']) && !isset($this->input_config[$field]['options'][$this->value[$field]])) { $valid = false; } } if (!$valid) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->invalid_error_found = true; } } $this->invalid_error_set = true; } public function min_date_set($error, $date) { $this->min_date_set_html(to_safe_html($error), $date); } public function min_date_set_html($error_html, $date) { if (!$this->invalid_error_set) { exit_with_error('Call invalid_error_set() before min_date_set()'); } if ($this->form_submitted && $this->value_provided && $this->invalid_error_found == false) { $value = $this->value_time_stamp_get(); if (!is_int($date)) { $date = strtotime($date); } if ($value !== false && $value < $date) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } } } public function max_date_set($error, $date) { $this->max_date_set_html(to_safe_html($error), $date); } public function max_date_set_html($error_html, $date) { if (!$this->invalid_error_set) { exit_with_error('Call invalid_error_set() before max_date_set()'); } if ($this->form_submitted && $this->value_provided && $this->invalid_error_found == false) { $value = $this->value_time_stamp_get(); if (!is_int($date)) { $date = strtotime($date); } if ($value !== false && $value > $date) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } } } //-------------------------------------------------- // Value public function value_set($value, $month = NULL, $year = NULL) { $this->value = $this->_value_parse($value, $month, $year); $this->value_provided = ($this->value !== NULL); } public function value_get($field = NULL) { if ($field !== NULL) { if (!in_array($field, $this->fields)) { exit_with_error('Invalid field specified "' . $field . '"'); } if ($this->value_provided) { return $this->value[$field]; } else { return NULL; } } else if ($this->value_provided) { return $this->_value_string($this->value); } else { return NULL; } } public function value_date_get() { $value = $this->value_get(); if ($value === NULL) { $value = $this->value_default; } return $value; } public function value_timestamp_get() { return new timestamp($this->value_date_get(), 'db'); } public function value_time_stamp_get() { // Legacy name... but you should look at the timestamp helper anyway :-) if ($this->value === NULL || $this->value['M'] == 0 && $this->value['D'] == 0 && $this->value['Y'] == 0) { $timestamp = false; } else { $timestamp = mktime(0, 0, 0, $this->value['M'], ($this->input_day ? $this->value['D'] : 1), $this->value['Y']); } return $timestamp; } protected function _value_string($value) { if ($value !== NULL) { return str_pad(intval($value['Y']), 4, '0', STR_PAD_LEFT) . '-' . str_pad(intval($value['M']), 2, '0', STR_PAD_LEFT) . '-' . str_pad(intval($this->input_day ? $value['D'] : 1), 2, '0', STR_PAD_LEFT); } else { return NULL; } } protected function _value_parse($value, $month = NULL, $year = NULL) { if ($month === NULL && $year === NULL) { if (is_array($value)) { $return = []; foreach ($this->fields as $field) { $return[$field] = (isset($value[$field]) ? intval($value[$field]) : 0); } return $return; } if (!is_numeric($value)) { if ($value == '0000-00-00' || $value == '0000-00-00 00:00:00') { $value = NULL; } else if (preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})/', strval($value), $matches)) { // Should also match "2001-01-00" return array( 'D' => intval($matches[3]), 'M' => intval($matches[2]), 'Y' => intval($matches[1]), ); } else { $value = strtotime(strval($value)); if ($value == 943920000) { // "1999-11-30 00:00:00", same as the database "0000-00-00 00:00:00" $value = NULL; } } } if (is_numeric($value)) { return array( 'D' => intval(date('j', $value)), 'M' => intval(date('n', $value)), 'Y' => intval(date('Y', $value)), // Don't render year as "0013" ); } } else { return array( 'D' => intval($value), 'M' => intval($month), 'Y' => intval($year), ); } return NULL; } //-------------------------------------------------- // HTML public function html_input() { if ($this->input_single === true) { $value = $this->_value_print_get(); if ($value) { $value = $this->_value_string($value); } else { $value = ''; } // Use min/max attributes, like in `form_field_datetime`? return $this->_html_input(array('value' => $value, 'type' => 'date')); } else { return parent::html_input(); } } } class form_field_time extends form_field_fields { //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Fields setup $this->fields = array('H', 'I', 'S'); $this->format_html = array_merge(array('separator' => ':', 'H' => 'HH', 'I' => 'MM', 'S' => 'SS'), config::get('form.time_format_html', [])); $this->value_default = '00:00:00'; $this->input_separator = "\n\t\t\t\t\t\t\t\t\t"; $this->input_config = array( 'H' => array( 'size' => 2, 'pad_length' => 2, 'pad_char' => '0', 'label' => '', 'label_aria' => 'Hour', 'input_required' => true, 'options' => NULL, ), 'I' => array( 'size' => 2, 'pad_length' => 2, 'pad_char' => '0', 'label' => '', 'label_aria' => 'Minute', 'input_required' => false, 'options' => NULL, ), 'S' => array( 'size' => 2, 'pad_length' => 2, 'pad_char' => '0', 'label' => '', 'label_aria' => 'Second', 'input_required' => false, 'options' => NULL, )); $this->setup_fields($form, $label, $name, 'time'); $this->input_order_set(config::get('form.time_input_order', array('H', 'I'))); // Could also be array('H', 'I', 'S') //-------------------------------------------------- // Value provided $this->value_provided = false; if (is_array($this->value)) { foreach ($this->value as $field => $value) { if ($value !== NULL && $value !== '') { if ($field !== 'H' && $this->value['H'] === '' && $value == 0) { continue; // Special case, if the 'hours' is *blank* (0 is a value for midnight), then the 'minutes' or 'seconds' could be 0, but that can be treated as non-values (e.g. user has set the hours drop down to blank, and left minutes at '00'). } $this->value_provided = true; // Look for one non-blank value (i.e. treat '0' as a value); also the 'seconds' field probably does not exist. break; } } } } //-------------------------------------------------- // Errors public function invalid_error_set_html($error_html) { if ($this->form_submitted && $this->value_provided) { $valid = true; $int_values = array_map('intval', $this->value); if ($int_values['H'] < 0 || $int_values['H'] > 23) $valid = false; if ($int_values['I'] < 0 || $int_values['I'] > 59) $valid = false; if ($int_values['S'] < 0 || $int_values['S'] > 59) $valid = false; foreach ($this->fields as $field) { $value = $this->value[$field]; if ($value == '') { $value = 0; // Treat label as 0, same as when its not required. } if (is_array($this->input_config[$field]['options']) && !isset($this->input_config[$field]['options'][$value])) { $valid = false; } } if (!$valid) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->invalid_error_found = true; // Bypass min/max style validation } } $this->invalid_error_set = true; } public function min_time_set($error, $time) { $this->min_time_set_html(to_safe_html($error), $time); } public function min_time_set_html($error_html, $time) { if (!$this->invalid_error_set) { exit_with_error('Call invalid_error_set() before min_time_set()'); } if ($this->form_submitted && $this->value_provided && $this->invalid_error_found == false) { $value = strtotime($this->value_get()); if (!is_int($time)) { $time = strtotime($time); } if ($value !== false && $value < $time) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } } } public function max_time_set($error, $time) { $this->max_time_set_html(to_safe_html($error), $time); } public function max_time_set_html($error_html, $time) { if (!$this->invalid_error_set) { exit_with_error('Call invalid_error_set() before max_time_set()'); } if ($this->form_submitted && $this->value_provided && $this->invalid_error_found == false) { $value = strtotime($this->value_get()); if (!is_int($time)) { $time = strtotime($time); } if ($value !== false && $value > $time) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } } } //-------------------------------------------------- // Value public function value_set($value, $minute = NULL, $second = NULL) { $this->value = $this->_value_parse($value, $minute, $second); $this->value_provided = true; // If you are providing "00:00:00", then this is considered a value... if you want 00:00:00 to cause the field to be left blank, then don't call this function } public function value_get($field = NULL) { if ($field !== NULL) { if (!in_array($field, $this->fields)) { exit_with_error('Invalid field specified "' . $field . '"'); } if ($this->value_provided) { return intval($this->value[$field]); // Time field (unlike date) does not intval() submitted data, so empty value can be different to 0. } else { return NULL; } } else if ($this->value_provided) { return $this->_value_string($this->value); } else { return NULL; } } public function value_time_get() { $value = $this->value_get(); if ($value === NULL) { $value = $this->value_default; } return $value; } protected function _value_string($value) { return str_pad(intval($value['H']), 2, '0', STR_PAD_LEFT) . ':' . str_pad(intval($value['I']), 2, '0', STR_PAD_LEFT) . ':' . str_pad(intval($value['S']), 2, '0', STR_PAD_LEFT); } protected function _value_parse($value, $minute = NULL, $second = NULL) { if ($minute === NULL && $second === NULL) { if ($value instanceof timestamp) { $value = $value->format('H:i:s'); } else if (is_array($value)) { $return = []; foreach ($this->fields as $field) { $return[$field] = (isset($value[$field]) ? $value[$field] : ''); } return $return; } if (preg_match('/^([0-9]{1,2}):([0-9]{1,2})(:([0-9]{1,2}))?$/', strval($value), $matches)) { return array( 'H' => intval($matches[1]), 'I' => intval($matches[2]), 'S' => intval(isset($matches[4]) ? $matches[4] : 0), ); } } else { return array( 'H' => intval($value), 'I' => intval($minute), 'S' => intval($second), ); } return NULL; } } class form_field_datetime extends form_field_text { //-------------------------------------------------- // Variables protected $step_value = 1; // Can use useful to set to 60 or 3600 (so seconds or minutes are not collected). protected $value_provided = false; protected $value_timestamp = NULL; protected $min_timestamp = NULL; protected $max_timestamp = NULL; protected $invalid_error_set = false; protected $invalid_error_found = false; //-------------------------------------------------- // Setup public function __construct($form, $label, $name = NULL) { //-------------------------------------------------- // Perform the standard field setup $this->setup_text($form, $label, $name, 'datetime'); //-------------------------------------------------- // Clean input value if ($this->form_submitted) { $this->value_set($this->value); } //-------------------------------------------------- // Additional field configuration $this->max_length = -1; // Bypass the _post_validation on the text field (not used) $this->input_type = 'datetime-local'; } //-------------------------------------------------- // Errors public function required_error_set($error) { $this->required_error_set_html(to_safe_html($error)); } public function required_error_set_html($error_html) { if ($this->form_submitted && !$this->value_provided) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } $this->required = ($error_html !== NULL); } public function invalid_error_set($error) { $this->invalid_error_set_html(to_safe_html($error)); } public function invalid_error_set_html($error_html) { if ($this->form_submitted && $this->value_provided) { if ($this->value_timestamp === NULL) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); $this->invalid_error_found = true; } } $this->invalid_error_set = true; } public function min_timestamp_set($error, $timestamp) { $this->min_timestamp_set_html(to_safe_html($error), $timestamp); } public function min_timestamp_set_html($error_html, $timestamp) { if (!$this->invalid_error_set) { exit_with_error('Call invalid_error_set() before min_timestamp_set()'); } if (!($timestamp instanceof timestamp)) { $timestamp = new timestamp($timestamp); } $this->min_timestamp = $timestamp; if ($this->form_submitted && $this->value_provided && $this->invalid_error_found == false) { if ($this->value_timestamp !== NULL && $this->value_timestamp < $this->min_timestamp) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } } } public function max_timestamp_set($error, $timestamp) { $this->max_timestamp_set_html(to_safe_html($error), $timestamp); } public function max_timestamp_set_html($error_html, $timestamp) { if (!$this->invalid_error_set) { exit_with_error('Call invalid_error_set() before max_timestamp_set()'); } if (!($timestamp instanceof timestamp)) { $timestamp = new timestamp($timestamp); } $this->max_timestamp = $timestamp; if ($this->form_submitted && $this->value_provided && $this->invalid_error_found == false) { if ($this->value_timestamp !== NULL && $this->value_timestamp > $this->max_timestamp) { $this->form->_field_error_set_html($this->form_field_uid, $error_html); } } } public function step_value_set($error, $step = 1) { $this->step_value_set_html(to_safe_html($error), $step); } public function step_value_set_html($error_html, $step = 1) { if ($this->form_submitted && $this->value !== '') { // Not written yet, just copied from form_field_number // $value = $this->value_clean; // // if ($this->min_value !== NULL) { // $value += $this->min_value; // HTML step starts at the min value // } // // if (abs((round($value / $step) * $step) - $value) > 0.00001) { // ref 'epsilon' on https://php.net/manual/en/language.types.float.php // $this->form->_field_error_set_html($this->form_field_uid, str_replace('XXX', $step, $error_html)); // } } $this->step_value = $step; } //-------------------------------------------------- // Value public function value_set($value) { $this->value = $value; $this->value_provided = ($value != ''); $this->value_timestamp = NULL; if ($this->value_provided) { $timestamp = new timestamp($value); if (!$timestamp->null()) { $this->value_timestamp = $timestamp; } } } public function value_get() { return $this->value_timestamp; } protected function _value_print_get() { if ($this->value !== '') { // The user submitted blank, let them keep it. if ($this->value_timestamp !== NULL) { $timestamp = $this->value_timestamp; } else if ($this->db_field_name !== NULL) { $timestamp = new timestamp($this->db_field_value_get(), 'db'); } else { $timestamp = new timestamp('0000-00-00 00:00:00', 'db'); } if (!$timestamp->null()) { return $timestamp->format('Y-m-d\TH:i:s'); } } return $this->value; // The browser might not support this field type, so send back what they sent to us (so they can edit invalid values). } //-------------------------------------------------- // Validation public function _post_validation() { parent::_post_validation(); if ($this->invalid_error_set == false) { exit('

You need to call "invalid_error_set", on the field "' . $this->label_html . '"

'); } } //-------------------------------------------------- // Attributes protected function _input_attributes() { $attributes = parent::_input_attributes(); $attributes['step'] = $this->step_value; // Google Chrome 51 will complain if a min/max value is set, and the seconds are different from them (no min/max, then seconds cannot be set). if ($this->min_timestamp !== NULL) { $attributes['min'] = $this->min_timestamp->format('Y-m-d\TH:i:s'); } if ($this->max_timestamp !== NULL) { $attributes['max'] = $this->max_timestamp->format('Y-m-d\TH:i:s'); } return $attributes; } } class form_field_html extends form_field { //-------------------------------------------------- // Variables protected $value_html = ''; //-------------------------------------------------- // Setup public function __construct($form, $label = 'html', $name = NULL) { $this->setup_html($form, $label, $name, 'html'); } protected function setup_html($form, $label, $name, $type) { //-------------------------------------------------- // Perform the standard field setup $this->setup($form, $label, $name, $type); //-------------------------------------------------- // Value $this->value_html = ''; } //-------------------------------------------------- // Value public function value_set($value) { $this->value_html = to_safe_html($value); } public function value_set_html($html) { $this->value_html = $html; } //-------------------------------------------------- // HTML public function html() { return $this->value_html; } } class form_field_info extends form_field { //-------------------------------------------------- // Variables protected $value = NULL; protected $value_html = NULL; protected $label_custom_html = NULL; //-------------------------------------------------- // Setup public function __construct($form, $label = '', $name = NULL) { $this->setup_info($form, $label, $name, 'info'); } protected function setup_info($form, $label, $name, $type) { //-------------------------------------------------- // Perform the standard field setup $this->setup($form, $label, $name, $type); //-------------------------------------------------- // Additional field configuration $this->readonly = true; // Don't update if linked to a db field } //-------------------------------------------------- // Value public function value_set($value) { if ($value instanceof html_template || $value instanceof html_safe_value) { $this->value_html = $value->html(); $this->value = html_decode($this->value_html); } else { $this->value = $value; $this->value_html = text_to_html($value); } } public function value_set_html($html) { $this->value = html_decode($html); $this->value_html = $html; } public function value_set_link($url, $text) { if ($url) { $this->value_set_html('' . html($text) . ''); } else { $this->value_set($text); } } public function value_get() { return $this->value; } public function value_get_html() { return $this->value_html; } //-------------------------------------------------- // Label public function label_set($label) { $this->label_custom_html = to_safe_html($label); } public function label_set_html($html) { $this->label_custom_html = $html; } //-------------------------------------------------- // HTML public function html_input() { if ($this->value_html !== NULL) { return $this->value_html; } else if ($this->db_field_name !== NULL) { return text_to_html($this->db_field_value_get()); } else { return ''; } } public function html_label($label_html = NULL) { if ($label_html === NULL) { if ($this->label_custom_html !== NULL) { $label_html = $this->label_custom_html . $this->label_suffix_html; } else { $label_html = parent::html_label(); $label_html = preg_replace('/]+>(.*)<\/label>/', '$1', $label_html); // Ugly, but better than duplication } } return $label_html; } } ?>