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