PageRenderTime 55ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/system/core/Output.php

https://github.com/betchi/CodeIgniter
PHP | 963 lines | 480 code | 153 blank | 330 comment | 65 complexity | b64eecf87115e45976de029d74eb1b24 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP 5.2.4 or newer
  6. *
  7. * NOTICE OF LICENSE
  8. *
  9. * Licensed under the Open Software License version 3.0
  10. *
  11. * This source file is subject to the Open Software License (OSL 3.0) that is
  12. * bundled with this package in the files license.txt / license.rst. It is
  13. * also available through the world wide web at this URL:
  14. * http://opensource.org/licenses/OSL-3.0
  15. * If you did not receive a copy of the license and are unable to obtain it
  16. * through the world wide web, please send an email to
  17. * licensing@ellislab.com so we can send you a copy immediately.
  18. *
  19. * @package CodeIgniter
  20. * @author EllisLab Dev Team
  21. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
  22. * @license http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
  23. * @link http://codeigniter.com
  24. * @since Version 1.0
  25. * @filesource
  26. */
  27. defined('BASEPATH') OR exit('No direct script access allowed');
  28. /**
  29. * Output Class
  30. *
  31. * Responsible for sending final output to the browser.
  32. *
  33. * @package CodeIgniter
  34. * @subpackage Libraries
  35. * @category Output
  36. * @author EllisLab Dev Team
  37. * @link http://codeigniter.com/user_guide/libraries/output.html
  38. */
  39. class CI_Output {
  40. /**
  41. * Final output string
  42. *
  43. * @var string
  44. */
  45. public $final_output;
  46. /**
  47. * Cache expiration time
  48. *
  49. * @var int
  50. */
  51. public $cache_expiration = 0;
  52. /**
  53. * List of server headers
  54. *
  55. * @var array
  56. */
  57. public $headers = array();
  58. /**
  59. * List of mime types
  60. *
  61. * @var array
  62. */
  63. public $mimes = array();
  64. /**
  65. * Mime-type for the current page
  66. *
  67. * @var string
  68. */
  69. protected $mime_type = 'text/html';
  70. /**
  71. * Enable Profiler flag
  72. *
  73. * @var bool
  74. */
  75. public $enable_profiler = FALSE;
  76. /**
  77. * php.ini zlib.output_compression flag
  78. *
  79. * @var bool
  80. */
  81. protected $_zlib_oc = FALSE;
  82. /**
  83. * CI output compression flag
  84. *
  85. * @var bool
  86. */
  87. protected $_compress_output = FALSE;
  88. /**
  89. * List of profiler sections
  90. *
  91. * @var array
  92. */
  93. protected $_profiler_sections = array();
  94. /**
  95. * Parse markers flag
  96. *
  97. * Whether or not to parse variables like {elapsed_time} and {memory_usage}.
  98. *
  99. * @var bool
  100. */
  101. public $parse_exec_vars = TRUE;
  102. /**
  103. * Class constructor
  104. *
  105. * Determines whether zLib output compression will be used.
  106. *
  107. * @return void
  108. */
  109. public function __construct()
  110. {
  111. $this->_zlib_oc = (bool) ini_get('zlib.output_compression');
  112. $this->_compress_output = (
  113. $this->_zlib_oc === FALSE
  114. && config_item('compress_output') === TRUE
  115. && extension_loaded('zlib')
  116. );
  117. // Get mime types for later
  118. $this->mimes =& get_mimes();
  119. log_message('debug', 'Output Class Initialized');
  120. }
  121. // --------------------------------------------------------------------
  122. /**
  123. * Get Output
  124. *
  125. * Returns the current output string.
  126. *
  127. * @return string
  128. */
  129. public function get_output()
  130. {
  131. return $this->final_output;
  132. }
  133. // --------------------------------------------------------------------
  134. /**
  135. * Set Output
  136. *
  137. * Sets the output string.
  138. *
  139. * @param string $output Output data
  140. * @return CI_Output
  141. */
  142. public function set_output($output)
  143. {
  144. $this->final_output = $output;
  145. return $this;
  146. }
  147. // --------------------------------------------------------------------
  148. /**
  149. * Append Output
  150. *
  151. * Appends data onto the output string.
  152. *
  153. * @param string $output Data to append
  154. * @return CI_Output
  155. */
  156. public function append_output($output)
  157. {
  158. $this->final_output .= $output;
  159. return $this;
  160. }
  161. // --------------------------------------------------------------------
  162. /**
  163. * Set Header
  164. *
  165. * Lets you set a server header which will be sent with the final output.
  166. *
  167. * Note: If a file is cached, headers will not be sent.
  168. * @todo We need to figure out how to permit headers to be cached.
  169. *
  170. * @param string $header Header
  171. * @param bool $replace Whether to replace the old header value, if already set
  172. * @return CI_Output
  173. */
  174. public function set_header($header, $replace = TRUE)
  175. {
  176. // If zlib.output_compression is enabled it will compress the output,
  177. // but it will not modify the content-length header to compensate for
  178. // the reduction, causing the browser to hang waiting for more data.
  179. // We'll just skip content-length in those cases.
  180. if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0)
  181. {
  182. return $this;
  183. }
  184. $this->headers[] = array($header, $replace);
  185. return $this;
  186. }
  187. // --------------------------------------------------------------------
  188. /**
  189. * Set Content-Type Header
  190. *
  191. * @param string $mime_type Extension of the file we're outputting
  192. * @param string $charset Character set (default: NULL)
  193. * @return CI_Output
  194. */
  195. public function set_content_type($mime_type, $charset = NULL)
  196. {
  197. if (strpos($mime_type, '/') === FALSE)
  198. {
  199. $extension = ltrim($mime_type, '.');
  200. // Is this extension supported?
  201. if (isset($this->mimes[$extension]))
  202. {
  203. $mime_type =& $this->mimes[$extension];
  204. if (is_array($mime_type))
  205. {
  206. $mime_type = current($mime_type);
  207. }
  208. }
  209. }
  210. $this->mime_type = $mime_type;
  211. if (empty($charset))
  212. {
  213. $charset = config_item('charset');
  214. }
  215. $header = 'Content-Type: '.$mime_type
  216. .(empty($charset) ? '' : '; charset='.$charset);
  217. $this->headers[] = array($header, TRUE);
  218. return $this;
  219. }
  220. // --------------------------------------------------------------------
  221. /**
  222. * Get Current Content-Type Header
  223. *
  224. * @return string 'text/html', if not already set
  225. */
  226. public function get_content_type()
  227. {
  228. for ($i = 0, $c = count($this->headers); $i < $c; $i++)
  229. {
  230. if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)
  231. {
  232. return $content_type;
  233. }
  234. }
  235. return 'text/html';
  236. }
  237. // --------------------------------------------------------------------
  238. /**
  239. * Get Header
  240. *
  241. * @param string $header_name
  242. * @return string
  243. */
  244. public function get_header($header)
  245. {
  246. // Combine headers already sent with our batched headers
  247. $headers = array_merge(
  248. // We only need [x][0] from our multi-dimensional array
  249. array_map('array_shift', $this->headers),
  250. headers_list()
  251. );
  252. if (empty($headers) OR empty($header))
  253. {
  254. return NULL;
  255. }
  256. for ($i = 0, $c = count($headers); $i < $c; $i++)
  257. {
  258. if (strncasecmp($header, $headers[$i], $l = strlen($header)) === 0)
  259. {
  260. return trim(substr($headers[$i], $l+1));
  261. }
  262. }
  263. return NULL;
  264. }
  265. // --------------------------------------------------------------------
  266. /**
  267. * Set HTTP Status Header
  268. *
  269. * As of version 1.7.2, this is an alias for common function
  270. * set_status_header().
  271. *
  272. * @param int $code Status code (default: 200)
  273. * @param string $text Optional message
  274. * @return CI_Output
  275. */
  276. public function set_status_header($code = 200, $text = '')
  277. {
  278. set_status_header($code, $text);
  279. return $this;
  280. }
  281. // --------------------------------------------------------------------
  282. /**
  283. * Enable/disable Profiler
  284. *
  285. * @param bool $val TRUE to enable or FALSE to disable
  286. * @return CI_Output
  287. */
  288. public function enable_profiler($val = TRUE)
  289. {
  290. $this->enable_profiler = is_bool($val) ? $val : TRUE;
  291. return $this;
  292. }
  293. // --------------------------------------------------------------------
  294. /**
  295. * Set Profiler Sections
  296. *
  297. * Allows override of default/config settings for
  298. * Profiler section display.
  299. *
  300. * @param array $sections Profiler sections
  301. * @return CI_Output
  302. */
  303. public function set_profiler_sections($sections)
  304. {
  305. if (isset($sections['query_toggle_count']))
  306. {
  307. $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
  308. unset($sections['query_toggle_count']);
  309. }
  310. foreach ($sections as $section => $enable)
  311. {
  312. $this->_profiler_sections[$section] = ($enable !== FALSE);
  313. }
  314. return $this;
  315. }
  316. // --------------------------------------------------------------------
  317. /**
  318. * Set Cache
  319. *
  320. * @param int $time Cache expiration time in seconds
  321. * @return CI_Output
  322. */
  323. public function cache($time)
  324. {
  325. $this->cache_expiration = is_numeric($time) ? $time : 0;
  326. return $this;
  327. }
  328. // --------------------------------------------------------------------
  329. /**
  330. * Display Output
  331. *
  332. * Processes sends the sends finalized output data to the browser along
  333. * with any server headers and profile data. It also stops benchmark
  334. * timers so the page rendering speed and memory usage can be shown.
  335. *
  336. * Note: All "view" data is automatically put into $this->final_output
  337. * by controller class.
  338. *
  339. * @uses CI_Output::$final_output
  340. * @param string $output Output data override
  341. * @return void
  342. */
  343. public function _display($output = '')
  344. {
  345. // Note: We use load_class() because we can't use $CI =& get_instance()
  346. // since this function is sometimes called by the caching mechanism,
  347. // which happens before the CI super object is available.
  348. $BM =& load_class('Benchmark', 'core');
  349. $CFG =& load_class('Config', 'core');
  350. // Grab the super object if we can.
  351. if (class_exists('CI_Controller', FALSE))
  352. {
  353. $CI =& get_instance();
  354. }
  355. // --------------------------------------------------------------------
  356. // Set the output data
  357. if ($output === '')
  358. {
  359. $output =& $this->final_output;
  360. }
  361. // --------------------------------------------------------------------
  362. // Is minify requested?
  363. if ($CFG->item('minify_output') === TRUE)
  364. {
  365. $output = $this->minify($output, $this->mime_type);
  366. }
  367. // --------------------------------------------------------------------
  368. // Do we need to write a cache file? Only if the controller does not have its
  369. // own _output() method and we are not dealing with a cache file, which we
  370. // can determine by the existence of the $CI object above
  371. if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
  372. {
  373. $this->_write_cache($output);
  374. }
  375. // --------------------------------------------------------------------
  376. // Parse out the elapsed time and memory usage,
  377. // then swap the pseudo-variables with the data
  378. $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
  379. if ($this->parse_exec_vars === TRUE)
  380. {
  381. $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
  382. $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
  383. }
  384. // --------------------------------------------------------------------
  385. // Is compression requested?
  386. if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
  387. && $this->_compress_output === TRUE
  388. && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  389. {
  390. ob_start('ob_gzhandler');
  391. }
  392. // --------------------------------------------------------------------
  393. // Are there any server headers to send?
  394. if (count($this->headers) > 0)
  395. {
  396. foreach ($this->headers as $header)
  397. {
  398. @header($header[0], $header[1]);
  399. }
  400. }
  401. // --------------------------------------------------------------------
  402. // Does the $CI object exist?
  403. // If not we know we are dealing with a cache file so we'll
  404. // simply echo out the data and exit.
  405. if ( ! isset($CI))
  406. {
  407. if ($this->_compress_output === TRUE)
  408. {
  409. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  410. {
  411. header('Content-Encoding: gzip');
  412. header('Content-Length: '.strlen($output));
  413. }
  414. else
  415. {
  416. // User agent doesn't support gzip compression,
  417. // so we'll have to decompress our cache
  418. $output = gzinflate(substr($output, 10, -8));
  419. }
  420. }
  421. echo $output;
  422. log_message('debug', 'Final output sent to browser');
  423. log_message('debug', 'Total execution time: '.$elapsed);
  424. return;
  425. }
  426. // --------------------------------------------------------------------
  427. // Do we need to generate profile data?
  428. // If so, load the Profile class and run it.
  429. if ($this->enable_profiler === TRUE)
  430. {
  431. $CI->load->library('profiler');
  432. if ( ! empty($this->_profiler_sections))
  433. {
  434. $CI->profiler->set_sections($this->_profiler_sections);
  435. }
  436. // If the output data contains closing </body> and </html> tags
  437. // we will remove them and add them back after we insert the profile data
  438. $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
  439. if ($count > 0)
  440. {
  441. $output .= '</body></html>';
  442. }
  443. }
  444. // Does the controller contain a function named _output()?
  445. // If so send the output there. Otherwise, echo it.
  446. if (method_exists($CI, '_output'))
  447. {
  448. $CI->_output($output);
  449. }
  450. else
  451. {
  452. echo $output; // Send it to the browser!
  453. }
  454. log_message('debug', 'Final output sent to browser');
  455. log_message('debug', 'Total execution time: '.$elapsed);
  456. }
  457. // --------------------------------------------------------------------
  458. /**
  459. * Write Cache
  460. *
  461. * @param string $output Output data to cache
  462. * @return void
  463. */
  464. public function _write_cache($output)
  465. {
  466. $CI =& get_instance();
  467. $path = $CI->config->item('cache_path');
  468. $cache_path = ($path === '') ? APPPATH.'cache/' : $path;
  469. if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
  470. {
  471. log_message('error', 'Unable to write cache file: '.$cache_path);
  472. return;
  473. }
  474. $uri = $CI->config->item('base_url')
  475. .$CI->config->item('index_page')
  476. .$CI->uri->uri_string();
  477. $cache_path .= md5($uri);
  478. if ( ! $fp = @fopen($cache_path, 'w+b'))
  479. {
  480. log_message('error', 'Unable to write cache file: '.$cache_path);
  481. return;
  482. }
  483. if (flock($fp, LOCK_EX))
  484. {
  485. // If output compression is enabled, compress the cache
  486. // itself, so that we don't have to do that each time
  487. // we're serving it
  488. if ($this->_compress_output === TRUE)
  489. {
  490. $output = gzencode($output);
  491. if ($this->get_header('content-type') === NULL)
  492. {
  493. $this->set_content_type($this->mime_type);
  494. }
  495. }
  496. $expire = time() + ($this->cache_expiration * 60);
  497. // Put together our serialized info.
  498. $cache_info = serialize(array(
  499. 'expire' => $expire,
  500. 'headers' => $this->headers
  501. ));
  502. $output = $cache_info.'ENDCI--->'.$output;
  503. for ($written = 0, $length = strlen($output); $written < $length; $written += $result)
  504. {
  505. if (($result = fwrite($fp, substr($output, $written))) === FALSE)
  506. {
  507. break;
  508. }
  509. }
  510. flock($fp, LOCK_UN);
  511. }
  512. else
  513. {
  514. log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
  515. return;
  516. }
  517. fclose($fp);
  518. if (is_int($result))
  519. {
  520. @chmod($cache_path, 0666);
  521. log_message('debug', 'Cache file written: '.$cache_path);
  522. // Send HTTP cache-control headers to browser to match file cache settings.
  523. $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
  524. }
  525. else
  526. {
  527. @unlink($cache_path);
  528. log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
  529. }
  530. }
  531. // --------------------------------------------------------------------
  532. /**
  533. * Update/serve cached output
  534. *
  535. * @uses CI_Config
  536. * @uses CI_URI
  537. *
  538. * @param object &$CFG CI_Config class instance
  539. * @param object &$URI CI_URI class instance
  540. * @return bool TRUE on success or FALSE on failure
  541. */
  542. public function _display_cache(&$CFG, &$URI)
  543. {
  544. $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
  545. // Build the file path. The file name is an MD5 hash of the full URI
  546. $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
  547. $filepath = $cache_path.md5($uri);
  548. if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb'))
  549. {
  550. return FALSE;
  551. }
  552. flock($fp, LOCK_SH);
  553. $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
  554. flock($fp, LOCK_UN);
  555. fclose($fp);
  556. // Look for embedded serialized file info.
  557. if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
  558. {
  559. return FALSE;
  560. }
  561. $cache_info = unserialize($match[1]);
  562. $expire = $cache_info['expire'];
  563. $last_modified = filemtime($cache_path);
  564. // Has the file expired?
  565. if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
  566. {
  567. // If so we'll delete it.
  568. @unlink($filepath);
  569. log_message('debug', 'Cache file has expired. File deleted.');
  570. return FALSE;
  571. }
  572. else
  573. {
  574. // Or else send the HTTP cache control headers.
  575. $this->set_cache_header($last_modified, $expire);
  576. }
  577. // Add headers from cache file.
  578. foreach ($cache_info['headers'] as $header)
  579. {
  580. $this->set_header($header[0], $header[1]);
  581. }
  582. // Display the cache
  583. $this->_display(substr($cache, strlen($match[0])));
  584. log_message('debug', 'Cache file is current. Sending it to browser.');
  585. return TRUE;
  586. }
  587. // --------------------------------------------------------------------
  588. /**
  589. * Delete cache
  590. *
  591. * @param string $uri URI string
  592. * @return bool
  593. */
  594. public function delete_cache($uri = '')
  595. {
  596. $CI =& get_instance();
  597. $cache_path = $CI->config->item('cache_path');
  598. if ($cache_path === '')
  599. {
  600. $cache_path = APPPATH.'cache/';
  601. }
  602. if ( ! is_dir($cache_path))
  603. {
  604. log_message('error', 'Unable to find cache path: '.$cache_path);
  605. return FALSE;
  606. }
  607. if (empty($uri))
  608. {
  609. $uri = $CI->uri->uri_string();
  610. }
  611. $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').$uri);
  612. if ( ! @unlink($cache_path))
  613. {
  614. log_message('error', 'Unable to delete cache file for '.$uri);
  615. return FALSE;
  616. }
  617. return TRUE;
  618. }
  619. // --------------------------------------------------------------------
  620. /**
  621. * Set Cache Header
  622. *
  623. * Set the HTTP headers to match the server-side file cache settings
  624. * in order to reduce bandwidth.
  625. *
  626. * @param int $last_modified Timestamp of when the page was last modified
  627. * @param int $expiration Timestamp of when should the requested page expire from cache
  628. * @return void
  629. */
  630. public function set_cache_header($last_modified, $expiration)
  631. {
  632. $max_age = $expiration - $_SERVER['REQUEST_TIME'];
  633. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
  634. {
  635. $this->set_status_header(304);
  636. exit;
  637. }
  638. else
  639. {
  640. header('Pragma: public');
  641. header('Cache-Control: max-age='.$max_age.', public');
  642. header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
  643. header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
  644. }
  645. }
  646. // --------------------------------------------------------------------
  647. /**
  648. * Minify
  649. *
  650. * Reduce excessive size of HTML/CSS/JavaScript content.
  651. *
  652. * @param string $output Output to minify
  653. * @param string $type Output content MIME type
  654. * @return string Minified output
  655. */
  656. public function minify($output, $type = 'text/html')
  657. {
  658. switch ($type)
  659. {
  660. case 'text/html':
  661. if (($size_before = strlen($output)) === 0)
  662. {
  663. return '';
  664. }
  665. // Find all the <pre>,<code>,<textarea>, and <javascript> tags
  666. // We'll want to return them to this unprocessed state later.
  667. preg_match_all('{<pre.+</pre>}msU', $output, $pres_clean);
  668. preg_match_all('{<code.+</code>}msU', $output, $codes_clean);
  669. preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_clean);
  670. preg_match_all('{<script.+</script>}msU', $output, $javascript_clean);
  671. // Minify the CSS in all the <style> tags.
  672. preg_match_all('{<style.+</style>}msU', $output, $style_clean);
  673. foreach ($style_clean[0] as $s)
  674. {
  675. $output = str_replace($s, $this->_minify_js_css($s, 'css', TRUE), $output);
  676. }
  677. // Minify the javascript in <script> tags.
  678. foreach ($javascript_clean[0] as $s)
  679. {
  680. $javascript_mini[] = $this->_minify_js_css($s, 'js', TRUE);
  681. }
  682. // Replace multiple spaces with a single space.
  683. $output = preg_replace('!\s{2,}!', ' ', $output);
  684. // Remove comments (non-MSIE conditionals)
  685. $output = preg_replace('{\s*<!--[^\[<>].*(?<!!)-->\s*}msU', '', $output);
  686. // Remove spaces around block-level elements.
  687. $output = preg_replace('/\s*(<\/?(html|head|title|meta|script|link|style|body|table|thead|tbody|tfoot|tr|th|td|h[1-6]|div|p|br)[^>]*>)\s*/is', '$1', $output);
  688. // Replace mangled <pre> etc. tags with unprocessed ones.
  689. if ( ! empty($pres_clean))
  690. {
  691. preg_match_all('{<pre.+</pre>}msU', $output, $pres_messed);
  692. $output = str_replace($pres_messed[0], $pres_clean[0], $output);
  693. }
  694. if ( ! empty($codes_clean))
  695. {
  696. preg_match_all('{<code.+</code>}msU', $output, $codes_messed);
  697. $output = str_replace($codes_messed[0], $codes_clean[0], $output);
  698. }
  699. if ( ! empty($textareas_clean))
  700. {
  701. preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_messed);
  702. $output = str_replace($textareas_messed[0], $textareas_clean[0], $output);
  703. }
  704. if (isset($javascript_mini))
  705. {
  706. preg_match_all('{<script.+</script>}msU', $output, $javascript_messed);
  707. $output = str_replace($javascript_messed[0], $javascript_mini, $output);
  708. }
  709. $size_removed = $size_before - strlen($output);
  710. $savings_percent = round(($size_removed / $size_before * 100));
  711. log_message('debug', 'Minifier shaved '.($size_removed / 1000).'KB ('.$savings_percent.'%) off final HTML output.');
  712. break;
  713. case 'text/css':
  714. return $this->_minify_js_css($output, 'css');
  715. case 'text/javascript':
  716. case 'application/javascript':
  717. case 'application/x-javascript':
  718. return $this->_minify_js_css($output, 'js');
  719. default: break;
  720. }
  721. return $output;
  722. }
  723. // --------------------------------------------------------------------
  724. /**
  725. * Minify JavaScript and CSS code
  726. *
  727. * Strips comments and excessive whitespace characters
  728. *
  729. * @param string $output
  730. * @param string $type 'js' or 'css'
  731. * @param bool $tags Whether $output contains the 'script' or 'style' tag
  732. * @return string
  733. */
  734. protected function _minify_js_css($output, $type, $tags = FALSE)
  735. {
  736. if ($tags === TRUE)
  737. {
  738. $tags = array('close' => strrchr($output, '<'));
  739. $open_length = strpos($output, '>') + 1;
  740. $tags['open'] = substr($output, 0, $open_length);
  741. $output = substr($output, $open_length, -strlen($tags['close']));
  742. // Strip spaces from the tags
  743. $tags = preg_replace('#\s{2,}#', ' ', $tags);
  744. }
  745. $output = trim($output);
  746. if ($type === 'js')
  747. {
  748. // Catch all string literals and comment blocks
  749. if (preg_match_all('#((?:((?<!\\\)\'|")|(/\*)|(//)).*(?(2)(?<!\\\)\2|(?(3)\*/|\n)))#msuUS', $output, $match, PREG_OFFSET_CAPTURE))
  750. {
  751. $js_literals = $js_code = array();
  752. for ($match = $match[0], $c = count($match), $i = $pos = $offset = 0; $i < $c; $i++)
  753. {
  754. $js_code[$pos++] = trim(substr($output, $offset, $match[$i][1] - $offset));
  755. $offset = $match[$i][1] + strlen($match[$i][0]);
  756. // Save only if we haven't matched a comment block
  757. if ($match[$i][0][0] !== '/')
  758. {
  759. $js_literals[$pos++] = array_shift($match[$i]);
  760. }
  761. }
  762. $js_code[$pos] = substr($output, $offset);
  763. // $match might be quite large, so free it up together with other vars that we no longer need
  764. unset($match, $offset, $pos);
  765. }
  766. else
  767. {
  768. $js_code = array($output);
  769. $js_literals = array();
  770. }
  771. $varname = 'js_code';
  772. }
  773. else
  774. {
  775. $varname = 'output';
  776. }
  777. // Standartize new lines
  778. $$varname = str_replace(array("\r\n", "\r"), "\n", $$varname);
  779. if ($type === 'js')
  780. {
  781. $patterns = array(
  782. '#\s*([!\#%&()*+,\-./:;<=>?@\[\]^`{|}~])\s*#' => '$1', // Remove spaces following and preceeding JS-wise non-special & non-word characters
  783. '#\s{2,}#' => ' ' // Reduce the remaining multiple whitespace characters to a single space
  784. );
  785. }
  786. else
  787. {
  788. $patterns = array(
  789. '#/\*.*(?=\*/)\*/#s' => '', // Remove /* block comments */
  790. '#\n?//[^\n]*#' => '', // Remove // line comments
  791. '#\s*([^\w.\#%])\s*#U' => '$1', // Remove spaces following and preceeding non-word characters, excluding dots, hashes and the percent sign
  792. '#\s{2,}#' => ' ' // Reduce the remaining multiple space characters to a single space
  793. );
  794. }
  795. $$varname = preg_replace(array_keys($patterns), array_values($patterns), $$varname);
  796. // Glue back JS quoted strings
  797. if ($type === 'js')
  798. {
  799. $js_code += $js_literals;
  800. ksort($js_code);
  801. $output = implode($js_code);
  802. unset($js_code, $js_literals, $varname, $patterns);
  803. }
  804. return is_array($tags)
  805. ? $tags['open'].$output.$tags['close']
  806. : $output;
  807. }
  808. }
  809. /* End of file Output.php */
  810. /* Location: ./system/core/Output.php */