Passed
Push — 1.0.0-dev ( 247c52...b3c42a )
by nguereza
02:31
created

Response::loadView()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 5
nop 3
dl 0
loc 23
rs 9.1111
c 0
b 0
f 0
1
<?php
2
    defined('ROOT_PATH') or exit('Access denied');
3
    /**
4
     * TNH Framework
5
     *
6
     * A simple PHP framework using HMVC architecture
7
     *
8
     * This content is released under the GNU GPL License (GPL)
9
     *
10
     * Copyright (C) 2017 Tony NGUEREZA
11
     *
12
     * This program is free software; you can redistribute it and/or
13
     * modify it under the terms of the GNU General Public License
14
     * as published by the Free Software Foundation; either version 3
15
     * of the License, or (at your option) any later version.
16
     *
17
     * This program is distributed in the hope that it will be useful,
18
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
     * GNU General Public License for more details.
21
     *
22
     * You should have received a copy of the GNU General Public License
23
     * along with this program; if not, write to the Free Software
24
     * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25
     */
26
27
    class Response extends BaseStaticClass {
28
29
        /**
30
         * The list of request header to send with response
31
         * @var array
32
         */
33
        private static $headers = array();
34
		
35
        /**
36
         * The final page content to display to user
37
         * @var string
38
         */
39
        private $_pageRender = null;
40
		
41
        /**
42
         * The current request URL
43
         * @var string
44
         */
45
        private $_currentUrl = null;
46
		
47
        /**
48
         * The current request URL cache key
49
         * @var string
50
         */
51
        private $_currentUrlCacheKey = null;
52
		
53
        /**
54
         * Whether we can compress the output using Gzip
55
         * @var boolean
56
         */
57
        private static $_canCompressOutput = false;
58
		
59
        /**
60
         * Construct new instance
61
         */
62
        public function __construct() {
63
            $currentUrl = '';
64
            if (!empty($_SERVER['REQUEST_URI'])) {
65
                $currentUrl = $_SERVER['REQUEST_URI'];
66
            }
67
            if (!empty($_SERVER['QUERY_STRING'])) {
68
                $currentUrl .= '?' . $_SERVER['QUERY_STRING'];
69
            }
70
            $this->_currentUrl = $currentUrl;
71
					
72
            $this->_currentUrlCacheKey = md5($this->_currentUrl);
73
			
74
            self::$_canCompressOutput = get_config('compress_output')
75
                                          && isset($_SERVER['HTTP_ACCEPT_ENCODING']) 
76
                                          && stripos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false 
77
                                          && extension_loaded('zlib')
78
                                          && (bool) ini_get('zlib.output_compression') === false;
79
        }
80
81
		
82
        /**
83
         * Send the HTTP Response headers
84
         * @param  integer $httpCode the HTTP status code
85
         * @param  array   $headers   the additional headers to add to the existing headers list
86
         */
87
        public static function sendHeaders($httpCode = 200, array $headers = array()) {
88
            set_http_status_header($httpCode);
89
            self::setHeaders($headers);
90
            self::setRequiredHeaders();
91
            if (!headers_sent()) {
92
                foreach (self::getHeaders() as $key => $value) {
93
                    header($key . ': ' . $value);
94
                }
95
            }
96
        }
97
98
        /**
99
         * Get the list of the headers
100
         * @return array the headers list
101
         */
102
        public static function getHeaders() {
103
            return self::$headers;
104
        }
105
106
        /**
107
         * Get the header value for the given name
108
         * @param  string $name the header name
109
         * @return string|null       the header value
110
         */
111
        public static function getHeader($name) {
112
            if (array_key_exists($name, self::$headers)) {
113
                return self::$headers[$name];
114
            }
115
            return null;
116
        }
117
118
119
        /**
120
         * Set the header value for the specified name
121
         * @param string $name  the header name
122
         * @param string $value the header value to be set
123
         */
124
        public static function setHeader($name, $value) {
125
            self::$headers[$name] = $value;
126
        }
127
128
        /**
129
         * Set the headers using array
130
         * @param array $headers the list of the headers to set. 
131
         * Note: this will merge with the existing headers
132
         */
133
        public static function setHeaders(array $headers) {
134
            self::$headers = array_merge(self::getHeaders(), $headers);
135
        }
136
		
137
        /**
138
         * Redirect user to the specified page
139
         * @param  string $path the URL or URI to be redirect to
140
         */
141
        public static function redirect($path = '') {
142
            $logger = self::getLogger();
143
            $url = Url::site_url($path);
144
            $logger->info('Redirect to URL [' . $url . ']');
145
            if (!headers_sent()) {
146
                header('Location: ' . $url);
147
                exit;
148
            }
149
            echo '<script>
150
					location.href = "'.$url . '";
151
				</script>';
152
        }
153
154
        /**
155
         * Render the view to display later or return the content
156
         * @param  string  $view   the view name or path
157
         * @param  array|object   $data   the variable data to use in the view
158
         * @param  boolean $return whether to return the view generated content or display it directly
159
         * @return void|string          if $return is true will return the view content otherwise
160
         * will display the view content.
161
         */
162
        public function render($view, $data = null, $return = false) {
163
            $logger = self::getLogger();
164
            //convert data to an array
165
            $data = (array) $data;
166
            $view = str_ireplace('.php', '', $view);
167
            $view = trim($view, '/\\');
168
            $viewFile = $view . '.php';
169
            $path = null;
170
			
171
            //check in module first
172
            $logger->debug('Checking the view [' . $view . '] from module list ...');
173
            $moduleInfo = $this->getModuleInfoForView($view);
174
            $module = $moduleInfo['module'];
175
            $view = $moduleInfo['view'];
176
			
177
            $moduleViewPath = Module::findViewFullPath($view, $module);
178
            if ($moduleViewPath) {
179
                $path = $moduleViewPath;
180
                $logger->info('Found view [' . $view . '] in module [' . $module . '], the file path is [' . $moduleViewPath . '] we will used it');
181
            } else {
182
                $logger->info('Cannot find view [' . $view . '] in module [' . $module . '] using the default location');
183
            }
184
			if (!$path) {
185
                $path = $this->getDefaultFilePathForView($viewFile);
186
            }
187
            $logger->info('The view file path to be loaded is [' . $path . ']');
188
			
189
            if ($return) {
190
                return $this->loadView($path, $data, true);
191
            }
192
            $this->loadView($path, $data, false);
193
        }
194
195
		
196
        /**
197
         * Send the final page output to user
198
         */
199
        public function renderFinalPage() {
200
            $logger = self::getLogger();
201
            $obj = & get_instance();
202
            $cachePageStatus = get_config('cache_enable', false) && !empty($obj->view_cache_enable);
203
            $dispatcher = $obj->eventdispatcher;
204
            $content = $this->_pageRender;
205
            if (!$content) {
206
                $logger->warning('The final view content is empty.');
207
                return;
208
            }
209
            //dispatch
210
            $event = $dispatcher->dispatch(new EventInfo('FINAL_VIEW_READY', $content, true));
211
            $content = null;
212
            if (!empty($event->payload)) {
213
                $content = $event->payload;
214
            }
215
            if (empty($content)) {
216
                $logger->warning('The view content is empty after dispatch to event listeners.');
217
            }
218
            //check whether need save the page into cache.
219
            if ($cachePageStatus) {
220
                $this->savePageContentIntoCache($content);
221
            }
222
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
223
224
            //compress the output if is available
225
            $type = null;
226
            if (self::$_canCompressOutput) {
227
                $type = 'ob_gzhandler';
228
            }
229
            ob_start($type);
230
            self::sendHeaders(200);
231
            echo $content;
232
            ob_end_flush();
233
        }
234
235
		
236
        /**
237
         * Send the final page output to user if is cached
238
         * @param object $cache the cache instance
239
         *
240
         * @return boolean whether the page content if available or not
241
         */
242
        public function renderFinalPageFromCache(&$cache) {
243
            $logger = self::getLogger();
244
            //the current page cache key for identification
245
            $pageCacheKey = $this->_currentUrlCacheKey;
246
			
247
            $logger->debug('Checking if the page content for the URL [' . $this->_currentUrl . '] is cached ...');
248
            //get the cache information to prepare header to send to browser
249
            $cacheInfo = $cache->getInfo($pageCacheKey);
250
            if ($cacheInfo) {
251
                $status = $this->sendCacheNotYetExpireInfoToBrowser($cacheInfo);
252
                if ($status === false) {
253
                    return $this->sendCachePageContentToBrowser($cache);
254
                }
255
                return true;
256
            }
257
            return false;
258
        }
259
	
260
		
261
        /**
262
         * Get the final page to be rendered
263
         * @return string
264
         */
265
        public function getFinalPageRendered() {
266
            return $this->_pageRender;
267
        }
268
269
        /**
270
         * Send the HTTP 404 error if can not found the 
271
         * routing information for the current request
272
         */
273
        public function send404() {
274
            $logger = self::getLogger();
275
            $obj = & get_instance();
276
            $cachePageStatus = get_config('cache_enable', false) && !empty($obj->view_cache_enable);
277
            $dispatcher = $obj->eventdispatcher;
278
            $content = $this->_pageRender;
279
            if (!$content) {
280
                $logger->warning('The final view content is empty.');
281
                return;
282
            }
283
            //dispatch
284
            $dispatcher->dispatch(new EventInfo('PAGE_NOT_FOUND'));
285
            //check whether need save the page into cache.
286
            if ($cachePageStatus) {
287
                $this->savePageContentIntoCache($content);
288
            }
289
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
290
291
            /**************************************** save the content into logs **************/
292
            $bwsr = & class_loader('Browser');
293
            $browser = $bwsr->getPlatform() . ', ' . $bwsr->getBrowser() . ' ' . $bwsr->getVersion();
294
            $obj->loader->functions('user_agent');
295
            $str = '[404 page not found] : ';
296
            $str .= ' Unable to find the request page [' . $obj->request->requestUri() . ']. The visitor IP address [' . get_ip() . '], browser [' . $browser . ']';
297
            
298
            //Todo fix the issue the logger name change after load new class
299
            $logger = self::getLogger();
300
            $logger->error($str);
301
            /**********************************************************************/
302
303
            //compress the output if is available
304
            $type = null;
305
            if (self::$_canCompressOutput) {
306
                $type = 'ob_gzhandler';
307
            }
308
            ob_start($type);
309
            self::sendHeaders(404);
310
            echo $content;
311
            ob_end_flush();
312
        }
313
314
        /**
315
         * Display the error to user
316
         * @param  array  $data the error information
317
         */
318
        public static function sendError(array $data = array()) {
319
            $path = CORE_VIEWS_PATH . 'errors.php';
320
            if (file_exists($path)) {
321
                //compress the output if is available
322
                $type = null;
323
                if (self::$_canCompressOutput) {
324
                    $type = 'ob_gzhandler';
325
                }
326
                ob_start($type);
327
                extract($data);
328
                require_once $path;
329
                $output = ob_get_clean();
330
                self::sendHeaders(503);
331
                echo $output;
332
            } else {
333
                //can't use show_error() at this time because some dependencies not yet loaded and to prevent loop
334
                set_http_status_header(503);
335
                echo 'The error view [' . $path . '] does not exist';
336
            }
337
        }
338
339
         /**
340
         * Return the default full file path for view
341
         * @param  string $file    the filename
342
         * 
343
         * @return string|null          the full file path
344
         */
345
        protected static function getDefaultFilePathForView($file){
346
            $searchDir = array(APPS_VIEWS_PATH, CORE_VIEWS_PATH);
347
            $fullFilePath = null;
348
            foreach ($searchDir as $dir) {
349
                $filePath = $dir . $file;
350
                if (file_exists($filePath)) {
351
                    $fullFilePath = $filePath;
352
                    //is already found not to continue
353
                    break;
354
                }
355
            }
356
            return $fullFilePath;
357
        }
358
359
        /**
360
         * Send the cache not yet expire to browser
361
         * @param  array $cacheInfo the cache information
362
         * @return boolean            true if the information is sent otherwise false
363
         */
364
        protected function sendCacheNotYetExpireInfoToBrowser($cacheInfo) {
365
            if (!empty($cacheInfo)) {
366
                $logger = self::getLogger();
367
                $lastModified = $cacheInfo['mtime'];
368
                $expire = $cacheInfo['expire'];
369
                $maxAge = $expire - $_SERVER['REQUEST_TIME'];
370
                self::setHeader('Pragma', 'public');
371
                self::setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
372
                self::setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
373
                self::setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
374
                if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $lastModified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
375
                    $logger->info('The cache page content is not yet expire for the URL [' . $this->_currentUrl . '] send 304 header to browser');
376
                    self::sendHeaders(304);
377
                    return true;
378
                }
379
            }
380
            return false;
381
        }
