Passed
Push — 1.0.0-dev ( 8edc19...2b6704 )
by nguereza
03:32
created

Response::render()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 22
nc 8
nop 3
dl 0
loc 33
rs 9.568
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 GNU GPL License (GPL)
9
	 *
10
	 * Copyright (C) 2017 Tony NGUEREZA
11
	 *
12
	 * This program is free software; you can redistribute it and/or
13
	 * modify it under the terms of the GNU General Public License
14
	 * as published by the Free Software Foundation; either version 3
15
	 * of the License, or (at your option) any later version.
16
	 *
17
	 * This program is distributed in the hope that it will be useful,
18
	 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
	 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
	 * GNU General Public License for more details.
21
	 *
22
	 * You should have received a copy of the GNU General Public License
23
	 * along with this program; if not, write to the Free Software
24
	 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25
	*/
26
27
	class Response{
28
29
		/**
30
		 * The list of request header to send with response
31
		 * @var array
32
		 */
33
		private static $headers = array();
34
35
		/**
36
		 * The logger instance
37
		 * @var object
38
		 */
39
		private static $logger;
40
		
41
		/**
42
		 * The final page content to display to user
43
		 * @var string
44
		 */
45
		private $_pageRender = null;
46
		
47
		/**
48
		 * The current request URL
49
		 * @var string
50
		 */
51
		private $_currentUrl = null;
52
		
53
		/**
54
		 * The current request URL cache key
55
		 * @var string
56
		 */
57
		private $_currentUrlCacheKey = null;
58
		
59
		/**
60
		* Whether we can compress the output using Gzip
61
		* @var boolean
62
		*/
63
		private static $_canCompressOutput = false;
64
		
65
		/**
66
		 * Construct new response instance
67
		 */
68
		public function __construct(){
69
			$currentUrl = '';
70
			if (! empty($_SERVER['REQUEST_URI'])){
71
				$currentUrl = $_SERVER['REQUEST_URI'];
72
			}
73
			if (! empty($_SERVER['QUERY_STRING'])){
74
				$currentUrl .= '?' . $_SERVER['QUERY_STRING'];
75
			}
76
			$this->_currentUrl =  $currentUrl;
77
					
78
			$this->_currentUrlCacheKey = md5($this->_currentUrl);
79
			
80
			self::$_canCompressOutput = get_config('compress_output')
81
										  && isset($_SERVER['HTTP_ACCEPT_ENCODING']) 
82
										  && stripos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false 
83
										  && extension_loaded('zlib')
84
										  && (bool) ini_get('zlib.output_compression') === false;
85
		}
86
87
		
88
		/**
89
		 * The signleton of the logger
90
		 * @return Object the Log instance
91
		 */
92
		public static function getLogger(){
93
			if(self::$logger == null){
94
				$logger = array();
95
				$logger[0] =& class_loader('Log', 'classes');
96
				$logger[0]->setLogger('Library::Response');
97
				self::$logger = $logger[0];
98
			}
99
			return self::$logger;			
100
		}
101
102
		/**
103
		 * Set the log instance for future use
104
		 * @param object $logger the log object
105
		 * @return object the log instance
106
		 */
107
		public static function setLogger($logger){
108
			self::$logger = $logger;
109
			return self::$logger;
110
		}
111
112
113
		/**
114
		 * Send the HTTP Response headers
115
		 * @param  integer $httpCode the HTTP status code
116
		 * @param  array   $headers   the additional headers to add to the existing headers list
117
		 */
118
		public static function sendHeaders($httpCode = 200, array $headers = array()){
119
			set_http_status_header($httpCode);
120
			self::setHeaders($headers);
121
			if(! headers_sent()){
122
				foreach(self::getHeaders() as $key => $value){
123
					header($key .': '.$value);
124
				}
125
			}
126
		}
127
128
		/**
129
		 * Get the list of the headers
130
		 * @return array the headers list
131
		 */
132
		public static function getHeaders(){
133
			return self::$headers;
134
		}
135
136
		/**
137
		 * Get the header value for the given name
138
		 * @param  string $name the header name
139
		 * @return string|null       the header value
140
		 */
141
		public static function getHeader($name){
142
			if(array_key_exists($name, self::$headers)){
143
				return self::$headers[$name];
144
			}
145
			return null;
146
		}
147
148
149
		/**
150
		 * Set the header value for the specified name
151
		 * @param string $name  the header name
152
		 * @param string $value the header value to be set
153
		 */
154
		public static function setHeader($name, $value){
155
			self::$headers[$name] = $value;
156
		}
157
158
		/**
159
		 * Set the headers using array
160
		 * @param array $headers the list of the headers to set. 
161
		 * Note: this will merge with the existing headers
162
		 */
163
		public static function setHeaders(array $headers){
164
			self::$headers = array_merge(self::getHeaders(), $headers);
165
		}
166
		
167
		/**
168
		 * Redirect user to the specified page
169
		 * @param  string $path the URL or URI to be redirect to
170
		 */
171
		public static function redirect($path = ''){
172
			$logger = self::getLogger();
173
			$url = Url::site_url($path);
174
			$logger->info('Redirect to URL [' .$url. ']');
175
			if(! headers_sent()){
176
				header('Location: '.$url);
177
				exit;
178
			}
179
			echo '<script>
180
					location.href = "'.$url.'";
181
				</script>';
182
		}
183
184
		/**
185
		 * Render the view to display later or return the content
186
		 * @param  string  $view   the view name or path
187
		 * @param  array|object   $data   the variable data to use in the view
188
		 * @param  boolean $return whether to return the view generated content or display it directly
189
		 * @return void|string          if $return is true will return the view content otherwise
190
		 * will display the view content.
191
		 */
192
		public function render($view, $data = null, $return = false){
193
			$logger = self::getLogger();
194
			//convert data to an array
195
			$data = (array) $data;
196
			$view = str_ireplace('.php', '', $view);
197
			$view = trim($view, '/\\');
198
			$viewFile = $view . '.php';
199
			$path = APPS_VIEWS_PATH . $viewFile;
200
			
201
			//check in module first
202
			$logger->debug('Checking the view [' . $view . '] from module list ...');
203
			$moduleInfo = self::getModuleInfoForView($view);
0 ignored issues
show
Bug Best Practice introduced by
The method Response::getModuleInfoForView() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

203
			/** @scrutinizer ignore-call */ 
204
   $moduleInfo = self::getModuleInfoForView($view);
Loading history...
204
			$module    = $moduleInfo['module'];
205
			$view  = $moduleInfo['view'];
206
			if(! empty($moduleInfo['viewFile'])){
207
				$viewFile = $moduleInfo['viewFile'];
0 ignored issues
show
Unused Code introduced by
The assignment to $viewFile is dead and can be removed.
Loading history...
208
			}
209
			$moduleViewPath = Module::findViewFullPath($view, $module);
210
			if($moduleViewPath){
211
				$path = $moduleViewPath;
212
				$logger->info('Found view [' . $view . '] in module [' .$module. '], the file path is [' .$moduleViewPath. '] we will used it');
213
			}
214
			else{
215
				$logger->info('Cannot find view [' . $view . '] in module [' .$module. '] using the default location');
216
			}
217
			
218
			$logger->info('The view file path to be loaded is [' . $path . ']');
219
			
220
			/////////
221
			if($return){
222
				return $this->loadView($path, $data, true);
223
			}
224
			$this->loadView($path, $data, false);
225
		}
226
227
		
228
		/**
229
		* Send the final page output to user
230
		*/
231
		public function renderFinalPage(){
232
			$logger = self::getLogger();
233
			$obj = & get_instance();
234
			$cachePageStatus = get_config('cache_enable', false) && !empty($obj->view_cache_enable);
235
			$dispatcher = $obj->eventdispatcher;
236
			$content = $this->_pageRender;
237
			if(! $content){
238
				$logger->warning('The final view content is empty.');
239
				return;
240
			}
241
			//dispatch
242
			$event = $dispatcher->dispatch(new EventInfo('FINAL_VIEW_READY', $content, true));
243
			$content = null;
244
			if(! empty($event->payload)){
245
				$content = $event->payload;
246
			}
247
			if(empty($content)){
248
				$logger->warning('The view content is empty after dispatch to event listeners.');
249
			}
250
			
251
			//check whether need save the page into cache.
252
			if($cachePageStatus){
253
				$this->savePageContentIntoCache($content);
254
			}
255
			
256
			// Parse out the elapsed time and memory usage,
257
			// then swap the pseudo-variables with the data
258
			$elapsedTime = $obj->benchmark->elapsedTime('APP_EXECUTION_START', 'APP_EXECUTION_END');
259
			$memoryUsage	= round($obj->benchmark->memoryUsage('APP_EXECUTION_START', 'APP_EXECUTION_END') / 1024 / 1024, 6) . 'MB';
260
			$content = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsedTime, $memoryUsage), $content);
