1: <?php
2:
3: /**
4: * Module classes
5: * @package framework
6: * @subpackage module
7: */
8:
9: /**
10: * Module data management. These functions provide an interface for modules (both handler and output)
11: * to fetch data set by other modules and to return their own output. Handler modules must use these
12: * methods to set a response, output modules must if the format is AJAX, otherwise they should return
13: * an HTML5 string
14: */
15: trait Hm_Module_Output {
16:
17: /* module output */
18: protected $output = [];
19:
20: /* protected output keys */
21: protected $protected = [];
22:
23: /* list of appendable keys */
24: protected $appendable = [];
25:
26: /**
27: * @param string $name name to check for
28: * @param array $list array to look for name in
29: * @param string $type
30: * @param mixed $value value
31: * @return bool
32: */
33: protected function check_overwrite($name, $list, $type, $value) {
34: if (in_array($name, $list, true)) {
35: Hm_Debug::add(sprintf('MODULES: Cannot overwrite %s %s with %s', $type, $name, print_r($value,true)));
36: return false;
37: }
38: return true;
39: }
40:
41: /**
42: * Add a name value pair to the output array
43: * @param string $name name of value to store
44: * @param mixed $value value
45: * @param bool $protected true disallows overwriting
46: * @return bool true on success
47: */
48: public function out($name, $value, $protected = true) {
49: if (!$this->check_overwrite($name, $this->protected, 'protected', $value)) {
50: return false;
51: }
52: if (!$this->check_overwrite($name, $this->appendable, 'protected', $value)) {
53: return false;
54: }
55: if ($protected) {
56: $this->protected[] = $name;
57: }
58: $this->output[$name] = $value;
59: return true;
60: }
61:
62: /**
63: * append a value to an array, create it if does not exist
64: * @param string $name array name
65: * @param string $value value to add
66: * @return bool true on success
67: */
68: public function append($name, $value) {
69: if (!$this->check_overwrite($name, $this->protected, 'protected', $value)) {
70: return false;
71: }
72: if (array_key_exists($name, $this->output)) {
73: if (is_array($this->output[$name])) {
74: $this->output[$name][] = $value;
75: return true;
76: } else {
77: Hm_Debug::add(sprintf('Tried to append %s to scaler %s', $value, $name));
78: return false;
79: }
80: } else {
81: $this->output[$name] = [$value];
82: $this->appendable[] = $name;
83: return true;
84: }
85: }
86:
87: /**
88: * Sanitize input string
89: * @param string $string text to sanitize
90: * @param bool $special_only only use htmlspecialchars not htmlentities
91: * @return string sanitized value
92: */
93: public function html_safe($string, $special_only = false) {
94: if ($special_only) {
95: return htmlspecialchars((string) $string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
96: }
97: return htmlentities((string) $string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
98: }
99:
100: /**
101: * Concatenate a value
102: * @param string $name name to add to
103: * @param string $value value to add
104: * @return bool true on success
105: */
106: public function concat($name, $value) {
107: if (array_key_exists($name, $this->output)) {
108: if (is_string($this->output[$name])) {
109: $this->output[$name] .= $value;
110: return true;
111: } else {
112: Hm_Debug::add(sprintf('Could not append %s to %s', print_r($value,true), $name));
113: return false;
114: }
115: } else {
116: $this->output[$name] = $value;
117: return true;
118: }
119: }
120:
121: /**
122: * Return module output from process()
123: * @return array
124: */
125: public function module_output() {
126: return $this->output;
127: }
128:
129: /**
130: * Return protected output field list
131: * @return array
132: */
133: public function output_protected() {
134: return $this->protected;
135: }
136:
137: /**
138: * Fetch an output value
139: * @param string $name key to fetch the value for
140: * @param mixed $default default return value if not found
141: * @param string $typed if a default value is given, typecast the result to it's type
142: * @return mixed value if found or default
143: */
144: public function get($name, $default = NULL, $typed = true) {
145: if (array_key_exists($name, $this->output)) {
146: $val = $this->output[$name];
147: if (!is_null($default) && $typed) {
148: if (gettype($default) != gettype($val)) {
149: Hm_Debug::add(sprintf('TYPE CONVERSION: %s to %s for %s', gettype($val), gettype($default), $name), 'info');
150: settype($val, gettype($default));
151: }
152: }
153: return $val;
154: }
155: return $default;
156: }
157:
158: /**
159: * Check for a key
160: * @param string $name key name
161: * @return bool true if found
162: */
163: public function exists($name) {
164: return array_key_exists($name, $this->output);
165: }
166:
167: /**
168: * Check to see if a value matches a list
169: * @param string $name name to check
170: * @param array $values list to check against
171: * @return bool true if found
172: */
173: public function in($name, $values) {
174: if (array_key_exists($name, $this->output) && in_array($this->output[$name], $values, true)) {
175: return true;
176: }
177: return false;
178: }
179: }
180:
181: /**
182: * Methods used to validate handler module operations, like the HTTP request
183: * type and target/origin values
184: */
185: trait Hm_Handler_Validate {
186:
187: /**
188: * Validate HTTP request type, only GET and POST are allowed
189: * @param object $session
190: * @param object $request
191: * @return bool
192: */
193: public function validate_method($session, $request) {
194: if (!empty($request->method) && is_string($request->method)) {
195: if (!in_array(mb_strtolower($request->method), ['get', 'post'], true)) {
196: if ($session->loaded) {
197: $session->destroy($request);
198: Hm_Debug::add(sprintf('LOGGED OUT: invalid method %s', $request->method));
199: }
200: return false;
201: }
202: return true;
203: }
204: // Handle the case where method is null or invalid
205: if ($session->loaded) {
206: $session->destroy($request);
207: Hm_Debug::add('LOGGED OUT: missing or invalid request method');
208: }
209: return false;
210: }
211:
212: /**
213: * Validate that the request has matching source and target origins
214: * @return bool
215: */
216: public function validate_origin($session, $request, $config) {
217: if (!$session->loaded) {
218: return true;
219: }
220: list($source, $target) = $this->source_and_target($request, $config);
221: if (!$this->validate_target($target, $source, $session, $request) ||
222: !$this->validate_source($target, $source, $session, $request)) {
223: return false;
224: }
225: return true;
226: }
227:
228: /**
229: * Find source and target values for validate_origin
230: * @return string[]
231: */
232: private function source_and_target($request, $config) {
233: $source = false;
234: $target = $config->get('cookie_domain', false);
235: if ($target == 'none') {
236: $target = false;
237: }
238: $server_vars = [
239: 'HTTP_REFERER' => 'source',
240: 'HTTP_ORIGIN' => 'source',
241: 'HTTP_HOST' => 'target',
242: 'HTTP_X_FORWARDED_HOST' => 'target'
243: ];
244: foreach ($server_vars as $header => $type) {
245: if (!empty($request->server[$header])) {
246: $$type = $request->server[$header];
247: }
248: }
249: return [$source, $target];
250: }
251:
252: /**
253: * @param string $target
254: * @param string $source
255: * @return boolean
256: */
257: private function validate_target($target, $source, $session, $request) {
258: if (!$target || !$source) {
259: $session->destroy($request);
260: Hm_Debug::add('LOGGED OUT: missing target origin');
261: return false;
262: }
263: return true;
264: }
265:
266: /**
267: * @param string $target
268: * @param string $source
269: * @return boolean
270: */
271: private function validate_source($target, $source, $session, $request) {
272: $source = parse_url($source);
273: if (!is_array($source) || !array_key_exists('host', $source)) {
274: $session->destroy($request);
275: Hm_Debug::add('LOGGED OUT: invalid source origin');
276: return false;
277: }
278: if (array_key_exists('port', $source)) {
279: $source['host'] .= ':'.$source['port'];
280: }
281: if ($source['host'] !== $target) {
282: $session->destroy($request);
283: Hm_Debug::add('LOGGED OUT: invalid source origin');
284: return false;
285: }
286: return true;
287: }
288: }
289:
290: /**
291: * Base class for data input processing modules, called "handler modules"
292: *
293: * All modules that deal with processing input data extend from this class.
294: * It provides access to input and state through the following member variables:
295: *
296: * $session The session interface object
297: * $request The HTTP request details object
298: * $config The site config object
299: * $user_config The user settings object for the current user
300: *
301: * Modules that extend this class need to override the process function
302: * Modules can pass information to the output modules using the out() and append() methods,
303: * and see data from other modules with the get() method
304: * @abstract
305: */
306: abstract class Hm_Handler_Module {
307:
308: use Hm_Module_Output;
309: use Hm_Handler_Validate;
310:
311: /* session object */
312: public $session;
313:
314: /* request object */
315: public $request;
316:
317: /* site configuration object */
318: public $config;
319:
320: /* current request id */
321: protected $page = '';
322:
323: /* user settings */
324: public $user_config;
325:
326: public $cache;
327:
328: /**
329: * Assign input and state sources
330: * @param object $parent instance of the Hm_Request_Handler class
331: * @param string $page page id
332: * @param array $output data from handler modules
333: * @param array $protected list of protected output names
334: */
335: public function __construct($parent, $page, $output = [], $protected = []) {
336: $this->session = $parent->session;
337: $this->request = $parent->request;
338: $this->cache = $parent->cache;
339: $this->page = $page;
340: $this->config = $parent->site_config;
341: $this->user_config = $parent->user_config;
342: $this->output = $output;
343: $this->protected = $protected;
344: }
345:
346: /**
347: * @return string
348: */
349: private function invalid_ajax_key() {
350: if (DEBUG_MODE or $this->config->get('debug_log')) {
351: Hm_Debug::add('REQUEST KEY check failed');
352: Hm_Debug::load_page_stats();
353: Hm_Debug::show();
354: }
355: Hm_Functions::cease(json_encode(['status' => 'not callable']));
356: return 'exit';
357: }
358:
359: /**
360: * @return string
361: */
362: private function invalid_http_key() {
363: if ($this->session->loaded) {
364: $this->session->destroy($this->request);
365: Hm_Debug::add('LOGGED OUT: request key check failed');
366: }
367: Hm_Dispatch::page_redirect('?page=home');
368: return 'redirect';
369: }
370:
371: /**
372: * Validate a form key. If this is a non-empty POST form from an
373: * HTTP request or AJAX update, it will take the user to the home
374: * page if the page_key value is either not present or not valid
375: * @return false|string
376: */
377: public function process_key() {
378: if (empty($this->request->post)) {
379: return false;
380: }
381: $key = array_key_exists('hm_page_key', $this->request->post) ? $this->request->post['hm_page_key'] : false;
382: $valid = Hm_Request_Key::validate($key);
383: if ($valid) {
384: return false;
385: }
386: if ($this->request->type == 'AJAX') {
387: return $this->invalid_ajax_key();
388: } else {
389: return $this->invalid_http_key();
390: }
391: }
392:
393: /**
394: * Validate a value in a HTTP POST form
395: * @param mixed $val
396: * @return mixed
397: */
398: private function check_field($val) {
399: switch (true) {
400: case is_array($val):
401: case is_string($val):
402: case is_int($val):
403: case is_float($val):
404: case is_bool($val):
405: case $val === '0':
406: case $val === 0:
407: return $val;
408: default:
409: return NULL;
410: }
411: }
412:
413: /**
414: * Process an HTTP POST form
415: * @param array $form list of required field names in the form
416: * @return array tuple with a bool indicating success, and an array of valid form values
417: */
418: public function process_form($form) {
419: $new_form = [];
420: foreach($form as $name) {
421: if (!array_key_exists($name, $this->request->post)) {
422: continue;
423: }
424: $val = $this->check_field($this->request->post[$name]);
425: if ($val !== NULL) {
426: $new_form[$name] = $val;
427: }
428: }
429: return [(count($form) === count($new_form)), $new_form];
430: }
431:
432: /**
433: * Determine if a module set is enabled
434: * @param string $name the module set name to check for
435: * @return bool
436: */
437: public function module_is_supported($name) {
438: return in_array(mb_strtolower($name), $this->config->get_modules(true), true);
439: }
440:
441: /**
442: * Checks if a config setting is disabled and signals whether to skip further execution.
443: *
444: * @param string $setting_key The configuration key to check.
445: * @param mixed $default The default value to use if the key is not set.
446: * @return bool True if the feature is disabled and should be skipped.
447: */
448: public function should_skip_execution($setting_key, $default = false) {
449: return !$this->user_config->get($setting_key, $default);
450: }
451:
452: public function save_hm_msgs() {
453: $msgs = Hm_Msgs::getRaw();
454: if (!empty($msgs)) {
455: Hm_Msgs::flush();
456: $this->session->secure_cookie($this->request, 'hm_msgs', base64_encode(json_encode($msgs)));
457: }
458: }
459:
460: /**
461: * Handler modules need to override this method to do work
462: */
463: abstract public function process();
464: }
465:
466: /**
467: * Base class for output modules
468: * All modules that output data to a request must extend this class and define
469: * an output() method. It provides form validation, html sanitizing,
470: * and string translation services to modules
471: * @abstract
472: */
473: abstract class Hm_Output_Module {
474:
475: use Hm_Module_Output;
476:
477: /* translated language strings */
478: protected $lstr = [];
479:
480: /* langauge name */
481: protected $lang = false;
482:
483: /* UI layout direction */
484: protected $dir = 'ltr';
485:
486: /* Output format (AJAX or HTML5) */
487: protected $format = '';
488:
489: /**
490: * Constructor
491: * @param array $input data from handler modules
492: * @param array $protected list of protected keys
493: */
494: public function __construct($input, $protected) {
495: $this->output = $input;
496: $this->protected = $protected;
497: }
498:
499: /**
500: * Return a translated string if possible
501: * @param string $string the string to be translated
502: * @return string translated string
503: */
504: public function trans($string) {
505: if (array_key_exists($string, $this->lstr)) {
506: if ($this->lstr[$string] === false) {
507: return strip_tags($string);
508: } else {
509: return strip_tags($this->lstr[$string]);
510: }
511: }
512: else {
513: Hm_Debug::add(sprintf('TRANSLATION NOT FOUND :%s:', $string), 'warning');
514: }
515: return str_replace('\n', '<br />', strip_tags($string));
516: }
517:
518: /**
519: * Return all translations for earch supported language
520: * @return array translations
521: */
522: public function all_trans() {
523: // Get all files in the language directory
524: $language_files = glob(APP_PATH.'language/'. '*.php');
525: $translations = [];
526:
527: foreach ($language_files as $file) {
528: // Extract the language code from the file name
529: $language_code = pathinfo($file, PATHINFO_FILENAME);
530:
531: // Read the content of the file
532: $content = include $file;
533:
534: // Store the content in the translations array
535: $translations[$language_code] = $content;
536: }
537:
538: return $translations;
539: }
540:
541:
542: /**
543: * Return a translated string of numbers if possible and if language is farsi
544: * @param string $string the string to be translated which has to be numbers
545: * @return string translated string
546: */
547: public function translate_number($number) {
548: if (!is_numeric($number) || !in_array($this->lang, ['fa'])) {
549: return $number;
550: }
551: $number_splitted = mb_str_split($number);
552: $translated_number = "";
553: foreach ($number_splitted as $number_splitted) {
554: $translated_number .= $this->trans($number_splitted);
555: }
556: return $translated_number;
557: }
558:
559: /**
560: * Build output by calling module specific output functions
561: * @param string $format output type, either HTML5 or AJAX
562: * @param array $lang_str list of language translation strings
563: * @return string
564: */
565: public function output_content($format, $lang_str) {
566: $this->lstr = $lang_str;
567: $this->format = str_replace('Hm_Format_', '', $format);
568: if (array_key_exists('interface_lang', $lang_str)) {
569: $this->lang = $lang_str['interface_lang'];
570: }
571: if (array_key_exists('interface_direction', $lang_str)) {
572: $this->dir = $lang_str['interface_direction'];
573: }
574: return $this->output();
575: }
576:
577: /**
578: * Output modules need to override this method to add to a page or AJAX response
579: * @return string
580: */
581: abstract protected function output();
582: }
583:
584: /**
585: * Placeholder classes for disabling a module in a set. These allow a module set
586: * to replace another module set's assignments with "false" to disable them
587: */
588: class Hm_Output_ extends Hm_Output_Module { protected function output() {} }
589: class Hm_Handler_ extends Hm_Handler_Module { public function process() {} }
590: