Passed
Push — 1.0.0-dev ( a1ce08...4f1d46 )
by nguereza
10:50
created

Response::setFinalPageContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
            //@codeCoverageIgnoreStart
92
            //not available when running in CLI mode
93
            if (!headers_sent()) {
94
                foreach (self::getHeaders() as $key => $value) {
95
                    header($key . ': ' . $value);
96
                }
97
            }
98
            //@codeCoverageIgnoreEnd
99
        }
100
101
        /**
102
         * Get the list of the headers
103
         * @return array the headers list
104
         */
105
        public static function getHeaders() {
106
            return self::$headers;
107
        }
108
109
        /**
110
         * Get the header value for the given name
111
         * @param  string $name the header name
112
         * @return string|null       the header value
113
         */
114
        public static function getHeader($name) {
115
            if (array_key_exists($name, self::$headers)) {
116
                return self::$headers[$name];
117
            }
118
            return null;
119
        }
120
121
122
        /**
123
         * Set the header value for the specified name
124
         * @param string $name  the header name
125
         * @param string $value the header value to be set
126
         */
127
        public static function setHeader($name, $value) {
128
            self::$headers[$name] = $value;
129
        }
130
131
        /**
132
         * Set the headers using array
133
         * @param array $headers the list of the headers to set. 
134
         * Note: this will merge with the existing headers
135
         */
136
        public static function setHeaders(array $headers) {
137
            self::$headers = array_merge(self::getHeaders(), $headers);
138
        }
139
		
140
        /**
141
         * Redirect user to the specified page
142
         * @param  string $path the URL or URI to be redirect to
143
         * @codeCoverageIgnore
144
         */
145
        public static function redirect($path = '') {
146
            $logger = self::getLogger();
147
            $url = Url::site_url($path);
148
            $logger->info('Redirect to URL [' . $url . ']');
149
            if (!headers_sent()) {
150
                header('Location: ' . $url);
151
                exit;
152
            }
153
            echo '<script>
154
					location.href = "'.$url . '";
155
				</script>';
156
        }
157
158
        /**
159
         * Render the view to display later or return the content
160
         * @param  string  $view   the view name or path
161
         * @param  array|object   $data   the variable data to use in the view
162
         * @param  boolean $return whether to return the view generated content or display it directly
163
         * @return void|string          if $return is true will return the view content otherwise
164
         * will display the view content.
165
         */
166
        public function render($view, $data = null, $return = false) {
167
            $logger = self::getLogger();
168
            //convert data to an array
169
            $data = (array) $data;
170
            $view = str_ireplace('.php', '', $view);
171
            $view = trim($view, '/\\');
172
            $viewFile = $view . '.php';
173
            $path = null;
174
			
175
            //check in module first
176
            $logger->debug('Checking the view [' . $view . '] from module list ...');
177
            $moduleInfo = $this->getModuleInfoForView($view);
178
            $module = $moduleInfo['module'];
179
            $view = $moduleInfo['view'];
180
			
181
            $moduleViewPath = Module::findViewFullPath($view, $module);
182
            if ($moduleViewPath) {
183
                $path = $moduleViewPath;
184
                $logger->info('Found view [' . $view . '] in module [' . $module . '], the file path is [' . $moduleViewPath . '] we will used it');
185
            } else {
186
                $logger->info('Cannot find view [' . $view . '] in module [' . $module . '] using the default location');
187
            }
188
			if (!$path) {
189
                $path = $this->getDefaultFilePathForView($viewFile);
190
            }
191
            $logger->info('The view file path to be loaded is [' . $path . ']');
192
			
193
            if ($return) {
194
                return $this->loadView($path, $data, true);
195
            }
196
            $this->loadView($path, $data, false);
197
        }
198
199
		
200
        /**
201
         * Send the final page output to user
202
         */
