blob: d2005cc417143367b44d9bb4497fe4fa209cc9ff [file] [log] [blame]
Michael Dodge362b8002013-01-04 23:18:39 -07001<?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 - 2013, 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 */
27defined('BASEPATH') OR exit('No direct script access allowed');
28
29/**
30 * Output Class
31 *
32 * Responsible for sending final output to the browser.
33 *
34 * @package CodeIgniter
35 * @subpackage Libraries
36 * @category Output
37 * @author EllisLab Dev Team
38 * @link http://codeigniter.com/user_guide/libraries/output.html
39 */
40class CI_Output {
41
42 /**
43 * Final output string
44 *
45 * @var string
46 */
47 public $final_output;
48
49 /**
50 * Cache expiration time
51 *
52 * @var int
53 */
54 public $cache_expiration = 0;
55
56 /**
57 * List of server headers
58 *
59 * @var array
60 */
Andrey Andreev155ee722014-01-10 15:50:54 +020061 public $headers = array();
Michael Dodge362b8002013-01-04 23:18:39 -070062
63 /**
64 * List of mime types
65 *
66 * @var array
67 */
Andrey Andreev155ee722014-01-10 15:50:54 +020068 public $mimes = array();
Michael Dodge362b8002013-01-04 23:18:39 -070069
70 /**
71 * Mime-type for the current page
72 *
73 * @var string
74 */
Andrey Andreev155ee722014-01-10 15:50:54 +020075 protected $mime_type = 'text/html';
Michael Dodge362b8002013-01-04 23:18:39 -070076
77 /**
78 * Enable Profiler flag
79 *
80 * @var bool
81 */
82 public $enable_profiler = FALSE;
83
84 /**
Andrey Andreev155ee722014-01-10 15:50:54 +020085 * php.ini zlib.output_compression flag
Michael Dodge362b8002013-01-04 23:18:39 -070086 *
87 * @var bool
88 */
Andrey Andreev155ee722014-01-10 15:50:54 +020089 protected $_zlib_oc = FALSE;
90
91 /**
92 * CI output compression flag
93 *
94 * @var bool
95 */
96 protected $_compress_output = FALSE;
Michael Dodge362b8002013-01-04 23:18:39 -070097
98 /**
99 * List of profiler sections
100 *
101 * @var array
102 */
Eric Roberts3e6b5822013-01-17 18:30:25 -0600103 protected $_profiler_sections = array();
Michael Dodge362b8002013-01-04 23:18:39 -0700104
105 /**
106 * Parse markers flag
107 *
108 * Whether or not to parse variables like {elapsed_time} and {memory_usage}.
109 *
110 * @var bool
111 */
Andrey Andreev155ee722014-01-10 15:50:54 +0200112 public $parse_exec_vars = TRUE;
Michael Dodge362b8002013-01-04 23:18:39 -0700113
114 /**
115 * Class constructor
116 *
117 * Determines whether zLib output compression will be used.
118 *
119 * @return void
120 */
121 public function __construct()
122 {
Andrey Andreev155ee722014-01-10 15:50:54 +0200123 global $CFG;
124
Michael Dodge362b8002013-01-04 23:18:39 -0700125 $this->_zlib_oc = (bool) @ini_get('zlib.output_compression');
Andrey Andreev155ee722014-01-10 15:50:54 +0200126 $this->_compress_output = (
127 $this->_zlib_oc === FALSE
128 && $CFG->item('compress_output') === TRUE
129 && extension_loaded('zlib')
130 );
Michael Dodge362b8002013-01-04 23:18:39 -0700131
132 // Get mime types for later
133 $this->mimes =& get_mimes();
134
135 log_message('debug', 'Output Class Initialized');
136 }
137
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
154 /**
155 * Set Output
156 *
157 * Sets the output string.
158 *
159 * @param string $output Output data
160 * @return CI_Output
161 */
162 public function set_output($output)
163 {
164 $this->final_output = $output;
165 return $this;
166 }
167
168 // --------------------------------------------------------------------
169
170 /**
171 * Append Output
172 *
173 * Appends data onto the output string.
174 *
175 * @param string $output Data to append
176 * @return CI_Output
177 */
178 public function append_output($output)
179 {
180 if (empty($this->final_output))
181 {
182 $this->final_output = $output;
183 }
184 else
185 {
186 $this->final_output .= $output;
187 }
188
189 return $this;
190 }
191
192 // --------------------------------------------------------------------
193
194 /**
195 * Set Header
196 *
197 * Lets you set a server header which will be sent with the final output.
198 *
199 * Note: If a file is cached, headers will not be sent.
200 * @todo We need to figure out how to permit headers to be cached.
201 *
202 * @param string $header Header
203 * @param bool $replace Whether to replace the old header value, if already set
204 * @return CI_Output
205 */
206 public function set_header($header, $replace = TRUE)
207 {
208 // If zlib.output_compression is enabled it will compress the output,
209 // but it will not modify the content-length header to compensate for
210 // the reduction, causing the browser to hang waiting for more data.
211 // We'll just skip content-length in those cases.
212 if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0)
213 {
214 return $this;
215 }
216
217 $this->headers[] = array($header, $replace);
218 return $this;
219 }
220
221 // --------------------------------------------------------------------
222
223 /**
224 * Set Content-Type Header
225 *
226 * @param string $mime_type Extension of the file we're outputting
227 * @param string $charset Character set (default: NULL)
228 * @return CI_Output
229 */
230 public function set_content_type($mime_type, $charset = NULL)
231 {
232 if (strpos($mime_type, '/') === FALSE)
233 {
234 $extension = ltrim($mime_type, '.');
235
236 // Is this extension supported?
237 if (isset($this->mimes[$extension]))
238 {
239 $mime_type =& $this->mimes[$extension];
240
241 if (is_array($mime_type))
242 {
243 $mime_type = current($mime_type);
244 }
245 }
246 }
247
248 $this->mime_type = $mime_type;
249
250 if (empty($charset))
251 {
252 $charset = config_item('charset');
253 }
254
255 $header = 'Content-Type: '.$mime_type
256 .(empty($charset) ? NULL : '; charset='.$charset);
257
258 $this->headers[] = array($header, TRUE);
259 return $this;
260 }
261
262 // --------------------------------------------------------------------
263
264 /**
265 * Get Current Content-Type Header
266 *
267 * @return string 'text/html', if not already set
268 */
269 public function get_content_type()
270 {
271 for ($i = 0, $c = count($this->headers); $i < $c; $i++)
272 {
273 if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)
274 {
275 return $content_type;
276 }
277 }
278
279 return 'text/html';
280 }
281
282 // --------------------------------------------------------------------
283
284 /**
285 * Get Header
286 *
287 * @param string $header_name
288 * @return string
289 */
290 public function get_header($header)
291 {
292 // Combine headers already sent with our batched headers
293 $headers = array_merge(
294 // We only need [x][0] from our multi-dimensional array
295 array_map('array_shift', $this->headers),
296 headers_list()
297 );
298
299 if (empty($headers) OR empty($header))
300 {
301 return NULL;
302 }
303
304 for ($i = 0, $c = count($headers); $i < $c; $i++)
305 {
306 if (strncasecmp($header, $headers[$i], $l = strlen($header)) === 0)
307 {
308 return trim(substr($headers[$i], $l+1));
309 }
310 }
311
312 return NULL;
313 }
314
315 // --------------------------------------------------------------------
316
317 /**
318 * Set HTTP Status Header
319 *
320 * As of version 1.7.2, this is an alias for common function
321 * set_status_header().
322 *
323 * @param int $code Status code (default: 200)
324 * @param string $text Optional message
325 * @return CI_Output
326 */
327 public function set_status_header($code = 200, $text = '')
328 {
329 set_status_header($code, $text);
330 return $this;
331 }
332
333 // --------------------------------------------------------------------
334
335 /**
336 * Enable/disable Profiler
337 *
338 * @param bool $val TRUE to enable or FALSE to disable
339 * @return CI_Output
340 */
341 public function enable_profiler($val = TRUE)
342 {
343 $this->enable_profiler = is_bool($val) ? $val : TRUE;
344 return $this;
345 }
346
347 // --------------------------------------------------------------------
348
349 /**
350 * Set Profiler Sections
351 *
352 * Allows override of default/config settings for
353 * Profiler section display.
354 *
355 * @param array $sections Profiler sections
356 * @return CI_Output
357 */
358 public function set_profiler_sections($sections)
359 {
360 if (isset($sections['query_toggle_count']))
361 {
362 $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
363 unset($sections['query_toggle_count']);
364 }
365
366 foreach ($sections as $section => $enable)
367 {
368 $this->_profiler_sections[$section] = ($enable !== FALSE);
369 }
370
371 return $this;
372 }
373
374 // --------------------------------------------------------------------
375
376 /**
377 * Set Cache
378 *
379 * @param int $time Cache expiration time in seconds
380 * @return CI_Output
381 */
382 public function cache($time)
383 {
384 $this->cache_expiration = is_numeric($time) ? $time : 0;
385 return $this;
386 }
387
388 // --------------------------------------------------------------------
389
390 /**
391 * Display Output
392 *
393 * Processes sends the sends finalized output data to the browser along
394 * with any server headers and profile data. It also stops benchmark
395 * timers so the page rendering speed and memory usage can be shown.
396 *
397 * Note: All "view" data is automatically put into $this->final_output
398 * by controller class.
399 *
400 * @uses CI_Output::$final_output
401 * @param string $output Output data override
402 * @return void
403 */
404 public function _display($output = '')
405 {
406 // Note: We use globals because we can't use $CI =& get_instance()
407 // since this function is sometimes called by the caching mechanism,
408 // which happens before the CI super object is available.
409 global $BM, $CFG;
410
411 // Grab the super object if we can.
Andrey Andreev49e68de2013-02-21 16:30:55 +0200412 if (class_exists('CI_Controller', FALSE))
Michael Dodge362b8002013-01-04 23:18:39 -0700413 {
414 $CI =& get_instance();
415 }
416
417 // --------------------------------------------------------------------
418
419 // Set the output data
420 if ($output === '')
421 {
422 $output =& $this->final_output;
423 }
424
425 // --------------------------------------------------------------------
426
427 // Is minify requested?
428 if ($CFG->item('minify_output') === TRUE)
429 {
430 $output = $this->minify($output, $this->mime_type);
431 }
432
433 // --------------------------------------------------------------------
434
435 // Do we need to write a cache file? Only if the controller does not have its
436 // own _output() method and we are not dealing with a cache file, which we
437 // can determine by the existence of the $CI object above
438 if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
439 {
440 $this->_write_cache($output);
441 }
442
443 // --------------------------------------------------------------------
444
445 // Parse out the elapsed time and memory usage,
446 // then swap the pseudo-variables with the data
447
448 $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
449
450 if ($this->parse_exec_vars === TRUE)
451 {
452 $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
Michael Dodge362b8002013-01-04 23:18:39 -0700453 $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
454 }
455
456 // --------------------------------------------------------------------
457
458 // Is compression requested?
Andrey Andreev155ee722014-01-10 15:50:54 +0200459 if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
460 && $this->_compress_output === TRUE
Michael Dodge362b8002013-01-04 23:18:39 -0700461 && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
462 {
463 ob_start('ob_gzhandler');
464 }
465
466 // --------------------------------------------------------------------
467
468 // Are there any server headers to send?
469 if (count($this->headers) > 0)
470 {
471 foreach ($this->headers as $header)
472 {
473 @header($header[0], $header[1]);
474 }
475 }
476
477 // --------------------------------------------------------------------
478
479 // Does the $CI object exist?
480 // If not we know we are dealing with a cache file so we'll
481 // simply echo out the data and exit.
482 if ( ! isset($CI))
483 {
Andrey Andreev155ee722014-01-10 15:50:54 +0200484 if ($this->_compress_output === TRUE)
485 {
486 if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
487 {
488 header('Content-Encoding: gzip');
489 header('Content-Length: '.strlen($output));
490 }
491 else
492 {
493 // User agent doesn't support gzip compression,
494 // so we'll have to decompress our cache
495 $output = gzinflate(substr($output, 10, -8));
496 }
497 }
498
Michael Dodge362b8002013-01-04 23:18:39 -0700499 echo $output;
500 log_message('debug', 'Final output sent to browser');
501 log_message('debug', 'Total execution time: '.$elapsed);
502 return;
503 }
504
505 // --------------------------------------------------------------------
506
507 // Do we need to generate profile data?
508 // If so, load the Profile class and run it.
509 if ($this->enable_profiler === TRUE)
510 {
511 $CI->load->library('profiler');
512 if ( ! empty($this->_profiler_sections))
513 {
514 $CI->profiler->set_sections($this->_profiler_sections);
515 }
516
517 // If the output data contains closing </body> and </html> tags
518 // we will remove them and add them back after we insert the profile data
519 $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
520 if ($count > 0)
521 {
522 $output .= '</body></html>';
523 }
524 }
525
526 // Does the controller contain a function named _output()?
527 // If so send the output there. Otherwise, echo it.
528 if (method_exists($CI, '_output'))
529 {
530 $CI->_output($output);
531 }
532 else
533 {
534 echo $output; // Send it to the browser!
535 }
536
537 log_message('debug', 'Final output sent to browser');
538 log_message('debug', 'Total execution time: '.$elapsed);
539 }
540
541 // --------------------------------------------------------------------
542
543 /**
544 * Write Cache
545 *
546 * @param string $output Output data to cache
547 * @return void
548 */
549 public function _write_cache($output)
550 {
551 $CI =& get_instance();
552 $path = $CI->config->item('cache_path');
553 $cache_path = ($path === '') ? APPPATH.'cache/' : $path;
554
555 if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
556 {
557 log_message('error', 'Unable to write cache file: '.$cache_path);
558 return;
559 }
560
Andrey Andreev155ee722014-01-10 15:50:54 +0200561 $uri = $CI->config->item('base_url')
562 .$CI->config->item('index_page')
563 .$CI->uri->uri_string();
Michael Dodge362b8002013-01-04 23:18:39 -0700564
565 $cache_path .= md5($uri);
566
567 if ( ! $fp = @fopen($cache_path, FOPEN_WRITE_CREATE_DESTRUCTIVE))
568 {
569 log_message('error', 'Unable to write cache file: '.$cache_path);
570 return;
571 }
572
Michael Dodge362b8002013-01-04 23:18:39 -0700573 if (flock($fp, LOCK_EX))
574 {
Andrey Andreev155ee722014-01-10 15:50:54 +0200575 // If output compression is enabled, compress the cache
576 // itself, so that we don't have to do that each time
577 // we're serving it
578 if ($this->_compress_output === TRUE)
579 {
580 $output = gzencode($output);
581
582 if ($this->get_header('content-type') === NULL)
583 {
584 $this->set_content_type($this->mime_type);
585 }
586 }
587
588 $expire = time() + ($this->cache_expiration * 60);
589
590 // Put together our serialized info.
591 $cache_info = serialize(array(
592 'expire' => $expire,
593 'headers' => $this->headers
594 ));
595
Eric Robertsc90e67e2013-01-11 21:20:54 -0600596 fwrite($fp, $cache_info.'ENDCI--->'.$output);
Michael Dodge362b8002013-01-04 23:18:39 -0700597 flock($fp, LOCK_UN);
598 }
599 else
600 {
601 log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
602 return;
603 }
Andrey Andreev155ee722014-01-10 15:50:54 +0200604
Michael Dodge362b8002013-01-04 23:18:39 -0700605 fclose($fp);
606 @chmod($cache_path, FILE_WRITE_MODE);
607
608 log_message('debug', 'Cache file written: '.$cache_path);
609
610 // Send HTTP cache-control headers to browser to match file cache settings.
611 $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
612 }
613
614 // --------------------------------------------------------------------
615
616 /**
617 * Update/serve cached output
618 *
619 * @uses CI_Config
620 * @uses CI_URI
621 *
622 * @param object &$CFG CI_Config class instance
623 * @param object &$URI CI_URI class instance
624 * @return bool TRUE on success or FALSE on failure
625 */
626 public function _display_cache(&$CFG, &$URI)
627 {
628 $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
629
630 // Build the file path. The file name is an MD5 hash of the full URI
631 $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
632 $filepath = $cache_path.md5($uri);
633
634 if ( ! @file_exists($filepath) OR ! $fp = @fopen($filepath, FOPEN_READ))
635 {
636 return FALSE;
637 }
638
639 flock($fp, LOCK_SH);
640
641 $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
642
643 flock($fp, LOCK_UN);
644 fclose($fp);
645
Eric Robertsc90e67e2013-01-11 21:20:54 -0600646 // Look for embedded serialized file info.
647 if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
Michael Dodge362b8002013-01-04 23:18:39 -0700648 {
649 return FALSE;
650 }
Purwandi5dc6d512013-01-19 17:43:08 +0700651
Eric Robertsc90e67e2013-01-11 21:20:54 -0600652 $cache_info = unserialize($match[1]);
653 $expire = $cache_info['expire'];
Michael Dodge362b8002013-01-04 23:18:39 -0700654
655 $last_modified = filemtime($cache_path);
Michael Dodge362b8002013-01-04 23:18:39 -0700656
657 // Has the file expired?
658 if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
659 {
660 // If so we'll delete it.
661 @unlink($filepath);
662 log_message('debug', 'Cache file has expired. File deleted.');
663 return FALSE;
664 }
665 else
666 {
667 // Or else send the HTTP cache control headers.
668 $this->set_cache_header($last_modified, $expire);
669 }
Purwandi5dc6d512013-01-19 17:43:08 +0700670
Eric Robertsc90e67e2013-01-11 21:20:54 -0600671 // Add headers from cache file.
672 foreach ($cache_info['headers'] as $header)
673 {
674 $this->set_header($header[0], $header[1]);
675 }
Michael Dodge362b8002013-01-04 23:18:39 -0700676
677 // Display the cache
678 $this->_display(substr($cache, strlen($match[0])));
679 log_message('debug', 'Cache file is current. Sending it to browser.');
680 return TRUE;
681 }
682
683 // --------------------------------------------------------------------
684
685 /**
686 * Delete cache
687 *
688 * @param string $uri URI string
689 * @return bool
690 */
691 public function delete_cache($uri = '')
692 {
693 $CI =& get_instance();
694 $cache_path = $CI->config->item('cache_path');
695 if ($cache_path === '')
696 {
697 $cache_path = APPPATH.'cache/';
698 }
699
700 if ( ! is_dir($cache_path))
701 {
702 log_message('error', 'Unable to find cache path: '.$cache_path);
703 return FALSE;
704 }
705
706 if (empty($uri))
707 {
708 $uri = $CI->uri->uri_string();
709 }
710
711 $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').$uri);
712
713 if ( ! @unlink($cache_path))
714 {
715 log_message('error', 'Unable to delete cache file for '.$uri);
716 return FALSE;
717 }
718
719 return TRUE;
720 }
721
722 // --------------------------------------------------------------------
723
724 /**
725 * Set Cache Header
726 *
727 * Set the HTTP headers to match the server-side file cache settings
728 * in order to reduce bandwidth.
729 *
730 * @param int $last_modified Timestamp of when the page was last modified
731 * @param int $expiration Timestamp of when should the requested page expire from cache
732 * @return void
733 */
734 public function set_cache_header($last_modified, $expiration)
735 {
736 $max_age = $expiration - $_SERVER['REQUEST_TIME'];
737
738 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
739 {
740 $this->set_status_header(304);
741 exit;
742 }
743 else
744 {
745 header('Pragma: public');
Andrey Andreev3ca060a2013-11-27 16:30:31 +0200746 header('Cache-Control: max-age='.$max_age.', public');
Michael Dodge362b8002013-01-04 23:18:39 -0700747 header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
748 header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
749 }
750 }
751
752 // --------------------------------------------------------------------
753
754 /**
755 * Minify
756 *
757 * Reduce excessive size of HTML/CSS/JavaScript content.
758 *
759 * @param string $output Output to minify
760 * @param string $type Output content MIME type
761 * @return string Minified output
762 */
763 public function minify($output, $type = 'text/html')
764 {
765 switch ($type)
766 {
767 case 'text/html':
768
769 if (($size_before = strlen($output)) === 0)
770 {
771 return '';
772 }
773
774 // Find all the <pre>,<code>,<textarea>, and <javascript> tags
775 // We'll want to return them to this unprocessed state later.
776 preg_match_all('{<pre.+</pre>}msU', $output, $pres_clean);
777 preg_match_all('{<code.+</code>}msU', $output, $codes_clean);
778 preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_clean);
779 preg_match_all('{<script.+</script>}msU', $output, $javascript_clean);
780
781 // Minify the CSS in all the <style> tags.
782 preg_match_all('{<style.+</style>}msU', $output, $style_clean);
783 foreach ($style_clean[0] as $s)
784 {
Andrey Andreev6a424902013-10-28 14:16:18 +0200785 $output = str_replace($s, $this->_minify_js_css($s, 'css', TRUE), $output);
Michael Dodge362b8002013-01-04 23:18:39 -0700786 }
787
788 // Minify the javascript in <script> tags.
789 foreach ($javascript_clean[0] as $s)
790 {
Andrey Andreev6a424902013-10-28 14:16:18 +0200791 $javascript_mini[] = $this->_minify_js_css($s, 'js', TRUE);
Michael Dodge362b8002013-01-04 23:18:39 -0700792 }
793
794 // Replace multiple spaces with a single space.
795 $output = preg_replace('!\s{2,}!', ' ', $output);
796
797 // Remove comments (non-MSIE conditionals)
Michael Dodge4d02e352013-01-04 23:22:51 -0700798 $output = preg_replace('{\s*<!--[^\[<>].*(?<!!)-->\s*}msU', '', $output);
Michael Dodge362b8002013-01-04 23:18:39 -0700799
800 // Remove spaces around block-level elements.
Purwandi5dc6d512013-01-19 17:43:08 +0700801 $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);
Michael Dodge362b8002013-01-04 23:18:39 -0700802
803 // Replace mangled <pre> etc. tags with unprocessed ones.
804
805 if ( ! empty($pres_clean))
806 {
807 preg_match_all('{<pre.+</pre>}msU', $output, $pres_messed);
808 $output = str_replace($pres_messed[0], $pres_clean[0], $output);
809 }
810
811 if ( ! empty($codes_clean))
812 {
813 preg_match_all('{<code.+</code>}msU', $output, $codes_messed);
814 $output = str_replace($codes_messed[0], $codes_clean[0], $output);
815 }
816
Andrey Andreev3ffce982013-01-21 15:24:09 +0200817 if ( ! empty($textareas_clean))
Michael Dodge362b8002013-01-04 23:18:39 -0700818 {
819 preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_messed);
820 $output = str_replace($textareas_messed[0], $textareas_clean[0], $output);
821 }
822
823 if (isset($javascript_mini))
824 {
825 preg_match_all('{<script.+</script>}msU', $output, $javascript_messed);
826 $output = str_replace($javascript_messed[0], $javascript_mini, $output);
827 }
828
829 $size_removed = $size_before - strlen($output);
830 $savings_percent = round(($size_removed / $size_before * 100));
831
832 log_message('debug', 'Minifier shaved '.($size_removed / 1000).'KB ('.$savings_percent.'%) off final HTML output.');
833
834 break;
835
836 case 'text/css':
Andrey Andreev6a424902013-10-28 14:16:18 +0200837
838 return $this->_minify_js_css($output, 'css');
839
Michael Dodge362b8002013-01-04 23:18:39 -0700840 case 'text/javascript':
bayssmekanique7b903252013-03-12 13:25:24 -0700841 case 'application/javascript':
842 case 'application/x-javascript':
Michael Dodge362b8002013-01-04 23:18:39 -0700843
Andrey Andreev6a424902013-10-28 14:16:18 +0200844 return $this->_minify_js_css($output, 'js');
Michael Dodge362b8002013-01-04 23:18:39 -0700845
846 default: break;
847 }
848
849 return $output;
850 }
851
852 // --------------------------------------------------------------------
853
Andrey Andreev3c3bbac2013-10-31 15:10:49 +0200854 /**
855 * Minify JavaScript and CSS code
856 *
857 * Strips comments and excessive whitespace characters
858 *
859 * @param string $output
860 * @param string $type 'js' or 'css'
861 * @param bool $tags Whether $output contains the 'script' or 'style' tag
862 * @return string
863 */
Andrey Andreev6a424902013-10-28 14:16:18 +0200864 protected function _minify_js_css($output, $type, $tags = FALSE)
865 {
866 if ($tags === TRUE)
867 {
868 $tags = array('close' => strrchr($output, '<'));
869
870 $open_length = strpos($output, '>') + 1;
871 $tags['open'] = substr($output, 0, $open_length);
872
873 $output = substr($output, $open_length, -strlen($tags['close']));
874
875 // Strip spaces from the tags
876 $tags = preg_replace('#\s{2,}#', ' ', $tags);
877 }
878
879 $output = trim($output);
880
881 if ($type === 'js')
882 {
883 // Catch all string literals and comment blocks
Andrey Andreev0949b362013-10-31 16:07:40 +0200884 if (preg_match_all('#((?:((?<!\\\)\'|")|(/\*)|(//)).*(?(2)(?<!\\\)\2|(?(3)\*/|\n)))#msuUS', $output, $match, PREG_OFFSET_CAPTURE))
Andrey Andreev6a424902013-10-28 14:16:18 +0200885 {
886 $js_literals = $js_code = array();
887 for ($match = $match[0], $c = count($match), $i = $pos = $offset = 0; $i < $c; $i++)
888 {
Andrey Andreev3c3bbac2013-10-31 15:10:49 +0200889 $js_code[$pos++] = trim(substr($output, $offset, $match[$i][1] - $offset));
Andrey Andreev6a424902013-10-28 14:16:18 +0200890 $offset = $match[$i][1] + strlen($match[$i][0]);
891
892 // Save only if we haven't matched a comment block
893 if ($match[$i][0][0] !== '/')
894 {
895 $js_literals[$pos++] = array_shift($match[$i]);
896 }
897 }
898 $js_code[$pos] = substr($output, $offset);
899
900 // $match might be quite large, so free it up together with other vars that we no longer need
901 unset($match, $offset, $pos);
902 }
903 else
904 {
905 $js_code = array($output);
906 $js_literals = array();
907 }
908
909 $varname = 'js_code';
910 }
911 else
912 {
913 $varname = 'output';
914 }
915
916 // Standartize new lines
917 $$varname = str_replace(array("\r\n", "\r"), "\n", $$varname);
918
919 if ($type === 'js')
920 {
921 $patterns = array(
Andrey Andreev99f9b9a2013-10-30 23:07:23 +0200922 '#\s*([!\#%&()*+,\-./:;<=>?@\[\]^`{|}~])\s*#' => '$1', // Remove spaces following and preceeding JS-wise non-special & non-word characters
Andrey Andreev6a424902013-10-28 14:16:18 +0200923 '#\s{2,}#' => ' ' // Reduce the remaining multiple whitespace characters to a single space
924 );
925 }
926 else
927 {
928 $patterns = array(
929 '#/\*.*(?=\*/)\*/#s' => '', // Remove /* block comments */
930 '#\n?//[^\n]*#' => '', // Remove // line comments
Andrey Andreev99f9b9a2013-10-30 23:07:23 +0200931 '#\s*([^\w.\#%])\s*#U' => '$1', // Remove spaces following and preceeding non-word characters, excluding dots, hashes and the percent sign
Andrey Andreev6a424902013-10-28 14:16:18 +0200932 '#\s{2,}#' => ' ' // Reduce the remaining multiple space characters to a single space
933 );
934 }
935
936 $$varname = preg_replace(array_keys($patterns), array_values($patterns), $$varname);
937
938 // Glue back JS quoted strings
939 if ($type === 'js')
940 {
941 $js_code += $js_literals;
942 ksort($js_code);
943 $output = implode($js_code);
944 unset($js_code, $js_literals, $varname, $patterns);
945 }
946
947 return is_array($tags)
948 ? $tags['open'].$output.$tags['close']
949 : $output;
950 }
951
Michael Dodge362b8002013-01-04 23:18:39 -0700952}
953
954/* End of file Output.php */
Andrey Andreevd8dba5d2012-12-17 15:42:01 +0200955/* Location: ./system/core/Output.php */