261
			
262
			//compress the output if is available
263
			$type = null;
264
			if (self::$_canCompressOutput){
265
				$type = 'ob_gzhandler';
266
			}
267
			ob_start($type);
268
			self::sendHeaders(200);
269
			echo $content;
270
			ob_end_flush();
271
		}
272
273
		
274
		/**
275
		* Send the final page output to user if is cached
276
		* @param object $cache the cache instance
277
		*
278
		* @return boolean whether the page content if available or not
279
		*/
280
		public function renderFinalPageFromCache(&$cache){
281
			$logger = self::getLogger();
282
			//the current page cache key for identification
283
			$pageCacheKey = $this->_currentUrlCacheKey;
284
			
285
			$logger->debug('Checking if the page content for the URL [' . $this->_currentUrl . '] is cached ...');
286
			//get the cache information to prepare header to send to browser
287
			$cacheInfo = $cache->getInfo($pageCacheKey);
288
			if($cacheInfo){
289
				$status = $this->sendCacheNotYetExpireInfo($cacheInfo);
290
				if($status === false){
291
					return $this->sendPageContentToBrowser($cache);
292
				}
293
			}
294
			return false;
295
		}
296
	
297
		
298
		/**
299
		* Get the final page to be rendered
300
		* @return string
301
		*/
