Passed
Push — 1.0.0-dev ( e3bc99...72449d )
by nguereza
03:01
created

Response::renderFinalPage()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 17
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 26
rs 9.7
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 MIT License (MIT)
9
     *
10
     * Copyright (c) 2017 TNH Framework
11
     *
12
     * Permission is hereby granted, free of charge, to any person obtaining a copy
13
     * of this software and associated documentation files (the "Software"), to deal
14
     * in the Software without restriction, including without limitation the rights
15
     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
     * copies of the Software, and to permit persons to whom the Software is
17
     * furnished to do so, subject to the following conditions:
18
     *
19
     * The above copyright notice and this permission notice shall be included in all
20
     * copies or substantial portions of the Software.
21
     *
22
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
     * SOFTWARE.
29
     */
30
31
    class Response extends BaseClass {
32
33
        /**
34
         * The list of request header to send with response
35
         * @var array
36
         */
37
        private $headers = array();
38
		
39
        /**
40
         * The final page content to display to user
41
         * @var string
42
         */
43
        private $finalPageContent = null;
44
		
45
        /**
46
         * The current request URL
47
         * @var string
48
         */
49
        private $currentUrl = null;
50
		
51
        /**
52
         * The current request URL cache key
53
         * @var string
54
         */
55
        private $currentUrlCacheKey = null;
56
		
57
        /**
58
         * Whether we can compress the output using Gzip
59
         * @var boolean
60
         */
61
        private $canCompressOutput = false;
62
		
63
        /**
64
         * Construct new instance
65
         */
66
        public function __construct() {
67
            parent::__construct();
68
            $globals = & class_loader('GlobalVar', 'classes');
69
            $currentUrl = '';
70
            if ($globals->server('REQUEST_URI')) {
71
                $currentUrl = $globals->server('REQUEST_URI');
72
            }
73
            if ($globals->server('QUERY_STRING')) {
74
                $currentUrl .= '?' . $globals->server('QUERY_STRING');
75
            }
76
            $this->currentUrl = $currentUrl;		
77
            $this->currentUrlCacheKey = md5($this->currentUrl);
78
			
79
            $this->canCompressOutput = get_config('compress_output')
80
                                          && $globals->server('HTTP_ACCEPT_ENCODING') !== null 
81
                                          && stripos($globals->server('HTTP_ACCEPT_ENCODING'), 'gzip') !== false 
82
                                          && extension_loaded('zlib')
83
                                          && (bool) ini_get('zlib.output_compression') === false;
84
        }
85
86
		
87
        /**
88
         * Send the HTTP Response headers
89
         * @param  integer $httpCode the HTTP status code
90
         * @param  array   $headers   the additional headers to add to the existing headers list
91
         */
92
        public function sendHeaders($httpCode = 200, array $headers = array()) {
93
            set_http_status_header($httpCode);
94
            $this->setHeaders($headers);
95
            $this->setRequiredHeaders();
96
            //@codeCoverageIgnoreStart
97
            //not available when running in CLI mode
98
            if (!headers_sent()) {
99
                foreach ($this->getHeaders() as $key => $value) {
100
                    header($key . ': ' . $value);
101
                }
102
            }
103
            //@codeCoverageIgnoreEnd
104
        }
105
106
        /**
107
         * Get the list of the headers
108
         * @return array the headers list
109
         */
110
        public function getHeaders() {
111
            return $this->headers;
112
        }
113
114
        /**
115
         * Get the header value for the given name
116
         * @param  string $name the header name
117
         * @return string|null       the header value
118
         */
119
        public function getHeader($name) {
120
            if (array_key_exists($name, $this->headers)) {
121
                return $this->headers[$name];
122
            }
123
            return null;
124
        }
125
126
127
        /**
128
         * Set the header value for the specified name
129
         * @param string $name  the header name
130
         * @param string $value the header value to be set
131
         */
132
        public function setHeader($name, $value) {
133
            $this->headers[$name] = $value;
134
        }
135
136
        /**
137
         * Set the headers using array
138
         * @param array $headers the list of the headers to set. 
139
         * Note: this will merge with the existing headers
140
         */
141
        public function setHeaders(array $headers) {
142
            $this->headers = array_merge($this->headers, $headers);
143
        }
144
		
145
        /**
146
         * Redirect user to the specified page
147
         * @param  string $path the URL or URI to be redirect to
148
         * @codeCoverageIgnore
149
         */
150
        public function redirect($path = '') {
151
            $url = get_instance()->url->appUrl($path);
152
            if (!headers_sent()) {
153
                header('Location: ' . $url);
154
                exit;
155
            }
156
            echo '<script>
157
					location.href = "'.$url . '";
158
				</script>';
159
        }
160
161
        /**
162
         * Render the view to display later or return the content
163
         * 
164
         * @param  string  $view   the view name or path
165
         * @param  array|object   $data   the variable data to use in the view
166
         * @param  boolean $return whether to return the view generated content or display it directly
167
         * 
168
         * @return void|string          if $return is true will return the view content otherwise
169
         * will display the view content.
170
         */
171
        public function render($view, $data = null, $return = false) {
172
            //try to convert data to an array if is object or other thing
173
            $data = (array) $data;
174
            $view = str_ireplace('.php', '', $view);
175
            $view = trim($view, '/\\');
176
            $viewFile = $view . '.php';
177
            $path = null;
178
			
179
            //check in module first
180
            $this->logger->debug('Checking the view [' . $view . '] from module list ...');
181
            $moduleInfo = $this->getModuleInfoForView($view);
182
            $module = $moduleInfo['module'];
183
            $view = $moduleInfo['view'];
184
            $moduleViewPath = get_instance()->module->findViewFullPath($view, $module);
185
            if ($moduleViewPath) {
186
                $path = $moduleViewPath;
187
                $this->logger->info('Found view [' . $view . '] in module [' . $module . '], the file path is [' . $moduleViewPath . '] we will used it');
188
            } else {
189
                $this->logger->info('Cannot find view [' . $view . '] in module [' . $module . '] using the default location');
190
            }
191
			if (!$path) {
192
                $path = $this->getDefaultFilePathForView($viewFile);
193
            }
194
            $this->logger->info('The view file path to be loaded is [' . $path . ']');
195
			
196
            if ($return) {
197
                return $this->loadView($path, $data, true);
198
            }
199
            $this->loadView($path, $data, false);
200
        }
201
202
        /**
203
         * Send the final page output
204
         */
205
        public function renderFinalPage() {
206
            $content = $this->finalPageContent;
207
            if (!$content) {
208
                $this->logger->warning('The final view content is empty.');
209
                return;
210
            }
211
            $obj = & get_instance();
212
            $cachePageStatus = get_instance()->config->get('cache_enable', false) 
213
                               && !empty($obj->view_cache_enable);
214
            
215
            $content = $this->dispatchFinalViewEvent();
216
            
217
            //check whether need save the page into cache.
218
            if ($cachePageStatus) {
219
                $this->savePageContentIntoCache($content);
220
            }
221
            //update final page content
222
            $this->finalPageContent = $content;
223
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
224
225
            //compress the output if is available
226
            $compressOutputHandler = $this->getCompressOutputHandler();
227
            ob_start($compressOutputHandler);
228
            $this->sendHeaders(200);
229
            echo $content;
230
            ob_end_flush();
231
        }
232
233
        /**
234
         * Dispatch the FINAL_VIEW_READY event
235
         *             
236
         * @return string|null the final view content after processing by each listener
237
         * if they exists otherwise the same content will be returned
238
         */
239
        protected function dispatchFinalViewEvent() {
240
            //dispatch
241
            $event = get_instance()->eventdispatcher->dispatch(
242
                                                                new EventInfo(
243
                                                                                'FINAL_VIEW_READY', 
244
                                                                                $this->finalPageContent, 
245
                                                                                true
246
                                                                            )
247
                                                            );
248
            $content = null;
249
            if (!empty($event->payload)) {
250
                $content = $event->payload;
251
            }
252
            if (empty($content)) {
253
                $this->logger->warning('The view content is empty after dispatch to event listeners.');
254
            }
255
            return $content;
256
        }
257
258
		
259
        /**
260
         * Send the final page output to user if is cached
261
         * @param object $cache the cache instance
262
         *
263
         * @return boolean whether the page content if available or not
264
         */
265
        public function renderFinalPageFromCache(&$cache) {
266
            //the current page cache key for identification
267
            $pageCacheKey = $this->currentUrlCacheKey;
268
			
269
            $this->logger->debug('Checking if the page content for the URL [' . $this->currentUrl . '] is cached ...');
270
            //get the cache information to prepare header to send to browser
271
            $cacheInfo = $cache->getInfo($pageCacheKey);
272
            if ($cacheInfo) {
273
                $status = $this->sendCacheNotYetExpireInfoToBrowser($cacheInfo);
274
                if ($status === false) {
0 ignored issues
show
introduced by
The condition $status === false is always true.
Loading history...
275
                    return $this->sendCachePageContentToBrowser($cache);
276
                }
277
                return true;
278
            }
279
            return false;
280
        }
281
	
282
		
283
        /**
284
         * Get the final page to be rendered
285
         * @return string
286
         */
287
        public function getFinalPageRendered() {
288
            return $this->finalPageContent;
289
        }
290
291
         /**
292
         * Set the final page to be rendered
293
         * @param string $finalPage the content of the final page
294
         * 
295
         * @return object
296
         */
297
        public function setFinalPageContent($finalPage) {
298
            $this->finalPageContent = $finalPage;
299
            return $this;
300
        }
301
302
        /**
303
         * Send the HTTP 404 error if can not found the 
304
         * routing information for the current request
305
         */
306
        public function send404() {
307
            $content = $this->finalPageContent;
308
            if (!$content) {
309
                $this->logger->warning('The final view content is empty.');
310
                return;
311
            }
312
            $obj = & get_instance();
313
            $cachePageStatus = get_instance()->config->get('cache_enable', false) 
314
                                && !empty($obj->view_cache_enable);
315
            //dispatch
316
            get_instance()->eventdispatcher->dispatch(new EventInfo('PAGE_NOT_FOUND'));
317
            //check whether need save the page into cache.
318
            if ($cachePageStatus) {
319
                $this->savePageContentIntoCache($content);
320
            }
321
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
322
323
            /**************************************** save the content into logs **************/
324
            $userAgent = & class_loader('Browser');
325
            $browser = $userAgent->getPlatform() . ', ' . $userAgent->getBrowser() . ' ' . $userAgent->getVersion();
326
            $obj->loader->functions('user_agent');
327
            $str = '[404 page not found] : ';
328
            $str .= ' Unable to find the request page [' . $obj->request->requestUri() . '].'
329
                    .' The visitor IP address [' . get_ip() . '], browser [' . $browser . ']';
330
            $this->logger->error($str);
331
            /**********************************************************************/
332
            
333
            //compress the output if is available
334
            $compressOutputHandler = $this->getCompressOutputHandler();
335
            ob_start($compressOutputHandler);
336
            $this->sendHeaders(404);
337
            echo $content;
338
            ob_end_flush();
339
        }
340
341
        /**
342
         * Display the error to user
343
         *
344
         * @param  array  $data the error information
345
         */
346
        public function sendError(array $data = array()) {
347
            $path = CORE_VIEWS_PATH . 'errors.php';
348
            if(file_exists($path)){
349
                //compress the output if is available
350
                $compressOutputHandler = $this->getCompressOutputHandler();
351
                ob_start($compressOutputHandler);
352
                extract($data);
353
                require $path;
354
                $content = ob_get_clean();
355
                $this->finalPageContent = $content;
356
                $this->sendHeaders(503);
357
                echo $content;
358
            }
359
            //@codeCoverageIgnoreStart
360
            else{
361
                //can't use show_error() at this time because 
362
                //some dependencies not yet loaded
363
                set_http_status_header(503);
364
                echo 'The error view [' . $path . '] does not exist';
365
            }
366
            //@codeCoverageIgnoreEnd
367
        }
368
369
        /**
370
         * Get the compress output handler is can compress the page content
371
         * before send
372
         * @return null|string the name of function to handler compression
373
         */
374
        protected function getCompressOutputHandler() {
375
            $handler = null;
376
            if ($this->canCompressOutput) {
377
                $handler = 'ob_gzhandler';
378
            }
379
            return $handler;
380
        }
381
382
383
         /**
384
         * Return the default full file path for view
385
         * @param  string $file    the filename
386
         * 
387
         * @return string|null          the full file path
388
         */
389
        protected function getDefaultFilePathForView($file){
390
            $searchDir = array(APPS_VIEWS_PATH, CORE_VIEWS_PATH);
391
            $fullFilePath = null;
392
            foreach ($searchDir as $dir) {
393
                $filePath = $dir . $file;
394
                if (file_exists($filePath)) {
395
                    $fullFilePath = $filePath;
396
                    //is already found not to continue
397
                    break;
398
                }
399
            }
400
            return $fullFilePath;
401
        }
402
403
        /**
404
         * Send the cache not yet expire to browser
405
         * @param  array $cacheInfo the cache information
406
         * @return boolean            true if the information is sent otherwise false
407
         */
408
        protected function sendCacheNotYetExpireInfoToBrowser($cacheInfo) {
409
            if (!empty($cacheInfo)) {
410
                $lastModified = $cacheInfo['mtime'];
411
                $expire = $cacheInfo['expire'];
412
                $globals = & class_loader('GlobalVar', 'classes');
413
                $maxAge = $expire - (double) $globals->server('REQUEST_TIME');
414
                $this->setHeader('Pragma', 'public');
415
                $this->setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
416
                $this->setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
417
                $this->setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
418
                $headerModifiedSince = $globals->server('HTTP_IF_MODIFIED_SINCE');
419
                if (!empty($headerModifiedSince) && $lastModified <= strtotime($headerModifiedSince)) {
420
                    $this->logger->info('The cache page content is not yet expire for the '
421
                                         . 'URL [' . $this->currentUrl . '] send 304 header to browser');
422
                    $this->sendHeaders(304);
423
                    return true;
424
                }
425
            }
426
            return false;
427
        }
428
429
        /**
430
         * Send the page content from cache to browser
431
         * @param object $cache the cache instance
432
         * @return boolean     the status of the operation
433
         */
434
        protected function sendCachePageContentToBrowser(&$cache) {
435
            $this->logger->info('The cache page content is expired or the browser does '
436
                 . 'not send the HTTP_IF_MODIFIED_SINCE header for the URL [' . $this->currentUrl . '] '
437
                 . 'send cache headers to tell the browser');
438
            $this->sendHeaders(200);
439
            //current page cache key
440
            $pageCacheKey = $this->currentUrlCacheKey;
441
            //get the cache content
442
            $content = $cache->get($pageCacheKey);
443
            if ($content) {
444
                $this->logger->info('The page content for the URL [' . $this->currentUrl . '] already cached just display it');
445
                $content = $this->replaceElapseTimeAndMemoryUsage($content);
446
                ///display the final output
447
                //compress the output if is available
448
                $compressOutputHandler = $this->getCompressOutputHandler();
449
                ob_start($compressOutputHandler);
450
                echo $content;
451
                ob_end_flush();
452
                return true;
453
            }
454
            $this->logger->info('The page cache content for the URL [' . $this->currentUrl . '] is not valid may be already expired');
455
            $cache->delete($pageCacheKey);
456
            return false;
457
        }
458
459
        /**
460
         * Save the content of page into cache
461
         * @param  string $content the page content to be saved
462
         * @return void
463
         */
464
        protected function savePageContentIntoCache($content) {
465
            $obj = & get_instance();
466
            //current page URL
467
            $url = $this->currentUrl;
468
            //Cache view Time to live in second
469
            $viewCacheTtl = get_instance()->config->get('cache_ttl');
470
            if (!empty($obj->view_cache_ttl)) {
471
                $viewCacheTtl = $obj->view_cache_ttl;
472
            }
473
            //the cache handler instance
474
            $cacheInstance = $obj->cache;
475
            //the current page cache key for identification
476
            $cacheKey = $this->currentUrlCacheKey;
477
            $this->logger->debug('Save the page content for URL [' . $url . '] into the cache ...');
478
            $cacheInstance->set($cacheKey, $content, $viewCacheTtl);
479
			
480
            //get the cache information to prepare header to send to browser
481
            $cacheInfo = $cacheInstance->getInfo($cacheKey);
482
            if ($cacheInfo) {
483
                $lastModified = $cacheInfo['mtime'];
484
                $expire = $cacheInfo['expire'];
485
                $maxAge = $expire - time();
486
                $this->setHeader('Pragma', 'public');
487
                $this->setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
488
                $this->setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
489
                $this->setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');	
490
            }
491
        }
492
493
        /**
494
         * Set the value of '{elapsed_time}' and '{memory_usage}'
495
         * @param  string $content the page content
496
         * @return string          the page content after replace 
497
         * '{elapsed_time}', '{memory_usage}'
498
         */
499
        protected function replaceElapseTimeAndMemoryUsage($content) {
500
            // Parse out the elapsed time and memory usage,
501
            // then swap the pseudo-variables with the data
502
            $elapsedTime = get_instance()->benchmark->elapsedTime('APP_EXECUTION_START', 'APP_EXECUTION_END');
503
            $memoryUsage = round(get_instance()->benchmark->memoryUsage(
504
                                                                        'APP_EXECUTION_START', 
505
                                                                        'APP_EXECUTION_END') / 1024 / 1024, 6) . 'MB';
506
            return str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsedTime, $memoryUsage), $content); 
