1: <?php
2:
3: /**
4: * Deal with redirects
5: * @package framework
6: * @subpackage dispatch
7: */
8:
9: trait Hm_Dispatch_Redirect {
10:
11: /**
12: * Redirect after an HTTP POST form
13: * @param Hm_Request $request request object
14: * @param object $session session object
15: * @param Hm_Module_Exec $mod_exec module manager object
16: * @return boolean
17: */
18: private function post_redirect($request, $session, $mod_exec) {
19: if (!empty($request->post) && $request->type == 'HTTP') {
20: $this->forward_messages($session, $request);
21: $session->end();
22: $this->redirect_to_url($mod_exec);
23: $this->redirect_to_current($request);
24: return true;
25: }
26: return false;
27: }
28:
29: /**
30: * Redirect to the current url
31: * @param Hm_Request $request
32: * @return void
33: */
34: private function redirect_to_current($request) {
35: if (array_key_exists('REQUEST_URI', $request->server)) {
36: Hm_Dispatch::page_redirect($this->validate_request_uri($request->server['REQUEST_URI']));
37: }
38: }
39:
40: /**
41: * Redirect to a specified url
42: * @param object $mod_exec
43: * @return void
44: */
45: private function redirect_to_url($mod_exec) {
46: if (!empty($mod_exec->handler_response['redirect_url'])) {
47: Hm_Dispatch::page_redirect($mod_exec->handler_response['redirect_url']);
48: }
49: }
50:
51: /**
52: * Forward messages on a POST redirect
53: * @param object $session session object
54: * @return void
55: */
56: private function forward_messages($session, $request) {
57: $msgs = Hm_Msgs::getRaw();
58: if (!empty($msgs)) {
59: $session->secure_cookie($request, 'hm_msgs', base64_encode(json_encode($msgs)));
60: }
61: }
62:
63: /**
64: * Validate a request uri
65: * @param string $uri URI to validate
66: * @return string
67: */
68: public function validate_request_uri($uri) {
69: $uri = html_entity_decode($uri);
70: if ($uri === '') {
71: return '/';
72: }
73: $parts = parse_url($uri);
74: if (array_key_exists('scheme', $parts)) {
75: return '/';
76: }
77: if ($parts === false || !array_key_exists('path', $parts) || mb_strpos($parts['path'], '..') !== false) {
78: return '/';
79: }
80: return $uri;
81: }
82:
83:
84: /**
85: * Redirect the page after a POST form is submitted and forward any user notices
86: * @param Hm_Request $request request object
87: * @param Hm_Module_Exec $mod_exec module manager object
88: * @param object $session session object
89: * @return string|false
90: */
91: public function check_for_redirect($request, $mod_exec, $session) {
92: if (!empty($mod_exec->handler_response['no_redirect'])) {
93: return 'noredirect';
94: }
95: if ($this->post_redirect($request, $session, $mod_exec)) {
96: return 'redirect';
97: } elseif (!empty($mod_exec->handler_response['redirect_url'])) {
98: $session->end();
99: $this->redirect_to_url($mod_exec);
100: return 'redirect';
101: } elseif ($this->unpack_messages($request, $session)) {
102: return 'msg_forward';
103: }
104: return false;
105: }
106:
107: /**
108: * Unpack forwarded messages
109: * @param Hm_Request $request request object
110: * @param object $session session object
111: * @return boolean
112: */
113: public function unpack_messages($request, $session) {
114: if (!empty($request->cookie['hm_msgs'])) {
115: $msgs = @json_decode(base64_decode($request->cookie['hm_msgs']), true);
116: if (is_array($msgs)) {
117: array_walk($msgs, function($v) { Hm_Msgs::add($v['text'], $v['type']); });
118: }
119: $session->delete_cookie($request, 'hm_msgs');
120: return true;
121: }
122: return false;
123: }
124:
125: /**
126: * Perform an HTTP redirect
127: * @param string $url url to redirect to
128: * @param int $status current HTTP status
129: * @return void
130: */
131: static public function page_redirect($url, $status = false) {
132: if (DEBUG_MODE) {
133: Hm_Debug::add(sprintf('Redirecting to %s', $url), 'info');
134: Hm_Debug::load_page_stats();
135: Hm_Debug::show();
136: }
137: if ($status == 303) {
138: Hm_Debug::add('Redirect loop found', 'warning');
139: Hm_Functions::cease('Redirect loop discovered');
140: }
141: Hm_Functions::header('HTTP/1.1 303 Found');
142: Hm_Functions::header('Location: '.$url);
143: Hm_Functions::cease();
144: }
145:
146: }
147:
148: /**
149: * Page request router that ties all the framework pieces together
150: * @package framework
151: * @subpackage dispatch
152: */
153:
154: class Hm_Dispatch {
155:
156: use Hm_Dispatch_Redirect;
157:
158: public $site_config;
159: public $request;
160: public $session;
161: public $module_exec;
162: public $page;
163: public $output;
164:
165: /**
166: * Setup object needed to process a request
167: * @param object $config site configuration
168: */
169: public function __construct($config) {
170:
171: /* get the site config defined in the config/app.php file */
172: $this->site_config = $config;
173:
174: /* check for the site module set override */
175: $this->load_site_lib();
176:
177: /* setup a session handler, but don't actually start a session yet */
178: $session_config = new Hm_Session_Setup($this->site_config);
179: $this->session = $session_config->setup_session();
180:
181: /* instantiate the module runner */
182: $this->module_exec = new Hm_Module_Exec($this->site_config);
183:
184: /* process request input using the white-lists defined in modules */
185: $this->request = new Hm_Request($this->module_exec->filters, $config);
186:
187: /* do it */
188: $this->process_request();
189: }
190:
191: /**
192: * Possibly include the site module lib.php overrides
193: * @return void
194: */
195: private function load_site_lib() {
196: if (!is_array($this->site_config->get_modules()) || !in_array('site', $this->site_config->get_modules(), true)) {
197: return;
198: }
199: if (is_readable(APP_PATH.'modules/site/lib.php')) {
200: Hm_Debug::add('Including site module set lib.php', 'info');
201: require APP_PATH.'modules/site/lib.php';
202: }
203: }
204:
205: /**
206: * Process a request
207: * @return void
208: */
209: private function process_request() {
210:
211: /* get the request identifier */
212: $this->get_page($this->module_exec->filters, $this->request);
213:
214: /* load up the module requirements */
215: $this->module_exec->load_module_sets($this->page);
216:
217: /* run handler modules to process input and perform actions */
218: $this->module_exec->run_handler_modules($this->request, $this->session, $this->page);
219:
220: /* clean up any network connections */
221: $this->close_connections();
222:
223: /* check for update settings on login */
224: $this->save_settings_on_login();
225:
226: /* check to see if a handler module told us to redirect the browser */
227: $this->check_for_redirect($this->request, $this->module_exec, $this->session);
228:
229: /* save and close a session if one is open */
230: $active_session = $this->save_session();
231:
232: /* run the output formatting modules */
233: $this->module_exec->run_output_modules($this->request, $active_session, $this->page,
234: $this->module_exec->handler_response);
235:
236: /* output content to the browser */
237: $this->render_output();
238: }
239:
240: /**
241: * Check for a flag to save settings on login
242: */
243: private function save_settings_on_login() {
244: if (!$this->session->loaded) {
245: return;
246: }
247: try {
248: if ($this->module_exec->user_config->save_on_login) {
249: $this->module_exec->user_config->save($this->request->post['username'], $this->request->post['password']);
250: }
251: } catch (Exception $e) {
252: Hm_Msgs::add('Could not save settings: ' . $e->getMessage(), 'warning');
253: }
254: }
255:
256: /**
257: * Close network connections if they exist
258: * @return void
259: */
260: private function close_connections() {
261: foreach (['Hm_IMAP_List', 'Hm_SMTP_List'] as $class) {
262: if (Hm_Functions::class_exists($class)) {
263: $class::clean_up();
264: }
265: }
266: }
267:
268: /**
269: * Close and save the session
270: * @return bool true if the session is active
271: */
272: private function save_session() {
273: $res = $this->session->is_active();
274: $this->session->end();
275: return $res;
276: }
277:
278: /**
279: * Format and send the request output to the browser
280: * @return void
281: */
282: private function render_output() {
283: $formatter = new $this->request->format($this->site_config);
284: $class = $this->site_config->get('output_class', 'Hm_Output_HTTP');
285: $renderer = new $class;
286: $content = $formatter->content($this->module_exec->output_response, $this->request->allowed_output);
287: /* TODO: might be a good idea to use a custom render class that can return
288: * the output on demand */
289: $this->output = $renderer->send_response($content, $this->module_exec->output_data);
290: }
291:
292: /**
293: * Get a list of valid pages
294: * @param array $filters list of filters
295: * @param return array
296: */
297: private function get_pages($filters) {
298: return $filters['allowed_pages'] ?? [];
299: }
300:
301: /**
302: * Check for a valid ajax request
303: * @param Hm_Request $request request details
304: * @param array $filters list of filters
305: * @return boolean
306: */
307: public function validate_ajax_request($request, $filters) {
308: return $this->validate_hook($request->get, $filters) || $this->validate_hook($request->post, $filters);
309: }
310:
311: /**
312: * Validates ajax hook
313: * @param array $input POST or GET data
314: * @param array $filters list of filters
315: * @return boolean
316: */
317: private function validate_hook($input, $filters) {
318: if (array_key_exists('hm_ajax_hook', $input)) {
319: if (in_array($input['hm_ajax_hook'], $this->get_pages($filters), true)) {
320: return true;
321: } else {
322: Hm_Functions::cease(json_encode(['status' => 'not callable']));
323: }
324: }
325: return false;
326: }
327:
328: /**
329: * Determine the page id
330: * @param array $filters list of filters
331: * @param Hm_Request $request request details
332: * @return void
333: */
334: public function get_page($filters, $request) {
335: $this->page = 'notfound';
336: if ($request->type == 'AJAX' && $this->validate_ajax_request($request, $filters)) {
337: $this->page = $request->get['hm_ajax_hook'] ?? $request->post['hm_ajax_hook'];
338: } elseif (array_key_exists('page', $request->get) && in_array($request->get['page'], $this->get_pages($filters), true)) {
339: $this->page = $request->get['page'];
340: } elseif (!array_key_exists('page', $request->get)) {
341: $this->page = 'home';
342: }
343: $this->module_exec->page = $this->page;
344: Hm_Debug::add('Page ID: '.$this->page, 'info');
345: }
346:
347: /**
348: * Check to see if PHP is configured properly
349: * @return bool
350: */
351: static public function is_php_setup() {
352: return
353: version_compare(PHP_VERSION, '7.4', '>=') &&
354: Hm_Functions::function_exists('mb_strpos') &&
355: Hm_Functions::function_exists('curl_exec') &&
356: Hm_Functions::function_exists('openssl_random_pseudo_bytes') &&
357: Hm_Functions::class_exists('PDO');
358: }
359: }
360: