blob: a7680b3d000cedb7c6cecb6bc63f76731ec1293a [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 */
Eric Roberts3e6b5822013-01-17 18:30:25 -060061 public $headers = array();
Michael Dodge362b8002013-01-04 23:18:39 -070062
63 /**
64 * List of mime types
65 *
66 * @var array
67 */
Eric Roberts3e6b5822013-01-17 18:30:25 -060068 public $mimes = array();
Michael Dodge362b8002013-01-04 23:18:39 -070069
70 /**
71 * Mime-type for the current page
72 *
73 * @var string
74 */
Eric Roberts3e6b5822013-01-17 18:30:25 -060075 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 /**
85 * zLib output compression flag
86 *
87 * @var bool
88 */
Eric Roberts3e6b5822013-01-17 18:30:25 -060089 protected $_zlib_oc = FALSE;
Michael Dodge362b8002013-01-04 23:18:39 -070090
91 /**
92 * List of profiler sections
93 *
94 * @var array
95 */
Eric Roberts3e6b5822013-01-17 18:30:25 -060096 protected $_profiler_sections = array();
Michael Dodge362b8002013-01-04 23:18:39 -070097
98 /**
99 * Parse markers flag
100 *
101 * Whether or not to parse variables like {elapsed_time} and {memory_usage}.
102 *
103 * @var bool
104 */
Eric Roberts3e6b5822013-01-17 18:30:25 -0600105 public $parse_exec_vars = TRUE;
Michael Dodge362b8002013-01-04 23:18:39 -0700106
107 /**
108 * Class constructor
109 *
110 * Determines whether zLib output compression will be used.
111 *
112 * @return void
113 */
114 public function __construct()
115 {
116 $this->_zlib_oc = (bool) @ini_get('zlib.output_compression');
117
118 // Get mime types for later
119 $this->mimes =& get_mimes();
120
121 log_message('debug', 'Output Class Initialized');
122 }
123
124 // --------------------------------------------------------------------
125
126 /**
127 * Get Output
128 *
129 * Returns the current output string.
130 *
131 * @return string
132 */
133 public function get_output()
134 {
135 return $this->final_output;
136 }
137
138 // --------------------------------------------------------------------
139
140 /**
141 * Set Output
142 *
143 * Sets the output string.
144 *
145 * @param string $output Output data
146 * @return CI_Output
147 */
148 public function set_output($output)
149 {
150 $this->final_output = $output;
151 return $this;
152 }
153
154 // --------------------------------------------------------------------
155
156 /**
157 * Append Output
158 *
159 * Appends data onto the output string.
160 *
161 * @param string $output Data to append
162 * @return CI_Output
163 */
164 public function append_output($output)
165 {
166 if (empty($this->final_output))
167 {
168 $this->final_output = $output;
169 }
170 else
171 {
172 $this->final_output .= $output;
173 }
174
175 return $this;
176 }
177
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
203 $this->headers[] = array($header, $replace);
204 return $this;
205 }
206
207 // --------------------------------------------------------------------
208
209 /**
210 * Set Content-Type Header
211 *
212 * @param string $mime_type Extension of the file we're outputting
213 * @param string $charset Character set (default: NULL)
214 * @return CI_Output
215 */
216 public function set_content_type($mime_type, $charset = NULL)
217 {
218 if (strpos($mime_type, '/') === FALSE)
219 {
220 $extension = ltrim($mime_type, '.');
221
222 // Is this extension supported?
223 if (isset($this->mimes[$extension]))
224 {
225 $mime_type =& $this->mimes[$extension];
226
227 if (is_array($mime_type))
228 {
229 $mime_type = current($mime_type);
230 }
231 }
232 }
233
234 $this->mime_type = $mime_type;
235
236 if (empty($charset))
237 {
238 $charset = config_item('charset');
239 }
240
241 $header = 'Content-Type: '.$mime_type
242 .(empty($charset) ? NULL : '; charset='.$charset);
243
244 $this->headers[] = array($header, TRUE);
245 return $this;
246 }
247
248 // --------------------------------------------------------------------
249
250 /**
251 * Get Current Content-Type Header
252 *
253 * @return string 'text/html', if not already set
254 */
255 public function get_content_type()
256 {
257 for ($i = 0, $c = count($this->headers); $i < $c; $i++)
258 {
259 if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)
260 {
261 return $content_type;
262 }
263 }
264
265 return 'text/html';
266 }
267
268 // --------------------------------------------------------------------
269
270 /**
271 * Get Header
272 *
273 * @param string $header_name
274 * @return string
275 */
276 public function get_header($header)
277 {
278 // Combine headers already sent with our batched headers
279 $headers = array_merge(
280 // We only need [x][0] from our multi-dimensional array
281 array_map('array_shift', $this->headers),
282 headers_list()
283 );
284
285 if (empty($headers) OR empty($header))
286 {
287 return NULL;
288 }
289
290 for ($i = 0, $c = count($headers); $i < $c; $i++)
291 {
292 if (strncasecmp($header, $headers[$i], $l = strlen($header)) === 0)
293 {
294 return trim(substr($headers[$i], $l+1));
295 }
296 }
297
298 return NULL;
299 }
300
301 // --------------------------------------------------------------------
302
303 /**
304 * Set HTTP Status Header
305 *
306 * As of version 1.7.2, this is an alias for common function
307 * set_status_header().
308 *
309 * @param int $code Status code (default: 200)
310 * @param string $text Optional message
311 * @return CI_Output
312 */
313 public function set_status_header($code = 200, $text = '')
314 {
315 set_status_header($code, $text);
316 return $this;
317 }
318
319 // --------------------------------------------------------------------
320
321 /**
322 * Enable/disable Profiler
323 *
324 * @param bool $val TRUE to enable or FALSE to disable
325 * @return CI_Output
326 */
327 public function enable_profiler($val = TRUE)
328 {
329 $this->enable_profiler = is_bool($val) ? $val : TRUE;
330 return $this;
331 }
332
333 // --------------------------------------------------------------------
334
335 /**
336 * Set Profiler Sections
337 *
338 * Allows override of default/config settings for
339 * Profiler section display.
340 *
341 * @param array $sections Profiler sections
342 * @return CI_Output
343 */
344 public function set_profiler_sections($sections)
345 {
346 if (isset($sections['query_toggle_count']))
347 {
348 $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
349 unset($sections['query_toggle_count']);
350 }
351
352 foreach ($sections as $section => $enable)
353 {
354 $this->_profiler_sections[$section] = ($enable !== FALSE);
355 }
356
357 return $this;
358 }
359
360 // --------------------------------------------------------------------
361
362 /**
363 * Set Cache
364 *
365 * @param int $time Cache expiration time in seconds
366 * @return CI_Output
367 */
368 public function cache($time)
369 {
370 $this->cache_expiration = is_numeric($time) ? $time : 0;
371 return $this;
372 }
373
374 // --------------------------------------------------------------------
375
376 /**
377 * Display Output
378 *
379 * Processes sends the sends finalized output data to the browser along
380 * with any server headers and profile data. It also stops benchmark
381 * timers so the page rendering speed and memory usage can be shown.
382 *
383 * Note: All "view" data is automatically put into $this->final_output
384 * by controller class.
385 *
386 * @uses CI_Output::$final_output
387 * @param string $output Output data override
388 * @return void
389 */
390 public function _display($output = '')
391 {
392 // Note: We use globals because we can't use $CI =& get_instance()
393 // since this function is sometimes called by the caching mechanism,
394 // which happens before the CI super object is available.
395 global $BM, $CFG;
396
397 // Grab the super object if we can.
Andrey Andreev49e68de2013-02-21 16:30:55 +0200398 if (class_exists('CI_Controller', FALSE))
Michael Dodge362b8002013-01-04 23:18:39 -0700399 {
400 $CI =& get_instance();
401 }
402
403 // --------------------------------------------------------------------
404
405 // Set the output data
406 if ($output === '')
407 {
408 $output =& $this->final_output;
409 }
410
411 // --------------------------------------------------------------------
412
413 // Is minify requested?
414 if ($CFG->item('minify_output') === TRUE)
415 {
416 $output = $this->minify($output, $this->mime_type);
417 }
418
419 // --------------------------------------------------------------------
420
421 // Do we need to write a cache file? Only if the controller does not have its
422 // own _output() method and we are not dealing with a cache file, which we
423 // can determine by the existence of the $CI object above
424 if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
425 {
426 $this->_write_cache($output);
427 }
428
429 // --------------------------------------------------------------------
430
431 // Parse out the elapsed time and memory usage,
432 // then swap the pseudo-variables with the data
433
434 $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
435
436 if ($this->parse_exec_vars === TRUE)
437 {
438 $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
439
440 $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
441 }
442
443 // --------------------------------------------------------------------
444
445 // Is compression requested?
446 if ($CFG->item('compress_output') === TRUE && $this->_zlib_oc === FALSE
447 && extension_loaded('zlib')
448 && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
449 {
450 ob_start('ob_gzhandler');
451 }
452
453 // --------------------------------------------------------------------
454
455 // Are there any server headers to send?
456 if (count($this->headers) > 0)
457 {
458 foreach ($this->headers as $header)
459 {
460 @header($header[0], $header[1]);
461 }
462 }
463
464 // --------------------------------------------------------------------
465
466 // Does the $CI object exist?
467 // If not we know we are dealing with a cache file so we'll
468 // simply echo out the data and exit.
469 if ( ! isset($CI))
470 {
471 echo $output;
472 log_message('debug', 'Final output sent to browser');
473 log_message('debug', 'Total execution time: '.$elapsed);
474 return;
475 }
476
477 // --------------------------------------------------------------------
478
479 // Do we need to generate profile data?
480 // If so, load the Profile class and run it.
481 if ($this->enable_profiler === TRUE)
482 {
483 $CI->load->library('profiler');
484 if ( ! empty($this->_profiler_sections))
485 {
486 $CI->profiler->set_sections($this->_profiler_sections);
487 }
488
489 // If the output data contains closing </body> and </html> tags
490 // we will remove them and add them back after we insert the profile data
491 $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
492 if ($count > 0)
493 {
494 $output .= '</body></html>';
495 }
496 }
497
498 // Does the controller contain a function named _output()?
499 // If so send the output there. Otherwise, echo it.
500 if (method_exists($CI, '_output'))
501 {
502 $CI->_output($output);
503 }
504 else
505 {
506 echo $output; // Send it to the browser!
507 }
508
509 log_message('debug', 'Final output sent to browser');
510 log_message('debug', 'Total execution time: '.$elapsed);
511 }
512
513 // --------------------------------------------------------------------
514
515 /**
516 * Write Cache
517 *
518 * @param string $output Output data to cache
519 * @return void
520 */
521 public function _write_cache($output)
522 {
523 $CI =& get_instance();
524 $path = $CI->config->item('cache_path');
525 $cache_path = ($path === '') ? APPPATH.'cache/' : $path;
526
527 if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
528 {
529 log_message('error', 'Unable to write cache file: '.$cache_path);
530 return;
531 }
532
533 $uri = $CI->config->item('base_url').
534 $CI->config->item('index_page').
535 $CI->uri->uri_string();
536
537 $cache_path .= md5($uri);
538
539 if ( ! $fp = @fopen($cache_path, FOPEN_WRITE_CREATE_DESTRUCTIVE))
540 {
541 log_message('error', 'Unable to write cache file: '.$cache_path);
542 return;
543 }
544
Michael Dodge362b8002013-01-04 23:18:39 -0700545 if (flock($fp, LOCK_EX))
546 {
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200547 $expire = time() + ($this->cache_expiration * 60);
548
549 // Put together our serialized info.
550 $cache_info = serialize(array(
551 'expire' => $expire,
552 'headers' => $this->headers
553 ));
554
555 $output = $cache_info.'ENDCI--->'.$output;
556
557 for ($written = 0, $length = strlen($output); $written < $length; $written += $result)
558 {
559 if (($result = fwrite($fp, substr($output, $written))) === FALSE)
560 {
561 break;
562 }
563 }
564
Michael Dodge362b8002013-01-04 23:18:39 -0700565 flock($fp, LOCK_UN);
566 }
567 else
568 {
569 log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
570 return;
571 }
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200572
Michael Dodge362b8002013-01-04 23:18:39 -0700573 fclose($fp);
Michael Dodge362b8002013-01-04 23:18:39 -0700574
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200575 if (is_int($result))
576 {
577 @chmod($cache_path, FILE_WRITE_MODE);
578 log_message('debug', 'Cache file written: '.$cache_path);
Michael Dodge362b8002013-01-04 23:18:39 -0700579
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200580 // Send HTTP cache-control headers to browser to match file cache settings.
581 $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
582 }
583 else
584 {
585 @unlink($cache_path);
586 log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
587 }
Michael Dodge362b8002013-01-04 23:18:39 -0700588 }
589
590 // --------------------------------------------------------------------
591
592 /**
593 * Update/serve cached output
594 *
595 * @uses CI_Config
596 * @uses CI_URI
597 *
598 * @param object &$CFG CI_Config class instance
599 * @param object &$URI CI_URI class instance
600 * @return bool TRUE on success or FALSE on failure
601 */
602 public function _display_cache(&$CFG, &$URI)
603 {
604 $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
605
606 // Build the file path. The file name is an MD5 hash of the full URI
607 $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
608 $filepath = $cache_path.md5($uri);
609
610 if ( ! @file_exists($filepath) OR ! $fp = @fopen($filepath, FOPEN_READ))
611 {
612 return FALSE;
613 }
614
615 flock($fp, LOCK_SH);
616
617 $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
618
619 flock($fp, LOCK_UN);
620 fclose($fp);
621
Eric Robertsc90e67e2013-01-11 21:20:54 -0600622 // Look for embedded serialized file info.
623 if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
Michael Dodge362b8002013-01-04 23:18:39 -0700624 {
625 return FALSE;
626 }
Purwandi5dc6d512013-01-19 17:43:08 +0700627
Eric Robertsc90e67e2013-01-11 21:20:54 -0600628 $cache_info = unserialize($match[1]);
629 $expire = $cache_info['expire'];
Michael Dodge362b8002013-01-04 23:18:39 -0700630
631 $last_modified = filemtime($cache_path);
Michael Dodge362b8002013-01-04 23:18:39 -0700632
633 // Has the file expired?
634 if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
635 {
636 // If so we'll delete it.
637 @unlink($filepath);
638 log_message('debug', 'Cache file has expired. File deleted.');
639 return FALSE;
640 }
641 else
642 {
643 // Or else send the HTTP cache control headers.
644 $this->set_cache_header($last_modified, $expire);
645 }
Purwandi5dc6d512013-01-19 17:43:08 +0700646
Eric Robertsc90e67e2013-01-11 21:20:54 -0600647 // Add headers from cache file.
648 foreach ($cache_info['headers'] as $header)
649 {
650 $this->set_header($header[0], $header[1]);
651 }
Michael Dodge362b8002013-01-04 23:18:39 -0700652
653 // Display the cache
654 $this->_display(substr($cache, strlen($match[0])));
655 log_message('debug', 'Cache file is current. Sending it to browser.');
656 return TRUE;
657 }
658
659 // --------------------------------------------------------------------
660
661 /**
662 * Delete cache
663 *
664 * @param string $uri URI string
665 * @return bool
666 */
667 public function delete_cache($uri = '')
668 {
669 $CI =& get_instance();
670 $cache_path = $CI->config->item('cache_path');
671 if ($cache_path === '')
672 {
673 $cache_path = APPPATH.'cache/';
674 }
675
676 if ( ! is_dir($cache_path))
677 {
678 log_message('error', 'Unable to find cache path: '.$cache_path);
679 return FALSE;
680 }
681
682 if (empty($uri))
683 {
684 $uri = $CI->uri->uri_string();
685 }
686
687 $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').$uri);
688
689 if ( ! @unlink($cache_path))
690 {
691 log_message('error', 'Unable to delete cache file for '.$uri);
692 return FALSE;
693 }
694
695 return TRUE;
696 }
697
698 // --------------------------------------------------------------------
699
700 /**
701 * Set Cache Header
702 *
703 * Set the HTTP headers to match the server-side file cache settings
704 * in order to reduce bandwidth.
705 *
706 * @param int $last_modified Timestamp of when the page was last modified
707 * @param int $expiration Timestamp of when should the requested page expire from cache
708 * @return void
709 */
710 public function set_cache_header($last_modified, $expiration)
711 {
712 $max_age = $expiration - $_SERVER['REQUEST_TIME'];
713
714 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
715 {
716 $this->set_status_header(304);
717 exit;
718 }
719 else
720 {
721 header('Pragma: public');
Andrey Andreev3ca060a2013-11-27 16:30:31 +0200722 header('Cache-Control: max-age='.$max_age.', public');
Michael Dodge362b8002013-01-04 23:18:39 -0700723 header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
724 header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
725 }
726 }
727
728 // --------------------------------------------------------------------
729
730 /**
731 * Minify
732 *
733 * Reduce excessive size of HTML/CSS/JavaScript content.
734 *
735 * @param string $output Output to minify
736 * @param string $type Output content MIME type
737 * @return string Minified output
738 */
739 public function minify($output, $type = 'text/html')
740 {
741 switch ($type)
742 {
743 case 'text/html':
744
745 if (($size_before = strlen($output)) === 0)
746 {
747 return '';
748 }
749
750 // Find all the <pre>,<code>,<textarea>, and <javascript> tags
751 // We'll want to return them to this unprocessed state later.
752 preg_match_all('{<pre.+</pre>}msU', $output, $pres_clean);
753 preg_match_all('{<code.+</code>}msU', $output, $codes_clean);
754 preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_clean);
755 preg_match_all('{<script.+</script>}msU', $output, $javascript_clean);
756
757 // Minify the CSS in all the <style> tags.
758 preg_match_all('{<style.+</style>}msU', $output, $style_clean);
759 foreach ($style_clean[0] as $s)
760 {
Andrey Andreev6a424902013-10-28 14:16:18 +0200761 $output = str_replace($s, $this->_minify_js_css($s, 'css', TRUE), $output);
Michael Dodge362b8002013-01-04 23:18:39 -0700762 }
763
764 // Minify the javascript in <script> tags.
765 foreach ($javascript_clean[0] as $s)
766 {
Andrey Andreev6a424902013-10-28 14:16:18 +0200767 $javascript_mini[] = $this->_minify_js_css($s, 'js', TRUE);
Michael Dodge362b8002013-01-04 23:18:39 -0700768 }
769
770 // Replace multiple spaces with a single space.
771 $output = preg_replace('!\s{2,}!', ' ', $output);
772
773 // Remove comments (non-MSIE conditionals)
Michael Dodge4d02e352013-01-04 23:22:51 -0700774 $output = preg_replace('{\s*<!--[^\[<>].*(?<!!)-->\s*}msU', '', $output);
Michael Dodge362b8002013-01-04 23:18:39 -0700775
776 // Remove spaces around block-level elements.
Purwandi5dc6d512013-01-19 17:43:08 +0700777 $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 -0700778
779 // Replace mangled <pre> etc. tags with unprocessed ones.
780
781 if ( ! empty($pres_clean))
782 {
783 preg_match_all('{<pre.+</pre>}msU', $output, $pres_messed);
784 $output = str_replace($pres_messed[0], $pres_clean[0], $output);
785 }
786
787 if ( ! empty($codes_clean))
788 {
789 preg_match_all('{<code.+</code>}msU', $output, $codes_messed);
790 $output = str_replace($codes_messed[0], $codes_clean[0], $output);
791 }
792
Andrey Andreev3ffce982013-01-21 15:24:09 +0200793 if ( ! empty($textareas_clean))
Michael Dodge362b8002013-01-04 23:18:39 -0700794 {
795 preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_messed);
796 $output = str_replace($textareas_messed[0], $textareas_clean[0], $output);
797 }
798
799 if (isset($javascript_mini))
800 {
801 preg_match_all('{<script.+</script>}msU', $output, $javascript_messed);
802 $output = str_replace($javascript_messed[0], $javascript_mini, $output);
803 }
804
805 $size_removed = $size_before - strlen($output);
806 $savings_percent = round(($size_removed / $size_before * 100));
807
808 log_message('debug', 'Minifier shaved '.($size_removed / 1000).'KB ('.$savings_percent.'%) off final HTML output.');
809
810 break;
811
812 case 'text/css':
Andrey Andreev6a424902013-10-28 14:16:18 +0200813
814 return $this->_minify_js_css($output, 'css');
815
Michael Dodge362b8002013-01-04 23:18:39 -0700816 case 'text/javascript':
bayssmekanique7b903252013-03-12 13:25:24 -0700817 case 'application/javascript':
818 case 'application/x-javascript':
Michael Dodge362b8002013-01-04 23:18:39 -0700819
Andrey Andreev6a424902013-10-28 14:16:18 +0200820 return $this->_minify_js_css($output, 'js');
Michael Dodge362b8002013-01-04 23:18:39 -0700821
822 default: break;
823 }
824
825 return $output;
826 }
827
828 // --------------------------------------------------------------------
829
Andrey Andreev3c3bbac2013-10-31 15:10:49 +0200830 /**
831 * Minify JavaScript and CSS code
832 *
833 * Strips comments and excessive whitespace characters
834 *
835 * @param string $output
836 * @param string $type 'js' or 'css'
837 * @param bool $tags Whether $output contains the 'script' or 'style' tag
838 * @return string
839 */
Andrey Andreev6a424902013-10-28 14:16:18 +0200840 protected function _minify_js_css($output, $type, $tags = FALSE)
841 {
842 if ($tags === TRUE)
843 {
844 $tags = array('close' => strrchr($output, '<'));
845
846 $open_length = strpos($output, '>') + 1;
847 $tags['open'] = substr($output, 0, $open_length);
848
849 $output = substr($output, $open_length, -strlen($tags['close']));
850
851 // Strip spaces from the tags
852 $tags = preg_replace('#\s{2,}#', ' ', $tags);
853 }
854
855 $output = trim($output);
856
857 if ($type === 'js')
858 {
859 // Catch all string literals and comment blocks
Andrey Andreev0949b362013-10-31 16:07:40 +0200860 if (preg_match_all('#((?:((?<!\\\)\'|")|(/\*)|(//)).*(?(2)(?<!\\\)\2|(?(3)\*/|\n)))#msuUS', $output, $match, PREG_OFFSET_CAPTURE))
Andrey Andreev6a424902013-10-28 14:16:18 +0200861 {
862 $js_literals = $js_code = array();
863 for ($match = $match[0], $c = count($match), $i = $pos = $offset = 0; $i < $c; $i++)
864 {
Andrey Andreev3c3bbac2013-10-31 15:10:49 +0200865 $js_code[$pos++] = trim(substr($output, $offset, $match[$i][1] - $offset));
Andrey Andreev6a424902013-10-28 14:16:18 +0200866 $offset = $match[$i][1] + strlen($match[$i][0]);
867
868 // Save only if we haven't matched a comment block
869 if ($match[$i][0][0] !== '/')
870 {
871 $js_literals[$pos++] = array_shift($match[$i]);
872 }
873 }
874 $js_code[$pos] = substr($output, $offset);
875
876 // $match might be quite large, so free it up together with other vars that we no longer need
877 unset($match, $offset, $pos);
878 }
879 else
880 {
881 $js_code = array($output);
882 $js_literals = array();
883 }
884
885 $varname = 'js_code';
886 }
887 else
888 {
889 $varname = 'output';
890 }
891
892 // Standartize new lines
893 $$varname = str_replace(array("\r\n", "\r"), "\n", $$varname);
894
895 if ($type === 'js')
896 {
897 $patterns = array(
Andrey Andreev99f9b9a2013-10-30 23:07:23 +0200898 '#\s*([!\#%&()*+,\-./:;<=>?@\[\]^`{|}~])\s*#' => '$1', // Remove spaces following and preceeding JS-wise non-special & non-word characters
Andrey Andreev6a424902013-10-28 14:16:18 +0200899 '#\s{2,}#' => ' ' // Reduce the remaining multiple whitespace characters to a single space
900 );
901 }
902 else
903 {
904 $patterns = array(
905 '#/\*.*(?=\*/)\*/#s' => '', // Remove /* block comments */
906 '#\n?//[^\n]*#' => '', // Remove // line comments
Andrey Andreev99f9b9a2013-10-30 23:07:23 +0200907 '#\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 +0200908 '#\s{2,}#' => ' ' // Reduce the remaining multiple space characters to a single space
909 );
910 }
911
912 $$varname = preg_replace(array_keys($patterns), array_values($patterns), $$varname);
913
914 // Glue back JS quoted strings
915 if ($type === 'js')
916 {
917 $js_code += $js_literals;
918 ksort($js_code);
919 $output = implode($js_code);
920 unset($js_code, $js_literals, $varname, $patterns);
921 }
922
923 return is_array($tags)
924 ? $tags['open'].$output.$tags['close']
925 : $output;
926 }
927
Michael Dodge362b8002013-01-04 23:18:39 -0700928}
929
930/* End of file Output.php */
Andrey Andreevd8dba5d2012-12-17 15:42:01 +0200931/* Location: ./system/core/Output.php */