blob: e8c1c39721f38a7b3cb2b913bd373531711b1577 [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
darwinel871754a2014-02-11 17:34:57 +010021 * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
Michael Dodge362b8002013-01-04 23:18:39 -070022 * @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 Andreevf6274742014-02-20 18:05:58 +0200123 $this->_zlib_oc = (bool) ini_get('zlib.output_compression');
Andrey Andreev155ee722014-01-10 15:50:54 +0200124 $this->_compress_output = (
125 $this->_zlib_oc === FALSE
Andrey Andreev9916bfc2014-01-10 16:21:07 +0200126 && config_item('compress_output') === TRUE
Andrey Andreev155ee722014-01-10 15:50:54 +0200127 && extension_loaded('zlib')
128 );
Michael Dodge362b8002013-01-04 23:18:39 -0700129
130 // Get mime types for later
131 $this->mimes =& get_mimes();
132
133 log_message('debug', 'Output Class Initialized');
134 }
135
136 // --------------------------------------------------------------------
137
138 /**
139 * Get Output
140 *
141 * Returns the current output string.
142 *
143 * @return string
144 */
145 public function get_output()
146 {
147 return $this->final_output;
148 }
149
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
168 /**
169 * Append Output
170 *
171 * Appends data onto the output string.
172 *
173 * @param string $output Data to append
174 * @return CI_Output
175 */
176 public function append_output($output)
177 {
178 if (empty($this->final_output))
179 {
180 $this->final_output = $output;
181 }
182 else
183 {
184 $this->final_output .= $output;
185 }
186
187 return $this;
188 }
189
190 // --------------------------------------------------------------------
191
192 /**
193 * Set Header
194 *
195 * Lets you set a server header which will be sent with the final output.
196 *
197 * Note: If a file is cached, headers will not be sent.
198 * @todo We need to figure out how to permit headers to be cached.
199 *
200 * @param string $header Header
201 * @param bool $replace Whether to replace the old header value, if already set
202 * @return CI_Output
203 */
204 public function set_header($header, $replace = TRUE)
205 {
206 // If zlib.output_compression is enabled it will compress the output,
207 // but it will not modify the content-length header to compensate for
208 // the reduction, causing the browser to hang waiting for more data.
209 // We'll just skip content-length in those cases.
210 if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0)
211 {
212 return $this;
213 }
214
215 $this->headers[] = array($header, $replace);
216 return $this;
217 }
218
219 // --------------------------------------------------------------------
220
221 /**
222 * Set Content-Type Header
223 *
224 * @param string $mime_type Extension of the file we're outputting
225 * @param string $charset Character set (default: NULL)
226 * @return CI_Output
227 */
228 public function set_content_type($mime_type, $charset = NULL)
229 {
230 if (strpos($mime_type, '/') === FALSE)
231 {
232 $extension = ltrim($mime_type, '.');
233
234 // Is this extension supported?
235 if (isset($this->mimes[$extension]))
236 {
237 $mime_type =& $this->mimes[$extension];
238
239 if (is_array($mime_type))
240 {
241 $mime_type = current($mime_type);
242 }
243 }
244 }
245
246 $this->mime_type = $mime_type;
247
248 if (empty($charset))
249 {
250 $charset = config_item('charset');
251 }
252
253 $header = 'Content-Type: '.$mime_type
254 .(empty($charset) ? NULL : '; charset='.$charset);
255
256 $this->headers[] = array($header, TRUE);
257 return $this;
258 }
259
260 // --------------------------------------------------------------------
261
262 /**
263 * Get Current Content-Type Header
264 *
265 * @return string 'text/html', if not already set
266 */
267 public function get_content_type()
268 {
269 for ($i = 0, $c = count($this->headers); $i < $c; $i++)
270 {
271 if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)
272 {
273 return $content_type;
274 }
275 }
276
277 return 'text/html';
278 }
279
280 // --------------------------------------------------------------------
281
282 /**
283 * Get Header
284 *
285 * @param string $header_name
286 * @return string
287 */
288 public function get_header($header)
289 {
290 // Combine headers already sent with our batched headers
291 $headers = array_merge(
292 // We only need [x][0] from our multi-dimensional array
293 array_map('array_shift', $this->headers),
294 headers_list()
295 );
296
297 if (empty($headers) OR empty($header))
298 {
299 return NULL;
300 }
301
302 for ($i = 0, $c = count($headers); $i < $c; $i++)
303 {
304 if (strncasecmp($header, $headers[$i], $l = strlen($header)) === 0)
305 {
306 return trim(substr($headers[$i], $l+1));
307 }
308 }
309
310 return NULL;
311 }
312
313 // --------------------------------------------------------------------
314
315 /**
316 * Set HTTP Status Header
317 *
318 * As of version 1.7.2, this is an alias for common function
319 * set_status_header().
320 *
321 * @param int $code Status code (default: 200)
322 * @param string $text Optional message
323 * @return CI_Output
324 */
325 public function set_status_header($code = 200, $text = '')
326 {
327 set_status_header($code, $text);
328 return $this;
329 }
330
331 // --------------------------------------------------------------------
332
333 /**
334 * Enable/disable Profiler
335 *
336 * @param bool $val TRUE to enable or FALSE to disable
337 * @return CI_Output
338 */
339 public function enable_profiler($val = TRUE)
340 {
341 $this->enable_profiler = is_bool($val) ? $val : TRUE;
342 return $this;
343 }
344
345 // --------------------------------------------------------------------
346
347 /**
348 * Set Profiler Sections
349 *
350 * Allows override of default/config settings for
351 * Profiler section display.
352 *
353 * @param array $sections Profiler sections
354 * @return CI_Output
355 */
356 public function set_profiler_sections($sections)
357 {
358 if (isset($sections['query_toggle_count']))
359 {
360 $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
361 unset($sections['query_toggle_count']);
362 }
363
364 foreach ($sections as $section => $enable)
365 {
366 $this->_profiler_sections[$section] = ($enable !== FALSE);
367 }
368
369 return $this;
370 }
371
372 // --------------------------------------------------------------------
373
374 /**
375 * Set Cache
376 *
377 * @param int $time Cache expiration time in seconds
378 * @return CI_Output
379 */
380 public function cache($time)
381 {
382 $this->cache_expiration = is_numeric($time) ? $time : 0;
383 return $this;
384 }
385
386 // --------------------------------------------------------------------
387
388 /**
389 * Display Output
390 *
391 * Processes sends the sends finalized output data to the browser along
392 * with any server headers and profile data. It also stops benchmark
393 * timers so the page rendering speed and memory usage can be shown.
394 *
395 * Note: All "view" data is automatically put into $this->final_output
396 * by controller class.
397 *
398 * @uses CI_Output::$final_output
399 * @param string $output Output data override
400 * @return void
401 */
402 public function _display($output = '')
403 {
404 // Note: We use globals because we can't use $CI =& get_instance()
405 // since this function is sometimes called by the caching mechanism,
406 // which happens before the CI super object is available.
407 global $BM, $CFG;
408
409 // Grab the super object if we can.
Andrey Andreev49e68de2013-02-21 16:30:55 +0200410 if (class_exists('CI_Controller', FALSE))
Michael Dodge362b8002013-01-04 23:18:39 -0700411 {
412 $CI =& get_instance();
413 }
414
415 // --------------------------------------------------------------------
416
417 // Set the output data
418 if ($output === '')
419 {
420 $output =& $this->final_output;
421 }
422
423 // --------------------------------------------------------------------
424
425 // Is minify requested?
426 if ($CFG->item('minify_output') === TRUE)
427 {
428 $output = $this->minify($output, $this->mime_type);
429 }
430
431 // --------------------------------------------------------------------
432
433 // Do we need to write a cache file? Only if the controller does not have its
434 // own _output() method and we are not dealing with a cache file, which we
435 // can determine by the existence of the $CI object above
436 if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
437 {
438 $this->_write_cache($output);
439 }
440
441 // --------------------------------------------------------------------
442
443 // Parse out the elapsed time and memory usage,
444 // then swap the pseudo-variables with the data
445
446 $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
447
448 if ($this->parse_exec_vars === TRUE)
449 {
450 $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
Michael Dodge362b8002013-01-04 23:18:39 -0700451 $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
452 }
453
454 // --------------------------------------------------------------------
455
456 // Is compression requested?
Andrey Andreev155ee722014-01-10 15:50:54 +0200457 if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
458 && $this->_compress_output === TRUE
Michael Dodge362b8002013-01-04 23:18:39 -0700459 && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
460 {
461 ob_start('ob_gzhandler');
462 }
463
464 // --------------------------------------------------------------------
465
466 // Are there any server headers to send?
467 if (count($this->headers) > 0)
468 {
469 foreach ($this->headers as $header)
470 {
471 @header($header[0], $header[1]);
472 }
473 }
474
475 // --------------------------------------------------------------------
476
477 // Does the $CI object exist?
478 // If not we know we are dealing with a cache file so we'll
479 // simply echo out the data and exit.
480 if ( ! isset($CI))
481 {
Andrey Andreev155ee722014-01-10 15:50:54 +0200482 if ($this->_compress_output === TRUE)
483 {
484 if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
485 {
486 header('Content-Encoding: gzip');
487 header('Content-Length: '.strlen($output));
488 }
489 else
490 {
491 // User agent doesn't support gzip compression,
492 // so we'll have to decompress our cache
493 $output = gzinflate(substr($output, 10, -8));
494 }
495 }
496
Michael Dodge362b8002013-01-04 23:18:39 -0700497 echo $output;
498 log_message('debug', 'Final output sent to browser');
499 log_message('debug', 'Total execution time: '.$elapsed);
500 return;
501 }
502
503 // --------------------------------------------------------------------
504
505 // Do we need to generate profile data?
506 // If so, load the Profile class and run it.
507 if ($this->enable_profiler === TRUE)
508 {
509 $CI->load->library('profiler');
510 if ( ! empty($this->_profiler_sections))
511 {
512 $CI->profiler->set_sections($this->_profiler_sections);
513 }
514
515 // If the output data contains closing </body> and </html> tags
516 // we will remove them and add them back after we insert the profile data
517 $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
518 if ($count > 0)
519 {
520 $output .= '</body></html>';
521 }
522 }
523
524 // Does the controller contain a function named _output()?
525 // If so send the output there. Otherwise, echo it.
526 if (method_exists($CI, '_output'))
527 {
528 $CI->_output($output);
529 }
530 else
531 {
532 echo $output; // Send it to the browser!
533 }
534
535 log_message('debug', 'Final output sent to browser');
536 log_message('debug', 'Total execution time: '.$elapsed);
537 }
538
539 // --------------------------------------------------------------------
540
541 /**
542 * Write Cache
543 *
544 * @param string $output Output data to cache
545 * @return void
546 */
547 public function _write_cache($output)
548 {
549 $CI =& get_instance();
550 $path = $CI->config->item('cache_path');
551 $cache_path = ($path === '') ? APPPATH.'cache/' : $path;
552
553 if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
554 {
555 log_message('error', 'Unable to write cache file: '.$cache_path);
556 return;
557 }
558
Andrey Andreev155ee722014-01-10 15:50:54 +0200559 $uri = $CI->config->item('base_url')
560 .$CI->config->item('index_page')
561 .$CI->uri->uri_string();
Michael Dodge362b8002013-01-04 23:18:39 -0700562
563 $cache_path .= md5($uri);
564
565 if ( ! $fp = @fopen($cache_path, FOPEN_WRITE_CREATE_DESTRUCTIVE))
566 {
567 log_message('error', 'Unable to write cache file: '.$cache_path);
568 return;
569 }
570
Michael Dodge362b8002013-01-04 23:18:39 -0700571 if (flock($fp, LOCK_EX))
572 {
Andrey Andreev155ee722014-01-10 15:50:54 +0200573 // If output compression is enabled, compress the cache
574 // itself, so that we don't have to do that each time
575 // we're serving it
576 if ($this->_compress_output === TRUE)
577 {
578 $output = gzencode($output);
579
580 if ($this->get_header('content-type') === NULL)
581 {
582 $this->set_content_type($this->mime_type);
583 }
584 }
585
586 $expire = time() + ($this->cache_expiration * 60);
587
588 // Put together our serialized info.
589 $cache_info = serialize(array(
590 'expire' => $expire,
591 'headers' => $this->headers
592 ));
593
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200594 $output = $cache_info.'ENDCI--->'.$output;
595
596 for ($written = 0, $length = strlen($output); $written < $length; $written += $result)
597 {
598 if (($result = fwrite($fp, substr($output, $written))) === FALSE)
599 {
600 break;
601 }
602 }
603
Michael Dodge362b8002013-01-04 23:18:39 -0700604 flock($fp, LOCK_UN);
605 }
606 else
607 {
608 log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
609 return;
610 }
Andrey Andreev155ee722014-01-10 15:50:54 +0200611
Michael Dodge362b8002013-01-04 23:18:39 -0700612 fclose($fp);
Michael Dodge362b8002013-01-04 23:18:39 -0700613
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200614 if (is_int($result))
615 {
616 @chmod($cache_path, FILE_WRITE_MODE);
617 log_message('debug', 'Cache file written: '.$cache_path);
Michael Dodge362b8002013-01-04 23:18:39 -0700618
Andrey Andreevd8b1ad32014-01-15 17:42:52 +0200619 // Send HTTP cache-control headers to browser to match file cache settings.
620 $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
621 }
622 else
623 {
624 @unlink($cache_path);
625 log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
626 }
Michael Dodge362b8002013-01-04 23:18:39 -0700627 }
628
629 // --------------------------------------------------------------------
630
631 /**
632 * Update/serve cached output
633 *
634 * @uses CI_Config
635 * @uses CI_URI
636 *
637 * @param object &$CFG CI_Config class instance
638 * @param object &$URI CI_URI class instance
639 * @return bool TRUE on success or FALSE on failure
640 */
641 public function _display_cache(&$CFG, &$URI)
642 {
643 $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
644
645 // Build the file path. The file name is an MD5 hash of the full URI
646 $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
647 $filepath = $cache_path.md5($uri);
648
649 if ( ! @file_exists($filepath) OR ! $fp = @fopen($filepath, FOPEN_READ))
650 {
651 return FALSE;
652 }
653
654 flock($fp, LOCK_SH);
655
656 $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
657
658 flock($fp, LOCK_UN);
659 fclose($fp);
660
Eric Robertsc90e67e2013-01-11 21:20:54 -0600661 // Look for embedded serialized file info.
662 if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
Michael Dodge362b8002013-01-04 23:18:39 -0700663 {
664 return FALSE;
665 }
Purwandi5dc6d512013-01-19 17:43:08 +0700666
Eric Robertsc90e67e2013-01-11 21:20:54 -0600667 $cache_info = unserialize($match[1]);
668 $expire = $cache_info['expire'];
Michael Dodge362b8002013-01-04 23:18:39 -0700669
670 $last_modified = filemtime($cache_path);
Michael Dodge362b8002013-01-04 23:18:39 -0700671
672 // Has the file expired?
673 if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
674 {
675 // If so we'll delete it.
676 @unlink($filepath);
677 log_message('debug', 'Cache file has expired. File deleted.');
678 return FALSE;
679 }
680 else
681 {
682 // Or else send the HTTP cache control headers.
683 $this->set_cache_header($last_modified, $expire);
684 }
Purwandi5dc6d512013-01-19 17:43:08 +0700685
Eric Robertsc90e67e2013-01-11 21:20:54 -0600686 // Add headers from cache file.
687 foreach ($cache_info['headers'] as $header)
688 {
689 $this->set_header($header[0], $header[1]);
690 }
Michael Dodge362b8002013-01-04 23:18:39 -0700691
692 // Display the cache
693 $this->_display(substr($cache, strlen($match[0])));
694 log_message('debug', 'Cache file is current. Sending it to browser.');
695 return TRUE;
696 }
697
698 // --------------------------------------------------------------------
699
700 /**
701 * Delete cache
702 *
703 * @param string $uri URI string
704 * @return bool
705 */
706 public function delete_cache($uri = '')
707 {
708 $CI =& get_instance();
709 $cache_path = $CI->config->item('cache_path');
710 if ($cache_path === '')
711 {
712 $cache_path = APPPATH.'cache/';
713 }
714
715 if ( ! is_dir($cache_path))
716 {
717 log_message('error', 'Unable to find cache path: '.$cache_path);
718 return FALSE;
719 }
720
721 if (empty($uri))
722 {
723 $uri = $CI->uri->uri_string();
724 }
725
726 $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').$uri);
727
728 if ( ! @unlink($cache_path))
729 {
730 log_message('error', 'Unable to delete cache file for '.$uri);
731 return FALSE;
732 }
733
734 return TRUE;
735 }
736
737 // --------------------------------------------------------------------
738
739 /**
740 * Set Cache Header
741 *
742 * Set the HTTP headers to match the server-side file cache settings
743 * in order to reduce bandwidth.
744 *
745 * @param int $last_modified Timestamp of when the page was last modified
746 * @param int $expiration Timestamp of when should the requested page expire from cache
747 * @return void
748 */
749 public function set_cache_header($last_modified, $expiration)
750 {
751 $max_age = $expiration - $_SERVER['REQUEST_TIME'];
752
753 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
754 {
755 $this->set_status_header(304);
756 exit;
757 }
758 else
759 {
760 header('Pragma: public');
Andrey Andreev3ca060a2013-11-27 16:30:31 +0200761 header('Cache-Control: max-age='.$max_age.', public');
Michael Dodge362b8002013-01-04 23:18:39 -0700762 header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
763 header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
764 }
765 }
766
767 // --------------------------------------------------------------------
768
769 /**
770 * Minify
771 *
772 * Reduce excessive size of HTML/CSS/JavaScript content.
773 *
774 * @param string $output Output to minify
775 * @param string $type Output content MIME type
776 * @return string Minified output
777 */
778 public function minify($output, $type = 'text/html')
779 {
780 switch ($type)
781 {
782 case 'text/html':
783
784 if (($size_before = strlen($output)) === 0)
785 {
786 return '';
787 }
788
789 // Find all the <pre>,<code>,<textarea>, and <javascript> tags
790 // We'll want to return them to this unprocessed state later.
791 preg_match_all('{<pre.+</pre>}msU', $output, $pres_clean);
792 preg_match_all('{<code.+</code>}msU', $output, $codes_clean);
793 preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_clean);
794 preg_match_all('{<script.+</script>}msU', $output, $javascript_clean);
795
796 // Minify the CSS in all the <style> tags.
797 preg_match_all('{<style.+</style>}msU', $output, $style_clean);
798 foreach ($style_clean[0] as $s)
799 {
Andrey Andreev6a424902013-10-28 14:16:18 +0200800 $output = str_replace($s, $this->_minify_js_css($s, 'css', TRUE), $output);
Michael Dodge362b8002013-01-04 23:18:39 -0700801 }
802
803 // Minify the javascript in <script> tags.
804 foreach ($javascript_clean[0] as $s)
805 {
Andrey Andreev6a424902013-10-28 14:16:18 +0200806 $javascript_mini[] = $this->_minify_js_css($s, 'js', TRUE);
Michael Dodge362b8002013-01-04 23:18:39 -0700807 }
808
809 // Replace multiple spaces with a single space.
810 $output = preg_replace('!\s{2,}!', ' ', $output);
811
812 // Remove comments (non-MSIE conditionals)
Michael Dodge4d02e352013-01-04 23:22:51 -0700813 $output = preg_replace('{\s*<!--[^\[<>].*(?<!!)-->\s*}msU', '', $output);
Michael Dodge362b8002013-01-04 23:18:39 -0700814
815 // Remove spaces around block-level elements.
Purwandi5dc6d512013-01-19 17:43:08 +0700816 $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 -0700817
818 // Replace mangled <pre> etc. tags with unprocessed ones.
819
820 if ( ! empty($pres_clean))
821 {
822 preg_match_all('{<pre.+</pre>}msU', $output, $pres_messed);
823 $output = str_replace($pres_messed[0], $pres_clean[0], $output);
824 }
825
826 if ( ! empty($codes_clean))
827 {
828 preg_match_all('{<code.+</code>}msU', $output, $codes_messed);
829 $output = str_replace($codes_messed[0], $codes_clean[0], $output);
830 }
831
Andrey Andreev3ffce982013-01-21 15:24:09 +0200832 if ( ! empty($textareas_clean))
Michael Dodge362b8002013-01-04 23:18:39 -0700833 {
834 preg_match_all('{<textarea.+</textarea>}msU', $output, $textareas_messed);
835 $output = str_replace($textareas_messed[0], $textareas_clean[0], $output);
836 }
837
838 if (isset($javascript_mini))
839 {
840 preg_match_all('{<script.+</script>}msU', $output, $javascript_messed);
841 $output = str_replace($javascript_messed[0], $javascript_mini, $output);
842 }
843
844 $size_removed = $size_before - strlen($output);
845 $savings_percent = round(($size_removed / $size_before * 100));
846
847 log_message('debug', 'Minifier shaved '.($size_removed / 1000).'KB ('.$savings_percent.'%) off final HTML output.');
848
849 break;
850
851 case 'text/css':
Andrey Andreev6a424902013-10-28 14:16:18 +0200852
853 return $this->_minify_js_css($output, 'css');
854
Michael Dodge362b8002013-01-04 23:18:39 -0700855 case 'text/javascript':
bayssmekanique7b903252013-03-12 13:25:24 -0700856 case 'application/javascript':
857 case 'application/x-javascript':
Michael Dodge362b8002013-01-04 23:18:39 -0700858
Andrey Andreev6a424902013-10-28 14:16:18 +0200859 return $this->_minify_js_css($output, 'js');
Michael Dodge362b8002013-01-04 23:18:39 -0700860
861 default: break;
862 }
863
864 return $output;
865 }
866
867 // --------------------------------------------------------------------
868
Andrey Andreev3c3bbac2013-10-31 15:10:49 +0200869 /**
870 * Minify JavaScript and CSS code
871 *
872 * Strips comments and excessive whitespace characters
873 *
874 * @param string $output
875 * @param string $type 'js' or 'css'
876 * @param bool $tags Whether $output contains the 'script' or 'style' tag
877 * @return string
878 */
Andrey Andreev6a424902013-10-28 14:16:18 +0200879 protected function _minify_js_css($output, $type, $tags = FALSE)
880 {
881 if ($tags === TRUE)
882 {
883 $tags = array('close' => strrchr($output, '<'));
884
885 $open_length = strpos($output, '>') + 1;
886 $tags['open'] = substr($output, 0, $open_length);
887
888 $output = substr($output, $open_length, -strlen($tags['close']));
889
890 // Strip spaces from the tags
891 $tags = preg_replace('#\s{2,}#', ' ', $tags);
892 }
893
894 $output = trim($output);
895
896 if ($type === 'js')
897 {
898 // Catch all string literals and comment blocks
Andrey Andreev0949b362013-10-31 16:07:40 +0200899 if (preg_match_all('#((?:((?<!\\\)\'|")|(/\*)|(//)).*(?(2)(?<!\\\)\2|(?(3)\*/|\n)))#msuUS', $output, $match, PREG_OFFSET_CAPTURE))
Andrey Andreev6a424902013-10-28 14:16:18 +0200900 {
901 $js_literals = $js_code = array();
902 for ($match = $match[0], $c = count($match), $i = $pos = $offset = 0; $i < $c; $i++)
903 {
Andrey Andreev3c3bbac2013-10-31 15:10:49 +0200904 $js_code[$pos++] = trim(substr($output, $offset, $match[$i][1] - $offset));
Andrey Andreev6a424902013-10-28 14:16:18 +0200905 $offset = $match[$i][1] + strlen($match[$i][0]);
906
907 // Save only if we haven't matched a comment block
908 if ($match[$i][0][0] !== '/')
909 {
910 $js_literals[$pos++] = array_shift($match[$i]);
911 }
912 }
913 $js_code[$pos] = substr($output, $offset);
914
915 // $match might be quite large, so free it up together with other vars that we no longer need
916 unset($match, $offset, $pos);
917 }
918 else
919 {
920 $js_code = array($output);
921 $js_literals = array();
922 }
923
924 $varname = 'js_code';
925 }
926 else
927 {
928 $varname = 'output';
929 }
930
931 // Standartize new lines
932 $$varname = str_replace(array("\r\n", "\r"), "\n", $$varname);
933
934 if ($type === 'js')
935 {
936 $patterns = array(
Andrey Andreev99f9b9a2013-10-30 23:07:23 +0200937 '#\s*([!\#%&()*+,\-./:;<=>?@\[\]^`{|}~])\s*#' => '$1', // Remove spaces following and preceeding JS-wise non-special & non-word characters
Andrey Andreev6a424902013-10-28 14:16:18 +0200938 '#\s{2,}#' => ' ' // Reduce the remaining multiple whitespace characters to a single space
939 );
940 }
941 else
942 {
943 $patterns = array(
944 '#/\*.*(?=\*/)\*/#s' => '', // Remove /* block comments */
945 '#\n?//[^\n]*#' => '', // Remove // line comments
Andrey Andreev99f9b9a2013-10-30 23:07:23 +0200946 '#\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 +0200947 '#\s{2,}#' => ' ' // Reduce the remaining multiple space characters to a single space
948 );
949 }
950
951 $$varname = preg_replace(array_keys($patterns), array_values($patterns), $$varname);
952
953 // Glue back JS quoted strings
954 if ($type === 'js')
955 {
956 $js_code += $js_literals;
957 ksort($js_code);
958 $output = implode($js_code);
959 unset($js_code, $js_literals, $varname, $patterns);
960 }
961
962 return is_array($tags)
963 ? $tags['open'].$output.$tags['close']
964 : $output;
965 }
966
Michael Dodge362b8002013-01-04 23:18:39 -0700967}
968
969/* End of file Output.php */
Andrey Andreevd8dba5d2012-12-17 15:42:01 +0200970/* Location: ./system/core/Output.php */