203
        public function renderFinalPage() {
204
            $logger = self::getLogger();
205
            $obj = & get_instance();
206
            $cachePageStatus = get_config('cache_enable', false) && !empty($obj->view_cache_enable);
207
            $dispatcher = $obj->eventdispatcher;
208
            $content = $this->_pageRender;
209
            if (!$content) {
210
                $logger->warning('The final view content is empty.');
211
                return;
212
            }
213
            //dispatch
214
            $event = $dispatcher->dispatch(new EventInfo('FINAL_VIEW_READY', $content, true));
215
            $content = null;
216
            if (!empty($event->payload)) {
217
                $content = $event->payload;
218
            }
219
            if (empty($content)) {
220
                $logger->warning('The view content is empty after dispatch to event listeners.');
221
            }
222
            //check whether need save the page into cache.
223
            if ($cachePageStatus) {
224
                $this->savePageContentIntoCache($content);
225
            }
226
            //update content
227
            $this->_pageRender = $content;
228
229
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
230
231
            //compress the output if is available
232
            $type = null;
233
            if (self::$_canCompressOutput) {
234
                $type = 'ob_gzhandler';
235
            }
236
            ob_start($type);
237
            self::sendHeaders(200);
238
            echo $content;
239
            ob_end_flush();
240
        }
241
242
		
243
        /**
244
         * Send the final page output to user if is cached
245
         * @param object $cache the cache instance
246
         *
247
         * @return boolean whether the page content if available or not
248
         */
249
        public function renderFinalPageFromCache(&$cache) {
250
            $logger = self::getLogger();
251
            //the current page cache key for identification
252
            $pageCacheKey = $this->_currentUrlCacheKey;
253
			
254
            $logger->debug('Checking if the page content for the URL [' . $this->_currentUrl . '] is cached ...');
255
            //get the cache information to prepare header to send to browser
256
            $cacheInfo = $cache->getInfo($pageCacheKey);
257
            if ($cacheInfo) {
258
                $status = $this->sendCacheNotYetExpireInfoToBrowser($cacheInfo);
259
                if ($status === false) {
260
                    return $this->sendCachePageContentToBrowser($cache);
261
                }
262
                return true;
263
            }
264
            return false;
265
        }
266
	
267
		
268
        /**
269
         * Get the final page to be rendered
270
         * @return string
271
         */
272
        public function getFinalPageRendered() {
273
            return $this->_pageRender;
274
        }
275
276
         /**
277
         * Set the final page to be rendered
278
         * @param string $finalPage the content of the final page
279
         * 
280
         * @return object
281
         */
282
        public function setFinalPageContent($finalPage) {
283
            $this->_pageRender = $finalPage;
284
            return $this;
285
        }
286
287
        /**
288
         * Send the HTTP 404 error if can not found the 
289
         * routing information for the current request
290
         */
291
        public function send404() {
292
            $logger = self::getLogger();
293
            $obj = & get_instance();
294
            $cachePageStatus = get_config('cache_enable', false) && !empty($obj->view_cache_enable);
295
            $dispatcher = $obj->eventdispatcher;
296
            $content = $this->_pageRender;
297
            if (!$content) {
298
                $logger->warning('The final view content is empty.');
299
                return;
300
            }
301
            //dispatch
302
            $dispatcher->dispatch(new EventInfo('PAGE_NOT_FOUND'));
303
            //check whether need save the page into cache.
304
            if ($cachePageStatus) {
305
                $this->savePageContentIntoCache($content);
306
            }
307
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
308
309
            /**************************************** save the content into logs **************/
310
            $bwsr = & class_loader('Browser');
311
            $browser = $bwsr->getPlatform() . ', ' . $bwsr->getBrowser() . ' ' . $bwsr->getVersion();
312
            $obj->loader->functions('user_agent');
313
            $str = '[404 page not found] : ';
314
            $str .= ' Unable to find the request page [' . $obj->request->requestUri() . ']. The visitor IP address [' . get_ip() . '], browser [' . $browser . ']';
315
            
316
            //Todo fix the issue the logger name change after load new class
317
            $logger = self::getLogger();
318
            $logger->error($str);
319
            /**********************************************************************/
320
321
            //compress the output if is available
322
            $type = null;
323
            if (self::$_canCompressOutput) {
324
                $type = 'ob_gzhandler';
325
            }
326
            ob_start($type);
327
            self::sendHeaders(404);
328
            echo $content;
329
            ob_end_flush();
330
        }
331
332
        /**
333
         * Display the error to user
334
         */
335
        public function sendError() {
336
            $logger = self::getLogger();
337
            $content = $this->_pageRender;
338
            if (!$content) {
339
                $logger->warning('The final view content is empty.');
340
                return;
341
            }
342
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
343
            //compress the output if is available
344
            $type = null;
345
            if (self::$_canCompressOutput) {
346
                $type = 'ob_gzhandler';
347
            }
348
            ob_start($type);
349
            self::sendHeaders(503);
350
            echo $content;
351
            ob_end_flush();
352
        }
353
354
         /**
355
         * Return the default full file path for view
356
         * @param  string $file    the filename
357
         * 
358
         * @return string|null          the full file path
359
         */
360
        protected static function getDefaultFilePathForView($file){
361
            $searchDir = array(APPS_VIEWS_PATH, CORE_VIEWS_PATH);
362
            $fullFilePath = null;
363
            foreach ($searchDir as $dir) {
364
                $filePath = $dir . $file;
365
                if (file_exists($filePath)) {
366
                    $fullFilePath = $filePath;
367
                    //is already found not to continue
368
                    break;
369
                }
370
            }
371
            return $fullFilePath;
372
        }
373
374
        /**
375
         * Send the cache not yet expire to browser
376
         * @param  array $cacheInfo the cache information
377
         * @return boolean            true if the information is sent otherwise false
378
         */
379
        protected function sendCacheNotYetExpireInfoToBrowser($cacheInfo) {
380
            if (!empty($cacheInfo)) {
381
                $logger = self::getLogger();
382
                $lastModified = $cacheInfo['mtime'];
383
                $expire = $cacheInfo['expire'];
384
                $maxAge = $expire - $_SERVER['REQUEST_TIME'];
385
                self::setHeader('Pragma', 'public');
386
                self::setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
387
                self::setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
388
                self::setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
389
                if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $lastModified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
390
                    $logger->info('The cache page content is not yet expire for the URL [' . $this->_currentUrl . '] send 304 header to browser');
391
                    self::sendHeaders(304);
392
                    return true;
393
                }
394
            }
395
            return false;
396
        }
397
398
        /**
399
         * Send the page content from cache to browser
400
         * @param object $cache the cache instance
401
         * @return boolean     the status of the operation
402
         */
403
        protected function sendCachePageContentToBrowser(&$cache) {
404
            $logger = self::getLogger();
405
            $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');
406
            self::sendHeaders(200);
407
            //current page cache key
408
            $pageCacheKey = $this->_currentUrlCacheKey;
409
            //get the cache content
410
            $content = $cache->get($pageCacheKey);
411
            if ($content) {
412
                $logger->info('The page content for the URL [' . $this->_currentUrl . '] already cached just display it');
413
                $content = $this->replaceElapseTimeAndMemoryUsage($content);
414
                ///display the final output
415
                //compress the output if is available
416
                $type = null;
417
                if (self::$_canCompressOutput) {
418
                    $type = 'ob_gzhandler';
419
                }
420
                ob_start($type);
421
                echo $content;
422
                ob_end_flush();
423
                return true;
424
            }
425
            $logger->info('The page cache content for the URL [' . $this->_currentUrl . '] is not valid may be already expired');
426
            $cache->delete($pageCacheKey);
427
            return false;
428
        }
429
430
        /**
431
         * Save the content of page into cache
432
         * @param  string $content the page content to be saved
433
         * @return void
434
         */
435
        protected function savePageContentIntoCache($content) {
436
            $obj = & get_instance();
437
            $logger = self::getLogger();
438
439
            //current page URL
440
            $url = $this->_currentUrl;
441
            //Cache view Time to live in second
442
            $viewCacheTtl = get_config('cache_ttl');
443
            if (!empty($obj->view_cache_ttl)) {
444
                $viewCacheTtl = $obj->view_cache_ttl;
445
            }
446
            //the cache handler instance
447
            $cacheInstance = $obj->cache;
448
            //the current page cache key for identification
449
            $cacheKey = $this->_currentUrlCacheKey;
450
            $logger->debug('Save the page content for URL [' . $url . '] into the cache ...');
451
            $cacheInstance->set($cacheKey, $content, $viewCacheTtl);
452
			
453
            //get the cache information to prepare header to send to browser
454
            $cacheInfo = $cacheInstance->getInfo($cacheKey);
455
            if ($cacheInfo) {
456
                $lastModified = $cacheInfo['mtime'];
457
                $expire = $cacheInfo['expire'];
458
                $maxAge = $expire - time();
459
                self::setHeader('Pragma', 'public');
460
                self::setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
461
                self::setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
462
                self::setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');	
463
            }
464
        }
