Response::getCompressOutputHandler()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 6
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 HTTP status code
35
         * @var  integer
36
         */
37
        private $status = 200;
38
39
        /**
40
         * The list of request headers to send with response
41
         * @var array
42
         */
43
        private $headers = array();
44
		
45
        /**
46
         * The final page content to display to user
47
         * @var string
48
         */
49
        private $output = null;
50
		
51
        /**
52
         * The current request URL
53
         * @var string
54
         */
55
        private $currentUrl = null;
56
		
57
        /**
58
         * The current request URL cache key
59
         * @var string
60
         */
61
        private $currentUrlCacheKey = null;
62
		
63
        /**
64
         * Whether we can compress the output using Gzip
65
         * @var boolean
66
         */
67
        private $canCompressOutput = false;
68
		
69
        /**
70
         * Construct new instance
71
         */
72
        public function __construct() {
73
            parent::__construct();
74
            $globals = & class_loader('GlobalVar', 'classes');
75
            $currentUrl = '';
76
            if ($globals->server('REQUEST_URI')) {
77
                $currentUrl = $globals->server('REQUEST_URI');
78
            }
79
            if ($globals->server('QUERY_STRING')) {
80
                $currentUrl .= '?' . $globals->server('QUERY_STRING');
81
            }
82
            $this->currentUrl = $currentUrl;		
83
            $this->currentUrlCacheKey = md5($this->currentUrl);
84
85
            $this->setOutputCompressionStatus();
86
        }
87
88
        /**
89
         * Send the HTTP Response headers
90
         * @param  array   $headers   the additional headers to add to the existing headers list
91
         */
