Test Failed
Push — 1.0.0-dev ( b109e3...602d43 )
by nguereza
02:55
created

Response::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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