382
383
        /**
384
         * Set the value of '{elapsed_time}' and '{memory_usage}'
385
         * @param  string $content the page content
386
         * @return string          the page content after replace 
387
         * '{elapsed_time}', '{memory_usage}'
388
         */
389
        protected function replaceElapseTimeAndMemoryUsage($content) {
390
            //load benchmark class
391
            $benchmark = & class_loader('Benchmark');
392
			
393
            // Parse out the elapsed time and memory usage,
394
            // then swap the pseudo-variables with the data
395
            $elapsedTime = $benchmark->elapsedTime('APP_EXECUTION_START', 'APP_EXECUTION_END');
396
            $memoryUsage	= round($benchmark->memoryUsage('APP_EXECUTION_START', 'APP_EXECUTION_END') / 1024 / 1024, 6) . 'MB';
397
            return str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsedTime, $memoryUsage), $content);	
398
        }
399
400
        /**
401
         * Send the page content from cache to browser
402
         * @param object $cache the cache instance
403
         * @return boolean     the status of the operation
404
         */
405
        protected function sendCachePageContentToBrowser(&$cache) {
406
            $logger = self::getLogger();
407
            $logger->info('The cache page content is expired or the browser does not send the HTTP_IF_MODIFIED_SINCE header for the URL [' . $this->_currentUrl . '] send cache headers to tell the browser');
408
            self::sendHeaders(200);
409
            //current page cache key
410
            $pageCacheKey = $this->_currentUrlCacheKey;
411
            //get the cache content
412
            $content = $cache->get($pageCacheKey);
413
            if ($content) {
414
                $logger->info('The page content for the URL [' . $this->_currentUrl . '] already cached just display it');
415
                $content = $this->replaceElapseTimeAndMemoryUsage($content);
416
                ///display the final output
417
                //compress the output if is available
418
                $type = null;
419
                if (self::$_canCompressOutput) {
420
                    $type = 'ob_gzhandler';
421
                }
422
                ob_start($type);
423
                echo $content;
424
                ob_end_flush();
425
                return true;
426
            }
427
            $logger->info('The page cache content for the URL [' . $this->_currentUrl . '] is not valid may be already expired');
428
            $cache->delete($pageCacheKey);
429
            return false;
430
        }