92
        public function sendHeaders(array $headers = array()) {
93
            set_http_status_header($this->getStatus());
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 HTTP status code
116
         * @return integer
117
         */
118
        public function getStatus() {
119
            return $this->status;
120
        }
121
122
123
        /**
124
         * Set the HTTP status value
125
         * @param integer $value the new status code
126
         *
127
         * @return  object the current instance
128
         */
129
        public function setStatus($code = 200) {
130
            $this->status = $code;
131
            return $this;
132
        }
133
134
        /**
135
         * Get the header value for the given name
136
         * @param  string $name the header name
137
         * @return string|null       the header value
138
         */
139
        public function getHeader($name) {
140
            if (array_key_exists($name, $this->headers)) {
141
                return $this->headers[$name];
142
            }
143
            return null;
144
        }
145
146
147
        /**
148
         * Set the header value for the specified name
149
         * @param string $name  the header name
150
         * @param string $value the header value to be set
151
         */
152
        public function setHeader($name, $value) {
153
            $this->headers[$name] = $value;
154
        }
155
156
        /**
157
         * Set the headers using array
158
         * @param array $headers the list of the headers to set. 
159
         * Note: this will merge with the existing headers
160
         */
161
        public function setHeaders(array $headers) {
162
            $this->headers = array_merge($this->headers, $headers);
163
        }
164
		
165
        /**
166
         * Redirect user to the specified page
167
         * @param  string $path the URL or URI to be redirect to
168
         * @codeCoverageIgnore
169
         */
170
        public function redirect($path = '') {
171
            $url = get_instance()->url->appUrl($path);
172
            if (!headers_sent()) {
173
                header('Location: ' . $url);
174
                exit;
175
            }
176
            echo '<script>
177
        			location.href = "'.$url . '";
178
        		 </script>';
179
        }
180
181
        /**
182
         * Render the view to display later or return the content
183
         * 
184
         * @param  string  $view   the view name or path
185
         * @param  array|object   $data   the variable data to use in the view
186
         * @param  boolean $return whether to return the view generated content or display it directly
187
         * 
188
         * @return void|string          if $return is true will return the view content otherwise
189
         * will display the view content.
190
         */
191
        public function render($view, $data = null, $return = false) {
192
            //try to convert data to an array if is object or other thing
193
            $data = (array) $data;
194
            $view = str_ireplace('.php', '', $view);
195
            $view = trim($view, '/\\');
196
            $viewFile = $view . '.php';
197
            $path = null;
198
			
199
            //check in module first
200
            $this->logger->debug('Checking the view [' . $view . '] from module list ...');
201
            $moduleInfo = $this->getModuleInfoForView($view);
202
            $module = $moduleInfo['module'];
203
            $view = $moduleInfo['view'];
204
            $moduleViewPath = get_instance()->module->findViewFullPath($view, $module);
205
            if ($moduleViewPath) {
206
                $path = $moduleViewPath;
207
                $this->logger->info('Found view [' . $view . '] in module [' . $module . '], '
208
                                    . 'the file path is [' . $moduleViewPath . '] we will used it');
209
            } else {
210
                $this->logger->info('Cannot find view [' . $view . '] in module [' . $module . '] '
211
                                    . 'using the default location');
212
            }
213
	        
214
            if (!$path) {
215
                $path = $this->getDefaultFilePathForView($viewFile);
216
            }
217
            $this->logger->info('The view file path to be loaded is [' . $path . ']');
218
			
219
            if ($return) {
220
                return $this->loadView($path, $data, true);
221
            }
222
            $this->loadView($path, $data, false);
223
        }
224
225
        /**
226
         * Send the final page output
227
         */
228
        public function renderFinalPage() {
229
            $content = $this->output;
230
            if (!$content) {
231
                $this->logger->warning('The final view content is empty.');
232
                return;
233
            }
234
            $obj = & get_instance();
235
            $cachePageStatus = get_config('cache_enable', false) 
236
                               && !empty($obj->view_cache_enable);
237
            
238
            $content = $this->dispatchFinalViewEvent();
239
            
240
            //check whether need save the page into cache.
241
            if ($cachePageStatus) {
242
                $this->savePageContentIntoCache($content);
243
            }
244
            //update final page content
245
            $this->output = $content;
246
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
247
248
            //compress the output if is available
249
            $compressOutputHandler = $this->getCompressOutputHandler();
250
            ob_start($compressOutputHandler);
251
            $this->sendHeaders();
252
            echo $content;
253
            ob_end_flush();
254
        }
255
256
		
257
        /**
258
         * Send the final page output to user if is cached
259
         * @param object $cache the cache instance
260
         *
261
         * @return boolean whether the page content if available or not
262
         */
263
        public function renderFinalPageFromCache(CacheInterface &$cache) {
264
            //the current page cache key for identification
265
            $pageCacheKey = $this->currentUrlCacheKey;
266
			
267
            $this->logger->debug('Checking if the page content for the '
268
                                . 'URL [' . $this->currentUrl . '] is cached ...');
269
            //get the cache information to prepare header to send to browser
270
            $cacheInfo = $cache->getInfo($pageCacheKey);
271
            if ($cacheInfo) {
272
                $status = $this->sendCacheNotYetExpireInfoToBrowser($cacheInfo);
273
                if($status === false) {
0 ignored issues
show
introduced by
The condition $status === false is always true.
Loading history...
274
                    return $this->sendCachePageContentToBrowser($cache);
275
                }
276
                return true;
277
            }
278
            return false;
279
        }
280
	
281
		
282
        /**
283
         * Get the final page to be rendered
284
         * @return string
285
         */
286
        public function getOutput() {
287
            return $this->output;
288
        }
289
290
         /**
291
         * Set the final page to be rendered
292
         * @param string $output the content of the final page
293
         * 
294
         * @return object
295
         */
296
        public function setOutput($output) {
297
            $this->output = $output;
298
            return $this;
299
        }
300
301
        /**
302
         * Send the HTTP 404 error if can not found the 
303
         * routing information for the current request
304
         */
305
        public function send404() {
306
            $content = $this->output;
307
            if (!$content) {
308
                $this->logger->warning('The final view content is empty.');
309
                return;
310
            }
311
            $obj = & get_instance();
312
            $cachePageStatus = get_config('cache_enable', false) 
313
                                && !empty($obj->view_cache_enable);
314
            //dispatch
315
            get_instance()->eventdispatcher->dispatch(new EventInfo('PAGE_NOT_FOUND'));
316
            //check whether need save the page into cache.
317
            if ($cachePageStatus) {
318
                $this->savePageContentIntoCache($content);
319
            }
320
            $content = $this->replaceElapseTimeAndMemoryUsage($content);
321
322
            /**************************************** save the content into logs **************/
323
            $userAgent = & class_loader('Browser');
324
            $browser = $userAgent->getPlatform() . ', ' . $userAgent->getBrowser() . ' ' . $userAgent->getVersion();
325
            $obj->loader->functions('user_agent');
326
            $str = '[404 page not found] : ';
327
            $str .= ' Unable to find the request page [' . $obj->request->requestUri() . '].'
328
                    .' The visitor IP address [' . get_ip() . '], browser [' . $browser . ']';
329
            $this->logger->error($str);
330
            /**********************************************************************/
331
            
332
            //compress the output if is available
333
            $compressOutputHandler = $this->getCompressOutputHandler();
334
            ob_start($compressOutputHandler);
335
            $this->setStatus(404);
336
            $this->sendHeaders();
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->output = $content;
356
                $this->setStatus(503);
357
                $this->sendHeaders();
358
                echo $content;
359
            }
360
            //@codeCoverageIgnoreStart
361
            else{
362
                //can't use show_error() at this time because 
363
                //some dependencies not yet loaded
364
                set_http_status_header(503);
365
                echo 'The error view [' . $path . '] does not exist';
366
            }
367
            //@codeCoverageIgnoreEnd
368
        }
