You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

842 lines
21 KiB

7 years ago
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP
  6. *
  7. * This content is released under the MIT License (MIT)
  8. *
  9. * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in
  19. * all copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. * THE SOFTWARE.
  28. *
  29. * @package CodeIgniter
  30. * @author EllisLab Dev Team
  31. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
  32. * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
  33. * @license http://opensource.org/licenses/MIT MIT License
  34. * @link https://codeigniter.com
  35. * @since Version 1.0.0
  36. * @filesource
  37. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * Output Class
  41. *
  42. * Responsible for sending final output to the browser.
  43. *
  44. * @package CodeIgniter
  45. * @subpackage Libraries
  46. * @category Output
  47. * @author EllisLab Dev Team
  48. * @link https://codeigniter.com/user_guide/libraries/output.html
  49. */
  50. class CI_Output {
  51. /**
  52. * Final output string
  53. *
  54. * @var string
  55. */
  56. public $final_output;
  57. /**
  58. * Cache expiration time
  59. *
  60. * @var int
  61. */
  62. public $cache_expiration = 0;
  63. /**
  64. * List of server headers
  65. *
  66. * @var array
  67. */
  68. public $headers = array();
  69. /**
  70. * List of mime types
  71. *
  72. * @var array
  73. */
  74. public $mimes = array();
  75. /**
  76. * Mime-type for the current page
  77. *
  78. * @var string
  79. */
  80. protected $mime_type = 'text/html';
  81. /**
  82. * Enable Profiler flag
  83. *
  84. * @var bool
  85. */
  86. public $enable_profiler = FALSE;
  87. /**
  88. * php.ini zlib.output_compression flag
  89. *
  90. * @var bool
  91. */
  92. protected $_zlib_oc = FALSE;
  93. /**
  94. * CI output compression flag
  95. *
  96. * @var bool
  97. */
  98. protected $_compress_output = FALSE;
  99. /**
  100. * List of profiler sections
  101. *
  102. * @var array
  103. */
  104. protected $_profiler_sections = array();
  105. /**
  106. * Parse markers flag
  107. *
  108. * Whether or not to parse variables like {elapsed_time} and {memory_usage}.
  109. *
  110. * @var bool
  111. */
  112. public $parse_exec_vars = TRUE;
  113. /**
  114. * mbstring.func_overload flag
  115. *
  116. * @var bool
  117. */
  118. protected static $func_overload;
  119. /**
  120. * Class constructor
  121. *
  122. * Determines whether zLib output compression will be used.
  123. *
  124. * @return void
  125. */
  126. public function __construct()
  127. {
  128. $this->_zlib_oc = (bool) ini_get('zlib.output_compression');
  129. $this->_compress_output = (
  130. $this->_zlib_oc === FALSE
  131. && config_item('compress_output') === TRUE
  132. && extension_loaded('zlib')
  133. );
  134. isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload'));
  135. // Get mime types for later
  136. $this->mimes =& get_mimes();
  137. log_message('info', 'Output Class Initialized');
  138. }
  139. // --------------------------------------------------------------------
  140. /**
  141. * Get Output
  142. *
  143. * Returns the current output string.
  144. *
  145. * @return string
  146. */
  147. public function get_output()
  148. {
  149. return $this->final_output;
  150. }
  151. // --------------------------------------------------------------------
  152. /**
  153. * Set Output
  154. *
  155. * Sets the output string.
  156. *
  157. * @param string $output Output data
  158. * @return CI_Output
  159. */
  160. public function set_output($output)
  161. {
  162. $this->final_output = $output;
  163. return $this;
  164. }
  165. // --------------------------------------------------------------------
  166. /**
  167. * Append Output
  168. *
  169. * Appends data onto the output string.
  170. *
  171. * @param string $output Data to append
  172. * @return CI_Output
  173. */
  174. public function append_output($output)
  175. {
  176. $this->final_output .= $output;
  177. return $this;
  178. }
  179. // --------------------------------------------------------------------
  180. /**
  181. * Set Header
  182. *
  183. * Lets you set a server header which will be sent with the final output.
  184. *
  185. * Note: If a file is cached, headers will not be sent.
  186. * @todo We need to figure out how to permit headers to be cached.
  187. *
  188. * @param string $header Header
  189. * @param bool $replace Whether to replace the old header value, if already set
  190. * @return CI_Output
  191. */
  192. public function set_header($header, $replace = TRUE)
  193. {
  194. // If zlib.output_compression is enabled it will compress the output,
  195. // but it will not modify the content-length header to compensate for
  196. // the reduction, causing the browser to hang waiting for more data.
  197. // We'll just skip content-length in those cases.
  198. if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0)
  199. {
  200. return $this;
  201. }
  202. $this->headers[] = array($header, $replace);
  203. return $this;
  204. }
  205. // --------------------------------------------------------------------
  206. /**
  207. * Set Content-Type Header
  208. *
  209. * @param string $mime_type Extension of the file we're outputting
  210. * @param string $charset Character set (default: NULL)
  211. * @return CI_Output
  212. */
  213. public function set_content_type($mime_type, $charset = NULL)
  214. {
  215. if (strpos($mime_type, '/') === FALSE)
  216. {
  217. $extension = ltrim($mime_type, '.');
  218. // Is this extension supported?
  219. if (isset($this->mimes[$extension]))
  220. {
  221. $mime_type =& $this->mimes[$extension];
  222. if (is_array($mime_type))
  223. {
  224. $mime_type = current($mime_type);
  225. }
  226. }
  227. }
  228. $this->mime_type = $mime_type;
  229. if (empty($charset))
  230. {
  231. $charset = config_item('charset');
  232. }
  233. $header = 'Content-Type: '.$mime_type
  234. .(empty($charset) ? '' : '; charset='.$charset);
  235. $this->headers[] = array($header, TRUE);
  236. return $this;
  237. }
  238. // --------------------------------------------------------------------
  239. /**
  240. * Get Current Content-Type Header
  241. *
  242. * @return string 'text/html', if not already set
  243. */
  244. public function get_content_type()
  245. {
  246. for ($i = 0, $c = count($this->headers); $i < $c; $i++)
  247. {
  248. if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)
  249. {
  250. return $content_type;
  251. }
  252. }
  253. return 'text/html';
  254. }
  255. // --------------------------------------------------------------------
  256. /**
  257. * Get Header
  258. *
  259. * @param string $header
  260. * @return string
  261. */
  262. public function get_header($header)
  263. {
  264. // Combine headers already sent with our batched headers
  265. $headers = array_merge(
  266. // We only need [x][0] from our multi-dimensional array
  267. array_map('array_shift', $this->headers),
  268. headers_list()
  269. );
  270. if (empty($headers) OR empty($header))
  271. {
  272. return NULL;
  273. }
  274. // Count backwards, in order to get the last matching header
  275. for ($c = count($headers) - 1; $c > -1; $c--)
  276. {
  277. if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0)
  278. {
  279. return trim(self::substr($headers[$c], $l+1));
  280. }
  281. }
  282. return NULL;
  283. }
  284. // --------------------------------------------------------------------
  285. /**
  286. * Set HTTP Status Header
  287. *
  288. * As of version 1.7.2, this is an alias for common function
  289. * set_status_header().
  290. *
  291. * @param int $code Status code (default: 200)
  292. * @param string $text Optional message
  293. * @return CI_Output
  294. */
  295. public function set_status_header($code = 200, $text = '')
  296. {
  297. set_status_header($code, $text);
  298. return $this;
  299. }
  300. // --------------------------------------------------------------------
  301. /**
  302. * Enable/disable Profiler
  303. *
  304. * @param bool $val TRUE to enable or FALSE to disable
  305. * @return CI_Output
  306. */
  307. public function enable_profiler($val = TRUE)
  308. {
  309. $this->enable_profiler = is_bool($val) ? $val : TRUE;
  310. return $this;
  311. }
  312. // --------------------------------------------------------------------
  313. /**
  314. * Set Profiler Sections
  315. *
  316. * Allows override of default/config settings for
  317. * Profiler section display.
  318. *
  319. * @param array $sections Profiler sections
  320. * @return CI_Output
  321. */
  322. public function set_profiler_sections($sections)
  323. {
  324. if (isset($sections['query_toggle_count']))
  325. {
  326. $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
  327. unset($sections['query_toggle_count']);
  328. }
  329. foreach ($sections as $section => $enable)
  330. {
  331. $this->_profiler_sections[$section] = ($enable !== FALSE);
  332. }
  333. return $this;
  334. }
  335. // --------------------------------------------------------------------
  336. /**
  337. * Set Cache
  338. *
  339. * @param int $time Cache expiration time in minutes
  340. * @return CI_Output
  341. */
  342. public function cache($time)
  343. {
  344. $this->cache_expiration = is_numeric($time) ? $time : 0;
  345. return $this;
  346. }
  347. // --------------------------------------------------------------------
  348. /**
  349. * Display Output
  350. *
  351. * Processes and sends finalized output data to the browser along
  352. * with any server headers and profile data. It also stops benchmark
  353. * timers so the page rendering speed and memory usage can be shown.
  354. *
  355. * Note: All "view" data is automatically put into $this->final_output
  356. * by controller class.
  357. *
  358. * @uses CI_Output::$final_output
  359. * @param string $output Output data override
  360. * @return void
  361. */
  362. public function _display($output = '')
  363. {
  364. // Note: We use load_class() because we can't use $CI =& get_instance()
  365. // since this function is sometimes called by the caching mechanism,
  366. // which happens before the CI super object is available.
  367. $BM =& load_class('Benchmark', 'core');
  368. $CFG =& load_class('Config', 'core');
  369. // Grab the super object if we can.
  370. if (class_exists('CI_Controller', FALSE))
  371. {
  372. $CI =& get_instance();
  373. }
  374. // --------------------------------------------------------------------
  375. // Set the output data
  376. if ($output === '')
  377. {
  378. $output =& $this->final_output;
  379. }
  380. // --------------------------------------------------------------------
  381. // Do we need to write a cache file? Only if the controller does not have its
  382. // own _output() method and we are not dealing with a cache file, which we
  383. // can determine by the existence of the $CI object above
  384. if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
  385. {
  386. $this->_write_cache($output);
  387. }
  388. // --------------------------------------------------------------------
  389. // Parse out the elapsed time and memory usage,
  390. // then swap the pseudo-variables with the data
  391. $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
  392. if ($this->parse_exec_vars === TRUE)
  393. {
  394. $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
  395. $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
  396. }
  397. // --------------------------------------------------------------------
  398. // Is compression requested?
  399. if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
  400. && $this->_compress_output === TRUE
  401. && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  402. {
  403. ob_start('ob_gzhandler');
  404. }
  405. // --------------------------------------------------------------------
  406. // Are there any server headers to send?
  407. if (count($this->headers) > 0)
  408. {
  409. foreach ($this->headers as $header)
  410. {
  411. @header($header[0], $header[1]);
  412. }
  413. }
  414. // --------------------------------------------------------------------
  415. // Does the $CI object exist?
  416. // If not we know we are dealing with a cache file so we'll
  417. // simply echo out the data and exit.
  418. if ( ! isset($CI))
  419. {
  420. if ($this->_compress_output === TRUE)
  421. {
  422. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  423. {
  424. header('Content-Encoding: gzip');
  425. header('Content-Length: '.self::strlen($output));
  426. }
  427. else
  428. {
  429. // User agent doesn't support gzip compression,
  430. // so we'll have to decompress our cache
  431. $output = gzinflate(self::substr($output, 10, -8));
  432. }
  433. }
  434. echo $output;
  435. log_message('info', 'Final output sent to browser');
  436. log_message('debug', 'Total execution time: '.$elapsed);
  437. return;
  438. }
  439. // --------------------------------------------------------------------
  440. // Do we need to generate profile data?
  441. // If so, load the Profile class and run it.
  442. if ($this->enable_profiler === TRUE)
  443. {
  444. $CI->load->library('profiler');
  445. if ( ! empty($this->_profiler_sections))
  446. {
  447. $CI->profiler->set_sections($this->_profiler_sections);
  448. }
  449. // If the output data contains closing </body> and </html> tags
  450. // we will remove them and add them back after we insert the profile data
  451. $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
  452. if ($count > 0)
  453. {
  454. $output .= '</body></html>';
  455. }
  456. }
  457. // Does the controller contain a function named _output()?
  458. // If so send the output there. Otherwise, echo it.
  459. if (method_exists($CI, '_output'))
  460. {
  461. $CI->_output($output);
  462. }
  463. else
  464. {
  465. echo $output; // Send it to the browser!
  466. }
  467. log_message('info', 'Final output sent to browser');
  468. log_message('debug', 'Total execution time: '.$elapsed);
  469. }
  470. // --------------------------------------------------------------------
  471. /**
  472. * Write Cache
  473. *
  474. * @param string $output Output data to cache
  475. * @return void
  476. */
  477. public function _write_cache($output)
  478. {
  479. $CI =& get_instance();
  480. $path = $CI->config->item('cache_path');
  481. $cache_path = ($path === '') ? APPPATH.'cache/' : $path;
  482. if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
  483. {
  484. log_message('error', 'Unable to write cache file: '.$cache_path);
  485. return;
  486. }
  487. $uri = $CI->config->item('base_url')
  488. .$CI->config->item('index_page')
  489. .$CI->uri->uri_string();
  490. if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
  491. {
  492. if (is_array($cache_query_string))
  493. {
  494. $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
  495. }
  496. else
  497. {
  498. $uri .= '?'.$_SERVER['QUERY_STRING'];
  499. }
  500. }
  501. $cache_path .= md5($uri);
  502. if ( ! $fp = @fopen($cache_path, 'w+b'))
  503. {
  504. log_message('error', 'Unable to write cache file: '.$cache_path);
  505. return;
  506. }
  507. if ( ! flock($fp, LOCK_EX))
  508. {
  509. log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
  510. fclose($fp);
  511. return;
  512. }
  513. // If output compression is enabled, compress the cache
  514. // itself, so that we don't have to do that each time
  515. // we're serving it
  516. if ($this->_compress_output === TRUE)
  517. {
  518. $output = gzencode($output);
  519. if ($this->get_header('content-type') === NULL)
  520. {
  521. $this->set_content_type($this->mime_type);
  522. }
  523. }
  524. $expire = time() + ($this->cache_expiration * 60);
  525. // Put together our serialized info.
  526. $cache_info = serialize(array(
  527. 'expire' => $expire,
  528. 'headers' => $this->headers
  529. ));
  530. $output = $cache_info.'ENDCI--->'.$output;
  531. for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result)
  532. {
  533. if (($result = fwrite($fp, self::substr($output, $written))) === FALSE)
  534. {
  535. break;
  536. }
  537. }
  538. flock($fp, LOCK_UN);
  539. fclose($fp);
  540. if ( ! is_int($result))
  541. {
  542. @unlink($cache_path);
  543. log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
  544. return;
  545. }
  546. chmod($cache_path, 0640);
  547. log_message('debug', 'Cache file written: '.$cache_path);
  548. // Send HTTP cache-control headers to browser to match file cache settings.
  549. $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
  550. }
  551. // --------------------------------------------------------------------
  552. /**
  553. * Update/serve cached output
  554. *
  555. * @uses CI_Config
  556. * @uses CI_URI
  557. *
  558. * @param object &$CFG CI_Config class instance
  559. * @param object &$URI CI_URI class instance
  560. * @return bool TRUE on success or FALSE on failure
  561. */
  562. public function _display_cache(&$CFG, &$URI)
  563. {
  564. $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
  565. // Build the file path. The file name is an MD5 hash of the full URI
  566. $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
  567. if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
  568. {
  569. if (is_array($cache_query_string))
  570. {
  571. $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
  572. }
  573. else
  574. {
  575. $uri .= '?'.$_SERVER['QUERY_STRING'];
  576. }
  577. }
  578. $filepath = $cache_path.md5($uri);
  579. if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb'))
  580. {
  581. return FALSE;
  582. }
  583. flock($fp, LOCK_SH);
  584. $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
  585. flock($fp, LOCK_UN);
  586. fclose($fp);
  587. // Look for embedded serialized file info.
  588. if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
  589. {
  590. return FALSE;
  591. }
  592. $cache_info = unserialize($match[1]);
  593. $expire = $cache_info['expire'];
  594. $last_modified = filemtime($filepath);
  595. // Has the file expired?
  596. if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
  597. {
  598. // If so we'll delete it.
  599. @unlink($filepath);
  600. log_message('debug', 'Cache file has expired. File deleted.');
  601. return FALSE;
  602. }
  603. // Send the HTTP cache control headers
  604. $this->set_cache_header($last_modified, $expire);
  605. // Add headers from cache file.
  606. foreach ($cache_info['headers'] as $header)
  607. {
  608. $this->set_header($header[0], $header[1]);
  609. }
  610. // Display the cache
  611. $this->_display(self::substr($cache, self::strlen($match[0])));
  612. log_message('debug', 'Cache file is current. Sending it to browser.');
  613. return TRUE;
  614. }
  615. // --------------------------------------------------------------------
  616. /**
  617. * Delete cache
  618. *
  619. * @param string $uri URI string
  620. * @return bool
  621. */
  622. public function delete_cache($uri = '')
  623. {
  624. $CI =& get_instance();
  625. $cache_path = $CI->config->item('cache_path');
  626. if ($cache_path === '')
  627. {
  628. $cache_path = APPPATH.'cache/';
  629. }
  630. if ( ! is_dir($cache_path))
  631. {
  632. log_message('error', 'Unable to find cache path: '.$cache_path);
  633. return FALSE;
  634. }
  635. if (empty($uri))
  636. {
  637. $uri = $CI->uri->uri_string();
  638. if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
  639. {
  640. if (is_array($cache_query_string))
  641. {
  642. $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
  643. }
  644. else
  645. {
  646. $uri .= '?'.$_SERVER['QUERY_STRING'];
  647. }
  648. }
  649. }
  650. $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/'));
  651. if ( ! @unlink($cache_path))
  652. {
  653. log_message('error', 'Unable to delete cache file for '.$uri);
  654. return FALSE;
  655. }
  656. return TRUE;
  657. }
  658. // --------------------------------------------------------------------
  659. /**
  660. * Set Cache Header
  661. *
  662. * Set the HTTP headers to match the server-side file cache settings
  663. * in order to reduce bandwidth.
  664. *
  665. * @param int $last_modified Timestamp of when the page was last modified
  666. * @param int $expiration Timestamp of when should the requested page expire from cache
  667. * @return void
  668. */
  669. public function set_cache_header($last_modified, $expiration)
  670. {
  671. $max_age = $expiration - $_SERVER['REQUEST_TIME'];
  672. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
  673. {
  674. $this->set_status_header(304);
  675. exit;
  676. }
  677. header('Pragma: public');
  678. header('Cache-Control: max-age='.$max_age.', public');
  679. header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
  680. header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
  681. }
  682. // --------------------------------------------------------------------
  683. /**
  684. * Byte-safe strlen()
  685. *
  686. * @param string $str
  687. * @return int
  688. */
  689. protected static function strlen($str)
  690. {
  691. return (self::$func_overload)
  692. ? mb_strlen($str, '8bit')
  693. : strlen($str);
  694. }
  695. // --------------------------------------------------------------------
  696. /**
  697. * Byte-safe substr()
  698. *
  699. * @param string $str
  700. * @param int $start
  701. * @param int $length
  702. * @return string
  703. */
  704. protected static function substr($str, $start, $length = NULL)
  705. {
  706. if (self::$func_overload)
  707. {
  708. // mb_substr($str, $start, null, '8bit') returns an empty
  709. // string on PHP 5.3
  710. isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start);
  711. return mb_substr($str, $start, $length, '8bit');
  712. }
  713. return isset($length)
  714. ? substr($str, $start, $length)
  715. : substr($str, $start);
  716. }
  717. }