@@ 51-864 (lines=814) @@ | ||
48 | * @author EllisLab Dev Team |
|
49 | * @link https://codeigniter.com/user_guide/libraries/output.html |
|
50 | */ |
|
51 | class CI_Output { |
|
52 | ||
53 | /** |
|
54 | * Final output string |
|
55 | * |
|
56 | * @var string |
|
57 | */ |
|
58 | public $final_output; |
|
59 | ||
60 | /** |
|
61 | * Cache expiration time |
|
62 | * |
|
63 | * @var int |
|
64 | */ |
|
65 | public $cache_expiration = 0; |
|
66 | ||
67 | /** |
|
68 | * List of server headers |
|
69 | * |
|
70 | * @var array |
|
71 | */ |
|
72 | public $headers = array(); |
|
73 | ||
74 | /** |
|
75 | * List of mime types |
|
76 | * |
|
77 | * @var array |
|
78 | */ |
|
79 | public $mimes = array(); |
|
80 | ||
81 | /** |
|
82 | * Mime-type for the current page |
|
83 | * |
|
84 | * @var string |
|
85 | */ |
|
86 | protected $mime_type = 'text/html'; |
|
87 | ||
88 | /** |
|
89 | * Enable Profiler flag |
|
90 | * |
|
91 | * @var bool |
|
92 | */ |
|
93 | public $enable_profiler = FALSE; |
|
94 | ||
95 | /** |
|
96 | * php.ini zlib.output_compression flag |
|
97 | * |
|
98 | * @var bool |
|
99 | */ |
|
100 | protected $_zlib_oc = FALSE; |
|
101 | ||
102 | /** |
|
103 | * CI output compression flag |
|
104 | * |
|
105 | * @var bool |
|
106 | */ |
|
107 | protected $_compress_output = FALSE; |
|
108 | ||
109 | /** |
|
110 | * List of profiler sections |
|
111 | * |
|
112 | * @var array |
|
113 | */ |
|
114 | protected $_profiler_sections = array(); |
|
115 | ||
116 | /** |
|
117 | * Parse markers flag |
|
118 | * |
|
119 | * Whether or not to parse variables like {elapsed_time} and {memory_usage}. |
|
120 | * |
|
121 | * @var bool |
|
122 | */ |
|
123 | public $parse_exec_vars = TRUE; |
|
124 | ||
125 | /** |
|
126 | * mbstring.func_overload flag |
|
127 | * |
|
128 | * @var bool |
|
129 | */ |
|
130 | protected static $func_overload; |
|
131 | ||
132 | /** |
|
133 | * Response status (status code, text and is redirection?) |
|
134 | * |
|
135 | * @var array |
|
136 | * |
|
137 | * added by ci-phpunit-test |
|
138 | */ |
|
139 | public $_status; |
|
140 | ||
141 | /** |
|
142 | * Cookies |
|
143 | * |
|
144 | * @var array |
|
145 | * |
|
146 | * added by ci-phpunit-test |
|
147 | */ |
|
148 | public $_cookies; |
|
149 | ||
150 | /** |
|
151 | * Class constructor |
|
152 | * |
|
153 | * Determines whether zLib output compression will be used. |
|
154 | * |
|
155 | * @return void |
|
156 | */ |
|
157 | public function __construct() |
|
158 | { |
|
159 | $this->_zlib_oc = (bool) ini_get('zlib.output_compression'); |
|
160 | $this->_compress_output = ( |
|
161 | $this->_zlib_oc === FALSE |
|
162 | && config_item('compress_output') === TRUE |
|
163 | && extension_loaded('zlib') |
|
164 | ); |
|
165 | ||
166 | isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); |
|
167 | ||
168 | // Get mime types for later |
|
169 | $this->mimes =& get_mimes(); |
|
170 | ||
171 | log_message('info', 'Output Class Initialized'); |
|
172 | } |
|
173 | ||
174 | // -------------------------------------------------------------------- |
|
175 | ||
176 | /** |
|
177 | * Get Output |
|
178 | * |
|
179 | * Returns the current output string. |
|
180 | * |
|
181 | * @return string |
|
182 | */ |
|
183 | public function get_output() |
|
184 | { |
|
185 | return $this->final_output; |
|
186 | } |
|
187 | ||
188 | // -------------------------------------------------------------------- |
|
189 | ||
190 | /** |
|
191 | * Set Output |
|
192 | * |
|
193 | * Sets the output string. |
|
194 | * |
|
195 | * @param string $output Output data |
|
196 | * @return CI_Output |
|
197 | */ |
|
198 | public function set_output($output) |
|
199 | { |
|
200 | $this->final_output = $output; |
|
201 | return $this; |
|
202 | } |
|
203 | ||
204 | // -------------------------------------------------------------------- |
|
205 | ||
206 | /** |
|
207 | * Append Output |
|
208 | * |
|
209 | * Appends data onto the output string. |
|
210 | * |
|
211 | * @param string $output Data to append |
|
212 | * @return CI_Output |
|
213 | */ |
|
214 | public function append_output($output) |
|
215 | { |
|
216 | $this->final_output .= $output; |
|
217 | return $this; |
|
218 | } |
|
219 | ||
220 | // -------------------------------------------------------------------- |
|
221 | ||
222 | /** |
|
223 | * Set Header |
|
224 | * |
|
225 | * Lets you set a server header which will be sent with the final output. |
|
226 | * |
|
227 | * Note: If a file is cached, headers will not be sent. |
|
228 | * @todo We need to figure out how to permit headers to be cached. |
|
229 | * |
|
230 | * @param string $header Header |
|
231 | * @param bool $replace Whether to replace the old header value, if already set |
|
232 | * @return CI_Output |
|
233 | */ |
|
234 | public function set_header($header, $replace = TRUE) |
|
235 | { |
|
236 | // If zlib.output_compression is enabled it will compress the output, |
|
237 | // but it will not modify the content-length header to compensate for |
|
238 | // the reduction, causing the browser to hang waiting for more data. |
|
239 | // We'll just skip content-length in those cases. |
|
240 | if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) |
|
241 | { |
|
242 | return $this; |
|
243 | } |
|
244 | ||
245 | $this->headers[] = array($header, $replace); |
|
246 | return $this; |
|
247 | } |
|
248 | ||
249 | // -------------------------------------------------------------------- |
|
250 | ||
251 | /** |
|
252 | * Set Content-Type Header |
|
253 | * |
|
254 | * @param string $mime_type Extension of the file we're outputting |
|
255 | * @param string $charset Character set (default: NULL) |
|
256 | * @return CI_Output |
|
257 | */ |
|
258 | public function set_content_type($mime_type, $charset = NULL) |
|
259 | { |
|
260 | if (strpos($mime_type, '/') === FALSE) |
|
261 | { |
|
262 | $extension = ltrim($mime_type, '.'); |
|
263 | ||
264 | // Is this extension supported? |
|
265 | if (isset($this->mimes[$extension])) |
|
266 | { |
|
267 | $mime_type =& $this->mimes[$extension]; |
|
268 | ||
269 | if (is_array($mime_type)) |
|
270 | { |
|
271 | $mime_type = current($mime_type); |
|
272 | } |
|
273 | } |
|
274 | } |
|
275 | ||
276 | $this->mime_type = $mime_type; |
|
277 | ||
278 | if (empty($charset)) |
|
279 | { |
|
280 | $charset = config_item('charset'); |
|
281 | } |
|
282 | ||
283 | $header = 'Content-Type: '.$mime_type |
|
284 | .(empty($charset) ? '' : '; charset='.$charset); |
|
285 | ||
286 | $this->headers[] = array($header, TRUE); |
|
287 | return $this; |
|
288 | } |
|
289 | ||
290 | // -------------------------------------------------------------------- |
|
291 | ||
292 | /** |
|
293 | * Get Current Content-Type Header |
|
294 | * |
|
295 | * @return string 'text/html', if not already set |
|
296 | */ |
|
297 | public function get_content_type() |
|
298 | { |
|
299 | for ($i = 0, $c = count($this->headers); $i < $c; $i++) |
|
300 | { |
|
301 | if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) |
|
302 | { |
|
303 | return $content_type; |
|
304 | } |
|
305 | } |
|
306 | ||
307 | return 'text/html'; |
|
308 | } |
|
309 | ||
310 | // -------------------------------------------------------------------- |
|
311 | ||
312 | /** |
|
313 | * Get Header |
|
314 | * |
|
315 | * @param string $header |
|
316 | * @return string |
|
317 | */ |
|
318 | public function get_header($header) |
|
319 | { |
|
320 | // Combine headers already sent with our batched headers |
|
321 | $headers = array_merge( |
|
322 | // We only need [x][0] from our multi-dimensional array |
|
323 | array_map('array_shift', $this->headers), |
|
324 | headers_list() |
|
325 | ); |
|
326 | ||
327 | if (empty($headers) OR empty($header)) |
|
328 | { |
|
329 | return NULL; |
|
330 | } |
|
331 | ||
332 | // Count backwards, in order to get the last matching header |
|
333 | for ($c = count($headers) - 1; $c > -1; $c--) |
|
334 | { |
|
335 | if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0) |
|
336 | { |
|
337 | return trim(self::substr($headers[$c], $l+1)); |
|
338 | } |
|
339 | } |
|
340 | ||
341 | return NULL; |
|
342 | } |
|
343 | ||
344 | // -------------------------------------------------------------------- |
|
345 | ||
346 | /** |
|
347 | * Set HTTP Status Header |
|
348 | * |
|
349 | * As of version 1.7.2, this is an alias for common function |
|
350 | * set_status_header(). |
|
351 | * |
|
352 | * @param int $code Status code (default: 200) |
|
353 | * @param string $text Optional message |
|
354 | * @return CI_Output |
|
355 | */ |
|
356 | public function set_status_header($code = 200, $text = '') |
|
357 | { |
|
358 | set_status_header($code, $text); |
|
359 | return $this; |
|
360 | } |
|
361 | ||
362 | // -------------------------------------------------------------------- |
|
363 | ||
364 | /** |
|
365 | * Enable/disable Profiler |
|
366 | * |
|
367 | * @param bool $val TRUE to enable or FALSE to disable |
|
368 | * @return CI_Output |
|
369 | */ |
|
370 | public function enable_profiler($val = TRUE) |
|
371 | { |
|
372 | $this->enable_profiler = is_bool($val) ? $val : TRUE; |
|
373 | return $this; |
|
374 | } |
|
375 | ||
376 | // -------------------------------------------------------------------- |
|
377 | ||
378 | /** |
|
379 | * Set Profiler Sections |
|
380 | * |
|
381 | * Allows override of default/config settings for |
|
382 | * Profiler section display. |
|
383 | * |
|
384 | * @param array $sections Profiler sections |
|
385 | * @return CI_Output |
|
386 | */ |
|
387 | public function set_profiler_sections($sections) |
|
388 | { |
|
389 | if (isset($sections['query_toggle_count'])) |
|
390 | { |
|
391 | $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count']; |
|
392 | unset($sections['query_toggle_count']); |
|
393 | } |
|
394 | ||
395 | foreach ($sections as $section => $enable) |
|
396 | { |
|
397 | $this->_profiler_sections[$section] = ($enable !== FALSE); |
|
398 | } |
|
399 | ||
400 | return $this; |
|
401 | } |
|
402 | ||
403 | // -------------------------------------------------------------------- |
|
404 | ||
405 | /** |
|
406 | * Set Cache |
|
407 | * |
|
408 | * @param int $time Cache expiration time in minutes |
|
409 | * @return CI_Output |
|
410 | */ |
|
411 | public function cache($time) |
|
412 | { |
|
413 | $this->cache_expiration = is_numeric($time) ? $time : 0; |
|
414 | return $this; |
|
415 | } |
|
416 | ||
417 | // -------------------------------------------------------------------- |
|
418 | ||
419 | /** |
|
420 | * Display Output |
|
421 | * |
|
422 | * Processes and sends finalized output data to the browser along |
|
423 | * with any server headers and profile data. It also stops benchmark |
|
424 | * timers so the page rendering speed and memory usage can be shown. |
|
425 | * |
|
426 | * Note: All "view" data is automatically put into $this->final_output |
|
427 | * by controller class. |
|
428 | * |
|
429 | * @uses CI_Output::$final_output |
|
430 | * @param string $output Output data override |
|
431 | * @return void |
|
432 | * |
|
433 | * modified by ci-phpunit-test |
|
434 | */ |
|
435 | public function _display($output = '') |
|
436 | { |
|
437 | // Note: We use load_class() because we can't use $CI =& get_instance() |
|
438 | // since this function is sometimes called by the caching mechanism, |
|
439 | // which happens before the CI super object is available. |
|
440 | $BM =& load_class('Benchmark', 'core'); |
|
441 | $CFG =& load_class('Config', 'core'); |
|
442 | ||
443 | // Grab the super object if we can. |
|
444 | if (class_exists('CI_Controller', FALSE)) |
|
445 | { |
|
446 | $CI =& get_instance(); |
|
447 | } |
|
448 | ||
449 | // -------------------------------------------------------------------- |
|
450 | ||
451 | // Set the output data |
|
452 | if ($output === '') |
|
453 | { |
|
454 | $output =& $this->final_output; |
|
455 | } |
|
456 | ||
457 | // -------------------------------------------------------------------- |
|
458 | ||
459 | // Do we need to write a cache file? Only if the controller does not have its |
|
460 | // own _output() method and we are not dealing with a cache file, which we |
|
461 | // can determine by the existence of the $CI object above |
|
462 | if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')) |
|
463 | { |
|
464 | $this->_write_cache($output); |
|
465 | } |
|
466 | ||
467 | // -------------------------------------------------------------------- |
|
468 | ||
469 | // Parse out the elapsed time and memory usage, |
|
470 | // then swap the pseudo-variables with the data |
|
471 | ||
472 | $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); |
|
473 | ||
474 | if ($this->parse_exec_vars === TRUE) |
|
475 | { |
|
476 | $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; |
|
477 | $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); |
|
478 | } |
|
479 | ||
480 | // -------------------------------------------------------------------- |
|
481 | ||
482 | // Is compression requested? |
|
483 | if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed |
|
484 | && $this->_compress_output === TRUE |
|
485 | && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
486 | { |
|
487 | ob_start('ob_gzhandler'); |
|
488 | } |
|
489 | ||
490 | // -------------------------------------------------------------------- |
|
491 | ||
492 | // Are there any server headers to send? |
|
493 | if (count($this->headers) > 0) |
|
494 | { |
|
495 | foreach ($this->headers as $header) |
|
496 | { |
|
497 | // @header($header[0], $header[1]); |
|
498 | } |
|
499 | } |
|
500 | ||
501 | // -------------------------------------------------------------------- |
|
502 | ||
503 | // Does the $CI object exist? |
|
504 | // If not we know we are dealing with a cache file so we'll |
|
505 | // simply echo out the data and exit. |
|
506 | if ( ! isset($CI)) |
|
507 | { |
|
508 | if ($this->_compress_output === TRUE) |
|
509 | { |
|
510 | if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
511 | { |
|
512 | // header('Content-Encoding: gzip'); |
|
513 | // header('Content-Length: '.self::strlen($output)); |
|
514 | } |
|
515 | else |
|
516 | { |
|
517 | // User agent doesn't support gzip compression, |
|
518 | // so we'll have to decompress our cache |
|
519 | $output = gzinflate(self::substr($output, 10, -8)); |
|
520 | } |
|
521 | } |
|
522 | ||
523 | echo $output; |
|
524 | log_message('info', 'Final output sent to browser'); |
|
525 | log_message('debug', 'Total execution time: '.$elapsed); |
|
526 | return; |
|
527 | } |
|
528 | ||
529 | // -------------------------------------------------------------------- |
|
530 | ||
531 | // Do we need to generate profile data? |
|
532 | // If so, load the Profile class and run it. |
|
533 | if ($this->enable_profiler === TRUE) |
|
534 | { |
|
535 | $CI->load->library('profiler'); |
|
536 | if ( ! empty($this->_profiler_sections)) |
|
537 | { |
|
538 | $CI->profiler->set_sections($this->_profiler_sections); |
|
539 | } |
|
540 | ||
541 | // If the output data contains closing </body> and </html> tags |
|
542 | // we will remove them and add them back after we insert the profile data |
|
543 | $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run(); |
|
544 | if ($count > 0) |
|
545 | { |
|
546 | $output .= '</body></html>'; |
|
547 | } |
|
548 | } |
|
549 | ||
550 | // Does the controller contain a function named _output()? |
|
551 | // If so send the output there. Otherwise, echo it. |
|
552 | if (method_exists($CI, '_output')) |
|
553 | { |
|
554 | $CI->_output($output); |
|
555 | } |
|
556 | else |
|
557 | { |
|
558 | echo $output; // Send it to the browser! |
|
559 | } |
|
560 | ||
561 | log_message('info', 'Final output sent to browser'); |
|
562 | log_message('debug', 'Total execution time: '.$elapsed); |
|
563 | } |
|
564 | ||
565 | // -------------------------------------------------------------------- |
|
566 | ||
567 | /** |
|
568 | * Write Cache |
|
569 | * |
|
570 | * @param string $output Output data to cache |
|
571 | * @return void |
|
572 | */ |
|
573 | public function _write_cache($output) |
|
574 | { |
|
575 | $CI =& get_instance(); |
|
576 | $path = $CI->config->item('cache_path'); |
|
577 | $cache_path = ($path === '') ? APPPATH.'cache/' : $path; |
|
578 | ||
579 | if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) |
|
580 | { |
|
581 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
582 | return; |
|
583 | } |
|
584 | ||
585 | $uri = $CI->config->item('base_url') |
|
586 | .$CI->config->item('index_page') |
|
587 | .$CI->uri->uri_string(); |
|
588 | ||
589 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
590 | { |
|
591 | if (is_array($cache_query_string)) |
|
592 | { |
|
593 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
594 | } |
|
595 | else |
|
596 | { |
|
597 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
598 | } |
|
599 | } |
|
600 | ||
601 | $cache_path .= md5($uri); |
|
602 | ||
603 | if ( ! $fp = @fopen($cache_path, 'w+b')) |
|
604 | { |
|
605 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
606 | return; |
|
607 | } |
|
608 | ||
609 | if ( ! flock($fp, LOCK_EX)) |
|
610 | { |
|
611 | log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); |
|
612 | fclose($fp); |
|
613 | return; |
|
614 | } |
|
615 | ||
616 | // If output compression is enabled, compress the cache |
|
617 | // itself, so that we don't have to do that each time |
|
618 | // we're serving it |
|
619 | if ($this->_compress_output === TRUE) |
|
620 | { |
|
621 | $output = gzencode($output); |
|
622 | ||
623 | if ($this->get_header('content-type') === NULL) |
|
624 | { |
|
625 | $this->set_content_type($this->mime_type); |
|
626 | } |
|
627 | } |
|
628 | ||
629 | $expire = time() + ($this->cache_expiration * 60); |
|
630 | ||
631 | // Put together our serialized info. |
|
632 | $cache_info = serialize(array( |
|
633 | 'expire' => $expire, |
|
634 | 'headers' => $this->headers |
|
635 | )); |
|
636 | ||
637 | $output = $cache_info.'ENDCI--->'.$output; |
|
638 | ||
639 | for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result) |
|
640 | { |
|
641 | if (($result = fwrite($fp, self::substr($output, $written))) === FALSE) |
|
642 | { |
|
643 | break; |
|
644 | } |
|
645 | } |
|
646 | ||
647 | flock($fp, LOCK_UN); |
|
648 | fclose($fp); |
|
649 | ||
650 | if ( ! is_int($result)) |
|
651 | { |
|
652 | @unlink($cache_path); |
|
653 | log_message('error', 'Unable to write the complete cache content at: '.$cache_path); |
|
654 | return; |
|
655 | } |
|
656 | ||
657 | chmod($cache_path, 0640); |
|
658 | log_message('debug', 'Cache file written: '.$cache_path); |
|
659 | ||
660 | // Send HTTP cache-control headers to browser to match file cache settings. |
|
661 | $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); |
|
662 | } |
|
663 | ||
664 | // -------------------------------------------------------------------- |
|
665 | ||
666 | /** |
|
667 | * Update/serve cached output |
|
668 | * |
|
669 | * @uses CI_Config |
|
670 | * @uses CI_URI |
|
671 | * |
|
672 | * @param object &$CFG CI_Config class instance |
|
673 | * @param object &$URI CI_URI class instance |
|
674 | * @return bool TRUE on success or FALSE on failure |
|
675 | */ |
|
676 | public function _display_cache(&$CFG, &$URI) |
|
677 | { |
|
678 | $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path'); |
|
679 | ||
680 | // Build the file path. The file name is an MD5 hash of the full URI |
|
681 | $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string; |
|
682 | ||
683 | if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
684 | { |
|
685 | if (is_array($cache_query_string)) |
|
686 | { |
|
687 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
688 | } |
|
689 | else |
|
690 | { |
|
691 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
692 | } |
|
693 | } |
|
694 | ||
695 | $filepath = $cache_path.md5($uri); |
|
696 | ||
697 | if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb')) |
|
698 | { |
|
699 | return FALSE; |
|
700 | } |
|
701 | ||
702 | flock($fp, LOCK_SH); |
|
703 | ||
704 | $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : ''; |
|
705 | ||
706 | flock($fp, LOCK_UN); |
|
707 | fclose($fp); |
|
708 | ||
709 | // Look for embedded serialized file info. |
|
710 | if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match)) |
|
711 | { |
|
712 | return FALSE; |
|
713 | } |
|
714 | ||
715 | $cache_info = unserialize($match[1]); |
|
716 | $expire = $cache_info['expire']; |
|
717 | ||
718 | $last_modified = filemtime($filepath); |
|
719 | ||
720 | // Has the file expired? |
|
721 | if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) |
|
722 | { |
|
723 | // If so we'll delete it. |
|
724 | @unlink($filepath); |
|
725 | log_message('debug', 'Cache file has expired. File deleted.'); |
|
726 | return FALSE; |
|
727 | } |
|
728 | ||
729 | // Send the HTTP cache control headers |
|
730 | $this->set_cache_header($last_modified, $expire); |
|
731 | ||
732 | // Add headers from cache file. |
|
733 | foreach ($cache_info['headers'] as $header) |
|
734 | { |
|
735 | $this->set_header($header[0], $header[1]); |
|
736 | } |
|
737 | ||
738 | // Display the cache |
|
739 | $this->_display(self::substr($cache, self::strlen($match[0]))); |
|
740 | log_message('debug', 'Cache file is current. Sending it to browser.'); |
|
741 | return TRUE; |
|
742 | } |
|
743 | ||
744 | // -------------------------------------------------------------------- |
|
745 | ||
746 | /** |
|
747 | * Delete cache |
|
748 | * |
|
749 | * @param string $uri URI string |
|
750 | * @return bool |
|
751 | */ |
|
752 | public function delete_cache($uri = '') |
|
753 | { |
|
754 | $CI =& get_instance(); |
|
755 | $cache_path = $CI->config->item('cache_path'); |
|
756 | if ($cache_path === '') |
|
757 | { |
|
758 | $cache_path = APPPATH.'cache/'; |
|
759 | } |
|
760 | ||
761 | if ( ! is_dir($cache_path)) |
|
762 | { |
|
763 | log_message('error', 'Unable to find cache path: '.$cache_path); |
|
764 | return FALSE; |
|
765 | } |
|
766 | ||
767 | if (empty($uri)) |
|
768 | { |
|
769 | $uri = $CI->uri->uri_string(); |
|
770 | ||
771 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
772 | { |
|
773 | if (is_array($cache_query_string)) |
|
774 | { |
|
775 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
776 | } |
|
777 | else |
|
778 | { |
|
779 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
780 | } |
|
781 | } |
|
782 | } |
|
783 | ||
784 | $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/')); |
|
785 | ||
786 | if ( ! @unlink($cache_path)) |
|
787 | { |
|
788 | log_message('error', 'Unable to delete cache file for '.$uri); |
|
789 | return FALSE; |
|
790 | } |
|
791 | ||
792 | return TRUE; |
|
793 | } |
|
794 | ||
795 | // -------------------------------------------------------------------- |
|
796 | ||
797 | /** |
|
798 | * Set Cache Header |
|
799 | * |
|
800 | * Set the HTTP headers to match the server-side file cache settings |
|
801 | * in order to reduce bandwidth. |
|
802 | * |
|
803 | * @param int $last_modified Timestamp of when the page was last modified |
|
804 | * @param int $expiration Timestamp of when should the requested page expire from cache |
|
805 | * @return void |
|
806 | * |
|
807 | * modified by ci-phpunit-test |
|
808 | */ |
|
809 | public function set_cache_header($last_modified, $expiration) |
|
810 | { |
|
811 | $max_age = $expiration - $_SERVER['REQUEST_TIME']; |
|
812 | ||
813 | if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) |
|
814 | { |
|
815 | $this->set_status_header(304); |
|
816 | exit; |
|
817 | } |
|
818 | ||
819 | // header('Pragma: public'); |
|
820 | // header('Cache-Control: max-age='.$max_age.', public'); |
|
821 | // header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); |
|
822 | // header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); |
|
823 | } |
|
824 | ||
825 | // -------------------------------------------------------------------- |
|
826 | ||
827 | /** |
|
828 | * Byte-safe strlen() |
|
829 | * |
|
830 | * @param string $str |
|
831 | * @return int |
|
832 | */ |
|
833 | protected static function strlen($str) |
|
834 | { |
|
835 | return (self::$func_overload) |
|
836 | ? mb_strlen($str, '8bit') |
|
837 | : strlen($str); |
|
838 | } |
|
839 | ||
840 | // -------------------------------------------------------------------- |
|
841 | ||
842 | /** |
|
843 | * Byte-safe substr() |
|
844 | * |
|
845 | * @param string $str |
|
846 | * @param int $start |
|
847 | * @param int $length |
|
848 | * @return string |
|
849 | */ |
|
850 | protected static function substr($str, $start, $length = NULL) |
|
851 | { |
|
852 | if (self::$func_overload) |
|
853 | { |
|
854 | // mb_substr($str, $start, null, '8bit') returns an empty |
|
855 | // string on PHP 5.3 |
|
856 | isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); |
|
857 | return mb_substr($str, $start, $length, '8bit'); |
|
858 | } |
|
859 | ||
860 | return isset($length) |
|
861 | ? substr($str, $start, $length) |
|
862 | : substr($str, $start); |
|
863 | } |
|
864 | } |
|
865 |
@@ 51-864 (lines=814) @@ | ||
48 | * @author EllisLab Dev Team |
|
49 | * @link https://codeigniter.com/user_guide/libraries/output.html |
|
50 | */ |
|
51 | class CI_Output { |
|
52 | ||
53 | /** |
|
54 | * Final output string |
|
55 | * |
|
56 | * @var string |
|
57 | */ |
|
58 | public $final_output; |
|
59 | ||
60 | /** |
|
61 | * Cache expiration time |
|
62 | * |
|
63 | * @var int |
|
64 | */ |
|
65 | public $cache_expiration = 0; |
|
66 | ||
67 | /** |
|
68 | * List of server headers |
|
69 | * |
|
70 | * @var array |
|
71 | */ |
|
72 | public $headers = array(); |
|
73 | ||
74 | /** |
|
75 | * List of mime types |
|
76 | * |
|
77 | * @var array |
|
78 | */ |
|
79 | public $mimes = array(); |
|
80 | ||
81 | /** |
|
82 | * Mime-type for the current page |
|
83 | * |
|
84 | * @var string |
|
85 | */ |
|
86 | protected $mime_type = 'text/html'; |
|
87 | ||
88 | /** |
|
89 | * Enable Profiler flag |
|
90 | * |
|
91 | * @var bool |
|
92 | */ |
|
93 | public $enable_profiler = FALSE; |
|
94 | ||
95 | /** |
|
96 | * php.ini zlib.output_compression flag |
|
97 | * |
|
98 | * @var bool |
|
99 | */ |
|
100 | protected $_zlib_oc = FALSE; |
|
101 | ||
102 | /** |
|
103 | * CI output compression flag |
|
104 | * |
|
105 | * @var bool |
|
106 | */ |
|
107 | protected $_compress_output = FALSE; |
|
108 | ||
109 | /** |
|
110 | * List of profiler sections |
|
111 | * |
|
112 | * @var array |
|
113 | */ |
|
114 | protected $_profiler_sections = array(); |
|
115 | ||
116 | /** |
|
117 | * Parse markers flag |
|
118 | * |
|
119 | * Whether or not to parse variables like {elapsed_time} and {memory_usage}. |
|
120 | * |
|
121 | * @var bool |
|
122 | */ |
|
123 | public $parse_exec_vars = TRUE; |
|
124 | ||
125 | /** |
|
126 | * mbstring.func_overload flag |
|
127 | * |
|
128 | * @var bool |
|
129 | */ |
|
130 | protected static $func_overload; |
|
131 | ||
132 | /** |
|
133 | * Response status (status code, text and is redirection?) |
|
134 | * |
|
135 | * @var array |
|
136 | * |
|
137 | * added by ci-phpunit-test |
|
138 | */ |
|
139 | public $_status; |
|
140 | ||
141 | /** |
|
142 | * Cookies |
|
143 | * |
|
144 | * @var array |
|
145 | * |
|
146 | * added by ci-phpunit-test |
|
147 | */ |
|
148 | public $_cookies; |
|
149 | ||
150 | /** |
|
151 | * Class constructor |
|
152 | * |
|
153 | * Determines whether zLib output compression will be used. |
|
154 | * |
|
155 | * @return void |
|
156 | */ |
|
157 | public function __construct() |
|
158 | { |
|
159 | $this->_zlib_oc = (bool) ini_get('zlib.output_compression'); |
|
160 | $this->_compress_output = ( |
|
161 | $this->_zlib_oc === FALSE |
|
162 | && config_item('compress_output') === TRUE |
|
163 | && extension_loaded('zlib') |
|
164 | ); |
|
165 | ||
166 | isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); |
|
167 | ||
168 | // Get mime types for later |
|
169 | $this->mimes =& get_mimes(); |
|
170 | ||
171 | log_message('info', 'Output Class Initialized'); |
|
172 | } |
|
173 | ||
174 | // -------------------------------------------------------------------- |
|
175 | ||
176 | /** |
|
177 | * Get Output |
|
178 | * |
|
179 | * Returns the current output string. |
|
180 | * |
|
181 | * @return string |
|
182 | */ |
|
183 | public function get_output() |
|
184 | { |
|
185 | return $this->final_output; |
|
186 | } |
|
187 | ||
188 | // -------------------------------------------------------------------- |
|
189 | ||
190 | /** |
|
191 | * Set Output |
|
192 | * |
|
193 | * Sets the output string. |
|
194 | * |
|
195 | * @param string $output Output data |
|
196 | * @return CI_Output |
|
197 | */ |
|
198 | public function set_output($output) |
|
199 | { |
|
200 | $this->final_output = $output; |
|
201 | return $this; |
|
202 | } |
|
203 | ||
204 | // -------------------------------------------------------------------- |
|
205 | ||
206 | /** |
|
207 | * Append Output |
|
208 | * |
|
209 | * Appends data onto the output string. |
|
210 | * |
|
211 | * @param string $output Data to append |
|
212 | * @return CI_Output |
|
213 | */ |
|
214 | public function append_output($output) |
|
215 | { |
|
216 | $this->final_output .= $output; |
|
217 | return $this; |
|
218 | } |
|
219 | ||
220 | // -------------------------------------------------------------------- |
|
221 | ||
222 | /** |
|
223 | * Set Header |
|
224 | * |
|
225 | * Lets you set a server header which will be sent with the final output. |
|
226 | * |
|
227 | * Note: If a file is cached, headers will not be sent. |
|
228 | * @todo We need to figure out how to permit headers to be cached. |
|
229 | * |
|
230 | * @param string $header Header |
|
231 | * @param bool $replace Whether to replace the old header value, if already set |
|
232 | * @return CI_Output |
|
233 | */ |
|
234 | public function set_header($header, $replace = TRUE) |
|
235 | { |
|
236 | // If zlib.output_compression is enabled it will compress the output, |
|
237 | // but it will not modify the content-length header to compensate for |
|
238 | // the reduction, causing the browser to hang waiting for more data. |
|
239 | // We'll just skip content-length in those cases. |
|
240 | if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) |
|
241 | { |
|
242 | return $this; |
|
243 | } |
|
244 | ||
245 | $this->headers[] = array($header, $replace); |
|
246 | return $this; |
|
247 | } |
|
248 | ||
249 | // -------------------------------------------------------------------- |
|
250 | ||
251 | /** |
|
252 | * Set Content-Type Header |
|
253 | * |
|
254 | * @param string $mime_type Extension of the file we're outputting |
|
255 | * @param string $charset Character set (default: NULL) |
|
256 | * @return CI_Output |
|
257 | */ |
|
258 | public function set_content_type($mime_type, $charset = NULL) |
|
259 | { |
|
260 | if (strpos($mime_type, '/') === FALSE) |
|
261 | { |
|
262 | $extension = ltrim($mime_type, '.'); |
|
263 | ||
264 | // Is this extension supported? |
|
265 | if (isset($this->mimes[$extension])) |
|
266 | { |
|
267 | $mime_type =& $this->mimes[$extension]; |
|
268 | ||
269 | if (is_array($mime_type)) |
|
270 | { |
|
271 | $mime_type = current($mime_type); |
|
272 | } |
|
273 | } |
|
274 | } |
|
275 | ||
276 | $this->mime_type = $mime_type; |
|
277 | ||
278 | if (empty($charset)) |
|
279 | { |
|
280 | $charset = config_item('charset'); |
|
281 | } |
|
282 | ||
283 | $header = 'Content-Type: '.$mime_type |
|
284 | .(empty($charset) ? '' : '; charset='.$charset); |
|
285 | ||
286 | $this->headers[] = array($header, TRUE); |
|
287 | return $this; |
|
288 | } |
|
289 | ||
290 | // -------------------------------------------------------------------- |
|
291 | ||
292 | /** |
|
293 | * Get Current Content-Type Header |
|
294 | * |
|
295 | * @return string 'text/html', if not already set |
|
296 | */ |
|
297 | public function get_content_type() |
|
298 | { |
|
299 | for ($i = 0, $c = count($this->headers); $i < $c; $i++) |
|
300 | { |
|
301 | if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) |
|
302 | { |
|
303 | return $content_type; |
|
304 | } |
|
305 | } |
|
306 | ||
307 | return 'text/html'; |
|
308 | } |
|
309 | ||
310 | // -------------------------------------------------------------------- |
|
311 | ||
312 | /** |
|
313 | * Get Header |
|
314 | * |
|
315 | * @param string $header |
|
316 | * @return string |
|
317 | */ |
|
318 | public function get_header($header) |
|
319 | { |
|
320 | // Combine headers already sent with our batched headers |
|
321 | $headers = array_merge( |
|
322 | // We only need [x][0] from our multi-dimensional array |
|
323 | array_map('array_shift', $this->headers), |
|
324 | headers_list() |
|
325 | ); |
|
326 | ||
327 | if (empty($headers) OR empty($header)) |
|
328 | { |
|
329 | return NULL; |
|
330 | } |
|
331 | ||
332 | // Count backwards, in order to get the last matching header |
|
333 | for ($c = count($headers) - 1; $c > -1; $c--) |
|
334 | { |
|
335 | if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0) |
|
336 | { |
|
337 | return trim(self::substr($headers[$c], $l+1)); |
|
338 | } |
|
339 | } |
|
340 | ||
341 | return NULL; |
|
342 | } |
|
343 | ||
344 | // -------------------------------------------------------------------- |
|
345 | ||
346 | /** |
|
347 | * Set HTTP Status Header |
|
348 | * |
|
349 | * As of version 1.7.2, this is an alias for common function |
|
350 | * set_status_header(). |
|
351 | * |
|
352 | * @param int $code Status code (default: 200) |
|
353 | * @param string $text Optional message |
|
354 | * @return CI_Output |
|
355 | */ |
|
356 | public function set_status_header($code = 200, $text = '') |
|
357 | { |
|
358 | set_status_header($code, $text); |
|
359 | return $this; |
|
360 | } |
|
361 | ||
362 | // -------------------------------------------------------------------- |
|
363 | ||
364 | /** |
|
365 | * Enable/disable Profiler |
|
366 | * |
|
367 | * @param bool $val TRUE to enable or FALSE to disable |
|
368 | * @return CI_Output |
|
369 | */ |
|
370 | public function enable_profiler($val = TRUE) |
|
371 | { |
|
372 | $this->enable_profiler = is_bool($val) ? $val : TRUE; |
|
373 | return $this; |
|
374 | } |
|
375 | ||
376 | // -------------------------------------------------------------------- |
|
377 | ||
378 | /** |
|
379 | * Set Profiler Sections |
|
380 | * |
|
381 | * Allows override of default/config settings for |
|
382 | * Profiler section display. |
|
383 | * |
|
384 | * @param array $sections Profiler sections |
|
385 | * @return CI_Output |
|
386 | */ |
|
387 | public function set_profiler_sections($sections) |
|
388 | { |
|
389 | if (isset($sections['query_toggle_count'])) |
|
390 | { |
|
391 | $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count']; |
|
392 | unset($sections['query_toggle_count']); |
|
393 | } |
|
394 | ||
395 | foreach ($sections as $section => $enable) |
|
396 | { |
|
397 | $this->_profiler_sections[$section] = ($enable !== FALSE); |
|
398 | } |
|
399 | ||
400 | return $this; |
|
401 | } |
|
402 | ||
403 | // -------------------------------------------------------------------- |
|
404 | ||
405 | /** |
|
406 | * Set Cache |
|
407 | * |
|
408 | * @param int $time Cache expiration time in minutes |
|
409 | * @return CI_Output |
|
410 | */ |
|
411 | public function cache($time) |
|
412 | { |
|
413 | $this->cache_expiration = is_numeric($time) ? $time : 0; |
|
414 | return $this; |
|
415 | } |
|
416 | ||
417 | // -------------------------------------------------------------------- |
|
418 | ||
419 | /** |
|
420 | * Display Output |
|
421 | * |
|
422 | * Processes and sends finalized output data to the browser along |
|
423 | * with any server headers and profile data. It also stops benchmark |
|
424 | * timers so the page rendering speed and memory usage can be shown. |
|
425 | * |
|
426 | * Note: All "view" data is automatically put into $this->final_output |
|
427 | * by controller class. |
|
428 | * |
|
429 | * @uses CI_Output::$final_output |
|
430 | * @param string $output Output data override |
|
431 | * @return void |
|
432 | * |
|
433 | * modified by ci-phpunit-test |
|
434 | */ |
|
435 | public function _display($output = '') |
|
436 | { |
|
437 | // Note: We use load_class() because we can't use $CI =& get_instance() |
|
438 | // since this function is sometimes called by the caching mechanism, |
|
439 | // which happens before the CI super object is available. |
|
440 | $BM =& load_class('Benchmark', 'core'); |
|
441 | $CFG =& load_class('Config', 'core'); |
|
442 | ||
443 | // Grab the super object if we can. |
|
444 | if (class_exists('CI_Controller', FALSE)) |
|
445 | { |
|
446 | $CI =& get_instance(); |
|
447 | } |
|
448 | ||
449 | // -------------------------------------------------------------------- |
|
450 | ||
451 | // Set the output data |
|
452 | if ($output === '') |
|
453 | { |
|
454 | $output =& $this->final_output; |
|
455 | } |
|
456 | ||
457 | // -------------------------------------------------------------------- |
|
458 | ||
459 | // Do we need to write a cache file? Only if the controller does not have its |
|
460 | // own _output() method and we are not dealing with a cache file, which we |
|
461 | // can determine by the existence of the $CI object above |
|
462 | if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')) |
|
463 | { |
|
464 | $this->_write_cache($output); |
|
465 | } |
|
466 | ||
467 | // -------------------------------------------------------------------- |
|
468 | ||
469 | // Parse out the elapsed time and memory usage, |
|
470 | // then swap the pseudo-variables with the data |
|
471 | ||
472 | $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); |
|
473 | ||
474 | if ($this->parse_exec_vars === TRUE) |
|
475 | { |
|
476 | $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; |
|
477 | $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); |
|
478 | } |
|
479 | ||
480 | // -------------------------------------------------------------------- |
|
481 | ||
482 | // Is compression requested? |
|
483 | if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed |
|
484 | && $this->_compress_output === TRUE |
|
485 | && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
486 | { |
|
487 | ob_start('ob_gzhandler'); |
|
488 | } |
|
489 | ||
490 | // -------------------------------------------------------------------- |
|
491 | ||
492 | // Are there any server headers to send? |
|
493 | if (count($this->headers) > 0) |
|
494 | { |
|
495 | foreach ($this->headers as $header) |
|
496 | { |
|
497 | // @header($header[0], $header[1]); |
|
498 | } |
|
499 | } |
|
500 | ||
501 | // -------------------------------------------------------------------- |
|
502 | ||
503 | // Does the $CI object exist? |
|
504 | // If not we know we are dealing with a cache file so we'll |
|
505 | // simply echo out the data and exit. |
|
506 | if ( ! isset($CI)) |
|
507 | { |
|
508 | if ($this->_compress_output === TRUE) |
|
509 | { |
|
510 | if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
511 | { |
|
512 | // header('Content-Encoding: gzip'); |
|
513 | // header('Content-Length: '.self::strlen($output)); |
|
514 | } |
|
515 | else |
|
516 | { |
|
517 | // User agent doesn't support gzip compression, |
|
518 | // so we'll have to decompress our cache |
|
519 | $output = gzinflate(self::substr($output, 10, -8)); |
|
520 | } |
|
521 | } |
|
522 | ||
523 | echo $output; |
|
524 | log_message('info', 'Final output sent to browser'); |
|
525 | log_message('debug', 'Total execution time: '.$elapsed); |
|
526 | return; |
|
527 | } |
|
528 | ||
529 | // -------------------------------------------------------------------- |
|
530 | ||
531 | // Do we need to generate profile data? |
|
532 | // If so, load the Profile class and run it. |
|
533 | if ($this->enable_profiler === TRUE) |
|
534 | { |
|
535 | $CI->load->library('profiler'); |
|
536 | if ( ! empty($this->_profiler_sections)) |
|
537 | { |
|
538 | $CI->profiler->set_sections($this->_profiler_sections); |
|
539 | } |
|
540 | ||
541 | // If the output data contains closing </body> and </html> tags |
|
542 | // we will remove them and add them back after we insert the profile data |
|
543 | $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run(); |
|
544 | if ($count > 0) |
|
545 | { |
|
546 | $output .= '</body></html>'; |
|
547 | } |
|
548 | } |
|
549 | ||
550 | // Does the controller contain a function named _output()? |
|
551 | // If so send the output there. Otherwise, echo it. |
|
552 | if (method_exists($CI, '_output')) |
|
553 | { |
|
554 | $CI->_output($output); |
|
555 | } |
|
556 | else |
|
557 | { |
|
558 | echo $output; // Send it to the browser! |
|
559 | } |
|
560 | ||
561 | log_message('info', 'Final output sent to browser'); |
|
562 | log_message('debug', 'Total execution time: '.$elapsed); |
|
563 | } |
|
564 | ||
565 | // -------------------------------------------------------------------- |
|
566 | ||
567 | /** |
|
568 | * Write Cache |
|
569 | * |
|
570 | * @param string $output Output data to cache |
|
571 | * @return void |
|
572 | */ |
|
573 | public function _write_cache($output) |
|
574 | { |
|
575 | $CI =& get_instance(); |
|
576 | $path = $CI->config->item('cache_path'); |
|
577 | $cache_path = ($path === '') ? APPPATH.'cache/' : $path; |
|
578 | ||
579 | if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) |
|
580 | { |
|
581 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
582 | return; |
|
583 | } |
|
584 | ||
585 | $uri = $CI->config->item('base_url') |
|
586 | .$CI->config->item('index_page') |
|
587 | .$CI->uri->uri_string(); |
|
588 | ||
589 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
590 | { |
|
591 | if (is_array($cache_query_string)) |
|
592 | { |
|
593 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
594 | } |
|
595 | else |
|
596 | { |
|
597 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
598 | } |
|
599 | } |
|
600 | ||
601 | $cache_path .= md5($uri); |
|
602 | ||
603 | if ( ! $fp = @fopen($cache_path, 'w+b')) |
|
604 | { |
|
605 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
606 | return; |
|
607 | } |
|
608 | ||
609 | if ( ! flock($fp, LOCK_EX)) |
|
610 | { |
|
611 | log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); |
|
612 | fclose($fp); |
|
613 | return; |
|
614 | } |
|
615 | ||
616 | // If output compression is enabled, compress the cache |
|
617 | // itself, so that we don't have to do that each time |
|
618 | // we're serving it |
|
619 | if ($this->_compress_output === TRUE) |
|
620 | { |
|
621 | $output = gzencode($output); |
|
622 | ||
623 | if ($this->get_header('content-type') === NULL) |
|
624 | { |
|
625 | $this->set_content_type($this->mime_type); |
|
626 | } |
|
627 | } |
|
628 | ||
629 | $expire = time() + ($this->cache_expiration * 60); |
|
630 | ||
631 | // Put together our serialized info. |
|
632 | $cache_info = serialize(array( |
|
633 | 'expire' => $expire, |
|
634 | 'headers' => $this->headers |
|
635 | )); |
|
636 | ||
637 | $output = $cache_info.'ENDCI--->'.$output; |
|
638 | ||
639 | for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result) |
|
640 | { |
|
641 | if (($result = fwrite($fp, self::substr($output, $written))) === FALSE) |
|
642 | { |
|
643 | break; |
|
644 | } |
|
645 | } |
|
646 | ||
647 | flock($fp, LOCK_UN); |
|
648 | fclose($fp); |
|
649 | ||
650 | if ( ! is_int($result)) |
|
651 | { |
|
652 | @unlink($cache_path); |
|
653 | log_message('error', 'Unable to write the complete cache content at: '.$cache_path); |
|
654 | return; |
|
655 | } |
|
656 | ||
657 | chmod($cache_path, 0640); |
|
658 | log_message('debug', 'Cache file written: '.$cache_path); |
|
659 | ||
660 | // Send HTTP cache-control headers to browser to match file cache settings. |
|
661 | $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); |
|
662 | } |
|
663 | ||
664 | // -------------------------------------------------------------------- |
|
665 | ||
666 | /** |
|
667 | * Update/serve cached output |
|
668 | * |
|
669 | * @uses CI_Config |
|
670 | * @uses CI_URI |
|
671 | * |
|
672 | * @param object &$CFG CI_Config class instance |
|
673 | * @param object &$URI CI_URI class instance |
|
674 | * @return bool TRUE on success or FALSE on failure |
|
675 | */ |
|
676 | public function _display_cache(&$CFG, &$URI) |
|
677 | { |
|
678 | $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path'); |
|
679 | ||
680 | // Build the file path. The file name is an MD5 hash of the full URI |
|
681 | $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string; |
|
682 | ||
683 | if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
684 | { |
|
685 | if (is_array($cache_query_string)) |
|
686 | { |
|
687 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
688 | } |
|
689 | else |
|
690 | { |
|
691 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
692 | } |
|
693 | } |
|
694 | ||
695 | $filepath = $cache_path.md5($uri); |
|
696 | ||
697 | if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb')) |
|
698 | { |
|
699 | return FALSE; |
|
700 | } |
|
701 | ||
702 | flock($fp, LOCK_SH); |
|
703 | ||
704 | $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : ''; |
|
705 | ||
706 | flock($fp, LOCK_UN); |
|
707 | fclose($fp); |
|
708 | ||
709 | // Look for embedded serialized file info. |
|
710 | if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match)) |
|
711 | { |
|
712 | return FALSE; |
|
713 | } |
|
714 | ||
715 | $cache_info = unserialize($match[1]); |
|
716 | $expire = $cache_info['expire']; |
|
717 | ||
718 | $last_modified = filemtime($filepath); |
|
719 | ||
720 | // Has the file expired? |
|
721 | if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) |
|
722 | { |
|
723 | // If so we'll delete it. |
|
724 | @unlink($filepath); |
|
725 | log_message('debug', 'Cache file has expired. File deleted.'); |
|
726 | return FALSE; |
|
727 | } |
|
728 | ||
729 | // Send the HTTP cache control headers |
|
730 | $this->set_cache_header($last_modified, $expire); |
|
731 | ||
732 | // Add headers from cache file. |
|
733 | foreach ($cache_info['headers'] as $header) |
|
734 | { |
|
735 | $this->set_header($header[0], $header[1]); |
|
736 | } |
|
737 | ||
738 | // Display the cache |
|
739 | $this->_display(self::substr($cache, self::strlen($match[0]))); |
|
740 | log_message('debug', 'Cache file is current. Sending it to browser.'); |
|
741 | return TRUE; |
|
742 | } |
|
743 | ||
744 | // -------------------------------------------------------------------- |
|
745 | ||
746 | /** |
|
747 | * Delete cache |
|
748 | * |
|
749 | * @param string $uri URI string |
|
750 | * @return bool |
|
751 | */ |
|
752 | public function delete_cache($uri = '') |
|
753 | { |
|
754 | $CI =& get_instance(); |
|
755 | $cache_path = $CI->config->item('cache_path'); |
|
756 | if ($cache_path === '') |
|
757 | { |
|
758 | $cache_path = APPPATH.'cache/'; |
|
759 | } |
|
760 | ||
761 | if ( ! is_dir($cache_path)) |
|
762 | { |
|
763 | log_message('error', 'Unable to find cache path: '.$cache_path); |
|
764 | return FALSE; |
|
765 | } |
|
766 | ||
767 | if (empty($uri)) |
|
768 | { |
|
769 | $uri = $CI->uri->uri_string(); |
|
770 | ||
771 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
772 | { |
|
773 | if (is_array($cache_query_string)) |
|
774 | { |
|
775 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
776 | } |
|
777 | else |
|
778 | { |
|
779 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
780 | } |
|
781 | } |
|
782 | } |
|
783 | ||
784 | $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/')); |
|
785 | ||
786 | if ( ! @unlink($cache_path)) |
|
787 | { |
|
788 | log_message('error', 'Unable to delete cache file for '.$uri); |
|
789 | return FALSE; |
|
790 | } |
|
791 | ||
792 | return TRUE; |
|
793 | } |
|
794 | ||
795 | // -------------------------------------------------------------------- |
|
796 | ||
797 | /** |
|
798 | * Set Cache Header |
|
799 | * |
|
800 | * Set the HTTP headers to match the server-side file cache settings |
|
801 | * in order to reduce bandwidth. |
|
802 | * |
|
803 | * @param int $last_modified Timestamp of when the page was last modified |
|
804 | * @param int $expiration Timestamp of when should the requested page expire from cache |
|
805 | * @return void |
|
806 | * |
|
807 | * modified by ci-phpunit-test |
|
808 | */ |
|
809 | public function set_cache_header($last_modified, $expiration) |
|
810 | { |
|
811 | $max_age = $expiration - $_SERVER['REQUEST_TIME']; |
|
812 | ||
813 | if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) |
|
814 | { |
|
815 | $this->set_status_header(304); |
|
816 | exit; |
|
817 | } |
|
818 | ||
819 | // header('Pragma: public'); |
|
820 | // header('Cache-Control: max-age='.$max_age.', public'); |
|
821 | // header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); |
|
822 | // header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); |
|
823 | } |
|
824 | ||
825 | // -------------------------------------------------------------------- |
|
826 | ||
827 | /** |
|
828 | * Byte-safe strlen() |
|
829 | * |
|
830 | * @param string $str |
|
831 | * @return int |
|
832 | */ |
|
833 | protected static function strlen($str) |
|
834 | { |
|
835 | return (self::$func_overload) |
|
836 | ? mb_strlen($str, '8bit') |
|
837 | : strlen($str); |
|
838 | } |
|
839 | ||
840 | // -------------------------------------------------------------------- |
|
841 | ||
842 | /** |
|
843 | * Byte-safe substr() |
|
844 | * |
|
845 | * @param string $str |
|
846 | * @param int $start |
|
847 | * @param int $length |
|
848 | * @return string |
|
849 | */ |
|
850 | protected static function substr($str, $start, $length = NULL) |
|
851 | { |
|
852 | if (self::$func_overload) |
|
853 | { |
|
854 | // mb_substr($str, $start, null, '8bit') returns an empty |
|
855 | // string on PHP 5.3 |
|
856 | isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); |
|
857 | return mb_substr($str, $start, $length, '8bit'); |
|
858 | } |
|
859 | ||
860 | return isset($length) |
|
861 | ? substr($str, $start, $length) |
|
862 | : substr($str, $start); |
|
863 | } |
|
864 | } |
|
865 |
@@ 51-864 (lines=814) @@ | ||
48 | * @author EllisLab Dev Team |
|
49 | * @link https://codeigniter.com/user_guide/libraries/output.html |
|
50 | */ |
|
51 | class CI_Output { |
|
52 | ||
53 | /** |
|
54 | * Final output string |
|
55 | * |
|
56 | * @var string |
|
57 | */ |
|
58 | public $final_output; |
|
59 | ||
60 | /** |
|
61 | * Cache expiration time |
|
62 | * |
|
63 | * @var int |
|
64 | */ |
|
65 | public $cache_expiration = 0; |
|
66 | ||
67 | /** |
|
68 | * List of server headers |
|
69 | * |
|
70 | * @var array |
|
71 | */ |
|
72 | public $headers = array(); |
|
73 | ||
74 | /** |
|
75 | * List of mime types |
|
76 | * |
|
77 | * @var array |
|
78 | */ |
|
79 | public $mimes = array(); |
|
80 | ||
81 | /** |
|
82 | * Mime-type for the current page |
|
83 | * |
|
84 | * @var string |
|
85 | */ |
|
86 | protected $mime_type = 'text/html'; |
|
87 | ||
88 | /** |
|
89 | * Enable Profiler flag |
|
90 | * |
|
91 | * @var bool |
|
92 | */ |
|
93 | public $enable_profiler = FALSE; |
|
94 | ||
95 | /** |
|
96 | * php.ini zlib.output_compression flag |
|
97 | * |
|
98 | * @var bool |
|
99 | */ |
|
100 | protected $_zlib_oc = FALSE; |
|
101 | ||
102 | /** |
|
103 | * CI output compression flag |
|
104 | * |
|
105 | * @var bool |
|
106 | */ |
|
107 | protected $_compress_output = FALSE; |
|
108 | ||
109 | /** |
|
110 | * List of profiler sections |
|
111 | * |
|
112 | * @var array |
|
113 | */ |
|
114 | protected $_profiler_sections = array(); |
|
115 | ||
116 | /** |
|
117 | * Parse markers flag |
|
118 | * |
|
119 | * Whether or not to parse variables like {elapsed_time} and {memory_usage}. |
|
120 | * |
|
121 | * @var bool |
|
122 | */ |
|
123 | public $parse_exec_vars = TRUE; |
|
124 | ||
125 | /** |
|
126 | * mbstring.func_overload flag |
|
127 | * |
|
128 | * @var bool |
|
129 | */ |
|
130 | protected static $func_overload; |
|
131 | ||
132 | /** |
|
133 | * Response status (status code, text and is redirection?) |
|
134 | * |
|
135 | * @var array |
|
136 | * |
|
137 | * added by ci-phpunit-test |
|
138 | */ |
|
139 | public $_status; |
|
140 | ||
141 | /** |
|
142 | * Cookies |
|
143 | * |
|
144 | * @var array |
|
145 | * |
|
146 | * added by ci-phpunit-test |
|
147 | */ |
|
148 | public $_cookies; |
|
149 | ||
150 | /** |
|
151 | * Class constructor |
|
152 | * |
|
153 | * Determines whether zLib output compression will be used. |
|
154 | * |
|
155 | * @return void |
|
156 | */ |
|
157 | public function __construct() |
|
158 | { |
|
159 | $this->_zlib_oc = (bool) ini_get('zlib.output_compression'); |
|
160 | $this->_compress_output = ( |
|
161 | $this->_zlib_oc === FALSE |
|
162 | && config_item('compress_output') === TRUE |
|
163 | && extension_loaded('zlib') |
|
164 | ); |
|
165 | ||
166 | isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); |
|
167 | ||
168 | // Get mime types for later |
|
169 | $this->mimes =& get_mimes(); |
|
170 | ||
171 | log_message('info', 'Output Class Initialized'); |
|
172 | } |
|
173 | ||
174 | // -------------------------------------------------------------------- |
|
175 | ||
176 | /** |
|
177 | * Get Output |
|
178 | * |
|
179 | * Returns the current output string. |
|
180 | * |
|
181 | * @return string |
|
182 | */ |
|
183 | public function get_output() |
|
184 | { |
|
185 | return $this->final_output; |
|
186 | } |
|
187 | ||
188 | // -------------------------------------------------------------------- |
|
189 | ||
190 | /** |
|
191 | * Set Output |
|
192 | * |
|
193 | * Sets the output string. |
|
194 | * |
|
195 | * @param string $output Output data |
|
196 | * @return CI_Output |
|
197 | */ |
|
198 | public function set_output($output) |
|
199 | { |
|
200 | $this->final_output = $output; |
|
201 | return $this; |
|
202 | } |
|
203 | ||
204 | // -------------------------------------------------------------------- |
|
205 | ||
206 | /** |
|
207 | * Append Output |
|
208 | * |
|
209 | * Appends data onto the output string. |
|
210 | * |
|
211 | * @param string $output Data to append |
|
212 | * @return CI_Output |
|
213 | */ |
|
214 | public function append_output($output) |
|
215 | { |
|
216 | $this->final_output .= $output; |
|
217 | return $this; |
|
218 | } |
|
219 | ||
220 | // -------------------------------------------------------------------- |
|
221 | ||
222 | /** |
|
223 | * Set Header |
|
224 | * |
|
225 | * Lets you set a server header which will be sent with the final output. |
|
226 | * |
|
227 | * Note: If a file is cached, headers will not be sent. |
|
228 | * @todo We need to figure out how to permit headers to be cached. |
|
229 | * |
|
230 | * @param string $header Header |
|
231 | * @param bool $replace Whether to replace the old header value, if already set |
|
232 | * @return CI_Output |
|
233 | */ |
|
234 | public function set_header($header, $replace = TRUE) |
|
235 | { |
|
236 | // If zlib.output_compression is enabled it will compress the output, |
|
237 | // but it will not modify the content-length header to compensate for |
|
238 | // the reduction, causing the browser to hang waiting for more data. |
|
239 | // We'll just skip content-length in those cases. |
|
240 | if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) |
|
241 | { |
|
242 | return $this; |
|
243 | } |
|
244 | ||
245 | $this->headers[] = array($header, $replace); |
|
246 | return $this; |
|
247 | } |
|
248 | ||
249 | // -------------------------------------------------------------------- |
|
250 | ||
251 | /** |
|
252 | * Set Content-Type Header |
|
253 | * |
|
254 | * @param string $mime_type Extension of the file we're outputting |
|
255 | * @param string $charset Character set (default: NULL) |
|
256 | * @return CI_Output |
|
257 | */ |
|
258 | public function set_content_type($mime_type, $charset = NULL) |
|
259 | { |
|
260 | if (strpos($mime_type, '/') === FALSE) |
|
261 | { |
|
262 | $extension = ltrim($mime_type, '.'); |
|
263 | ||
264 | // Is this extension supported? |
|
265 | if (isset($this->mimes[$extension])) |
|
266 | { |
|
267 | $mime_type =& $this->mimes[$extension]; |
|
268 | ||
269 | if (is_array($mime_type)) |
|
270 | { |
|
271 | $mime_type = current($mime_type); |
|
272 | } |
|
273 | } |
|
274 | } |
|
275 | ||
276 | $this->mime_type = $mime_type; |
|
277 | ||
278 | if (empty($charset)) |
|
279 | { |
|
280 | $charset = config_item('charset'); |
|
281 | } |
|
282 | ||
283 | $header = 'Content-Type: '.$mime_type |
|
284 | .(empty($charset) ? '' : '; charset='.$charset); |
|
285 | ||
286 | $this->headers[] = array($header, TRUE); |
|
287 | return $this; |
|
288 | } |
|
289 | ||
290 | // -------------------------------------------------------------------- |
|
291 | ||
292 | /** |
|
293 | * Get Current Content-Type Header |
|
294 | * |
|
295 | * @return string 'text/html', if not already set |
|
296 | */ |
|
297 | public function get_content_type() |
|
298 | { |
|
299 | for ($i = 0, $c = count($this->headers); $i < $c; $i++) |
|
300 | { |
|
301 | if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) |
|
302 | { |
|
303 | return $content_type; |
|
304 | } |
|
305 | } |
|
306 | ||
307 | return 'text/html'; |
|
308 | } |
|
309 | ||
310 | // -------------------------------------------------------------------- |
|
311 | ||
312 | /** |
|
313 | * Get Header |
|
314 | * |
|
315 | * @param string $header |
|
316 | * @return string |
|
317 | */ |
|
318 | public function get_header($header) |
|
319 | { |
|
320 | // Combine headers already sent with our batched headers |
|
321 | $headers = array_merge( |
|
322 | // We only need [x][0] from our multi-dimensional array |
|
323 | array_map('array_shift', $this->headers), |
|
324 | headers_list() |
|
325 | ); |
|
326 | ||
327 | if (empty($headers) OR empty($header)) |
|
328 | { |
|
329 | return NULL; |
|
330 | } |
|
331 | ||
332 | // Count backwards, in order to get the last matching header |
|
333 | for ($c = count($headers) - 1; $c > -1; $c--) |
|
334 | { |
|
335 | if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0) |
|
336 | { |
|
337 | return trim(self::substr($headers[$c], $l+1)); |
|
338 | } |
|
339 | } |
|
340 | ||
341 | return NULL; |
|
342 | } |
|
343 | ||
344 | // -------------------------------------------------------------------- |
|
345 | ||
346 | /** |
|
347 | * Set HTTP Status Header |
|
348 | * |
|
349 | * As of version 1.7.2, this is an alias for common function |
|
350 | * set_status_header(). |
|
351 | * |
|
352 | * @param int $code Status code (default: 200) |
|
353 | * @param string $text Optional message |
|
354 | * @return CI_Output |
|
355 | */ |
|
356 | public function set_status_header($code = 200, $text = '') |
|
357 | { |
|
358 | set_status_header($code, $text); |
|
359 | return $this; |
|
360 | } |
|
361 | ||
362 | // -------------------------------------------------------------------- |
|
363 | ||
364 | /** |
|
365 | * Enable/disable Profiler |
|
366 | * |
|
367 | * @param bool $val TRUE to enable or FALSE to disable |
|
368 | * @return CI_Output |
|
369 | */ |
|
370 | public function enable_profiler($val = TRUE) |
|
371 | { |
|
372 | $this->enable_profiler = is_bool($val) ? $val : TRUE; |
|
373 | return $this; |
|
374 | } |
|
375 | ||
376 | // -------------------------------------------------------------------- |
|
377 | ||
378 | /** |
|
379 | * Set Profiler Sections |
|
380 | * |
|
381 | * Allows override of default/config settings for |
|
382 | * Profiler section display. |
|
383 | * |
|
384 | * @param array $sections Profiler sections |
|
385 | * @return CI_Output |
|
386 | */ |
|
387 | public function set_profiler_sections($sections) |
|
388 | { |
|
389 | if (isset($sections['query_toggle_count'])) |
|
390 | { |
|
391 | $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count']; |
|
392 | unset($sections['query_toggle_count']); |
|
393 | } |
|
394 | ||
395 | foreach ($sections as $section => $enable) |
|
396 | { |
|
397 | $this->_profiler_sections[$section] = ($enable !== FALSE); |
|
398 | } |
|
399 | ||
400 | return $this; |
|
401 | } |
|
402 | ||
403 | // -------------------------------------------------------------------- |
|
404 | ||
405 | /** |
|
406 | * Set Cache |
|
407 | * |
|
408 | * @param int $time Cache expiration time in minutes |
|
409 | * @return CI_Output |
|
410 | */ |
|
411 | public function cache($time) |
|
412 | { |
|
413 | $this->cache_expiration = is_numeric($time) ? $time : 0; |
|
414 | return $this; |
|
415 | } |
|
416 | ||
417 | // -------------------------------------------------------------------- |
|
418 | ||
419 | /** |
|
420 | * Display Output |
|
421 | * |
|
422 | * Processes and sends finalized output data to the browser along |
|
423 | * with any server headers and profile data. It also stops benchmark |
|
424 | * timers so the page rendering speed and memory usage can be shown. |
|
425 | * |
|
426 | * Note: All "view" data is automatically put into $this->final_output |
|
427 | * by controller class. |
|
428 | * |
|
429 | * @uses CI_Output::$final_output |
|
430 | * @param string $output Output data override |
|
431 | * @return void |
|
432 | * |
|
433 | * modified by ci-phpunit-test |
|
434 | */ |
|
435 | public function _display($output = '') |
|
436 | { |
|
437 | // Note: We use load_class() because we can't use $CI =& get_instance() |
|
438 | // since this function is sometimes called by the caching mechanism, |
|
439 | // which happens before the CI super object is available. |
|
440 | $BM =& load_class('Benchmark', 'core'); |
|
441 | $CFG =& load_class('Config', 'core'); |
|
442 | ||
443 | // Grab the super object if we can. |
|
444 | if (class_exists('CI_Controller', FALSE)) |
|
445 | { |
|
446 | $CI =& get_instance(); |
|
447 | } |
|
448 | ||
449 | // -------------------------------------------------------------------- |
|
450 | ||
451 | // Set the output data |
|
452 | if ($output === '') |
|
453 | { |
|
454 | $output =& $this->final_output; |
|
455 | } |
|
456 | ||
457 | // -------------------------------------------------------------------- |
|
458 | ||
459 | // Do we need to write a cache file? Only if the controller does not have its |
|
460 | // own _output() method and we are not dealing with a cache file, which we |
|
461 | // can determine by the existence of the $CI object above |
|
462 | if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')) |
|
463 | { |
|
464 | $this->_write_cache($output); |
|
465 | } |
|
466 | ||
467 | // -------------------------------------------------------------------- |
|
468 | ||
469 | // Parse out the elapsed time and memory usage, |
|
470 | // then swap the pseudo-variables with the data |
|
471 | ||
472 | $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); |
|
473 | ||
474 | if ($this->parse_exec_vars === TRUE) |
|
475 | { |
|
476 | $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; |
|
477 | $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); |
|
478 | } |
|
479 | ||
480 | // -------------------------------------------------------------------- |
|
481 | ||
482 | // Is compression requested? |
|
483 | if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed |
|
484 | && $this->_compress_output === TRUE |
|
485 | && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
486 | { |
|
487 | ob_start('ob_gzhandler'); |
|
488 | } |
|
489 | ||
490 | // -------------------------------------------------------------------- |
|
491 | ||
492 | // Are there any server headers to send? |
|
493 | if (count($this->headers) > 0) |
|
494 | { |
|
495 | foreach ($this->headers as $header) |
|
496 | { |
|
497 | // @header($header[0], $header[1]); |
|
498 | } |
|
499 | } |
|
500 | ||
501 | // -------------------------------------------------------------------- |
|
502 | ||
503 | // Does the $CI object exist? |
|
504 | // If not we know we are dealing with a cache file so we'll |
|
505 | // simply echo out the data and exit. |
|
506 | if ( ! isset($CI)) |
|
507 | { |
|
508 | if ($this->_compress_output === TRUE) |
|
509 | { |
|
510 | if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
511 | { |
|
512 | // header('Content-Encoding: gzip'); |
|
513 | // header('Content-Length: '.self::strlen($output)); |
|
514 | } |
|
515 | else |
|
516 | { |
|
517 | // User agent doesn't support gzip compression, |
|
518 | // so we'll have to decompress our cache |
|
519 | $output = gzinflate(self::substr($output, 10, -8)); |
|
520 | } |
|
521 | } |
|
522 | ||
523 | echo $output; |
|
524 | log_message('info', 'Final output sent to browser'); |
|
525 | log_message('debug', 'Total execution time: '.$elapsed); |
|
526 | return; |
|
527 | } |
|
528 | ||
529 | // -------------------------------------------------------------------- |
|
530 | ||
531 | // Do we need to generate profile data? |
|
532 | // If so, load the Profile class and run it. |
|
533 | if ($this->enable_profiler === TRUE) |
|
534 | { |
|
535 | $CI->load->library('profiler'); |
|
536 | if ( ! empty($this->_profiler_sections)) |
|
537 | { |
|
538 | $CI->profiler->set_sections($this->_profiler_sections); |
|
539 | } |
|
540 | ||
541 | // If the output data contains closing </body> and </html> tags |
|
542 | // we will remove them and add them back after we insert the profile data |
|
543 | $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run(); |
|
544 | if ($count > 0) |
|
545 | { |
|
546 | $output .= '</body></html>'; |
|
547 | } |
|
548 | } |
|
549 | ||
550 | // Does the controller contain a function named _output()? |
|
551 | // If so send the output there. Otherwise, echo it. |
|
552 | if (method_exists($CI, '_output')) |
|
553 | { |
|
554 | $CI->_output($output); |
|
555 | } |
|
556 | else |
|
557 | { |
|
558 | echo $output; // Send it to the browser! |
|
559 | } |
|
560 | ||
561 | log_message('info', 'Final output sent to browser'); |
|
562 | log_message('debug', 'Total execution time: '.$elapsed); |
|
563 | } |
|
564 | ||
565 | // -------------------------------------------------------------------- |
|
566 | ||
567 | /** |
|
568 | * Write Cache |
|
569 | * |
|
570 | * @param string $output Output data to cache |
|
571 | * @return void |
|
572 | */ |
|
573 | public function _write_cache($output) |
|
574 | { |
|
575 | $CI =& get_instance(); |
|
576 | $path = $CI->config->item('cache_path'); |
|
577 | $cache_path = ($path === '') ? APPPATH.'cache/' : $path; |
|
578 | ||
579 | if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) |
|
580 | { |
|
581 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
582 | return; |
|
583 | } |
|
584 | ||
585 | $uri = $CI->config->item('base_url') |
|
586 | .$CI->config->item('index_page') |
|
587 | .$CI->uri->uri_string(); |
|
588 | ||
589 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
590 | { |
|
591 | if (is_array($cache_query_string)) |
|
592 | { |
|
593 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
594 | } |
|
595 | else |
|
596 | { |
|
597 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
598 | } |
|
599 | } |
|
600 | ||
601 | $cache_path .= md5($uri); |
|
602 | ||
603 | if ( ! $fp = @fopen($cache_path, 'w+b')) |
|
604 | { |
|
605 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
606 | return; |
|
607 | } |
|
608 | ||
609 | if ( ! flock($fp, LOCK_EX)) |
|
610 | { |
|
611 | log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); |
|
612 | fclose($fp); |
|
613 | return; |
|
614 | } |
|
615 | ||
616 | // If output compression is enabled, compress the cache |
|
617 | // itself, so that we don't have to do that each time |
|
618 | // we're serving it |
|
619 | if ($this->_compress_output === TRUE) |
|
620 | { |
|
621 | $output = gzencode($output); |
|
622 | ||
623 | if ($this->get_header('content-type') === NULL) |
|
624 | { |
|
625 | $this->set_content_type($this->mime_type); |
|
626 | } |
|
627 | } |
|
628 | ||
629 | $expire = time() + ($this->cache_expiration * 60); |
|
630 | ||
631 | // Put together our serialized info. |
|
632 | $cache_info = serialize(array( |
|
633 | 'expire' => $expire, |
|
634 | 'headers' => $this->headers |
|
635 | )); |
|
636 | ||
637 | $output = $cache_info.'ENDCI--->'.$output; |
|
638 | ||
639 | for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result) |
|
640 | { |
|
641 | if (($result = fwrite($fp, self::substr($output, $written))) === FALSE) |
|
642 | { |
|
643 | break; |
|
644 | } |
|
645 | } |
|
646 | ||
647 | flock($fp, LOCK_UN); |
|
648 | fclose($fp); |
|
649 | ||
650 | if ( ! is_int($result)) |
|
651 | { |
|
652 | @unlink($cache_path); |
|
653 | log_message('error', 'Unable to write the complete cache content at: '.$cache_path); |
|
654 | return; |
|
655 | } |
|
656 | ||
657 | chmod($cache_path, 0640); |
|
658 | log_message('debug', 'Cache file written: '.$cache_path); |
|
659 | ||
660 | // Send HTTP cache-control headers to browser to match file cache settings. |
|
661 | $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); |
|
662 | } |
|
663 | ||
664 | // -------------------------------------------------------------------- |
|
665 | ||
666 | /** |
|
667 | * Update/serve cached output |
|
668 | * |
|
669 | * @uses CI_Config |
|
670 | * @uses CI_URI |
|
671 | * |
|
672 | * @param object &$CFG CI_Config class instance |
|
673 | * @param object &$URI CI_URI class instance |
|
674 | * @return bool TRUE on success or FALSE on failure |
|
675 | */ |
|
676 | public function _display_cache(&$CFG, &$URI) |
|
677 | { |
|
678 | $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path'); |
|
679 | ||
680 | // Build the file path. The file name is an MD5 hash of the full URI |
|
681 | $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string; |
|
682 | ||
683 | if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
684 | { |
|
685 | if (is_array($cache_query_string)) |
|
686 | { |
|
687 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
688 | } |
|
689 | else |
|
690 | { |
|
691 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
692 | } |
|
693 | } |
|
694 | ||
695 | $filepath = $cache_path.md5($uri); |
|
696 | ||
697 | if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb')) |
|
698 | { |
|
699 | return FALSE; |
|
700 | } |
|
701 | ||
702 | flock($fp, LOCK_SH); |
|
703 | ||
704 | $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : ''; |
|
705 | ||
706 | flock($fp, LOCK_UN); |
|
707 | fclose($fp); |
|
708 | ||
709 | // Look for embedded serialized file info. |
|
710 | if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match)) |
|
711 | { |
|
712 | return FALSE; |
|
713 | } |
|
714 | ||
715 | $cache_info = unserialize($match[1]); |
|
716 | $expire = $cache_info['expire']; |
|
717 | ||
718 | $last_modified = filemtime($filepath); |
|
719 | ||
720 | // Has the file expired? |
|
721 | if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) |
|
722 | { |
|
723 | // If so we'll delete it. |
|
724 | @unlink($filepath); |
|
725 | log_message('debug', 'Cache file has expired. File deleted.'); |
|
726 | return FALSE; |
|
727 | } |
|
728 | ||
729 | // Send the HTTP cache control headers |
|
730 | $this->set_cache_header($last_modified, $expire); |
|
731 | ||
732 | // Add headers from cache file. |
|
733 | foreach ($cache_info['headers'] as $header) |
|
734 | { |
|
735 | $this->set_header($header[0], $header[1]); |
|
736 | } |
|
737 | ||
738 | // Display the cache |
|
739 | $this->_display(self::substr($cache, self::strlen($match[0]))); |
|
740 | log_message('debug', 'Cache file is current. Sending it to browser.'); |
|
741 | return TRUE; |
|
742 | } |
|
743 | ||
744 | // -------------------------------------------------------------------- |
|
745 | ||
746 | /** |
|
747 | * Delete cache |
|
748 | * |
|
749 | * @param string $uri URI string |
|
750 | * @return bool |
|
751 | */ |
|
752 | public function delete_cache($uri = '') |
|
753 | { |
|
754 | $CI =& get_instance(); |
|
755 | $cache_path = $CI->config->item('cache_path'); |
|
756 | if ($cache_path === '') |
|
757 | { |
|
758 | $cache_path = APPPATH.'cache/'; |
|
759 | } |
|
760 | ||
761 | if ( ! is_dir($cache_path)) |
|
762 | { |
|
763 | log_message('error', 'Unable to find cache path: '.$cache_path); |
|
764 | return FALSE; |
|
765 | } |
|
766 | ||
767 | if (empty($uri)) |
|
768 | { |
|
769 | $uri = $CI->uri->uri_string(); |
|
770 | ||
771 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
772 | { |
|
773 | if (is_array($cache_query_string)) |
|
774 | { |
|
775 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
776 | } |
|
777 | else |
|
778 | { |
|
779 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
780 | } |
|
781 | } |
|
782 | } |
|
783 | ||
784 | $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/')); |
|
785 | ||
786 | if ( ! @unlink($cache_path)) |
|
787 | { |
|
788 | log_message('error', 'Unable to delete cache file for '.$uri); |
|
789 | return FALSE; |
|
790 | } |
|
791 | ||
792 | return TRUE; |
|
793 | } |
|
794 | ||
795 | // -------------------------------------------------------------------- |
|
796 | ||
797 | /** |
|
798 | * Set Cache Header |
|
799 | * |
|
800 | * Set the HTTP headers to match the server-side file cache settings |
|
801 | * in order to reduce bandwidth. |
|
802 | * |
|
803 | * @param int $last_modified Timestamp of when the page was last modified |
|
804 | * @param int $expiration Timestamp of when should the requested page expire from cache |
|
805 | * @return void |
|
806 | * |
|
807 | * modified by ci-phpunit-test |
|
808 | */ |
|
809 | public function set_cache_header($last_modified, $expiration) |
|
810 | { |
|
811 | $max_age = $expiration - $_SERVER['REQUEST_TIME']; |
|
812 | ||
813 | if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) |
|
814 | { |
|
815 | $this->set_status_header(304); |
|
816 | exit; |
|
817 | } |
|
818 | ||
819 | // header('Pragma: public'); |
|
820 | // header('Cache-Control: max-age='.$max_age.', public'); |
|
821 | // header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); |
|
822 | // header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); |
|
823 | } |
|
824 | ||
825 | // -------------------------------------------------------------------- |
|
826 | ||
827 | /** |
|
828 | * Byte-safe strlen() |
|
829 | * |
|
830 | * @param string $str |
|
831 | * @return int |
|
832 | */ |
|
833 | protected static function strlen($str) |
|
834 | { |
|
835 | return (self::$func_overload) |
|
836 | ? mb_strlen($str, '8bit') |
|
837 | : strlen($str); |
|
838 | } |
|
839 | ||
840 | // -------------------------------------------------------------------- |
|
841 | ||
842 | /** |
|
843 | * Byte-safe substr() |
|
844 | * |
|
845 | * @param string $str |
|
846 | * @param int $start |
|
847 | * @param int $length |
|
848 | * @return string |
|
849 | */ |
|
850 | protected static function substr($str, $start, $length = NULL) |
|
851 | { |
|
852 | if (self::$func_overload) |
|
853 | { |
|
854 | // mb_substr($str, $start, null, '8bit') returns an empty |
|
855 | // string on PHP 5.3 |
|
856 | isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); |
|
857 | return mb_substr($str, $start, $length, '8bit'); |
|
858 | } |
|
859 | ||
860 | return isset($length) |
|
861 | ? substr($str, $start, $length) |
|
862 | : substr($str, $start); |
|
863 | } |
|
864 | } |
|
865 |
@@ 51-864 (lines=814) @@ | ||
48 | * @author EllisLab Dev Team |
|
49 | * @link https://codeigniter.com/user_guide/libraries/output.html |
|
50 | */ |
|
51 | class CI_Output { |
|
52 | ||
53 | /** |
|
54 | * Final output string |
|
55 | * |
|
56 | * @var string |
|
57 | */ |
|
58 | public $final_output; |
|
59 | ||
60 | /** |
|
61 | * Cache expiration time |
|
62 | * |
|
63 | * @var int |
|
64 | */ |
|
65 | public $cache_expiration = 0; |
|
66 | ||
67 | /** |
|
68 | * List of server headers |
|
69 | * |
|
70 | * @var array |
|
71 | */ |
|
72 | public $headers = array(); |
|
73 | ||
74 | /** |
|
75 | * List of mime types |
|
76 | * |
|
77 | * @var array |
|
78 | */ |
|
79 | public $mimes = array(); |
|
80 | ||
81 | /** |
|
82 | * Mime-type for the current page |
|
83 | * |
|
84 | * @var string |
|
85 | */ |
|
86 | protected $mime_type = 'text/html'; |
|
87 | ||
88 | /** |
|
89 | * Enable Profiler flag |
|
90 | * |
|
91 | * @var bool |
|
92 | */ |
|
93 | public $enable_profiler = FALSE; |
|
94 | ||
95 | /** |
|
96 | * php.ini zlib.output_compression flag |
|
97 | * |
|
98 | * @var bool |
|
99 | */ |
|
100 | protected $_zlib_oc = FALSE; |
|
101 | ||
102 | /** |
|
103 | * CI output compression flag |
|
104 | * |
|
105 | * @var bool |
|
106 | */ |
|
107 | protected $_compress_output = FALSE; |
|
108 | ||
109 | /** |
|
110 | * List of profiler sections |
|
111 | * |
|
112 | * @var array |
|
113 | */ |
|
114 | protected $_profiler_sections = array(); |
|
115 | ||
116 | /** |
|
117 | * Parse markers flag |
|
118 | * |
|
119 | * Whether or not to parse variables like {elapsed_time} and {memory_usage}. |
|
120 | * |
|
121 | * @var bool |
|
122 | */ |
|
123 | public $parse_exec_vars = TRUE; |
|
124 | ||
125 | /** |
|
126 | * mbstring.func_overload flag |
|
127 | * |
|
128 | * @var bool |
|
129 | */ |
|
130 | protected static $func_overload; |
|
131 | ||
132 | /** |
|
133 | * Response status (status code, text and is redirection?) |
|
134 | * |
|
135 | * @var array |
|
136 | * |
|
137 | * added by ci-phpunit-test |
|
138 | */ |
|
139 | public $_status; |
|
140 | ||
141 | /** |
|
142 | * Cookies |
|
143 | * |
|
144 | * @var array |
|
145 | * |
|
146 | * added by ci-phpunit-test |
|
147 | */ |
|
148 | public $_cookies; |
|
149 | ||
150 | /** |
|
151 | * Class constructor |
|
152 | * |
|
153 | * Determines whether zLib output compression will be used. |
|
154 | * |
|
155 | * @return void |
|
156 | */ |
|
157 | public function __construct() |
|
158 | { |
|
159 | $this->_zlib_oc = (bool) ini_get('zlib.output_compression'); |
|
160 | $this->_compress_output = ( |
|
161 | $this->_zlib_oc === FALSE |
|
162 | && config_item('compress_output') === TRUE |
|
163 | && extension_loaded('zlib') |
|
164 | ); |
|
165 | ||
166 | isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); |
|
167 | ||
168 | // Get mime types for later |
|
169 | $this->mimes =& get_mimes(); |
|
170 | ||
171 | log_message('info', 'Output Class Initialized'); |
|
172 | } |
|
173 | ||
174 | // -------------------------------------------------------------------- |
|
175 | ||
176 | /** |
|
177 | * Get Output |
|
178 | * |
|
179 | * Returns the current output string. |
|
180 | * |
|
181 | * @return string |
|
182 | */ |
|
183 | public function get_output() |
|
184 | { |
|
185 | return $this->final_output; |
|
186 | } |
|
187 | ||
188 | // -------------------------------------------------------------------- |
|
189 | ||
190 | /** |
|
191 | * Set Output |
|
192 | * |
|
193 | * Sets the output string. |
|
194 | * |
|
195 | * @param string $output Output data |
|
196 | * @return CI_Output |
|
197 | */ |
|
198 | public function set_output($output) |
|
199 | { |
|
200 | $this->final_output = $output; |
|
201 | return $this; |
|
202 | } |
|
203 | ||
204 | // -------------------------------------------------------------------- |
|
205 | ||
206 | /** |
|
207 | * Append Output |
|
208 | * |
|
209 | * Appends data onto the output string. |
|
210 | * |
|
211 | * @param string $output Data to append |
|
212 | * @return CI_Output |
|
213 | */ |
|
214 | public function append_output($output) |
|
215 | { |
|
216 | $this->final_output .= $output; |
|
217 | return $this; |
|
218 | } |
|
219 | ||
220 | // -------------------------------------------------------------------- |
|
221 | ||
222 | /** |
|
223 | * Set Header |
|
224 | * |
|
225 | * Lets you set a server header which will be sent with the final output. |
|
226 | * |
|
227 | * Note: If a file is cached, headers will not be sent. |
|
228 | * @todo We need to figure out how to permit headers to be cached. |
|
229 | * |
|
230 | * @param string $header Header |
|
231 | * @param bool $replace Whether to replace the old header value, if already set |
|
232 | * @return CI_Output |
|
233 | */ |
|
234 | public function set_header($header, $replace = TRUE) |
|
235 | { |
|
236 | // If zlib.output_compression is enabled it will compress the output, |
|
237 | // but it will not modify the content-length header to compensate for |
|
238 | // the reduction, causing the browser to hang waiting for more data. |
|
239 | // We'll just skip content-length in those cases. |
|
240 | if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) |
|
241 | { |
|
242 | return $this; |
|
243 | } |
|
244 | ||
245 | $this->headers[] = array($header, $replace); |
|
246 | return $this; |
|
247 | } |
|
248 | ||
249 | // -------------------------------------------------------------------- |
|
250 | ||
251 | /** |
|
252 | * Set Content-Type Header |
|
253 | * |
|
254 | * @param string $mime_type Extension of the file we're outputting |
|
255 | * @param string $charset Character set (default: NULL) |
|
256 | * @return CI_Output |
|
257 | */ |
|
258 | public function set_content_type($mime_type, $charset = NULL) |
|
259 | { |
|
260 | if (strpos($mime_type, '/') === FALSE) |
|
261 | { |
|
262 | $extension = ltrim($mime_type, '.'); |
|
263 | ||
264 | // Is this extension supported? |
|
265 | if (isset($this->mimes[$extension])) |
|
266 | { |
|
267 | $mime_type =& $this->mimes[$extension]; |
|
268 | ||
269 | if (is_array($mime_type)) |
|
270 | { |
|
271 | $mime_type = current($mime_type); |
|
272 | } |
|
273 | } |
|
274 | } |
|
275 | ||
276 | $this->mime_type = $mime_type; |
|
277 | ||
278 | if (empty($charset)) |
|
279 | { |
|
280 | $charset = config_item('charset'); |
|
281 | } |
|
282 | ||
283 | $header = 'Content-Type: '.$mime_type |
|
284 | .(empty($charset) ? '' : '; charset='.$charset); |
|
285 | ||
286 | $this->headers[] = array($header, TRUE); |
|
287 | return $this; |
|
288 | } |
|
289 | ||
290 | // -------------------------------------------------------------------- |
|
291 | ||
292 | /** |
|
293 | * Get Current Content-Type Header |
|
294 | * |
|
295 | * @return string 'text/html', if not already set |
|
296 | */ |
|
297 | public function get_content_type() |
|
298 | { |
|
299 | for ($i = 0, $c = count($this->headers); $i < $c; $i++) |
|
300 | { |
|
301 | if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) |
|
302 | { |
|
303 | return $content_type; |
|
304 | } |
|
305 | } |
|
306 | ||
307 | return 'text/html'; |
|
308 | } |
|
309 | ||
310 | // -------------------------------------------------------------------- |
|
311 | ||
312 | /** |
|
313 | * Get Header |
|
314 | * |
|
315 | * @param string $header |
|
316 | * @return string |
|
317 | */ |
|
318 | public function get_header($header) |
|
319 | { |
|
320 | // Combine headers already sent with our batched headers |
|
321 | $headers = array_merge( |
|
322 | // We only need [x][0] from our multi-dimensional array |
|
323 | array_map('array_shift', $this->headers), |
|
324 | headers_list() |
|
325 | ); |
|
326 | ||
327 | if (empty($headers) OR empty($header)) |
|
328 | { |
|
329 | return NULL; |
|
330 | } |
|
331 | ||
332 | // Count backwards, in order to get the last matching header |
|
333 | for ($c = count($headers) - 1; $c > -1; $c--) |
|
334 | { |
|
335 | if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0) |
|
336 | { |
|
337 | return trim(self::substr($headers[$c], $l+1)); |
|
338 | } |
|
339 | } |
|
340 | ||
341 | return NULL; |
|
342 | } |
|
343 | ||
344 | // -------------------------------------------------------------------- |
|
345 | ||
346 | /** |
|
347 | * Set HTTP Status Header |
|
348 | * |
|
349 | * As of version 1.7.2, this is an alias for common function |
|
350 | * set_status_header(). |
|
351 | * |
|
352 | * @param int $code Status code (default: 200) |
|
353 | * @param string $text Optional message |
|
354 | * @return CI_Output |
|
355 | */ |
|
356 | public function set_status_header($code = 200, $text = '') |
|
357 | { |
|
358 | set_status_header($code, $text); |
|
359 | return $this; |
|
360 | } |
|
361 | ||
362 | // -------------------------------------------------------------------- |
|
363 | ||
364 | /** |
|
365 | * Enable/disable Profiler |
|
366 | * |
|
367 | * @param bool $val TRUE to enable or FALSE to disable |
|
368 | * @return CI_Output |
|
369 | */ |
|
370 | public function enable_profiler($val = TRUE) |
|
371 | { |
|
372 | $this->enable_profiler = is_bool($val) ? $val : TRUE; |
|
373 | return $this; |
|
374 | } |
|
375 | ||
376 | // -------------------------------------------------------------------- |
|
377 | ||
378 | /** |
|
379 | * Set Profiler Sections |
|
380 | * |
|
381 | * Allows override of default/config settings for |
|
382 | * Profiler section display. |
|
383 | * |
|
384 | * @param array $sections Profiler sections |
|
385 | * @return CI_Output |
|
386 | */ |
|
387 | public function set_profiler_sections($sections) |
|
388 | { |
|
389 | if (isset($sections['query_toggle_count'])) |
|
390 | { |
|
391 | $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count']; |
|
392 | unset($sections['query_toggle_count']); |
|
393 | } |
|
394 | ||
395 | foreach ($sections as $section => $enable) |
|
396 | { |
|
397 | $this->_profiler_sections[$section] = ($enable !== FALSE); |
|
398 | } |
|
399 | ||
400 | return $this; |
|
401 | } |
|
402 | ||
403 | // -------------------------------------------------------------------- |
|
404 | ||
405 | /** |
|
406 | * Set Cache |
|
407 | * |
|
408 | * @param int $time Cache expiration time in minutes |
|
409 | * @return CI_Output |
|
410 | */ |
|
411 | public function cache($time) |
|
412 | { |
|
413 | $this->cache_expiration = is_numeric($time) ? $time : 0; |
|
414 | return $this; |
|
415 | } |
|
416 | ||
417 | // -------------------------------------------------------------------- |
|
418 | ||
419 | /** |
|
420 | * Display Output |
|
421 | * |
|
422 | * Processes and sends finalized output data to the browser along |
|
423 | * with any server headers and profile data. It also stops benchmark |
|
424 | * timers so the page rendering speed and memory usage can be shown. |
|
425 | * |
|
426 | * Note: All "view" data is automatically put into $this->final_output |
|
427 | * by controller class. |
|
428 | * |
|
429 | * @uses CI_Output::$final_output |
|
430 | * @param string $output Output data override |
|
431 | * @return void |
|
432 | * |
|
433 | * modified by ci-phpunit-test |
|
434 | */ |
|
435 | public function _display($output = '') |
|
436 | { |
|
437 | // Note: We use load_class() because we can't use $CI =& get_instance() |
|
438 | // since this function is sometimes called by the caching mechanism, |
|
439 | // which happens before the CI super object is available. |
|
440 | $BM =& load_class('Benchmark', 'core'); |
|
441 | $CFG =& load_class('Config', 'core'); |
|
442 | ||
443 | // Grab the super object if we can. |
|
444 | if (class_exists('CI_Controller', FALSE)) |
|
445 | { |
|
446 | $CI =& get_instance(); |
|
447 | } |
|
448 | ||
449 | // -------------------------------------------------------------------- |
|
450 | ||
451 | // Set the output data |
|
452 | if ($output === '') |
|
453 | { |
|
454 | $output =& $this->final_output; |
|
455 | } |
|
456 | ||
457 | // -------------------------------------------------------------------- |
|
458 | ||
459 | // Do we need to write a cache file? Only if the controller does not have its |
|
460 | // own _output() method and we are not dealing with a cache file, which we |
|
461 | // can determine by the existence of the $CI object above |
|
462 | if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')) |
|
463 | { |
|
464 | $this->_write_cache($output); |
|
465 | } |
|
466 | ||
467 | // -------------------------------------------------------------------- |
|
468 | ||
469 | // Parse out the elapsed time and memory usage, |
|
470 | // then swap the pseudo-variables with the data |
|
471 | ||
472 | $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); |
|
473 | ||
474 | if ($this->parse_exec_vars === TRUE) |
|
475 | { |
|
476 | $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; |
|
477 | $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); |
|
478 | } |
|
479 | ||
480 | // -------------------------------------------------------------------- |
|
481 | ||
482 | // Is compression requested? |
|
483 | if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed |
|
484 | && $this->_compress_output === TRUE |
|
485 | && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
486 | { |
|
487 | ob_start('ob_gzhandler'); |
|
488 | } |
|
489 | ||
490 | // -------------------------------------------------------------------- |
|
491 | ||
492 | // Are there any server headers to send? |
|
493 | if (count($this->headers) > 0) |
|
494 | { |
|
495 | foreach ($this->headers as $header) |
|
496 | { |
|
497 | // @header($header[0], $header[1]); |
|
498 | } |
|
499 | } |
|
500 | ||
501 | // -------------------------------------------------------------------- |
|
502 | ||
503 | // Does the $CI object exist? |
|
504 | // If not we know we are dealing with a cache file so we'll |
|
505 | // simply echo out the data and exit. |
|
506 | if ( ! isset($CI)) |
|
507 | { |
|
508 | if ($this->_compress_output === TRUE) |
|
509 | { |
|
510 | if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) |
|
511 | { |
|
512 | // header('Content-Encoding: gzip'); |
|
513 | // header('Content-Length: '.self::strlen($output)); |
|
514 | } |
|
515 | else |
|
516 | { |
|
517 | // User agent doesn't support gzip compression, |
|
518 | // so we'll have to decompress our cache |
|
519 | $output = gzinflate(self::substr($output, 10, -8)); |
|
520 | } |
|
521 | } |
|
522 | ||
523 | echo $output; |
|
524 | log_message('info', 'Final output sent to browser'); |
|
525 | log_message('debug', 'Total execution time: '.$elapsed); |
|
526 | return; |
|
527 | } |
|
528 | ||
529 | // -------------------------------------------------------------------- |
|
530 | ||
531 | // Do we need to generate profile data? |
|
532 | // If so, load the Profile class and run it. |
|
533 | if ($this->enable_profiler === TRUE) |
|
534 | { |
|
535 | $CI->load->library('profiler'); |
|
536 | if ( ! empty($this->_profiler_sections)) |
|
537 | { |
|
538 | $CI->profiler->set_sections($this->_profiler_sections); |
|
539 | } |
|
540 | ||
541 | // If the output data contains closing </body> and </html> tags |
|
542 | // we will remove them and add them back after we insert the profile data |
|
543 | $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run(); |
|
544 | if ($count > 0) |
|
545 | { |
|
546 | $output .= '</body></html>'; |
|
547 | } |
|
548 | } |
|
549 | ||
550 | // Does the controller contain a function named _output()? |
|
551 | // If so send the output there. Otherwise, echo it. |
|
552 | if (method_exists($CI, '_output')) |
|
553 | { |
|
554 | $CI->_output($output); |
|
555 | } |
|
556 | else |
|
557 | { |
|
558 | echo $output; // Send it to the browser! |
|
559 | } |
|
560 | ||
561 | log_message('info', 'Final output sent to browser'); |
|
562 | log_message('debug', 'Total execution time: '.$elapsed); |
|
563 | } |
|
564 | ||
565 | // -------------------------------------------------------------------- |
|
566 | ||
567 | /** |
|
568 | * Write Cache |
|
569 | * |
|
570 | * @param string $output Output data to cache |
|
571 | * @return void |
|
572 | */ |
|
573 | public function _write_cache($output) |
|
574 | { |
|
575 | $CI =& get_instance(); |
|
576 | $path = $CI->config->item('cache_path'); |
|
577 | $cache_path = ($path === '') ? APPPATH.'cache/' : $path; |
|
578 | ||
579 | if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) |
|
580 | { |
|
581 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
582 | return; |
|
583 | } |
|
584 | ||
585 | $uri = $CI->config->item('base_url') |
|
586 | .$CI->config->item('index_page') |
|
587 | .$CI->uri->uri_string(); |
|
588 | ||
589 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
590 | { |
|
591 | if (is_array($cache_query_string)) |
|
592 | { |
|
593 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
594 | } |
|
595 | else |
|
596 | { |
|
597 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
598 | } |
|
599 | } |
|
600 | ||
601 | $cache_path .= md5($uri); |
|
602 | ||
603 | if ( ! $fp = @fopen($cache_path, 'w+b')) |
|
604 | { |
|
605 | log_message('error', 'Unable to write cache file: '.$cache_path); |
|
606 | return; |
|
607 | } |
|
608 | ||
609 | if ( ! flock($fp, LOCK_EX)) |
|
610 | { |
|
611 | log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); |
|
612 | fclose($fp); |
|
613 | return; |
|
614 | } |
|
615 | ||
616 | // If output compression is enabled, compress the cache |
|
617 | // itself, so that we don't have to do that each time |
|
618 | // we're serving it |
|
619 | if ($this->_compress_output === TRUE) |
|
620 | { |
|
621 | $output = gzencode($output); |
|
622 | ||
623 | if ($this->get_header('content-type') === NULL) |
|
624 | { |
|
625 | $this->set_content_type($this->mime_type); |
|
626 | } |
|
627 | } |
|
628 | ||
629 | $expire = time() + ($this->cache_expiration * 60); |
|
630 | ||
631 | // Put together our serialized info. |
|
632 | $cache_info = serialize(array( |
|
633 | 'expire' => $expire, |
|
634 | 'headers' => $this->headers |
|
635 | )); |
|
636 | ||
637 | $output = $cache_info.'ENDCI--->'.$output; |
|
638 | ||
639 | for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result) |
|
640 | { |
|
641 | if (($result = fwrite($fp, self::substr($output, $written))) === FALSE) |
|
642 | { |
|
643 | break; |
|
644 | } |
|
645 | } |
|
646 | ||
647 | flock($fp, LOCK_UN); |
|
648 | fclose($fp); |
|
649 | ||
650 | if ( ! is_int($result)) |
|
651 | { |
|
652 | @unlink($cache_path); |
|
653 | log_message('error', 'Unable to write the complete cache content at: '.$cache_path); |
|
654 | return; |
|
655 | } |
|
656 | ||
657 | chmod($cache_path, 0640); |
|
658 | log_message('debug', 'Cache file written: '.$cache_path); |
|
659 | ||
660 | // Send HTTP cache-control headers to browser to match file cache settings. |
|
661 | $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); |
|
662 | } |
|
663 | ||
664 | // -------------------------------------------------------------------- |
|
665 | ||
666 | /** |
|
667 | * Update/serve cached output |
|
668 | * |
|
669 | * @uses CI_Config |
|
670 | * @uses CI_URI |
|
671 | * |
|
672 | * @param object &$CFG CI_Config class instance |
|
673 | * @param object &$URI CI_URI class instance |
|
674 | * @return bool TRUE on success or FALSE on failure |
|
675 | */ |
|
676 | public function _display_cache(&$CFG, &$URI) |
|
677 | { |
|
678 | $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path'); |
|
679 | ||
680 | // Build the file path. The file name is an MD5 hash of the full URI |
|
681 | $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string; |
|
682 | ||
683 | if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
684 | { |
|
685 | if (is_array($cache_query_string)) |
|
686 | { |
|
687 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
688 | } |
|
689 | else |
|
690 | { |
|
691 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
692 | } |
|
693 | } |
|
694 | ||
695 | $filepath = $cache_path.md5($uri); |
|
696 | ||
697 | if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb')) |
|
698 | { |
|
699 | return FALSE; |
|
700 | } |
|
701 | ||
702 | flock($fp, LOCK_SH); |
|
703 | ||
704 | $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : ''; |
|
705 | ||
706 | flock($fp, LOCK_UN); |
|
707 | fclose($fp); |
|
708 | ||
709 | // Look for embedded serialized file info. |
|
710 | if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match)) |
|
711 | { |
|
712 | return FALSE; |
|
713 | } |
|
714 | ||
715 | $cache_info = unserialize($match[1]); |
|
716 | $expire = $cache_info['expire']; |
|
717 | ||
718 | $last_modified = filemtime($filepath); |
|
719 | ||
720 | // Has the file expired? |
|
721 | if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) |
|
722 | { |
|
723 | // If so we'll delete it. |
|
724 | @unlink($filepath); |
|
725 | log_message('debug', 'Cache file has expired. File deleted.'); |
|
726 | return FALSE; |
|
727 | } |
|
728 | ||
729 | // Send the HTTP cache control headers |
|
730 | $this->set_cache_header($last_modified, $expire); |
|
731 | ||
732 | // Add headers from cache file. |
|
733 | foreach ($cache_info['headers'] as $header) |
|
734 | { |
|
735 | $this->set_header($header[0], $header[1]); |
|
736 | } |
|
737 | ||
738 | // Display the cache |
|
739 | $this->_display(self::substr($cache, self::strlen($match[0]))); |
|
740 | log_message('debug', 'Cache file is current. Sending it to browser.'); |
|
741 | return TRUE; |
|
742 | } |
|
743 | ||
744 | // -------------------------------------------------------------------- |
|
745 | ||
746 | /** |
|
747 | * Delete cache |
|
748 | * |
|
749 | * @param string $uri URI string |
|
750 | * @return bool |
|
751 | */ |
|
752 | public function delete_cache($uri = '') |
|
753 | { |
|
754 | $CI =& get_instance(); |
|
755 | $cache_path = $CI->config->item('cache_path'); |
|
756 | if ($cache_path === '') |
|
757 | { |
|
758 | $cache_path = APPPATH.'cache/'; |
|
759 | } |
|
760 | ||
761 | if ( ! is_dir($cache_path)) |
|
762 | { |
|
763 | log_message('error', 'Unable to find cache path: '.$cache_path); |
|
764 | return FALSE; |
|
765 | } |
|
766 | ||
767 | if (empty($uri)) |
|
768 | { |
|
769 | $uri = $CI->uri->uri_string(); |
|
770 | ||
771 | if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) |
|
772 | { |
|
773 | if (is_array($cache_query_string)) |
|
774 | { |
|
775 | $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); |
|
776 | } |
|
777 | else |
|
778 | { |
|
779 | $uri .= '?'.$_SERVER['QUERY_STRING']; |
|
780 | } |
|
781 | } |
|
782 | } |
|
783 | ||
784 | $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/')); |
|
785 | ||
786 | if ( ! @unlink($cache_path)) |
|
787 | { |
|
788 | log_message('error', 'Unable to delete cache file for '.$uri); |
|
789 | return FALSE; |
|
790 | } |
|
791 | ||
792 | return TRUE; |
|
793 | } |
|
794 | ||
795 | // -------------------------------------------------------------------- |
|
796 | ||
797 | /** |
|
798 | * Set Cache Header |
|
799 | * |
|
800 | * Set the HTTP headers to match the server-side file cache settings |
|
801 | * in order to reduce bandwidth. |
|
802 | * |
|
803 | * @param int $last_modified Timestamp of when the page was last modified |
|
804 | * @param int $expiration Timestamp of when should the requested page expire from cache |
|
805 | * @return void |
|
806 | * |
|
807 | * modified by ci-phpunit-test |
|
808 | */ |
|
809 | public function set_cache_header($last_modified, $expiration) |
|
810 | { |
|
811 | $max_age = $expiration - $_SERVER['REQUEST_TIME']; |
|
812 | ||
813 | if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) |
|
814 | { |
|
815 | $this->set_status_header(304); |
|
816 | exit; |
|
817 | } |
|
818 | ||
819 | // header('Pragma: public'); |
|
820 | // header('Cache-Control: max-age='.$max_age.', public'); |
|
821 | // header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); |
|
822 | // header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); |
|
823 | } |
|
824 | ||
825 | // -------------------------------------------------------------------- |
|
826 | ||
827 | /** |
|
828 | * Byte-safe strlen() |
|
829 | * |
|
830 | * @param string $str |
|
831 | * @return int |
|
832 | */ |
|
833 | protected static function strlen($str) |
|
834 | { |
|
835 | return (self::$func_overload) |
|
836 | ? mb_strlen($str, '8bit') |
|
837 | : strlen($str); |
|
838 | } |
|
839 | ||
840 | // -------------------------------------------------------------------- |
|
841 | ||
842 | /** |
|
843 | * Byte-safe substr() |
|
844 | * |
|
845 | * @param string $str |
|
846 | * @param int $start |
|
847 | * @param int $length |
|
848 | * @return string |
|
849 | */ |
|
850 | protected static function substr($str, $start, $length = NULL) |
|
851 | { |
|
852 | if (self::$func_overload) |
|
853 | { |
|
854 | // mb_substr($str, $start, null, '8bit') returns an empty |
|
855 | // string on PHP 5.3 |
|
856 | isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); |
|
857 | return mb_substr($str, $start, $length, '8bit'); |
|
858 | } |
|
859 | ||
860 | return isset($length) |
|
861 | ? substr($str, $start, $length) |
|
862 | : substr($str, $start); |
|
863 | } |
|
864 | } |
|
865 |