369
370
         /**
371
         * Dispatch the FINAL_VIEW_READY event
372
         *             
373
         * @return string|null the final view content after processing by each listener
374
         * if they exists otherwise the same content will be returned
375
         */
376
        protected function dispatchFinalViewEvent() {
377
            //dispatch
378
            $event = get_instance()->eventdispatcher->dispatch(
379
                                                                new EventInfo(
380
                                                                                'FINAL_VIEW_READY', 
381
                                                                                $this->output, 
382
                                                                                true
383
                                                                            )
384
                                                            );
385
            $content = null;
386
            if ($event instanceof EventInfo) {
387
                $content = $event->getPayload();
388
            }
389
            if (empty($content)) {
390
                $this->logger->warning('The view content is empty after dispatch to event listeners.');
391
            }
392
            return $content;
393
        }
394
395
        /**
396
         * Get the compress output handler is can compress the page content
397
         * before send
398
         * @return null|string the name of function to handler compression
399
         */
400
        protected function getCompressOutputHandler() {
401
            $handler = null;
402
            if ($this->canCompressOutput) {
403
                $handler = 'ob_gzhandler';
404
            }
405
            return $handler;
406
        }
407
408
        /**
409
         * Set the status of output compression
410
         */
411
        protected function setOutputCompressionStatus() {
412
            $globals = & class_loader('GlobalVar', 'classes');
413
            $this->canCompressOutput = get_config('compress_output')
414
                                          && stripos($globals->server('HTTP_ACCEPT_ENCODING'), 'gzip') !== false 
415
                                          && extension_loaded('zlib')
416
                                          && (bool) ini_get('zlib.output_compression') === false;
417
        }
418
419
         /**
420
         * Return the default full file path for view
421
         * @param  string $file    the filename
422
         * 
423
         * @return string|null          the full file path
424
         */