431
432
        /**
433
         * Save the content of page into cache
434
         * @param  string $content the page content to be saved
435
         * @return void
436
         */
437
        protected function savePageContentIntoCache($content) {
438
            $obj = & get_instance();
439
            $logger = self::getLogger();
440
441
            //current page URL
442
            $url = $this->_currentUrl;
443
            //Cache view Time to live in second
444
            $viewCacheTtl = get_config('cache_ttl');
445
            if (!empty($obj->view_cache_ttl)) {
446
                $viewCacheTtl = $obj->view_cache_ttl;
447
            }
448
            //the cache handler instance
449
            $cacheInstance = $obj->cache;
450
            //the current page cache key for identification
451
            $cacheKey = $this->_currentUrlCacheKey;
452
            $logger->debug('Save the page content for URL [' . $url . '] into the cache ...');
453
            $cacheInstance->set($cacheKey, $content, $viewCacheTtl);
454
			
455
            //get the cache information to prepare header to send to browser
456
            $cacheInfo = $cacheInstance->getInfo($cacheKey);
457
            if ($cacheInfo) {
458
                $lastModified = $cacheInfo['mtime'];
459
                $expire = $cacheInfo['expire'];
460
                $maxAge = $expire - time();
461
                self::setHeader('Pragma', 'public');
462
                self::setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
463
                self::setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
464
                self::setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');	
465
            }
466
        }
