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