425
        protected function getDefaultFilePathForView($file){
426
            $searchDir = array(APPS_VIEWS_PATH, CORE_VIEWS_PATH);
427
            $fullFilePath = null;
428
            foreach ($searchDir as $dir) {
429
                $filePath = $dir . $file;
430
                if (file_exists($filePath)) {
431
                    $fullFilePath = $filePath;
432
                    //is already found not to continue
433
                    break;
434
                }
435
            }
436
            return $fullFilePath;
437
        }
438
439
        /**
440
         * Send the cache not yet expire to browser
441
         * @param  boolean|array $cacheInfo the cache information
442
         * @return boolean            true if the information is sent otherwise false
443
         */
444
        protected function sendCacheNotYetExpireInfoToBrowser($cacheInfo) {
445
            if (!empty($cacheInfo)) {
446
                $lastModified = $cacheInfo['mtime'];
447
                $expire = $cacheInfo['expire'];
448
                $globals = & class_loader('GlobalVar', 'classes');
449
                $maxAge = $expire - $globals->server('REQUEST_TIME');
450
                $this->setHeader('Pragma', 'public');
451
                $this->setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
452
                $this->setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
453
                $this->setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
454
                $headerModifiedSince = $globals->server('HTTP_IF_MODIFIED_SINCE');
455
                if (!empty($headerModifiedSince) && $lastModified <= strtotime($headerModifiedSince)) {
456
                    $this->logger->info('The cache page content is not yet expire for the '
457
                                         . 'URL [' . $this->currentUrl . '] send 304 header to browser');
458
                    $this->setStatus(304);
459
                    $this->sendHeaders();
460
                    return true;
461
                }
462
            }
463
            return false;
464
        }
465
466
        /**
467
         * Send the page content from cache to browser
468
         * @param object $cache the cache instance
469
         * @return boolean     the status of the operation
470
         */
471
        protected function sendCachePageContentToBrowser(&$cache) {
472
            $this->logger->info('The cache page content is expired or the browser does '
473
                 . 'not send the HTTP_IF_MODIFIED_SINCE header for the URL [' . $this->currentUrl . '] '
474
                 . 'send cache headers to tell the browser');
475
            $this->sendHeaders();
476
            //current page cache key
477
            $pageCacheKey = $this->currentUrlCacheKey;
478
            //get the cache content
479
            $content = $cache->get($pageCacheKey);
480
            if ($content) {
481
                $this->logger->info('The page content for the URL [' . $this->currentUrl . '] already cached just display it');
482
                $content = $this->replaceElapseTimeAndMemoryUsage($content);
483
                ///display the final output
484
                //compress the output if is available
485
                $compressOutputHandler = $this->getCompressOutputHandler();
486
                ob_start($compressOutputHandler);
487
                echo $content;
488
                ob_end_flush();
489
                return true;
490
            }
491
            $this->logger->info('The page cache content for the URL [' . $this->currentUrl . '] is not valid may be already expired');
492
            $cache->delete($pageCacheKey);
493
            return false;
494
        }
495
496
        /**
497
         * Save the content of page into cache
498
         * @param  string $content the page content to be saved
499
         * @return void
500
         */
501
        protected function savePageContentIntoCache($content) {
502
            $obj = & get_instance();
503
            //current page URL
504
            $url = $this->currentUrl;
505
            //Cache view Time to live in second
506
            $viewCacheTtl = get_config('cache_ttl');
507
            if (!empty($obj->view_cache_ttl)) {
508
                $viewCacheTtl = $obj->view_cache_ttl;
509
            }
510
            //the cache handler instance
511
            $cacheInstance = $obj->cache;
512
            //the current page cache key for identification
513
            $cacheKey = $this->currentUrlCacheKey;
514
            $this->logger->debug('Save the page content for URL [' . $url . '] into the cache ...');
515
            $cacheInstance->set($cacheKey, $content, $viewCacheTtl);
516
			
517
            //get the cache information to prepare header to send to browser
518
            $cacheInfo = $cacheInstance->getInfo($cacheKey);
519
            if ($cacheInfo) {
520
                $lastModified = $cacheInfo['mtime'];
521
                $expire = $cacheInfo['expire'];
522
                $maxAge = $expire - time();
523
                $this->setHeader('Pragma', 'public');
524
                $this->setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
525
                $this->setHeader('Expires', gmdate('D, d M Y H:i:s', $expire) . ' GMT');
526
                $this->setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');	
527
            }
528
        }