302
		public function getFinalPageRendered(){
303
			return $this->_pageRender;
304
		}
305
306
		/**
307
		 * Send the HTTP 404 error if can not found the 
308
		 * routing information for the current request
309
		 */
310
		public static function send404(){
311
			/********* for logs **************/
312
			//can't use $obj = & get_instance()  here because the global super object will be available until
313
			//the main controller is loaded even for Loader::library('xxxx');
314
			$logger = self::getLogger();
315
			$request =& class_loader('Request', 'classes');
316
			$userAgent =& class_loader('Browser');
317
			$browser = $userAgent->getPlatform().', '.$userAgent->getBrowser().' '.$userAgent->getVersion();
318
			
319
			//here can't use Loader::functions just include the helper manually
320
			require_once CORE_FUNCTIONS_PATH . 'function_user_agent.php';
321
322
			$str = '[404 page not found] : ';
323
			$str .= ' Unable to find the request page [' . $request->requestUri() . ']. The visitor IP address [' . get_ip() . '], browser [' . $browser . ']';
324
			$logger->error($str);
325
			/***********************************/
326
			$path = CORE_VIEWS_PATH . '404.php';
327
			if(file_exists($path)){
328
				//compress the output if is available
329
				$type = null;
330
				if (self::$_canCompressOutput){
331
					$type = 'ob_gzhandler';
332
				}
333
				ob_start($type);
334
				require_once $path;
335
				$output = ob_get_clean();
336
				self::sendHeaders(404);
337
				echo $output;
338
			}
339
			else{
340
				show_error('The 404 view [' .$path. '] does not exist');
341
			}
342
		}
343
344
		/**
345
		 * Display the error to user
346
		 * @param  array  $data the error information
347
		 */
348
		public static function sendError(array $data = array()){
349
			$path = CORE_VIEWS_PATH . 'errors.php';
350
			if(file_exists($path)){
351
				//compress the output if is available
352
				$type = null;
353
				if (self::$_canCompressOutput){
354
					$type = 'ob_gzhandler';
355
				}
356
				ob_start($type);
357
				extract($data);
358
				require_once $path;
359
				$output = ob_get_clean();
360
				self::sendHeaders(503);
361
				echo $output;
362
			}
363
			else{
364
				//can't use show_error() at this time because some dependencies not yet loaded and to prevent loop
365
				set_http_status_header(503);
366
				echo 'The error view [' . $path . '] does not exist';
367
			}
368
		}
369
370
		/**
371
		 * Send the cache not yet expire to browser
372
		 * @param  array $cacheInfo the cache information
373
		 * @return boolean            true if the information is sent otherwise false
374
		 */
375
		protected function sendCacheNotYetExpireInfo($cacheInfo){
376
			if(! empty($cacheInfo)){
377
				$logger = self::getLogger();
378
				$lastModified = $cacheInfo['mtime'];
379
				$expire = $cacheInfo['expire'];
380
				$maxAge = $expire - $_SERVER['REQUEST_TIME'];
381
				self::setHeader('Pragma', 'public');
382
				self::setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
383
				self::setHeader('Expires', gmdate('D, d M Y H:i:s', $expire).' GMT');
384
				self::setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified).' GMT');
385
				if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $lastModified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])){
386
					$logger->info('The cache page content is not yet expire for the URL [' . $this->_currentUrl . '] send 304 header to browser');
387
					self::sendHeaders(304);
388
					return true;
389
				}
390
			}
391
			return false;
392
		}
393
394
		/**
395
		 * Send the page content from cache to browser
396
		 * @param object $cache the cache instance
397
		 * @return boolean     the status of the operation
398
		 */
399
		protected function sendPageContentToBrowser(&$cache){
400
			$logger = self::getLogger();
401
			$logger->info('The cache page content is expired or the browser doesn\'t send the HTTP_IF_MODIFIED_SINCE header for the URL [' . $this->_currentUrl . '] send cache headers to tell the browser');
402
			self::sendHeaders(200);
403
			//current page cache key
404
			$pageCacheKey = $this->_currentUrlCacheKey;
405
			//get the cache content
406
			$content = $cache->get($pageCacheKey);
407
			if($content){
408
				$logger->info('The page content for the URL [' . $this->_currentUrl . '] already cached just display it');
409
				//load benchmark class
410
				$benchmark = & class_loader('Benchmark');
411
				
412
				// Parse out the elapsed time and memory usage,
413
				// then swap the pseudo-variables with the data
414
				$elapsedTime = $benchmark->elapsedTime('APP_EXECUTION_START', 'APP_EXECUTION_END');
415
				$memoryUsage	= round($benchmark->memoryUsage('APP_EXECUTION_START', 'APP_EXECUTION_END') / 1024 / 1024, 6) . 'MB';
416
				$content = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsedTime, $memoryUsage), $content);
417
				
418
				///display the final output
419
				//compress the output if is available
420
				$type = null;
421
				if (self::$_canCompressOutput){
422
					$type = 'ob_gzhandler';
423
				}
424
				ob_start($type);
425
				echo $content;
426
				ob_end_flush();
427
				return true;
428
			}