467
		
468
469
        /**
470
         * Get the module information for the view to load
471
         * @param  string $view the view name like moduleName/viewName, viewName
472
         * 
473
         * @return array        the module information
474
         * array(
475
         * 	'module'=> 'module_name'
476
         * 	'view' => 'view_name'
477
         * 	'viewFile' => 'view_file'
478
         * )
479
         */
480
        protected  function getModuleInfoForView($view) {
481
            $module = null;
482
            $viewFile = null;
483
            $obj = & get_instance();
484
            //check if the request class contains module name
485
            $viewPath = explode('/', $view);
486
            if (count($viewPath) >= 2 && isset($viewPath[0]) && in_array($viewPath[0], Module::getModuleList())) {
487
                $module = $viewPath[0];
488
                array_shift($viewPath);
489
                $view = implode('/', $viewPath);
490
                $viewFile = $view . '.php';
491
            }
492
            if (!$module && !empty($obj->moduleName)) {
493
                $module = $obj->moduleName;
494
            }
495
            return array(
496
                        'view' => $view,
497
                        'module' => $module,
498
                        'viewFile' => $viewFile
499
                    );
500
        }
501
502
        /**
503
         * Render the view page
504
         * @see  Response::render
505
         * @return void|string
506
         */
507
        protected  function loadView($path, array $data = array(), $return = false) {
508
            $found = false;
509
            if (file_exists($path)) {
510
                //super instance
511
                $obj = & get_instance();
512
                foreach (get_object_vars($obj) as $key => $value) {
513
                    if (!isset($this->{$key})) {
514
                        $this->{$key} = & $obj->{$key};
515
                    }
516
                }
517
                ob_start();
518
                extract($data);
519
                //need use require() instead of require_once because can load this view many time
520
                require $path;
521
                $content = ob_get_clean();
522
                if ($return) {
523
                    return $content;
524
                }
525
                $this->_pageRender .= $content;
526
                $found = true;
527
            }
528
            if (!$found) {
529
                show_error('Unable to find view [' . $path . ']');
530
            }
531
        }
532
533
         /**
534
         * Set the mandory headers, like security, etc.
535
         */
536
        protected static function setRequiredHeaders() {
537
            $requiredHeaders = array(
538
                                'X-XSS-Protection' => '1; mode=block',
539
                                'X-Frame-Options'  => 'SAMEORIGIN'
540
                            );
541
            foreach ($requiredHeaders as $key => $value) {
542
               if (!isset(self::$headers[$key])) {
543
                    self::$headers[$key] = $value;
544
               } 
545
            }
546
        }
547
    }
548