Passed
Push — 1.0.0-dev ( 896dae...8851fa )
by nguereza
02:32
created

Response::render()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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