529
530
        /**
531
         * Set the value of '{elapsed_time}' and '{memory_usage}'
532
         * @param  string $content the page content
533
         * @return string          the page content after replace 
534
         * '{elapsed_time}', '{memory_usage}'
535
         */
536
        protected function replaceElapseTimeAndMemoryUsage($content) {
537
            // Parse out the elapsed time and memory usage,
538
            // then swap the pseudo-variables with the data
539
            $elapsedTime = get_instance()->benchmark->elapsedTime('APP_EXECUTION_START', 'APP_EXECUTION_END');
540
            $memoryUsage = round(get_instance()->benchmark->memoryUsage(
541
                                                                        'APP_EXECUTION_START', 
542
                                                                        'APP_EXECUTION_END') / 1024 / 1024, 6) . 'MB';
543
            return str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsedTime, $memoryUsage), $content); 
544
        }
545
546
        /**
547
         * Get the module information for the view to load
548
         * 
549
         * @param  string $view the view name like moduleName/viewName, viewName
550
         * 
551
         * @return array        the module information
552
         * array(
553
         * 	'module'=> 'module_name'
554
         * 	'view' => 'view_name'
555
         * 	'viewFile' => 'view_file'
556
         * )
557
         */
558
        protected  function getModuleInfoForView($view) {
559
            $module = null;
560
            $viewFile = null;
561
            $obj = & get_instance();
562
            //check if the request class contains module name
563
            $viewPath = explode('/', $view);
564
            if (count($viewPath) >= 2 && in_array($viewPath[0], get_instance()->module->getModuleList())) {
565
                $module = $viewPath[0];
566
                array_shift($viewPath);
567
                $view = implode('/', $viewPath);
568
                $viewFile = $view . '.php';
569
            }
570
            if (!$module && !empty($obj->moduleName)) {
571
                $module = $obj->moduleName;
572
            }
573
            return array(
574
                        'view' => $view,
575
                        'module' => $module,
576
                        'viewFile' => $viewFile
577
                    );
578
        }
579
580
        /**
581
         * Render the view page
582
         * @see  Response::render
583
         * @return void|string
584
         */
585
        protected  function loadView($path, array $data = array(), $return = false) {
586
            $found = false;
587
            if (file_exists($path)) {
588
                //super instance
589
                $obj = & get_instance();
590
                if ($obj instanceof Controller) {
591
                    foreach (get_object_vars($obj) as $key => $value) {
592
                        if (!property_exists($this, $key)) {
593
                            $this->{$key} = & $obj->{$key};
594
                        }
595
                    }
596
                }
597
                ob_start();
598
                extract($data);
599
                //need use require() instead of require_once because can load this view many time
600
                require $path;
601
                $content = ob_get_clean();
602
                if ($return) {
603
                    return $content;
604
                }
605
                $this->output .= $content;
606
                $found = true;
607
            }
608
            if (!$found) {
609
                show_error('Unable to find view [' . $path . ']');
610
            }
611
        }
612
613
         /**
614
         * Set the mandory headers, like security, etc.
615
         */
616
        protected function setRequiredHeaders() {
617
            $requiredHeaders = array(
618
                                'X-XSS-Protection' => '1; mode=block',
619
                                'X-Frame-Options'  => 'SAMEORIGIN'
620
                            );
621
            foreach ($requiredHeaders as $key => $value) {
622
               if (!isset($this->headers[$key])) {
623
                    $this->headers[$key] = $value;
624
               } 
625
            }
626
        }
627
    }
628