429
			$logger->info('The page cache content for the URL [' . $this->_currentUrl . '] is not valid may be already expired');
430
			$cache->delete($pageCacheKey);
431
			return false;
432
		}
433
434
		/**
435
		 * Save the content of page into cache
436
		 * @param  string $content the page content to be saved
437
		 * @return void
438
		 */
439
		protected function savePageContentIntoCache($content){
440
			$obj = & get_instance();
441
			$logger = self::getLogger();
442
443
			//current page URL
444
			$url = $this->_currentUrl;
445
			//Cache view Time to live in second
446
			$viewCacheTtl = get_config('cache_ttl');
447
			if (!empty($obj->view_cache_ttl)){
448
				$viewCacheTtl = $obj->view_cache_ttl;
449
			}
450
			//the cache handler instance
451
			$cacheInstance = $obj->cache;
452
			//the current page cache key for identification
453
			$cacheKey = $this->_currentUrlCacheKey;
454
			
455
			$logger->debug('Save the page content for URL [' . $url . '] into the cache ...');
456
			$cacheInstance->set($cacheKey, $content, $viewCacheTtl);
457
			
458
			//get the cache information to prepare header to send to browser
459
			$cacheInfo = $cacheInstance->getInfo($cacheKey);
460
			if($cacheInfo){
461
				$lastModified = $cacheInfo['mtime'];
462
				$expire = $cacheInfo['expire'];
463
				$maxAge = $expire - time();
464
				self::setHeader('Pragma', 'public');
465
				self::setHeader('Cache-Control', 'max-age=' . $maxAge . ', public');
466
				self::setHeader('Expires', gmdate('D, d M Y H:i:s', $expire).' GMT');
467
				self::setHeader('Last-modified', gmdate('D, d M Y H:i:s', $lastModified).' GMT');	
468
			}
469
		}
470
		
471
472
		/**
473
		 * Get the module information for the view to load
474
		 * @param  string $view the view name like moduleName/viewName, viewName
475
		 * 
476
		 * @return array        the module information
477
		 * array(
478
		 * 	'module'=> 'module_name'
479
		 * 	'view' => 'view_name'
480
		 * 	'viewFile' => 'view_file'
481
		 * )
482
		 */
483
		protected  function getModuleInfoForView($view){
484
			$module = null;
485
			$viewFile = null;
486
			$obj = & get_instance();
487
			//check if the request class contains module name
488
			if(strpos($view, '/') !== false){
489
				$viewPath = explode('/', $view);
490
				if(isset($viewPath[0]) && in_array($viewPath[0], Module::getModuleList())){
491
					$module = $viewPath[0];
492
					array_shift($viewPath);
493
					$view = implode('/', $viewPath);
494
					$viewFile = $view . '.php';
495
				}
496
			}
497
			if(! $module && !empty($obj->moduleName)){
498
				$module = $obj->moduleName;
499
			}
500
			return array(
501
						'view' => $view,
502
						'module' => $module,
503
						'viewFile' => $viewFile
504
					);
505
		}
506
507
		/**
508
		 * Render the view page
509
		 * @see  Response::render
510
		 * @return void|string
511
		 */
512
		protected  function loadView($path, array $data = array(), $return = false){
513
			$found = false;
514
			if(file_exists($path)){
515
				//super instance
516
				$obj = & get_instance();
517
				foreach(get_object_vars($obj) as $key => $value){
518
					if(! isset($this->{$key})){
519
						$this->{$key} = & $obj->{$key};
520
					}
521
				}
522
				ob_start();
523
				extract($data);
524
				//need use require() instead of require_once because can load this view many time
525
				require $path;
526
				$content = ob_get_clean();
527
				if($return){
528
					return $content;
529
				}
530
				$this->_pageRender .= $content;
531
				$found = true;
532
			}
533
			if(! $found){
534
				show_error('Unable to find view [' .$view . ']');
535
			}
536
		}
537
538
	}
539