1: <?php
2: /**
3: * This file is part of the PHPLucidFrame library.
4: * Core utility for AJAX form handling and form validation
5: *
6: * @package PHPLucidFrame\Core
7: * @since PHPLucidFrame v 1.0.0
8: * @copyright Copyright (c), PHPLucidFrame.
9: * @link http://phplucidframe.com
10: * @license http://www.opensource.org/licenses/mit-license.php MIT License
11: *
12: * This source file is subject to the MIT license that is bundled
13: * with this source code in the file LICENSE
14: */
15:
16: namespace LucidFrame\Core;
17:
18: /**
19: * Core utility for AJAX form handling and form validation
20: */
21: class Form
22: {
23: /** @var string The HTML form ID */
24: private static $id;
25: /** @var array The error messages and their associated HTML ID */
26: private static $error = array();
27: /** @var boolean TRUE/FALSE for form validation success */
28: private static $success = false;
29: /** @var string The form message */
30: private static $message = '';
31: /** @var string URL to be redirect upon form submission completed */
32: private static $redirect = '';
33: /** @var string The Javascript callback function to be invoked upon form submission completed */
34: private static $callback = '';
35: /** @var array Array of data that need to send to the client */
36: private static $data = array();
37: /** @var string The hashed token */
38: public static $formToken = '';
39:
40: /**
41: * Constructor
42: */
43: public static function init()
44: {
45: self::$id = '';
46: self::$error = array();
47: self::$success = false;
48: self::$message = '';
49: self::$redirect = '';
50: self::$callback = '';
51: self::$data = array();
52: }
53:
54: /**
55: * Setter for the class properties
56: * @param string $key The property name
57: * @param mixed $value The value to be set
58: * @return void
59: */
60: public static function set($key, $value = '')
61: {
62: self::$$key = $value;
63: }
64:
65: /**
66: * Getter for the class properties
67: * @param string $key The property name
68: * @param mixed $value The default value for the property
69: * @return mixed
70: */
71: public static function get($key, $value = null)
72: {
73: if (isset(self::$$key)) {
74: return self::$$key;
75: }
76:
77: return $value;
78: }
79:
80: /**
81: * Get the posted CSRF token value
82: * @return string|null
83: */
84: protected static function getPostedCsrfToken(): ?string
85: {
86: if ($tokenValue = _post(self::getCsrfTokenName())) {
87: return $tokenValue;
88: }
89:
90: $tokenValue = _requestHeader(_cfg('csrfHeaderTokenName'));
91:
92: return $tokenValue ?: null;
93: }
94:
95: /**
96: * Get the form token name
97: * @return string
98: */
99: public static function getCsrfTokenName(): string
100: {
101: return 'lc_formToken_' . _cfg('formTokenName');
102: }
103:
104: /**
105: * Generates (Regenerates) the CSRF token Hash.
106: * @return string
107: */
108: public static function generateToken(): string
109: {
110: self::$formToken = bin2hex(random_bytes(16));
111: session_set(self::getCsrfTokenName(), self::$formToken);
112:
113: return self::$formToken;
114: }
115:
116: /**
117: * Restore the CSRF token Hash from Session
118: * @return void
119: */
120: public static function restoreToken()
121: {
122: self::$formToken = session_get(self::getCsrfTokenName());
123: }
124:
125: /**
126: * Form token generation
127: * @return void
128: */
129: public static function token()
130: {
131: echo '<input type="hidden" name="' . self::getCsrfTokenName() . '" value="' . _encrypt(self::$formToken) . '" />';
132: }
133:
134: /**
135: * Form token validation
136: * @param array $validations The array of validation rules
137: * @param array $data The optional data array (if no `value` in $validation, it will be looked up in $data)
138: * @return boolean
139: */
140: public static function validate($validations = null, $data = [])
141: {
142: $postedToken = self::getPostedCsrfToken();
143: if (!$postedToken) {
144: Validation::addError('', _t('Invalid form token.'));
145: return false;
146: }
147: $postedToken = _decrypt($postedToken);
148:
149: $result = false;
150: # check token first
151: if (self::$formToken == $postedToken) {
152: # check the referer if it is requesting in the same site
153: if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] && _cfg('siteDomain')) {
154: $siteDomain = _cfg('siteDomain');
155: $siteDomain = preg_replace('/^www\./', '', $siteDomain);
156: $parsedURL = parse_url($_SERVER['HTTP_REFERER']);
157: $parsedURL['host'] = preg_replace('/^www\./', '', $parsedURL['host']);
158: if (strcasecmp($siteDomain, $parsedURL['host']) == 0) {
159: $result = true;
160: }
161: }
162: }
163:
164: if (!$result) {
165: Validation::addError('', _t('Error occurred during form submission. Please refresh the page to try again.'));
166: return false;
167: }
168:
169: if ($validations && Validation::check($validations, $data) === false) {
170: return false;
171: }
172:
173: return true;
174: }
175:
176: /**
177: * AJAX form responder
178: * @param string $formId The HTML form ID
179: * @param array $errors The array of the errors (it is used only for generic form processing)
180: * @param bool $forceJson Send json header
181: * @return void
182: */
183: public static function respond($formId, $errors = null, $forceJson = false)
184: {
185: self::$id = $formId;
186: self::$error = validation_get('errors');
187: $ajaxResponse = $errors === null;
188:
189: if (is_array($errors) && count($errors)) {
190: self::$error = $errors;
191: $ajaxResponse = false;
192: # if no error message and no other message, no need to respond
193: if (count(self::$error) == 0 && empty(self::$message)) {
194: return;
195: }
196: }
197:
198: $response = array(
199: 'formId' => self::$id,
200: 'success' => self::$success ? true : false,
201: 'error' => self::$error,
202: 'msg' => self::$message,
203: 'redirect' => self::$redirect,
204: 'callback' => self::$callback,
205: 'data' => self::$data,
206: );
207:
208: if ($ajaxResponse) {
209: if ($forceJson) {
210: _json($response);
211: } else {
212: echo json_encode($response);
213: }
214: } else {
215: echo '<script type="text/javascript">';
216: echo 'LC.Form.submitHandler(' . json_encode($response) . ')';
217: echo '</script>';
218: }
219: }
220:
221: /**
222: * Permits you to set the value of an input or textarea.
223: * Allows you to safely use HTML and characters such as quotes within form elements without breaking out of the form
224: *
225: * @param string $name The input element field name
226: * @param mixed $defaultValue The default value of the input element (optional)
227: *
228: * @return mixed The value of the input element
229: */
230: public static function value($name, $defaultValue = null)
231: {
232: $value = _post($name);
233:
234: return $value ? _h($value) : _h($defaultValue);
235: }
236:
237: /**
238: * Permits you to set the value to a rich text editor or any input where HTML source is required to be rendered.
239: * Allows you to safely use HTML and characters such as quotes within form elements without breaking out of the form
240: *
241: * @param string $name The input element field name
242: * @param mixed $defaultValue The default value of the input element (optional)
243: *
244: * @return mixed The value of the input element
245: */
246: public static function htmlValue($name, $defaultValue = null)
247: {
248: if (count($_POST)) {
249: if (!isset($_POST[$name])) {
250: return '';
251: }
252: $value = _xss($_POST[$name]);
253:
254: return _h($value);
255: }
256:
257: return _h($defaultValue);
258: }
259:
260: /**
261: * Allow you to select the option of a drop-down list.
262: *
263: * @param string $name The field name of the drop-down list
264: * @param mixed $value The option value to check against
265: * @param mixed $defaultValue The default selected value (optional)
266: *
267: * @return string `'selected="selected"'` if the option is found, otherwise the empty string returned
268: */
269: public static function selected($name, $value, $defaultValue = null)
270: {
271: return self::inputSelection($name, $value, $defaultValue) ? 'selected="selected"' : '';
272: }
273:
274: /**
275: * Allow you to select a checkbox or a radio button
276: *
277: * @param string $name The field name of the checkbox or radio button
278: * @param mixed $value The value to check against
279: * @param mixed $defaultValue The default selected value (optional)
280: *
281: * @return string `'checked="checked"'` if the option is found, otherwise the empty string returned
282: */
283: public static function checked($name, $value, $defaultValue = null)
284: {
285: return self::inputSelection($name, $value, $defaultValue) ? 'checked="checked"' : '';
286: }
287:
288: /**
289: * @param string $name The field name of the checkbox or radio button or drop-down list
290: * @param mixed $value The value to check against
291: * @param mixed $defaultValue The default selected value (optional)
292: *
293: * @return bool TRUE if the option is found, otherwise FALSE
294: * @internal
295: * @ignore
296: *
297: * Allow you to select a checkbox or a radio button or an option of a drop-down list
298: *
299: */
300: public static function inputSelection($name, $value, $defaultValue = null)
301: {
302: if (count($_POST)) {
303: $name = preg_replace('/(\[\])$/', '', $name); // group[] will be replaced as group
304: if (!isset($_POST[$name])) {
305: return '';
306: }
307: $postedValue = _post($name);
308: if (is_array($postedValue) && in_array($value, $postedValue)) {
309: return true;
310: } elseif ($value == $postedValue) {
311: return true;
312: } else {
313: return false;
314: }
315: } else {
316: if (is_array($defaultValue) && in_array($value, $defaultValue)) {
317: return true;
318: } elseif ($value == $defaultValue) {
319: return true;
320: } else {
321: return false;
322: }
323: }
324: }
325: }
326: