Passed
Push — 1.0.0-dev ( 066288...93958a )
by nguereza
09:45
created

Response::sendCachePageContentToBrowser()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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