507
        }
508
509
        /**
510
         * Get the module information for the view to load
511
         * 
512
         * @param  string $view the view name like moduleName/viewName, viewName
513
         * 
514
         * @return array        the module information
515
         * array(
516
         * 	'module'=> 'module_name'
517
         * 	'view' => 'view_name'
518
         * 	'viewFile' => 'view_file'
519
         * )
520
         */
521
        protected  function getModuleInfoForView($view) {
522
            $module = null;
523
            $viewFile = null;
524
            $obj = & get_instance();
525
            //check if the request class contains module name
526
            $viewPath = explode('/', $view);
527
            if (count($viewPath) >= 2 && in_array($viewPath[0], get_instance()->module->getModuleList())) {
528
                $module = $viewPath[0];
529
                array_shift($viewPath);
530
                $view = implode('/', $viewPath);
531
                $viewFile = $view . '.php';
532
            }
533
            if (!$module && !empty($obj->moduleName)) {
534
                $module = $obj->moduleName;
535
            }
536
            return array(
537
                        'view' => $view,
538
                        'module' => $module,
539
                        'viewFile' => $viewFile
540
                    );
541
        }
542
543
        /**
544
         * Render the view page
545
         * @see  Response::render
546
         * @return void|string
547
         */
548
        protected  function loadView($path, array $data = array(), $return = false) {
549
            $found = false;
550
            if (file_exists($path)) {
551
                //super instance
552
                $obj = & get_instance();
553
                if ($obj instanceof Controller) {
554
                    foreach (get_object_vars($obj) as $key => $value) {
555
                        if (!property_exists($this, $key)) {
556
                            $this->{$key} = & $obj->{$key};
557
                        }
558
                    }
559
                }
560
                ob_start();
561
                extract($data);
562
                //need use require() instead of require_once because can load this view many time
563
                require $path;
564
                $content = ob_get_clean();
565
                if ($return) {
566
                    return $content;
567
                }
568
                $this->finalPageContent .= $content;
569
                $found = true;
570
            }
571
            if (!$found) {
572
                show_error('Unable to find view [' . $path . ']');
573
            }
574
        }
575
576
         /**
577
         * Set the mandory headers, like security, etc.
578
         */
579
        protected function setRequiredHeaders() {
580
            $requiredHeaders = array(
581
                                'X-XSS-Protection' => '1; mode=block',
582
                                'X-Frame-Options'  => 'SAMEORIGIN'
583
                            );
584
            foreach ($requiredHeaders as $key => $value) {
585
               if (!isset($this->headers[$key])) {
586
                    $this->headers[$key] = $value;
587
               } 
588
            }
589
        }
590
    }
591