Output.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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 - 2016, 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 - 2016, 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_override flag
  115. *
  116. * @var bool
  117. */
  118. protected static $func_override;
  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_override) OR self::$func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override'));
  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. for ($i = 0, $c = count($headers); $i < $c; $i++)
  275. {
  276. if (strncasecmp($header, $headers[$i], $l = self::strlen($header)) === 0)
  277. {
  278. return trim(self::substr($headers[$i], $l+1));
  279. }
  280. }
  281. return NULL;
  282. }
  283. // --------------------------------------------------------------------
  284. /**
  285. * Set HTTP Status Header
  286. *
  287. * As of version 1.7.2, this is an alias for common function
  288. * set_status_header().
  289. *
  290. * @param int $code Status code (default: 200)
  291. * @param string $text Optional message
  292. * @return CI_Output
  293. */
  294. public function set_status_header($code = 200, $text = '')
  295. {
  296. set_status_header($code, $text);
  297. return $this;
  298. }
  299. // --------------------------------------------------------------------
  300. /**
  301. * Enable/disable Profiler
  302. *
  303. * @param bool $val TRUE to enable or FALSE to disable
  304. * @return CI_Output
  305. */
  306. public function enable_profiler($val = TRUE)
  307. {
  308. $this->enable_profiler = is_bool($val) ? $val : TRUE;
  309. return $this;
  310. }
  311. // --------------------------------------------------------------------
  312. /**
  313. * Set Profiler Sections
  314. *
  315. * Allows override of default/config settings for
  316. * Profiler section display.
  317. *
  318. * @param array $sections Profiler sections
  319. * @return CI_Output
  320. */
  321. public function set_profiler_sections($sections)
  322. {
  323. if (isset($sections['query_toggle_count']))
  324. {
  325. $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
  326. unset($sections['query_toggle_count']);
  327. }
  328. foreach ($sections as $section => $enable)
  329. {
  330. $this->_profiler_sections[$section] = ($enable !== FALSE);
  331. }
  332. return $this;
  333. }
  334. // --------------------------------------------------------------------
  335. /**
  336. * Set Cache
  337. *
  338. * @param int $time Cache expiration time in minutes
  339. * @return CI_Output
  340. */
  341. public function cache($time)
  342. {
  343. $this->cache_expiration = is_numeric($time) ? $time : 0;
  344. return $this;
  345. }
  346. // --------------------------------------------------------------------
  347. /**
  348. * Display Output
  349. *
  350. * Processes and sends finalized output data to the browser along
  351. * with any server headers and profile data. It also stops benchmark
  352. * timers so the page rendering speed and memory usage can be shown.
  353. *
  354. * Note: All "view" data is automatically put into $this->final_output
  355. * by controller class.
  356. *
  357. * @uses CI_Output::$final_output
  358. * @param string $output Output data override
  359. * @return void
  360. */
  361. public function _display($output = '')
  362. {
  363. // Note: We use load_class() because we can't use $CI =& get_instance()
  364. // since this function is sometimes called by the caching mechanism,
  365. // which happens before the CI super object is available.
  366. $BM =& load_class('Benchmark', 'core');
  367. $CFG =& load_class('Config', 'core');
  368. // Grab the super object if we can.
  369. if (class_exists('CI_Controller', FALSE))
  370. {
  371. $CI =& get_instance();
  372. }
  373. // --------------------------------------------------------------------
  374. // Set the output data
  375. if ($output === '')
  376. {
  377. $output =& $this->final_output;
  378. }
  379. // --------------------------------------------------------------------
  380. // Do we need to write a cache file? Only if the controller does not have its
  381. // own _output() method and we are not dealing with a cache file, which we
  382. // can determine by the existence of the $CI object above
  383. if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
  384. {
  385. $this->_write_cache($output);
  386. }
  387. // --------------------------------------------------------------------
  388. // Parse out the elapsed time and memory usage,
  389. // then swap the pseudo-variables with the data
  390. $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
  391. if ($this->parse_exec_vars === TRUE)
  392. {
  393. $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
  394. $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
  395. }
  396. // --------------------------------------------------------------------
  397. // Is compression requested?
  398. if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
  399. && $this->_compress_output === TRUE
  400. && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  401. {
  402. ob_start('ob_gzhandler');
  403. }
  404. // --------------------------------------------------------------------
  405. // Are there any server headers to send?
  406. if (count($this->headers) > 0)
  407. {
  408. foreach ($this->headers as $header)
  409. {
  410. @header($header[0], $header[1]);
  411. }
  412. }
  413. // --------------------------------------------------------------------
  414. // Does the $CI object exist?
  415. // If not we know we are dealing with a cache file so we'll
  416. // simply echo out the data and exit.
  417. if ( ! isset($CI))
  418. {
  419. if ($this->_compress_output === TRUE)
  420. {
  421. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  422. {
  423. header('Content-Encoding: gzip');
  424. header('Content-Length: '.self::strlen($output));
  425. }
  426. else
  427. {
  428. // User agent doesn't support gzip compression,
  429. // so we'll have to decompress our cache
  430. $output = gzinflate(self::substr($output, 10, -8));
  431. }
  432. }
  433. echo $output;
  434. log_message('info', 'Final output sent to browser');
  435. log_message('debug', 'Total execution time: '.$elapsed);
  436. return;
  437. }
  438. // --------------------------------------------------------------------
  439. // Do we need to generate profile data?
  440. // If so, load the Profile class and run it.
  441. if ($this->enable_profiler === TRUE)
  442. {
  443. $CI->load->library('profiler');
  444. if ( ! empty($this->_profiler_sections))
  445. {
  446. $CI->profiler->set_sections($this->_profiler_sections);
  447. }
  448. // If the output data contains closing </body> and </html> tags
  449. // we will remove them and add them back after we insert the profile data
  450. $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
  451. if ($count > 0)
  452. {
  453. $output .= '</body></html>';
  454. }
  455. }
  456. // Does the controller contain a function named _output()?
  457. // If so send the output there. Otherwise, echo it.
  458. if (method_exists($CI, '_output'))
  459. {
  460. $CI->_output($output);
  461. }
  462. else
  463. {
  464. echo $output; // Send it to the browser!
  465. }
  466. log_message('info', 'Final output sent to browser');
  467. log_message('debug', 'Total execution time: '.$elapsed);
  468. }
  469. // --------------------------------------------------------------------
  470. /**
  471. * Write Cache
  472. *
  473. * @param string $output Output data to cache
  474. * @return void
  475. */
  476. public function _write_cache($output)
  477. {
  478. $CI =& get_instance();
  479. $path = $CI->config->item('cache_path');
  480. $cache_path = ($path === '') ? APPPATH.'cache/' : $path;
  481. if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
  482. {
  483. log_message('error', 'Unable to write cache file: '.$cache_path);
  484. return;
  485. }
  486. $uri = $CI->config->item('base_url')
  487. .$CI->config->item('index_page')
  488. .$CI->uri->uri_string();
  489. if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
  490. {
  491. if (is_array($cache_query_string))
  492. {
  493. $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
  494. }
  495. else
  496. {
  497. $uri .= '?'.$_SERVER['QUERY_STRING'];
  498. }
  499. }
  500. $cache_path .= md5($uri);
  501. if ( ! $fp = @fopen($cache_path, 'w+b'))
  502. {
  503. log_message('error', 'Unable to write cache file: '.$cache_path);
  504. return;
  505. }
  506. if (flock($fp, LOCK_EX))
  507. {
  508. // If output compression is enabled, compress the cache
  509. // itself, so that we don't have to do that each time
  510. // we're serving it
  511. if ($this->_compress_output === TRUE)
  512. {
  513. $output = gzencode($output);
  514. if ($this->get_header('content-type') === NULL)
  515. {
  516. $this->set_content_type($this->mime_type);
  517. }
  518. }
  519. $expire = time() + ($this->cache_expiration * 60);
  520. // Put together our serialized info.
  521. $cache_info = serialize(array(
  522. 'expire' => $expire,
  523. 'headers' => $this->headers
  524. ));
  525. $output = $cache_info.'ENDCI--->'.$output;
  526. for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result)
  527. {
  528. if (($result = fwrite($fp, self::substr($output, $written))) === FALSE)
  529. {
  530. break;
  531. }
  532. }
  533. flock($fp, LOCK_UN);
  534. }
  535. else
  536. {
  537. log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
  538. return;
  539. }
  540. fclose($fp);
  541. if (is_int($result))
  542. {
  543. chmod($cache_path, 0640);
  544. log_message('debug', 'Cache file written: '.$cache_path);
  545. // Send HTTP cache-control headers to browser to match file cache settings.
  546. $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
  547. }
  548. else
  549. {
  550. @unlink($cache_path);
  551. log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
  552. }
  553. }
  554. // --------------------------------------------------------------------
  555. /**
  556. * Update/serve cached output
  557. *
  558. * @uses CI_Config
  559. * @uses CI_URI
  560. *
  561. * @param object &$CFG CI_Config class instance
  562. * @param object &$URI CI_URI class instance
  563. * @return bool TRUE on success or FALSE on failure
  564. */
  565. public function _display_cache(&$CFG, &$URI)
  566. {
  567. $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
  568. // Build the file path. The file name is an MD5 hash of the full URI
  569. $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
  570. if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
  571. {
  572. if (is_array($cache_query_string))
  573. {
  574. $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
  575. }
  576. else
  577. {
  578. $uri .= '?'.$_SERVER['QUERY_STRING'];
  579. }
  580. }
  581. $filepath = $cache_path.md5($uri);
  582. if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb'))
  583. {
  584. return FALSE;
  585. }
  586. flock($fp, LOCK_SH);
  587. $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
  588. flock($fp, LOCK_UN);
  589. fclose($fp);
  590. // Look for embedded serialized file info.
  591. if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
  592. {
  593. return FALSE;
  594. }
  595. $cache_info = unserialize($match[1]);
  596. $expire = $cache_info['expire'];
  597. $last_modified = filemtime($filepath);
  598. // Has the file expired?
  599. if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
  600. {
  601. // If so we'll delete it.
  602. @unlink($filepath);
  603. log_message('debug', 'Cache file has expired. File deleted.');
  604. return FALSE;
  605. }
  606. else
  607. {
  608. // Or else send the HTTP cache control headers.
  609. $this->set_cache_header($last_modified, $expire);
  610. }
  611. // Add headers from cache file.
  612. foreach ($cache_info['headers'] as $header)
  613. {
  614. $this->set_header($header[0], $header[1]);
  615. }
  616. // Display the cache
  617. $this->_display(self::substr($cache, self::strlen($match[0])));
  618. log_message('debug', 'Cache file is current. Sending it to browser.');
  619. return TRUE;
  620. }
  621. // --------------------------------------------------------------------
  622. /**
  623. * Delete cache
  624. *
  625. * @param string $uri URI string
  626. * @return bool
  627. */
  628. public function delete_cache($uri = '')
  629. {
  630. $CI =& get_instance();
  631. $cache_path = $CI->config->item('cache_path');
  632. if ($cache_path === '')
  633. {
  634. $cache_path = APPPATH.'cache/';
  635. }
  636. if ( ! is_dir($cache_path))
  637. {
  638. log_message('error', 'Unable to find cache path: '.$cache_path);
  639. return FALSE;
  640. }
  641. if (empty($uri))
  642. {
  643. $uri = $CI->uri->uri_string();
  644. if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
  645. {
  646. if (is_array($cache_query_string))
  647. {
  648. $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
  649. }
  650. else
  651. {
  652. $uri .= '?'.$_SERVER['QUERY_STRING'];
  653. }
  654. }
  655. }
  656. $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/'));
  657. if ( ! @unlink($cache_path))
  658. {
  659. log_message('error', 'Unable to delete cache file for '.$uri);
  660. return FALSE;
  661. }
  662. return TRUE;
  663. }
  664. // --------------------------------------------------------------------
  665. /**
  666. * Set Cache Header
  667. *
  668. * Set the HTTP headers to match the server-side file cache settings
  669. * in order to reduce bandwidth.
  670. *
  671. * @param int $last_modified Timestamp of when the page was last modified
  672. * @param int $expiration Timestamp of when should the requested page expire from cache
  673. * @return void
  674. */
  675. public function set_cache_header($last_modified, $expiration)
  676. {
  677. $max_age = $expiration - $_SERVER['REQUEST_TIME'];
  678. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
  679. {
  680. $this->set_status_header(304);
  681. exit;
  682. }
  683. else
  684. {
  685. header('Pragma: public');
  686. header('Cache-Control: max-age='.$max_age.', public');
  687. header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
  688. header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
  689. }
  690. }
  691. // --------------------------------------------------------------------
  692. /**
  693. * Byte-safe strlen()
  694. *
  695. * @param string $str
  696. * @return int
  697. */
  698. protected static function strlen($str)
  699. {
  700. return (self::$func_override)
  701. ? mb_strlen($str, '8bit')
  702. : strlen($str);
  703. }
  704. // --------------------------------------------------------------------
  705. /**
  706. * Byte-safe substr()
  707. *
  708. * @param string $str
  709. * @param int $start
  710. * @param int $length
  711. * @return string
  712. */
  713. protected static function substr($str, $start, $length = NULL)
  714. {
  715. if (self::$func_override)
  716. {
  717. // mb_substr($str, $start, null, '8bit') returns an empty
  718. // string on PHP 5.3
  719. isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start);
  720. return mb_substr($str, $start, $length, '8bit');
  721. }
  722. return isset($length)
  723. ? substr($str, $start, $length)
  724. : substr($str, $start);
  725. }
  726. }