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 */
15trait Hm_Module_Output {
16
17    /* module output */
18    protected $output = array();
19
20    /* protected output keys */
21    protected $protected = array();
22
23    /* list of appendable keys */
24    protected $appendable = array();
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            }
77            else {
78                Hm_Debug::add(sprintf('Tried to append %s to scaler %s', $value, $name));
79                return false;
80            }
81        }
82        else {
83            $this->output[$name] = array($value);
84            $this->appendable[] = $name;
85            return true;
86        }
87    }
88
89    /**
90     * Sanitize input string
91     * @param string $string text to sanitize
92     * @param bool $special_only only use htmlspecialchars not htmlentities
93     * @return string sanitized value
94     */
95    public function html_safe($string, $special_only=false) {
96        if ($special_only) {
97            return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
98        }
99        return htmlentities($string, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
100    }
101
102    /**
103     * Concatenate a value
104     * @param string $name name to add to
105     * @param string $value value to add
106     * @return bool true on success
107     */
108    public function concat($name, $value) {
109        if (array_key_exists($name, $this->output)) {
110            if (is_string($this->output[$name])) {
111                $this->output[$name] .= $value;
112                return true;
113            }
114            else {
115                Hm_Debug::add(sprintf('Could not append %s to %s', print_r($value,true), $name));
116                return false;
117            }
118        }
119        else {
120            $this->output[$name] = $value;
121            return true;
122        }
123    }
124
125    /**
126     * Return module output from process()
127     * @return array
128     */
129    public function module_output() {
130        return $this->output;
131    }
132
133    /**
134     * Return protected output field list
135     * @return array
136     */
137    public function output_protected() {
138        return $this->protected;
139    }
140
141    /**
142     * Fetch an output value
143     * @param string $name key to fetch the value for
144     * @param mixed $default default return value if not found
145     * @param string $typed if a default value is given, typecast the result to it's type
146     * @return mixed value if found or default
147     */
148    public function get($name, $default=NULL, $typed=true) {
149        if (array_key_exists($name, $this->output)) {
150            $val = $this->output[$name];
151            if (!is_null($default) && $typed) {
152                if (gettype($default) != gettype($val)) {
153                    Hm_Debug::add(sprintf('TYPE CONVERSION: %s to %s for %s', gettype($val), gettype($default), $name));
154                    settype($val, gettype($default));
155                }
156            }
157            return $val;
158        }
159        return $default;
160    }
161
162    /**
163     * Check for a key
164     * @param string $name key name
165     * @return bool true if found
166     */
167    public function exists($name) {
168        return array_key_exists($name, $this->output);
169    }
170
171    /**
172     * Check to see if a value matches a list
173     * @param string $name name to check
174     * @param array $values list to check against
175     * @return bool true if found
176     */
177    public function in($name, $values) {
178        if (array_key_exists($name, $this->output) && in_array($this->output[$name], $values, true)) {
179            return true;
180        }
181        return false;
182    }
183}
184
185/**
186 * Methods used to validate handler module operations, like the HTTP request
187 * type and target/origin values
188 */
189trait Hm_Handler_Validate {
190
191    /**
192     * Validate HTTP request type, only GET and POST are allowed
193     * @param object $session
194     * @param object $request
195     * @return bool
196     */
197    public function validate_method($session, $request) {
198        if (!in_array(strtolower($request->method), array('get', 'post'), true)) {
199            if ($session->loaded) {
200                $session->destroy($request);
201                Hm_Debug::add(sprintf('LOGGED OUT: invalid method %s', $request->method));
202            }
203            return false;
204        }
205        return true;
206    }
207
208    /**
209     * Validate that the request has matching source and target origins
210     * @return bool
211     */
212    public function validate_origin($session, $request, $config) {
213        if (!$session->loaded) {
214            return true;
215        }
216        list($source, $target) = $this->source_and_target($request, $config);
217        if (!$this->validate_target($target, $source, $session, $request) ||
218            !$this->validate_source($target, $source, $session, $request)) {
219            return false;
220        }
221        return true;
222    }
223
224    /**
225     * Find source and target values for validate_origin
226     * @return string[]
227     */
228    private function source_and_target($request, $config) {
229        $source = false;
230        $target = $config->get('cookie_domain', false);
231        if ($target == 'none') {
232            $target = false;
233        }
234        $server_vars = array(
235            'HTTP_REFERER' => 'source',
236            'HTTP_ORIGIN' => 'source',
237            'HTTP_HOST' => 'target',
238            'HTTP_X_FORWARDED_HOST' => 'target'
239        );
240        foreach ($server_vars as $header => $type) {
241            if (array_key_exists($header, $request->server) && $request->server[$header]) {
242                $$type = $request->server[$header];
243            }
244        }
245        return array($source, $target);
246    }
247
248    /**
249     * @param string $target
250     * @param string $source
251     * @return boolean
252     */
253    private function validate_target($target, $source, $session, $request) {
254        if (!$target || !$source) {
255            $session->destroy($request);
256            Hm_Debug::add('LOGGED OUT: missing target origin');
257            return false;
258        }
259        return true;
260    }
261
262    /**
263     * @param string $target
264     * @param string $source
265     * @return boolean
266     */
267    private function validate_source($target, $source, $session, $request) {
268        $source = parse_url($source);
269        if (!is_array($source) || !array_key_exists('host', $source)) {
270            $session->destroy($request);
271            Hm_Debug::add('LOGGED OUT: invalid source origin');
272            return false;
273        }
274        if (array_key_exists('port', $source)) {
275            $source['host'] .= ':'.$source['port'];
276        }
277        if ($source['host'] !== $target) {
278            $session->destroy($request);
279            Hm_Debug::add('LOGGED OUT: invalid source origin');
280            return false;
281        }
282        return true;
283    }
284}
285
286/**
287 * Base class for data input processing modules, called "handler modules"
288 *
289 * All modules that deal with processing input data extend from this class.
290 * It provides access to input and state through the following member variables:
291 *
292 * $session      The session interface object
293 * $request      The HTTP request details object
294 * $config       The site config object
295 * $user_config  The user settings object for the current user
296 *
297 * Modules that extend this class need to override the process function
298 * Modules can pass information to the output modules using the out() and append() methods,
299 * and see data from other modules with the get() method
300 * @abstract
301 */
302abstract class Hm_Handler_Module {
303
304    use Hm_Module_Output;
305    use Hm_Handler_Validate;
306
307    /* session object */
308    public $session;
309
310    /* request object */
311    public $request;
312
313    /* site configuration object */
314    public $config;
315
316    /* current request id */
317    protected $page = '';
318
319    /* user settings */
320    public $user_config;
321
322    public $cache;
323    /**
324     * Assign input and state sources
325     * @param object $parent instance of the Hm_Request_Handler class
326     * @param string $page page id
327     * @param array $output data from handler modules
328     * @param array $protected list of protected output names
329     */
330    public function __construct($parent, $page, $output=array(), $protected=array()) {
331        $this->session = $parent->session;
332        $this->request = $parent->request;
333        $this->cache = $parent->cache;
334        $this->page = $page;
335        $this->config = $parent->site_config;
336        $this->user_config = $parent->user_config;
337        $this->output = $output;
338        $this->protected = $protected;
339    }
340
341    /**
342     * @return string
343     */
344    private function invalid_ajax_key() {
345        if (DEBUG_MODE) {
346            Hm_Debug::add('REQUEST KEY check failed');
347            Hm_Debug::load_page_stats();
348            Hm_Debug::show();
349        }
350        Hm_Functions::cease(json_encode(array('status' => 'not callable')));;
351        return 'exit';
352    }
353
354    /**
355     * @return string
356     */
357    private function invalid_http_key() {
358        if ($this->session->loaded) {
359            $this->session->destroy($this->request);
360            Hm_Debug::add('LOGGED OUT: request key check failed');
361        }
362        Hm_Dispatch::page_redirect('?page=home');
363        return 'redirect';
364    }
365
366    /**
367     * Validate a form key. If this is a non-empty POST form from an
368     * HTTP request or AJAX update, it will take the user to the home
369     * page if the page_key value is either not present or not valid
370     * @return false|string
371     */
372    public function process_key() {
373        if (empty($this->request->post)) {
374            return false;
375        }
376        $key = array_key_exists('hm_page_key', $this->request->post) ? $this->request->post['hm_page_key'] : false;
377        $valid = Hm_Request_Key::validate($key);
378        if ($valid) {
379            return false;
380        }
381        if ($this->request->type == 'AJAX') {
382            return $this->invalid_ajax_key();
383        }
384        else {
385            return $this->invalid_http_key();
386        }
387    }
388
389    /**
390     * Validate a value in a HTTP POST form
391     * @param mixed $val
392     * @return mixed
393     */
394    private function check_field($val) {
395        switch (true) {
396            case is_array($val):
397            case trim($val) !== '':
398            case $val === '0':
399            case $val === 0:
400                return $val;
401            default:
402                return NULL;
403        }
404    }
405
406    /**
407     * Process an HTTP POST form
408     * @param array $form list of required field names in the form
409     * @return array tuple with a bool indicating success, and an array of valid form values
410     */
411    public function process_form($form) {
412        $new_form = array();
413        foreach($form as $name) {
414            if (!array_key_exists($name, $this->request->post)) {
415                continue;
416            }
417            $val = $this->check_field($this->request->post[$name]);
418            if ($val !== NULL) {
419                $new_form[$name] = $val;
420            }
421        }
422        return array((count($form) === count($new_form)), $new_form);
423    }
424
425    /**
426     * Determine if a module set is enabled
427     * @param string $name the module set name to check for
428     * @return bool
429     */
430    public function module_is_supported($name) {
431        return in_array(strtolower($name), $this->config->get_modules(true), true);
432    }
433
434    /**
435     * Handler modules need to override this method to do work
436     */
437    abstract public function process();
438}
439
440/**
441 * Base class for output modules
442 * All modules that output data to a request must extend this class and define
443 * an output() method. It provides form validation, html sanitizing,
444 * and string translation services to modules
445 * @abstract
446 */
447abstract class Hm_Output_Module {
448
449    use Hm_Module_Output;
450
451    /* translated language strings */
452    protected $lstr = array();
453
454    /* langauge name */
455    protected $lang = false;
456
457    /* UI layout direction */
458    protected $dir = 'ltr';
459
460    /* Output format (AJAX or HTML5) */
461    protected $format = '';
462
463    /**
464     * Constructor
465     * @param array $input data from handler modules
466     * @param array $protected list of protected keys
467     */
468    public function __construct($input, $protected) {
469        $this->output = $input;
470        $this->protected = $protected;
471    }
472
473    /**
474     * Return a translated string if possible
475     * @param string $string the string to be translated
476     * @return string translated string
477     */
478    public function trans($string) {
479        if (array_key_exists($string, $this->lstr)) {
480            if ($this->lstr[$string] === false) {
481                return strip_tags($string);
482            }
483            else {
484                return strip_tags($this->lstr[$string]);
485            }
486        }
487        else {
488            Hm_Debug::add(sprintf('TRANSLATION NOT FOUND :%s:', $string));
489        }
490        return str_replace('\n', '<br />', strip_tags($string));
491    }
492
493    /**
494     * Build output by calling module specific output functions
495     * @param string $format output type, either HTML5 or AJAX
496     * @param array $lang_str list of language translation strings
497     * @return string
498     */
499    public function output_content($format, $lang_str) {
500        $this->lstr = $lang_str;
501        $this->format = str_replace('Hm_Format_', '', $format);
502        if (array_key_exists('interface_lang', $lang_str)) {
503            $this->lang = $lang_str['interface_lang'];
504        }
505        if (array_key_exists('interface_direction', $lang_str)) {
506            $this->dir = $lang_str['interface_direction'];
507        }
508        return $this->output();
509    }
510
511    /**
512     * Output modules need to override this method to add to a page or AJAX response
513     * @return string
514     */
515    abstract protected function output();
516}
517
518/**
519 * Placeholder classes for disabling a module in a set. These allow a module set
520 * to replace another module set's assignments with "false" to disable them
521 */
522class Hm_Output_ extends Hm_Output_Module { protected function output() {} }
523class Hm_Handler_ extends Hm_Handler_Module { public function process() {} }
524