465
466
        /**
467
         * Set the value of '{elapsed_time}' and '{memory_usage}'
468
         * @param  string $content the page content
469
         * @return string          the page content after replace 
470
         * '{elapsed_time}', '{memory_usage}'
471
         */
472
        protected function replaceElapseTimeAndMemoryUsage($content) {
473
            //load benchmark class
474
            $benchmark = & class_loader('Benchmark');
475
            
476
            // Parse out the elapsed time and memory usage,
477
            // then swap the pseudo-variables with the data
478
            $elapsedTime = $benchmark->elapsedTime('APP_EXECUTION_START', 'APP_EXECUTION_END');
479
            $memoryUsage    = round($benchmark->memoryUsage('APP_EXECUTION_START', 'APP_EXECUTION_END') / 1024 / 1024, 6) . 'MB';
480
            return str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsedTime, $memoryUsage), $content); 
481
        }
482
483
		
484
485
        /**
486
         * Get the module information for the view to load
487
         * @param  string $view the view name like moduleName/viewName, viewName
488
         * 
489
         * @return array        the module information
490
         * array(
491
         * 	'module'=> 'module_name'
492
         * 	'view' => 'view_name'
493
         * 	'viewFile' => 'view_file'
494
         * )
495
         */
496
        protected  function getModuleInfoForView($view) {
497
            $module = null;
498
            $viewFile = null;
499
            $obj = & get_instance();
500
            //check if the request class contains module name
501
            $viewPath = explode('/', $view);
502
            if (count($viewPath) >= 2 && isset($viewPath[0]) && in_array($viewPath[0], Module::getModuleList())) {
503
                $module = $viewPath[0];
504
                array_shift($viewPath);
505
                $view = implode('/', $viewPath);
506
                $viewFile = $view . '.php';
507
            }
508
            if (!$module && !empty($obj->moduleName)) {
509
                $module = $obj->moduleName;
510
            }
511
            return array(
512
                        'view' => $view,
513
                        'module' => $module,
514
                        'viewFile' => $viewFile
515
                    );
516
        }
517
518
        /**
519
         * Render the view page
520
         * @see  Response::render
521
         * @return void|string
522
         */
523
        protected  function loadView($path, array $data = array(), $return = false) {
524
            $found = false;
525
            if (file_exists($path)) {
526
                //super instance
527
                $obj = & get_instance();
528
                if ($obj instanceof Controller) {
529
                    foreach (get_object_vars($obj) as $key => $value) {
530
                        if (!isset($this->{$key})) {
531
                            $this->{$key} = & $obj->{$key};
532
                        }
533
                    }
534
                }
535
                ob_start();
536
                extract($data);
537
                //need use require() instead of require_once because can load this view many time
538
                require $path;
539
                $content = ob_get_clean();
540
                if ($return) {
541
                    return $content;
542
                }
543
                $this->_pageRender .= $content;
544
                $found = true;
545
            }
546
            if (!$found) {
547
                show_error('Unable to find view [' . $path . ']');
548
            }
549
        }
550
551
         /**
552
         * Set the mandory headers, like security, etc.
553
         */
554
        protected static function setRequiredHeaders() {
555
            $requiredHeaders = array(
556
                                'X-XSS-Protection' => '1; mode=block',
557
                                'X-Frame-Options'  => 'SAMEORIGIN'
558
                            );
559
            foreach ($requiredHeaders as $key => $value) {
560
               if (!isset(self::$headers[$key])) {
561
                    self::$headers[$key] = $value;
562
               } 
563
            }
564
        }
565
    }
566