From 7aa44ab06f7f062cb64629d8b241bcbf4b91c034 Mon Sep 17 00:00:00 2001 From: JasWSInc Date: Mon, 22 Sep 2014 00:29:07 -0800 Subject: [PATCH] Bug fix and refactoring. See: https://github.com/websharks/html-compressor/issues/45 --- html-compressor/includes/core.php | 1339 ++++++++++++++++------------- 1 file changed, 760 insertions(+), 579 deletions(-) diff --git a/html-compressor/includes/core.php b/html-compressor/includes/core.php index 918d634..d79532f 100644 --- a/html-compressor/includes/core.php +++ b/html-compressor/includes/core.php @@ -21,6 +21,12 @@ */ class core // Heart of the HTML Compressor. { + /********************************************************************************************************/ + + /* + * Private Properties + */ + /** * Current version string. * @@ -178,7 +184,7 @@ class core // Heart of the HTML Compressor. * * @var array Used by various routines for optimization. */ - protected static $cache = array(); + protected static $static = array(); /** * Data cache for this class instance. @@ -187,7 +193,13 @@ class core // Heart of the HTML Compressor. * * @var array Used by various routines for optimization. */ - protected $icache = array(); + protected $cache = array(); + + /********************************************************************************************************/ + + /* + * Constructor (Accepts Options) + */ /** * Class Constructor. @@ -255,6 +267,12 @@ public function __construct(array $options = array()) require_once dirname(__FILE__).'/externals/js-minifier.php'; } + /********************************************************************************************************/ + + /* + * Public API Methods + */ + /** * Handles compression. The heart of this class. * @@ -307,6 +325,12 @@ public function compress($input) return $html; } + /********************************************************************************************************/ + + /* + * Other API/Magic Methods + */ + /** * Magic method for access to read-only properties. * @@ -329,6 +353,13 @@ public function __get($property) throw new \exception(sprintf('Undefined property: `%1$s`.', $property)); } + /********************************************************************************************************/ + + /* + * CSS-Related Methods + * ~ See also: CSS Compression Utilities + */ + /** * Handles possible compression of head/body CSS. * @@ -388,122 +419,6 @@ protected function maybe_compress_combine_head_body_css($html) return $html; // With possible compression having been applied here. } - /** - * Handles possible compression of head JS. - * - * @since 140417 Initial release. - * - * @param string $html Input HTML code. - * - * @return string HTML code, after possible JS compression. - */ - protected function maybe_compress_combine_head_js($html) - { - $benchmark = !empty($this->options['benchmark']) - && $this->options['benchmark'] === 'details'; - if($benchmark) $time = microtime(TRUE); - - $html = (string)$html; // Force string value. - - if(isset($this->options['compress_combine_head_js'])) - if(!$this->options['compress_combine_head_js']) - $disabled = TRUE; // Disabled flag. - - if(!$html || !empty($disabled)) goto finale; // Nothing to do. - - if(($head_frag = $this->get_head_frag($html)) /* No need to get the HTML frag here; we're operating on the `` only. */) - if(($js_tag_frags = $this->get_js_tag_frags($head_frag)) && ($js_parts = $this->compile_js_tag_frags_into_parts($js_tag_frags))) - { - print_r($js_tag_frags); - $js_tag_frags_all_compiled = $this->compile_key_elements_deep($js_tag_frags, 'all'); - $html = $this->replace_once($head_frag['all'], '%%htmlc-head%%', $html); - $cleaned_head_contents = $this->replace_once($js_tag_frags_all_compiled, '', $head_frag['contents']); - $cleaned_head_contents = $this->cleanup_self_closing_html_tag_lines($cleaned_head_contents); - - $compressed_js_tags = array(); // Initialize. - - foreach($js_parts as $_js_part) - { - if(isset($_js_part['exclude_frag'], $js_tag_frags[$_js_part['exclude_frag']]['all'])) - $compressed_js_tags[] = $js_tag_frags[$_js_part['exclude_frag']]['all']; - else $compressed_js_tags[] = $_js_part['tag']; - } - unset($_js_part); // Housekeeping. - - $compressed_js_tags = implode("\n", $compressed_js_tags); - $compressed_head_parts = array($head_frag['open_tag'], $cleaned_head_contents, $compressed_js_tags, $head_frag['closing_tag']); - $html = $this->replace_once('%%htmlc-head%%', implode("\n", $compressed_head_parts), $html); - } - finale: // Target point; finale/return value. - - if($html) $html = trim($html); - - if($benchmark && !empty($time) && $html && empty($disabled)) - $this->benchmark_times[] = // Benchmark data. - array('function' => __FUNCTION__, // Function marker. - 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), - 'task' => sprintf('compressing/combining head JS in checksum: `%1$s`', md5($html))); - - return $html; // With possible compression having been applied here. - } - - /** - * Handles possible compression of footer JS. - * - * @since 140417 Initial release. - * - * @param string $html Input HTML code. - * - * @return string HTML code, after possible JS compression. - */ - protected function maybe_compress_combine_footer_js($html) - { - $benchmark = !empty($this->options['benchmark']) - && $this->options['benchmark'] === 'details'; - if($benchmark) $time = microtime(TRUE); - - $html = (string)$html; // Force string value. - - if(isset($this->options['compress_combine_footer_js'])) - if(!$this->options['compress_combine_footer_js']) - $disabled = TRUE; // Disabled flag. - - if(!$html || !empty($disabled)) goto finale; // Nothing to do. - - if(($footer_scripts_frag = $this->get_footer_scripts_frag($html)) /* e.g. */) - if(($js_tag_frags = $this->get_js_tag_frags($footer_scripts_frag)) && ($js_parts = $this->compile_js_tag_frags_into_parts($js_tag_frags))) - { - $js_tag_frags_all_compiled = $this->compile_key_elements_deep($js_tag_frags, 'all'); - $html = $this->replace_once($footer_scripts_frag['all'], '%%htmlc-footer-scripts%%', $html); - $cleaned_footer_scripts = $this->replace_once($js_tag_frags_all_compiled, '', $footer_scripts_frag['contents']); - - $compressed_js_tags = array(); // Initialize. - - foreach($js_parts as $_js_part) - { - if(isset($_js_part['exclude_frag'], $js_tag_frags[$_js_part['exclude_frag']]['all'])) - $compressed_js_tags[] = $js_tag_frags[$_js_part['exclude_frag']]['all']; - else $compressed_js_tags[] = $_js_part['tag']; - } - unset($_js_part); // Housekeeping. - - $compressed_js_tags = implode("\n", $compressed_js_tags); - $compressed_footer_script_parts = array($footer_scripts_frag['open_tag'], $cleaned_footer_scripts, $compressed_js_tags, $footer_scripts_frag['closing_tag']); - $html = $this->replace_once('%%htmlc-footer-scripts%%', implode("\n", $compressed_footer_script_parts), $html); - } - finale: // Target point; finale/return value. - - if($html) $html = trim($html); - - if($benchmark && !empty($time) && $html && empty($disabled)) - $this->benchmark_times[] = // Benchmark data. - array('function' => __FUNCTION__, // Function marker. - 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), - 'task' => sprintf('compressing/combining footer JS in checksum: `%1$s`', md5($html))); - - return $html; // With possible compression having been applied here. - } - /** * Compiles CSS tag fragments into CSS parts with compression. * @@ -656,325 +571,253 @@ protected function compile_css_tag_frags_into_parts(array $css_tag_frags) } /** - * Compiles JS tag fragments into JS parts with compression. + * Parses and returns an array of CSS tag fragments. * * @since 140417 Initial release. * - * @param array $js_tag_frags JS tag fragments. + * @param array $html_frag An HTML tag fragment array. * - * @return array Array of JS parts, else an empty array on failure. + * @return array An array of CSS tag fragments (ready to be converted into CSS parts). + * Else an empty array (i.e. no CSS tag fragments in the HTML fragment array). * - * @throws \exception If unable to cache JS parts. + * @see http://css-tricks.com/how-to-create-an-ie-only-stylesheet/ + * @see http://stackoverflow.com/a/12102131 */ - protected function compile_js_tag_frags_into_parts(array $js_tag_frags) + protected function get_css_tag_frags(array $html_frag) { $benchmark = !empty($this->options['benchmark']) && $this->options['benchmark'] === 'details'; if($benchmark) $time = microtime(TRUE); - $js_parts = array(); // Initialize. - $js_parts_checksum = ''; // Initialize. + $css_tag_frags = array(); // Initialize. - if(!$js_tag_frags) goto finale; + if(!$html_frag) goto finale; - $js_parts_checksum = $this->get_tag_frags_checksum($js_tag_frags); - $public_cache_dir = $this->cache_dir($this::dir_public_type, $js_parts_checksum); - $private_cache_dir = $this->cache_dir($this::dir_private_type, $js_parts_checksum); - $public_cache_dir_url = $this->cache_dir_url($this::dir_public_type, $js_parts_checksum); + $regex = '/(?P'. // Entire match. + '(?P\<\![^[>]*?\[if\W[^\]]*?\][^>]*?\>\s*)?'. + '(?:(?P\]*?)?\>)'. // Or a tag. + '|(?P\]*?)?\>)(?P.*?)(?P\<\/style\>))'. + '(?P\s*\<\![^[>]*?\[endif\][^>]*?\>)?'. + ')/is'; // Dot matches line breaks. - $cache_parts_file = $js_parts_checksum.'-compressor-parts.js-cache'; - $cache_parts_file_path = $private_cache_dir.'/'.$cache_parts_file; - $cache_parts_file_path_tmp = $cache_parts_file_path.'.'.uniqid('', TRUE).'.tmp'; - // Cache file creation is atomic; i.e. tmp file w/ rename. + if(!empty($html_frag['contents']) && preg_match_all($regex, $html_frag['contents'], $_tag_frags, PREG_SET_ORDER)) + { + foreach($_tag_frags as $_tag_frag) + { + $_link_href = $_style_css = $_media = ''; // Initialize. - $cache_part_file = '%%code-checksum%%-compressor-part.js'; - $cache_part_file_path = $public_cache_dir.'/'.$cache_part_file; - $cache_part_file_url = $public_cache_dir_url.'/'.$cache_part_file; + if(($_link_href = $this->get_link_css_href($_tag_frag, TRUE))) + $_media = $this->get_link_css_media($_tag_frag, FALSE); - if(is_file($cache_parts_file_path) && filemtime($cache_parts_file_path) > strtotime('-'.$this->cache_expiration_time)) - if(is_array($cached_parts = unserialize(file_get_contents($cache_parts_file_path)))) - { - $js_parts = $cached_parts; // Use cached parts. - goto finale; // Using the cache; we're all done here. - } - $_js_part = 0; // Initialize part counter. + else if(($_style_css = $this->get_style_css($_tag_frag, TRUE))) + $_media = $this->get_style_css_media($_tag_frag, FALSE); - foreach($js_tag_frags as $_js_tag_frag_pos => $_js_tag_frag) - { - if($_js_tag_frag['exclude']) - { - if($_js_tag_frag['script_src'] || $_js_tag_frag['script_js']) + if($_link_href || $_style_css) // One or the other is fine. { - if($js_parts) $_js_part++; // Starts new part. + $css_tag_frags[] = array( + 'all' => $_tag_frag['all'], - $js_parts[$_js_part]['tag'] = ''; - $js_parts[$_js_part]['exclude_frag'] = $_js_tag_frag_pos; + 'if_open_tag' => isset($_tag_frag['if_open_tag']) ? $_tag_frag['if_open_tag'] : '', + 'if_closing_tag' => isset($_tag_frag['if_closing_tag']) ? $_tag_frag['if_closing_tag'] : '', - $_js_part++; // Always indicates a new part in the next iteration. - } - } - else if($_js_tag_frag['script_src']) - { - if(($_js_tag_frag['script_src'] = $this->resolve_relative_url($_js_tag_frag['script_src']))) - if(($_js_code = $this->remote($_js_tag_frag['script_src']))) - { - $_js_code = rtrim($_js_code, ';').';'; + 'link_self_closing_tag' => isset($_tag_frag['link_self_closing_tag']) ? $_tag_frag['link_self_closing_tag'] : '', + 'link_href_external' => ($_link_href) ? $this->is_url_external($_link_href) : FALSE, + 'link_href' => $_link_href, // This could also be empty. - if($_js_code) // Now, DO we have something here? - { - if(!empty($js_parts[$_js_part]['code'])) - $js_parts[$_js_part]['code'] .= "\n\n".$_js_code; - else $js_parts[$_js_part]['code'] = $_js_code; - } - } - } - else if($_js_tag_frag['script_js']) - { - $_js_code = $_js_tag_frag['script_js']; - $_js_code = rtrim($_js_code, ';').';'; + 'style_open_tag' => isset($_tag_frag['style_open_tag']) ? $_tag_frag['style_open_tag'] : '', + 'style_css' => $_style_css, // This could also be empty. + 'style_closing_tag' => isset($_tag_frag['style_closing_tag']) ? $_tag_frag['style_closing_tag'] : '', - if($_js_code) // Now, DO we have something here? - { - if(!empty($js_parts[$_js_part]['code'])) - $js_parts[$_js_part]['code'] .= "\n\n".$_js_code; - else $js_parts[$_js_part]['code'] = $_js_code; + 'media' => $_media ? $_media : 'all', // Default value. + + 'exclude' => FALSE // Default value. + ); + $_tag_frag_r = &$css_tag_frags[count($css_tag_frags) - 1]; + + if($_tag_frag_r['if_open_tag'] || $_tag_frag_r['if_closing_tag']) + $_tag_frag_r['exclude'] = TRUE; + + else if($_tag_frag_r['link_href'] && $_tag_frag_r['link_href_external'] && isset($this->options['compress_combine_remote_css_js']) && !$this->options['compress_combine_remote_css_js']) + $_tag_frag_r['exclude'] = TRUE; + + else if($this->regex_css_exclusions && preg_match($this->regex_css_exclusions, $_tag_frag_r['link_href'].$_tag_frag_r['style_css'])) + $_tag_frag_r['exclude'] = TRUE; + + else if($this->built_in_regex_css_exclusions && preg_match($this->built_in_regex_css_exclusions, $_tag_frag_r['link_href'].$_tag_frag_r['style_css'])) + $_tag_frag_r['exclude'] = TRUE; } } } - unset($_js_part, $_js_tag_frag_pos, $_js_tag_frag, $_js_code); + unset($_tag_frags, $_tag_frag, $_tag_frag_r, $_link_href, $_style_css, $_media); - foreach(array_keys($js_parts = array_values($js_parts)) as $_js_part) - { - if(!empty($js_parts[$_js_part]['code'])) - { - $_js_code = $js_parts[$_js_part]['code']; - $_js_code_cs = md5($_js_code); // Before compression. - $_js_code = $this->maybe_compress_js_code($_js_code); + finale: // Target point; finale/return value. - $_js_code_path = str_replace('%%code-checksum%%', $_js_code_cs, $cache_part_file_path); - $_js_code_url = str_replace('%%code-checksum%%', $_js_code_cs, $cache_part_file_url); - $_js_code_path_tmp = $_js_code_path.'.'.uniqid('', TRUE).'.tmp'; - // Cache file creation is atomic; e.g. tmp file w/ rename. - - if(!(file_put_contents($_js_code_path_tmp, $_js_code) && rename($_js_code_path_tmp, $_js_code_path))) - throw new \exception(sprintf('Unable to cache JS code file: `%1$s`.', $_js_code_path)); - - $js_parts[$_js_part]['tag'] = ''; - - unset($js_parts[$_js_part]['code']); // Ditch this; no need to cache this code too. - } - } - unset($_js_part, $_js_code, $_js_code_cs, $_js_code_path, $_js_code_path_tmp, $_js_code_url); - - if(!(file_put_contents($cache_parts_file_path_tmp, serialize($js_parts)) && rename($cache_parts_file_path_tmp, $cache_parts_file_path))) - throw new \exception(sprintf('Unable to cache JS parts into: `%1$s`.', $cache_parts_file_path)); - - finale: // Target point; finale/return value. - - if($benchmark && !empty($time) && $js_parts_checksum) + if($benchmark && !empty($time) && $html_frag) $this->benchmark_times[] = // Benchmark data. array('function' => __FUNCTION__, // Function marker. 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), - 'task' => sprintf('building parts based on JS tag frags in checksum: `%1$s`', $js_parts_checksum)); + 'task' => sprintf('compiling CSS tag frags in checksum: `%1$s`', md5(serialize($html_frag)))); - return $js_parts; + return $css_tag_frags; } /** - * Parses and returns an array of CSS tag fragments. - * - * @since 140417 Initial release. + * Test a tag fragment to see if it's CSS. * - * @param array $html_frag An HTML tag fragment array. + * @since 140921 Improving tag tests. * - * @return array An array of CSS tag fragments (ready to be converted into CSS parts). - * Else an empty array (i.e. no CSS tag fragments in the HTML fragment array). + * @param array $tag_frag A tag fragment. * - * @see http://css-tricks.com/how-to-create-an-ie-only-stylesheet/ - * @see http://stackoverflow.com/a/12102131 + * @return boolean TRUE if it contains CSS. */ - protected function get_css_tag_frags(array $html_frag) + protected function is_link_tag_frag_css(array $tag_frag) { - $benchmark = !empty($this->options['benchmark']) - && $this->options['benchmark'] === 'details'; - if($benchmark) $time = microtime(TRUE); - - $css_tag_frags = array(); // Initialize. - - if(!$html_frag) goto finale; - - $regex = '/(?P'. // Entire match. - '(?P\<\![^[>]*?\[if\W[^\]]*?\][^>]*?\>\s*)?'. - '(?:(?P\]*?)?\>)'. // Or a tag. - '|(?P\]*?)?\>)(?P.*?)(?P\<\/style\>))'. - '(?P\s*\<\![^[>]*?\[endif\][^>]*?\>)?'. - ')/is'; // Dot matches line breaks. - - if(!empty($html_frag['contents']) && preg_match_all($regex, $html_frag['contents'], $_tag_frags, PREG_SET_ORDER)) - { - foreach($_tag_frags as $_tag_frag) - { - $_link_href = $_style_css = $_media = 'all'; + if(empty($tag_frag['link_self_closing_tag'])) + return FALSE; // Nope; missing tag. - if(($_link_href = $this->get_link_css_href($_tag_frag))) - $_media = $this->get_link_css_media($_tag_frag); + $type = $rel = ''; // Initialize. - else if(($_style_css = $this->get_style_css($_tag_frag))) - $_media = $this->get_style_css_media($_tag_frag); - - if($_link_href || $_style_css) // One or the other is fine. - { - $css_tag_frags[] = array( - 'all' => $_tag_frag['all'], + if(stripos($tag_frag['link_self_closing_tag'], 'type') !== 0) + if(preg_match('/\stype\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['link_self_closing_tag'], $_m)) + $type = $_m['value']; - 'if_open_tag' => isset($_tag_frag['if_open_tag']) ? $_tag_frag['if_open_tag'] : '', - 'if_closing_tag' => isset($_tag_frag['if_closing_tag']) ? $_tag_frag['if_closing_tag'] : '', + unset($_m); // Just a little housekeeping. - 'link_self_closing_tag' => isset($_tag_frag['link_self_closing_tag']) ? $_tag_frag['link_self_closing_tag'] : '', - 'link_href_external' => ($_link_href) ? $this->is_url_external($_link_href) : FALSE, - 'link_href' => $_link_href, // This could also be empty. + if(stripos($tag_frag['link_self_closing_tag'], 'rel') !== 0) + if(preg_match('/\srel\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['link_self_closing_tag'], $_m)) + $rel = $_m['value']; - 'style_open_tag' => isset($_tag_frag['style_open_tag']) ? $_tag_frag['style_open_tag'] : '', - 'style_css' => $_style_css, // This could also be empty. - 'style_closing_tag' => isset($_tag_frag['style_closing_tag']) ? $_tag_frag['style_closing_tag'] : '', + unset($_m); // Just a little housekeeping. - 'media' => ($_media) ? $_media : 'all', // Defaults to `all`. + if($type && stripos($type, 'css') === FALSE) + return FALSE; // Not CSS. - 'exclude' => FALSE // Default value. - ); - $_tag_frag_r = &$css_tag_frags[count($css_tag_frags) - 1]; + if($rel && stripos($rel, 'stylesheet') === FALSE) + return FALSE; // Not CSS. - if($_tag_frag_r['if_open_tag'] || $_tag_frag_r['if_closing_tag']) - $_tag_frag_r['exclude'] = TRUE; + return TRUE; // Yes, this is CSS. + } - else if($_tag_frag_r['link_href'] && $_tag_frag_r['link_href_external'] && isset($this->options['compress_combine_remote_css_js']) && !$this->options['compress_combine_remote_css_js']) - $_tag_frag_r['exclude'] = TRUE; + /** + * Test a tag fragment to see if it's CSS. + * + * @since 140921 Improving tag tests. + * + * @param array $tag_frag A tag fragment. + * + * @return boolean TRUE if it contains CSS. + */ + protected function is_style_tag_frag_css(array $tag_frag) + { + if(empty($tag_frag['style_open_tag']) || empty($tag_frag['style_closing_tag'])) + return FALSE; // Nope; missing open|closing tag. - else if($this->regex_css_exclusions && preg_match($this->regex_css_exclusions, $_tag_frag_r['link_href'].$_tag_frag_r['style_css'])) - $_tag_frag_r['exclude'] = TRUE; + $type = ''; // Initialize. - else if($this->built_in_regex_css_exclusions && preg_match($this->built_in_regex_css_exclusions, $_tag_frag_r['link_href'].$_tag_frag_r['style_css'])) - $_tag_frag_r['exclude'] = TRUE; - } - } - } - unset($_tag_frags, $_tag_frag, $_tag_frag_r, $_link_href, $_style_css, $_media); + if(stripos($tag_frag['script_open_tag'], 'type') !== 0) + if(preg_match('/\stype\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['script_open_tag'], $_m)) + $type = $_m['value']; - finale: // Target point; finale/return value. + unset($_m); // Just a little housekeeping. - if($benchmark && !empty($time) && $html_frag) - $this->benchmark_times[] = // Benchmark data. - array('function' => __FUNCTION__, // Function marker. - 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), - 'task' => sprintf('compiling CSS tag frags in checksum: `%1$s`', md5(serialize($html_frag)))); + if($type && stripos($type, 'css') === FALSE) + return FALSE; // Not CSS. - return $css_tag_frags; + return TRUE; // Yes, this is CSS. } /** - * Parses and return an array of JS tag fragments. + * Get a CSS link href value from a tag fragment. * * @since 140417 Initial release. * - * @param array $html_frag An HTML tag fragment array. - * - * @return array An array of JS tag fragments (ready to be converted into JS parts). - * Else an empty array (i.e. no JS tag fragments in the HTML fragment array). + * @param array $tag_frag A CSS tag fragment. + * @param boolean $test_for_css Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's CSS. * - * @see http://css-tricks.com/how-to-create-an-ie-only-stylesheet/ - * @see http://stackoverflow.com/a/12102131 + * @return string The link href value if possible; else an empty string. */ - protected function get_js_tag_frags(array $html_frag) + protected function get_link_css_href(array $tag_frag, $test_for_css = TRUE) { - $benchmark = !empty($this->options['benchmark']) - && $this->options['benchmark'] === 'details'; - if($benchmark) $time = microtime(TRUE); - - $js_tag_frags = array(); // Initialize. - - if(!$html_frag) goto finale; - - $regex = '/(?P'. // Entire match. - '(?P\<\![^[>]*?\[if\W[^\]]*?\][^>]*?\>\s*)?'. - '(?P\]*?)?\>)(?P.*?)(?P\<\/script\>)'. - '(?P\s*\<\![^[>]*?\[endif\][^>]*?\>)?'. - ')/is'; // Dot matches line breaks. - - if(!empty($html_frag['contents']) && preg_match_all($regex, $html_frag['contents'], $_tag_frags, PREG_SET_ORDER)) - { - foreach($_tag_frags as $_tag_frag) - { - $_script_src = $_script_js = $_script_async = ''; + if($test_for_css && !$this->is_link_tag_frag_css($tag_frag)) + return ''; // This tag does not contain CSS. - if(($_script_src = $this->get_script_js_src($_tag_frag)) || ($_script_js = $this->get_script_js($_tag_frag))) - $_script_async = $this->get_script_js_async($_tag_frag); - - if($_script_src || $_script_js) // One or the other is fine. - { - $js_tag_frags[] = array( - 'all' => $_tag_frag['all'], + if(preg_match('/\shref\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['link_self_closing_tag'], $_m)) + return trim($this->n_url_amps($_m['value'])); - 'if_open_tag' => isset($_tag_frag['if_open_tag']) ? $_tag_frag['if_open_tag'] : '', - 'if_closing_tag' => isset($_tag_frag['if_closing_tag']) ? $_tag_frag['if_closing_tag'] : '', + unset($_m); // Just a little housekeeping. - 'script_open_tag' => isset($_tag_frag['script_open_tag']) ? $_tag_frag['script_open_tag'] : '', - 'script_src_external' => ($_script_src) ? $this->is_url_external($_script_src) : FALSE, - 'script_src' => $_script_src, // This could also be empty. - 'script_js' => $_script_js, // This could also be empty. - 'script_async' => $_script_async, // This could also be empty. - 'script_closing_tag' => isset($_tag_frag['script_closing_tag']) ? $_tag_frag['script_closing_tag'] : '', + return ''; // Unable to find an `href` attribute value. + } - 'exclude' => FALSE // Default value. - ); - $_tag_frag_r = &$js_tag_frags[count($js_tag_frags) - 1]; + /** + * Get a CSS link media rule from a tag fragment. + * + * @since 140417 Initial release. + * + * @param array $tag_frag A CSS tag fragment. + * @param boolean $test_for_css Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's CSS. + * + * @return string The link media value if possible; else an empty string. + */ + protected function get_link_css_media(array $tag_frag, $test_for_css = TRUE) + { + if($test_for_css && !$this->is_link_tag_frag_css($tag_frag)) + return ''; // This tag does not contain CSS. - if($_tag_frag_r['if_open_tag'] || $_tag_frag_r['if_closing_tag'] || $_tag_frag_r['script_async']) - $_tag_frag_r['exclude'] = TRUE; + if(preg_match('/\smedia\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['link_self_closing_tag'], $_m)) + return trim(strtolower($_m['value'])); - else if($_tag_frag_r['script_src'] && $_tag_frag_r['script_src_external'] && isset($this->options['compress_combine_remote_css_js']) && !$this->options['compress_combine_remote_css_js']) - $_tag_frag_r['exclude'] = TRUE; + unset($_m); // Just a little housekeeping. - else if($this->regex_js_exclusions && preg_match($this->regex_js_exclusions, $_tag_frag_r['script_src'].$_tag_frag_r['script_js'])) - $_tag_frag_r['exclude'] = TRUE; + return ''; // Unable to find a `media` attribute value. + } - else if($this->built_in_regex_js_exclusions && preg_match($this->built_in_regex_js_exclusions, $_tag_frag_r['script_src'].$_tag_frag_r['script_js'])) - $_tag_frag_r['exclude'] = TRUE; - } - } - } - unset($_tag_frags, $_tag_frag, $_tag_frag_r, $_script_src, $_script_js, $_script_async); + /** + * Get a CSS style media rule from a tag fragment. + * + * @since 140417 Initial release. + * + * @param array $tag_frag A CSS tag fragment. + * @param boolean $test_for_css Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's CSS. + * + * @return string The style media value if possible; else an empty string. + */ + protected function get_style_css_media(array $tag_frag, $test_for_css = TRUE) + { + if($test_for_css && !$this->is_style_tag_frag_css($tag_frag)) + return ''; // This tag does not contain CSS. - finale: // Target point; finale/return value. + if(preg_match('/\smedia\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['style_open_tag'], $_m)) + return trim(strtolower($_m['value'])); - if($benchmark && !empty($time) && $html_frag) - $this->benchmark_times[] = // Benchmark data. - array('function' => __FUNCTION__, // Function marker. - 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), - 'task' => sprintf('compiling JS tag frags in checksum: `%1$s`', md5(serialize($html_frag)))); + unset($_m); // Just a little housekeeping. - return $js_tag_frags; + return ''; // Unable to find a `media` attribute value. } /** - * Construct a checksum for an array of tag fragments. + * Get style CSS from a CSS tag fragment. * * @since 140417 Initial release. * - * @note This routine purposely excludes any "exclusions" from the checksum. - * All that's important here is an exclusion's position in the array, - * not its fragmentation; it's excluded anyway. - * - * @param array $tag_frags Array of tag fragments. + * @param array $tag_frag A CSS tag fragment. + * @param boolean $test_for_css Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's CSS. * - * @return string MD5 checksum. + * @return string The style CSS code (if possible); else an empty string. */ - protected function get_tag_frags_checksum(array $tag_frags) + protected function get_style_css(array $tag_frag, $test_for_css = TRUE) { - foreach($tag_frags as &$_frag) // Exclude exclusions. - $_frag = ($_frag['exclude']) ? array('exclude' => TRUE) : $_frag; - unset($_frag); // A little housekeeping. + if(empty($tag_frag['style_css'])) // An obvious issue. + return ''; // Not possible; no CSS code. - return md5(serialize($tag_frags)); + if($test_for_css && !$this->is_style_tag_frag_css($tag_frag)) + return ''; // This tag does not contain CSS. + + return trim($tag_frag['style_css']); // CSS code. } /** @@ -1035,219 +878,513 @@ protected function move_special_css_at_rules_to_top($css, $___recursion = 0) if(!($css = (string)$css)) return $css; // Nothing to do. - $max_recursions = 2; // `preg_match_all()` calls. - if($___recursion >= $max_recursions) - return $css; // All done here. + $max_recursions = 2; // `preg_match_all()` calls. + if($___recursion >= $max_recursions) return $css; // All done. + + if(stripos($css, 'charset') === FALSE && stripos($css, 'import') === FALSE) + return $css; // Save some time. Nothing to do here. + + if(preg_match_all('/(?P@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?charset(?:\s+[^;]*?)?;)/i', $css, $rules, PREG_SET_ORDER) + || preg_match_all('/(?P@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import(?:\s+[^;]*?)?;)/i', $css, $rules, PREG_SET_ORDER) + ) // Searched in a specific order. Recursion dictates a precise order based on what we find in these regex patterns. + { + $top_rules = array(); // Initialize. + foreach($rules as $_rule) $top_rules[] = $_rule['rule']; + unset($_rule); // Just a little housekeeping. + + $css = $this->replace_once($top_rules, '', $css); + $css = $this->move_special_css_at_rules_to_top($css, $___recursion + 1); + $css = implode("\n\n", $top_rules)."\n\n".$css; + } + return $css; // With special `@rules` to the top. + } + + /** + * Resolves `@import` rules in CSS code recursively. + * + * @since 140417 Initial release. + * + * @param string $css CSS code. + * @param string $media Current media specification. + * @param boolean $___recursion Internal use only. + * + * @return string CSS code after all `@import` rules have been resolved recursively. + */ + protected function resolve_resolved_css_imports($css, $media, $___recursion = FALSE) + { + if(!($css = (string)$css)) + return $css; // Nothing to do. + + $media = $this->current_css_media = (string)$media; + if(!$media) $media = $this->current_css_media = 'all'; + + $import_media_without_url_regex = '/@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import\s*(["\'])(?P.+?)\\1(?P[^;]*?);/i'; + $import_media_with_url_regex = '/@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import\s+url\s*\(\s*(["\']?)(?P.+?)\\1\s*\)(?P[^;]*?);/i'; + + $css = preg_replace_callback($import_media_without_url_regex, array($this, '_resolve_resolved_css_imports_cb'), $css); + $css = preg_replace_callback($import_media_with_url_regex, array($this, '_resolve_resolved_css_imports_cb'), $css); + + if(preg_match_all($import_media_without_url_regex, $css, $_m)) + foreach($_m['media'] as $_media) if(!$_media || $_media === $this->current_css_media) + return $this->resolve_resolved_css_imports($css, $this->current_css_media, TRUE); // Recursive. + unset($_m, $_media); // Housekeeping. + + if(preg_match_all($import_media_with_url_regex, $css, $_m)) + foreach($_m['media'] as $_media) if(!$_media || $_media === $this->current_css_media) + return $this->resolve_resolved_css_imports($css, $this->current_css_media, TRUE); // Recursive. + unset($_m, $_media); // Housekeeping. + + return $css; + } + + /** + * Callback handler for resolving @ import rules. + * + * @since 140417 Initial release. + * + * @param array $m An array of regex matches. + * + * @return string CSS after import resolution, else an empty string. + */ + protected function _resolve_resolved_css_imports_cb(array $m) + { + if(empty($m['url'])) + return ''; // Nothing to resolve. + + if(!empty($m['media']) && $m['media'] !== $this->current_css_media) + return $m[0]; // Not possible; different media. + + if(($css = $this->remote($m['url']))) + $css = $this->resolve_css_relatives($css, $m['url']); + + return $css; + } + + /** + * Resolve relative URLs in CSS code. + * + * @since 140417 Initial release. + * + * @param string $css CSS code. + * @param string $base Optional. Base URL to calculate from. + * Defaults to the current HTTP location for the browser. + * + * @return string CSS code after having all URLs resolved. + */ + protected function resolve_css_relatives($css, $base = '') + { + if(!($css = (string)$css)) + return $css; // Nothing to do. + + $this->current_base = $base; // Make this available to callback handlers (possible empty string here). + + $import_without_url_regex = '/(?P@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import\s*)(?P["\'])(?P.+?)(?P\\2)/i'; + $any_url_regex = '/(?Purl\s*)(?P\(\s*)(?P["\']?)(?P.+?)(?P\\3)(?P\s*\))/i'; + + $css = preg_replace_callback($import_without_url_regex, array($this, '_resolve_css_relatives_import_cb'), $css); + $css = preg_replace_callback($any_url_regex, array($this, '_resolve_css_relatives_url_cb'), $css); + + return $css; + } + + /** + * Callback handler for CSS relative URL resolutions. + * + * @since 140417 Initial release. + * + * @param array $m An array of regex matches. + * + * @return string CSS `@import` rule with relative URL resolved. + */ + protected function _resolve_css_relatives_import_cb(array $m) + { + return $m['import'].$m['open_encap'].$this->resolve_relative_url($m['url'], $this->current_base).$m['close_encap']; + } + + /** + * Callback handler for CSS relative URL resolutions. + * + * @since 140417 Initial release. + * + * @param array $m An array of regex matches. + * + * @return string CSS `url()` resource with relative URL resolved. + */ + protected function _resolve_css_relatives_url_cb(array $m) + { + if(stripos($m['url'], 'data:') === 0) + return $m[0]; // Don't resolve `data:` URIs. + + return $m['url_'].$m['open_bracket'].$m['open_encap'].$this->resolve_relative_url($m['url'], $this->current_base).$m['close_encap'].$m['close_bracket']; + } + + /********************************************************************************************************/ + + /* + * JS-Related Methods + * ~ See also: JS Compression Utilities + */ + + /** + * Handles possible compression of head JS. + * + * @since 140417 Initial release. + * + * @param string $html Input HTML code. + * + * @return string HTML code, after possible JS compression. + */ + protected function maybe_compress_combine_head_js($html) + { + $benchmark = !empty($this->options['benchmark']) + && $this->options['benchmark'] === 'details'; + if($benchmark) $time = microtime(TRUE); + + $html = (string)$html; // Force string value. + + if(isset($this->options['compress_combine_head_js'])) + if(!$this->options['compress_combine_head_js']) + $disabled = TRUE; // Disabled flag. + + if(!$html || !empty($disabled)) goto finale; // Nothing to do. + + if(($head_frag = $this->get_head_frag($html)) /* No need to get the HTML frag here; we're operating on the `` only. */) + if(($js_tag_frags = $this->get_js_tag_frags($head_frag)) && ($js_parts = $this->compile_js_tag_frags_into_parts($js_tag_frags))) + { + $js_tag_frags_all_compiled = $this->compile_key_elements_deep($js_tag_frags, 'all'); + $html = $this->replace_once($head_frag['all'], '%%htmlc-head%%', $html); + $cleaned_head_contents = $this->replace_once($js_tag_frags_all_compiled, '', $head_frag['contents']); + $cleaned_head_contents = $this->cleanup_self_closing_html_tag_lines($cleaned_head_contents); + + $compressed_js_tags = array(); // Initialize. + + foreach($js_parts as $_js_part) + { + if(isset($_js_part['exclude_frag'], $js_tag_frags[$_js_part['exclude_frag']]['all'])) + $compressed_js_tags[] = $js_tag_frags[$_js_part['exclude_frag']]['all']; + else $compressed_js_tags[] = $_js_part['tag']; + } + unset($_js_part); // Housekeeping. + + $compressed_js_tags = implode("\n", $compressed_js_tags); + $compressed_head_parts = array($head_frag['open_tag'], $cleaned_head_contents, $compressed_js_tags, $head_frag['closing_tag']); + $html = $this->replace_once('%%htmlc-head%%', implode("\n", $compressed_head_parts), $html); + } + finale: // Target point; finale/return value. + + if($html) $html = trim($html); + + if($benchmark && !empty($time) && $html && empty($disabled)) + $this->benchmark_times[] = // Benchmark data. + array('function' => __FUNCTION__, // Function marker. + 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), + 'task' => sprintf('compressing/combining head JS in checksum: `%1$s`', md5($html))); + + return $html; // With possible compression having been applied here. + } + + /** + * Handles possible compression of footer JS. + * + * @since 140417 Initial release. + * + * @param string $html Input HTML code. + * + * @return string HTML code, after possible JS compression. + */ + protected function maybe_compress_combine_footer_js($html) + { + $benchmark = !empty($this->options['benchmark']) + && $this->options['benchmark'] === 'details'; + if($benchmark) $time = microtime(TRUE); + + $html = (string)$html; // Force string value. + + if(isset($this->options['compress_combine_footer_js'])) + if(!$this->options['compress_combine_footer_js']) + $disabled = TRUE; // Disabled flag. + + if(!$html || !empty($disabled)) goto finale; // Nothing to do. + + if(($footer_scripts_frag = $this->get_footer_scripts_frag($html)) /* e.g. */) + if(($js_tag_frags = $this->get_js_tag_frags($footer_scripts_frag)) && ($js_parts = $this->compile_js_tag_frags_into_parts($js_tag_frags))) + { + $js_tag_frags_all_compiled = $this->compile_key_elements_deep($js_tag_frags, 'all'); + $html = $this->replace_once($footer_scripts_frag['all'], '%%htmlc-footer-scripts%%', $html); + $cleaned_footer_scripts = $this->replace_once($js_tag_frags_all_compiled, '', $footer_scripts_frag['contents']); + + $compressed_js_tags = array(); // Initialize. + + foreach($js_parts as $_js_part) + { + if(isset($_js_part['exclude_frag'], $js_tag_frags[$_js_part['exclude_frag']]['all'])) + $compressed_js_tags[] = $js_tag_frags[$_js_part['exclude_frag']]['all']; + else $compressed_js_tags[] = $_js_part['tag']; + } + unset($_js_part); // Housekeeping. + + $compressed_js_tags = implode("\n", $compressed_js_tags); + $compressed_footer_script_parts = array($footer_scripts_frag['open_tag'], $cleaned_footer_scripts, $compressed_js_tags, $footer_scripts_frag['closing_tag']); + $html = $this->replace_once('%%htmlc-footer-scripts%%', implode("\n", $compressed_footer_script_parts), $html); + } + finale: // Target point; finale/return value. + + if($html) $html = trim($html); + + if($benchmark && !empty($time) && $html && empty($disabled)) + $this->benchmark_times[] = // Benchmark data. + array('function' => __FUNCTION__, // Function marker. + 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), + 'task' => sprintf('compressing/combining footer JS in checksum: `%1$s`', md5($html))); + + return $html; // With possible compression having been applied here. + } + + /** + * Compiles JS tag fragments into JS parts with compression. + * + * @since 140417 Initial release. + * + * @param array $js_tag_frags JS tag fragments. + * + * @return array Array of JS parts, else an empty array on failure. + * + * @throws \exception If unable to cache JS parts. + */ + protected function compile_js_tag_frags_into_parts(array $js_tag_frags) + { + $benchmark = !empty($this->options['benchmark']) + && $this->options['benchmark'] === 'details'; + if($benchmark) $time = microtime(TRUE); + + $js_parts = array(); // Initialize. + $js_parts_checksum = ''; // Initialize. + + if(!$js_tag_frags) goto finale; + + $js_parts_checksum = $this->get_tag_frags_checksum($js_tag_frags); + $public_cache_dir = $this->cache_dir($this::dir_public_type, $js_parts_checksum); + $private_cache_dir = $this->cache_dir($this::dir_private_type, $js_parts_checksum); + $public_cache_dir_url = $this->cache_dir_url($this::dir_public_type, $js_parts_checksum); + + $cache_parts_file = $js_parts_checksum.'-compressor-parts.js-cache'; + $cache_parts_file_path = $private_cache_dir.'/'.$cache_parts_file; + $cache_parts_file_path_tmp = $cache_parts_file_path.'.'.uniqid('', TRUE).'.tmp'; + // Cache file creation is atomic; i.e. tmp file w/ rename. + + $cache_part_file = '%%code-checksum%%-compressor-part.js'; + $cache_part_file_path = $public_cache_dir.'/'.$cache_part_file; + $cache_part_file_url = $public_cache_dir_url.'/'.$cache_part_file; + + if(is_file($cache_parts_file_path) && filemtime($cache_parts_file_path) > strtotime('-'.$this->cache_expiration_time)) + if(is_array($cached_parts = unserialize(file_get_contents($cache_parts_file_path)))) + { + $js_parts = $cached_parts; // Use cached parts. + goto finale; // Using the cache; we're all done here. + } + $_js_part = 0; // Initialize part counter. + + foreach($js_tag_frags as $_js_tag_frag_pos => $_js_tag_frag) + { + if($_js_tag_frag['exclude']) + { + if($_js_tag_frag['script_src'] || $_js_tag_frag['script_js']) + { + if($js_parts) $_js_part++; // Starts new part. + + $js_parts[$_js_part]['tag'] = ''; + $js_parts[$_js_part]['exclude_frag'] = $_js_tag_frag_pos; + + $_js_part++; // Always indicates a new part in the next iteration. + } + } + else if($_js_tag_frag['script_src']) + { + if(($_js_tag_frag['script_src'] = $this->resolve_relative_url($_js_tag_frag['script_src']))) + if(($_js_code = $this->remote($_js_tag_frag['script_src']))) + { + $_js_code = rtrim($_js_code, ';').';'; + + if($_js_code) // Now, DO we have something here? + { + if(!empty($js_parts[$_js_part]['code'])) + $js_parts[$_js_part]['code'] .= "\n\n".$_js_code; + else $js_parts[$_js_part]['code'] = $_js_code; + } + } + } + else if($_js_tag_frag['script_js']) + { + $_js_code = $_js_tag_frag['script_js']; + $_js_code = rtrim($_js_code, ';').';'; + + if($_js_code) // Now, DO we have something here? + { + if(!empty($js_parts[$_js_part]['code'])) + $js_parts[$_js_part]['code'] .= "\n\n".$_js_code; + else $js_parts[$_js_part]['code'] = $_js_code; + } + } + } + unset($_js_part, $_js_tag_frag_pos, $_js_tag_frag, $_js_code); + + foreach(array_keys($js_parts = array_values($js_parts)) as $_js_part) + { + if(!empty($js_parts[$_js_part]['code'])) + { + $_js_code = $js_parts[$_js_part]['code']; + $_js_code_cs = md5($_js_code); // Before compression. + $_js_code = $this->maybe_compress_js_code($_js_code); + + $_js_code_path = str_replace('%%code-checksum%%', $_js_code_cs, $cache_part_file_path); + $_js_code_url = str_replace('%%code-checksum%%', $_js_code_cs, $cache_part_file_url); + $_js_code_path_tmp = $_js_code_path.'.'.uniqid('', TRUE).'.tmp'; + // Cache file creation is atomic; e.g. tmp file w/ rename. + + if(!(file_put_contents($_js_code_path_tmp, $_js_code) && rename($_js_code_path_tmp, $_js_code_path))) + throw new \exception(sprintf('Unable to cache JS code file: `%1$s`.', $_js_code_path)); + + $js_parts[$_js_part]['tag'] = ''; + + unset($js_parts[$_js_part]['code']); // Ditch this; no need to cache this code too. + } + } + unset($_js_part, $_js_code, $_js_code_cs, $_js_code_path, $_js_code_path_tmp, $_js_code_url); - if(stripos($css, 'charset') === FALSE && stripos($css, 'import') === FALSE) - return $css; // Save some time. Nothing to do here. + if(!(file_put_contents($cache_parts_file_path_tmp, serialize($js_parts)) && rename($cache_parts_file_path_tmp, $cache_parts_file_path))) + throw new \exception(sprintf('Unable to cache JS parts into: `%1$s`.', $cache_parts_file_path)); - if(preg_match_all('/(?P@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?charset(?:\s+[^;]*?)?;)/i', $css, $rules, PREG_SET_ORDER) - || preg_match_all('/(?P@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import(?:\s+[^;]*?)?;)/i', $css, $rules, PREG_SET_ORDER) - ) // Searched in a specific order. Recursion dictates a precise order based on what we find in these regex patterns. - { - $top_rules = array(); // Initialize. + finale: // Target point; finale/return value. - foreach($rules as $_rule) - $top_rules[] = $_rule['rule']; - unset($_rule); // Housekeeping. + if($benchmark && !empty($time) && $js_parts_checksum) + $this->benchmark_times[] = // Benchmark data. + array('function' => __FUNCTION__, // Function marker. + 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), + 'task' => sprintf('building parts based on JS tag frags in checksum: `%1$s`', $js_parts_checksum)); - $css = $this->replace_once($top_rules, '', $css); - $css = $this->move_special_css_at_rules_to_top($css, $___recursion + 1); - $css = implode("\n\n", $top_rules)."\n\n".$css; - } - return $css; + return $js_parts; } /** - * Resolves `@import` rules in CSS code recursively. + * Parses and return an array of JS tag fragments. * * @since 140417 Initial release. * - * @param string $css CSS code. - * @param string $media Current media specification. - * @param boolean $___recursion Internal use only. + * @param array $html_frag An HTML tag fragment array. * - * @return string CSS code after all `@import` rules have been resolved recursively. + * @return array An array of JS tag fragments (ready to be converted into JS parts). + * Else an empty array (i.e. no JS tag fragments in the HTML fragment array). + * + * @see http://css-tricks.com/how-to-create-an-ie-only-stylesheet/ + * @see http://stackoverflow.com/a/12102131 */ - protected function resolve_resolved_css_imports($css, $media, $___recursion = FALSE) + protected function get_js_tag_frags(array $html_frag) { - if(!($css = (string)$css)) - return $css; // Nothing to do. + $benchmark = !empty($this->options['benchmark']) + && $this->options['benchmark'] === 'details'; + if($benchmark) $time = microtime(TRUE); - $media = $this->current_css_media = (string)$media; - if(!$media) $media = $this->current_css_media = 'all'; + $js_tag_frags = array(); // Initialize. - $import_media_without_url_regex = '/@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import\s*(["\'])(?P.+?)\\1(?P[^;]*?);/i'; - $import_media_with_url_regex = '/@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import\s+url\s*\(\s*(["\']?)(?P.+?)\\1\s*\)(?P[^;]*?);/i'; + if(!$html_frag) goto finale; - $css = preg_replace_callback($import_media_without_url_regex, array($this, '_resolve_resolved_css_imports_cb'), $css); - $css = preg_replace_callback($import_media_with_url_regex, array($this, '_resolve_resolved_css_imports_cb'), $css); + $regex = '/(?P'. // Entire match. + '(?P\<\![^[>]*?\[if\W[^\]]*?\][^>]*?\>\s*)?'. + '(?P\]*?)?\>)(?P.*?)(?P\<\/script\>)'. + '(?P\s*\<\![^[>]*?\[endif\][^>]*?\>)?'. + ')/is'; // Dot matches line breaks. - if(preg_match_all($import_media_without_url_regex, $css, $_m)) - foreach($_m['media'] as $_media) if(!$_media || $_media === $this->current_css_media) - return $this->resolve_resolved_css_imports($css, $this->current_css_media, TRUE); // Recursive. - unset($_m, $_media); // Housekeeping. + if(!empty($html_frag['contents']) && preg_match_all($regex, $html_frag['contents'], $_tag_frags, PREG_SET_ORDER)) + { + foreach($_tag_frags as $_tag_frag) + { + $_script_src = $_script_js = $_script_async = ''; // Initialize. - if(preg_match_all($import_media_with_url_regex, $css, $_m)) - foreach($_m['media'] as $_media) if(!$_media || $_media === $this->current_css_media) - return $this->resolve_resolved_css_imports($css, $this->current_css_media, TRUE); // Recursive. - unset($_m, $_media); // Housekeeping. + if($this->is_script_tag_frag_js($_tag_frag)) // Check this first (for optimization). + if(($_script_src = $this->get_script_js_src($_tag_frag, FALSE)) || ($_script_js = $this->get_script_js($_tag_frag, FALSE))) + $_script_async = $this->get_script_js_async($_tag_frag, FALSE); - return $css; - } + if($_script_src || $_script_js) // One or the other is fine. + { + $js_tag_frags[] = array( + 'all' => $_tag_frag['all'], - /** - * Callback handler for resolving @ import rules. - * - * @since 140417 Initial release. - * - * @param array $m An array of regex matches. - * - * @return string CSS after import resolution, else an empty string. - */ - protected function _resolve_resolved_css_imports_cb(array $m) - { - if(empty($m['url'])) - return ''; // Nothing to resolve. + 'if_open_tag' => isset($_tag_frag['if_open_tag']) ? $_tag_frag['if_open_tag'] : '', + 'if_closing_tag' => isset($_tag_frag['if_closing_tag']) ? $_tag_frag['if_closing_tag'] : '', - if(!empty($m['media']) && $m['media'] !== $this->current_css_media) - return $m[0]; // Not possible; different media. + 'script_open_tag' => isset($_tag_frag['script_open_tag']) ? $_tag_frag['script_open_tag'] : '', + 'script_src_external' => ($_script_src) ? $this->is_url_external($_script_src) : FALSE, + 'script_src' => $_script_src, // This could also be empty. + 'script_js' => $_script_js, // This could also be empty. + 'script_async' => $_script_async, // This could also be empty. + 'script_closing_tag' => isset($_tag_frag['script_closing_tag']) ? $_tag_frag['script_closing_tag'] : '', - if(($css = $this->remote($m['url']))) - $css = $this->resolve_css_relatives($css, $m['url']); + 'exclude' => FALSE // Default value. + ); + $_tag_frag_r = &$js_tag_frags[count($js_tag_frags) - 1]; - return $css; - } + if($_tag_frag_r['if_open_tag'] || $_tag_frag_r['if_closing_tag'] || $_tag_frag_r['script_async']) + $_tag_frag_r['exclude'] = TRUE; - /** - * Resolve relative URLs in CSS code. - * - * @since 140417 Initial release. - * - * @param string $css CSS code. - * @param string $base Optional. Base URL to calculate from. - * Defaults to the current HTTP location for the browser. - * - * @return string CSS code after having all URLs resolved. - */ - protected function resolve_css_relatives($css, $base = '') - { - if(!($css = (string)$css)) - return $css; // Nothing to do. + else if($_tag_frag_r['script_src'] && $_tag_frag_r['script_src_external'] && isset($this->options['compress_combine_remote_css_js']) && !$this->options['compress_combine_remote_css_js']) + $_tag_frag_r['exclude'] = TRUE; - $this->current_base = $base; // Make this available to callback handlers (possible empty string here). + else if($this->regex_js_exclusions && preg_match($this->regex_js_exclusions, $_tag_frag_r['script_src'].$_tag_frag_r['script_js'])) + $_tag_frag_r['exclude'] = TRUE; - $import_without_url_regex = '/(?P@(?:\-(?:'.$this->regex_vendor_css_prefixes.')\-)?import\s*)(?P["\'])(?P.+?)(?P\\2)/i'; - $any_url_regex = '/(?Purl\s*)(?P\(\s*)(?P["\']?)(?P.+?)(?P\\3)(?P\s*\))/i'; + else if($this->built_in_regex_js_exclusions && preg_match($this->built_in_regex_js_exclusions, $_tag_frag_r['script_src'].$_tag_frag_r['script_js'])) + $_tag_frag_r['exclude'] = TRUE; + } + } + } + unset($_tag_frags, $_tag_frag, $_tag_frag_r, $_script_src, $_script_js, $_script_async); - $css = preg_replace_callback($import_without_url_regex, array($this, '_resolve_css_relatives_import_cb'), $css); - $css = preg_replace_callback($any_url_regex, array($this, '_resolve_css_relatives_url_cb'), $css); + finale: // Target point; finale/return value. - return $css; - } + if($benchmark && !empty($time) && $html_frag) + $this->benchmark_times[] = // Benchmark data. + array('function' => __FUNCTION__, // Function marker. + 'time' => number_format(microtime(TRUE) - $time, 5, '.', ''), + 'task' => sprintf('compiling JS tag frags in checksum: `%1$s`', md5(serialize($html_frag)))); - /** - * Callback handler for CSS relative URL resolutions. - * - * @since 140417 Initial release. - * - * @param array $m An array of regex matches. - * - * @return string CSS `@import` rule with relative URL resolved. - */ - protected function _resolve_css_relatives_import_cb(array $m) - { - return $m['import'].$m['open_encap'].$this->resolve_relative_url($m['url'], $this->current_base).$m['close_encap']; + return $js_tag_frags; } /** - * Callback handler for CSS relative URL resolutions. + * Test a script tag fragment to see if it's JavaScript. * - * @since 140417 Initial release. + * @since 140921 Initial release. * - * @param array $m An array of regex matches. + * @param array $tag_frag A JS tag fragment. * - * @return string CSS `url()` resource with relative URL resolved. + * @return boolean TRUE if it contains JavaScript. */ - protected function _resolve_css_relatives_url_cb(array $m) + protected function is_script_tag_frag_js(array $tag_frag) { - if(stripos($m['url'], 'data:') === 0) - return $m[0]; // Don't resolve `data:` URIs. - - return $m['url_'].$m['open_bracket'].$m['open_encap'].$this->resolve_relative_url($m['url'], $this->current_base).$m['close_encap'].$m['close_bracket']; - } + if(empty($tag_frag['script_open_tag']) || empty($tag_frag['script_closing_tag'])) + return FALSE; // Nope; missing open|closing tag. - /** - * Get a CSS link href value from a tag fragment. - * - * @since 140417 Initial release. - * - * @param array $tag_frag A CSS tag fragment. - * - * @return string The link href value if possible; else an empty string. - */ - protected function get_link_css_href(array $tag_frag) - { - if(!empty($tag_frag['link_self_closing_tag']) && preg_match('/type\s*\=\s*(["\'])text\/css\\1|rel\s*=\s*(["\'])stylesheet\\2/i', $tag_frag['link_self_closing_tag'])) - if(preg_match('/\s+href\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['link_self_closing_tag'], $href) && ($link_css_href = trim($this->n_url_amps($href['value'])))) - return $link_css_href; + $type = $language = ''; // Initialize. - return ''; - } + if(stripos($tag_frag['script_open_tag'], 'type') !== 0) + if(preg_match('/\stype\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['script_open_tag'], $_m)) + $type = $_m['value']; - /** - * Get a CSS link media rule from a tag fragment. - * - * @since 140417 Initial release. - * - * @param array $tag_frag A CSS tag fragment. - * - * @return string The link media value if possible; else a default value of `all`. - */ - protected function get_link_css_media(array $tag_frag) - { - if(!empty($tag_frag['link_self_closing_tag']) && preg_match('/type\s*\=\s*(["\'])text\/css\\1|rel\s*=\s*(["\'])stylesheet\\2/i', $tag_frag['link_self_closing_tag'])) - if(preg_match('/\s+media\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['link_self_closing_tag'], $media) && ($link_css_media = trim($media['value']))) - return $link_css_media; + unset($_m); // Just a little housekeeping. - return 'all'; - } + if(stripos($tag_frag['script_open_tag'], 'language') !== 0) + if(preg_match('/\slanguage\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['script_open_tag'], $_m)) + $language = $_m['value']; - /** - * Get a CSS style media rule from a tag fragment. - * - * @since 140417 Initial release. - * - * @param array $tag_frag A CSS tag fragment. - * - * @return string The style media value if possible; else a default value of `all`. - */ - protected function get_style_css_media(array $tag_frag) - { - if(!empty($tag_frag['style_open_tag']) && !empty($tag_frag['style_closing_tag']) && preg_match('/\|type\s*\=\s*(["\'])text\/css\\1/i', $tag_frag['style_open_tag'])) - if(preg_match('/\s+media\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['style_open_tag'], $media) && ($style_css_media = trim($media['value']))) - return $style_css_media; + unset($_m); // Just a little housekeeping. - return 'all'; - } + if($type && stripos($type, 'javascript') === FALSE) + return FALSE; // Not JavaScript. - /** - * Get style CSS from a CSS tag fragment. - * - * @since 140417 Initial release. - * - * @param array $tag_frag A CSS tag fragment. - * - * @return string The style CSS code (if possible); else an empty string. - */ - protected function get_style_css(array $tag_frag) - { - if(!empty($tag_frag['style_open_tag']) && !empty($tag_frag['style_closing_tag']) && preg_match('/\|type\s*\=\s*(["\'])text\/css\\1/i', $tag_frag['style_open_tag'])) - if(!empty($tag_frag['style_css']) && ($style_css = trim($tag_frag['style_css']))) - return $style_css; + if($language && stripos($language, 'javascript') === FALSE) + return FALSE; // Not JavaScript. - return ''; + return TRUE; // Yes, this is JavaScript. } /** @@ -1255,17 +1392,23 @@ protected function get_style_css(array $tag_frag) * * @since 140417 Initial release. * - * @param array $tag_frag A JS tag fragment. + * @param array $tag_frag A JS tag fragment. + * @param boolean $test_for_js Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's JavaScript. * * @return string The script JS src value (if possible); else an empty string. */ - protected function get_script_js_src(array $tag_frag) + protected function get_script_js_src(array $tag_frag, $test_for_js = TRUE) { - if(!empty($tag_frag['script_open_tag']) && !empty($tag_frag['script_closing_tag']) && preg_match('/\|type\s*\=\s*(["\'])(?:text\/javascript|application\/(?:x\-)?javascript)\\1|language\s*\=\s*(["\'])javascript\\2/i', $tag_frag['script_open_tag'])) - if(preg_match('/\s+src\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['script_open_tag'], $src) && ($script_js_src = trim($this->n_url_amps($src['value'])))) - return $script_js_src; + if($test_for_js && !$this->is_script_tag_frag_js($tag_frag)) + return ''; // This script tag does not contain JavaScript. - return ''; + if(preg_match('/\ssrc\s*\=\s*(["\'])(?P.+?)\\1/i', $tag_frag['script_open_tag'], $_m)) + return trim($this->n_url_amps($_m['value'])); + + unset($_m); // Just a little housekeeping. + + return ''; // Unable to find an `src` attribute value. } /** @@ -1273,17 +1416,23 @@ protected function get_script_js_src(array $tag_frag) * * @since 140417 Initial release. * - * @param array $tag_frag A JS tag fragment. + * @param array $tag_frag A JS tag fragment. + * @param boolean $test_for_js Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's JavaScript. * * @return string The script JS async|defer value (if possible); else an empty string. */ - protected function get_script_js_async(array $tag_frag) + protected function get_script_js_async(array $tag_frag, $test_for_js = TRUE) { - if(!empty($tag_frag['script_open_tag']) && !empty($tag_frag['script_closing_tag']) && preg_match('/\|type\s*\=\s*(["\'])(?:text\/javascript|application\/(?:x\-)?javascript)\\1|language\s*\=\s*(["\'])javascript\\2/i', $tag_frag['script_open_tag'])) - if(preg_match('/\s+(?:async|defer)(?:\>|\s*\=\s*(["\'])(?P[^"\']*?)\\1|\s+)?/i', $tag_frag['script_open_tag'], $async) && (empty($async['value']) || in_array(strtolower($async), array('1', 'on', 'yes', 'true', 'defer', 'async'), TRUE)) && ($script_js_async = 'async')) - return $script_js_async; + if($test_for_js && !$this->is_script_tag_frag_js($tag_frag)) + return ''; // This script tag does not contain JavaScript. + + if(preg_match('/\s(?:async|defer)(?:\>|\s+[^=]|\s*\=\s*(["\'])(?:1|on|yes|true|async|defer)\\1)/i', $tag_frag['script_open_tag'], $_m)) + return 'async'; // Yes, load this asynchronously. - return ''; + unset($_m); // Just a little housekeeping. + + return ''; // Unable to find a TRUE `async|defer` attribute. } /** @@ -1291,19 +1440,29 @@ protected function get_script_js_async(array $tag_frag) * * @since 140417 Initial release. * - * @param array $tag_frag A JS tag fragment. + * @param array $tag_frag A JS tag fragment. + * @param boolean $test_for_js Defaults to a TRUE value. + * If TRUE, we will test tag fragment to make sure it's JavaScript. * * @return string The script JS code (if possible); else an empty string. */ - protected function get_script_js(array $tag_frag) + protected function get_script_js(array $tag_frag, $test_for_js = TRUE) { - if(!empty($tag_frag['script_open_tag']) && !empty($tag_frag['script_closing_tag']) && preg_match('/\|type\s*\=\s*(["\'])(?:text\/javascript|application\/(?:x\-)?javascript)\\1|language\s*\=\s*(["\'])javascript\\2/i', $tag_frag['script_open_tag'])) - if(!empty($tag_frag['script_js']) && ($script_js = trim($tag_frag['script_js']))) - return $script_js; + if(empty($tag_frag['script_js'])) // An obvious issue. + return ''; // Not possible; no JavaScript code. - return ''; + if($test_for_js && !$this->is_script_tag_frag_js($tag_frag)) + return ''; // This script tag does not contain JavaScript. + + return trim($tag_frag['script_js']); // JavaScript code. } + /********************************************************************************************************/ + + /* + * Frag-Related Utilities + */ + /** * Build an HTML fragment from HTML source code. * @@ -1318,7 +1477,7 @@ protected function get_html_frag($html) if(!($html = (string)$html)) return array(); // Nothing to do. - if(preg_match('/(?P(?P\]*?)?\>)(?P.*?)(?P\<\/html\>))/is', $html, $html_frag)) + if(preg_match('/(?P(?P\]*?)?\>)(?P.*?)(?P\<\/html\>))/is', $html, $html_frag)) return $this->remove_numeric_keys_deep($html_frag); return array(); @@ -1338,7 +1497,7 @@ protected function get_head_frag($html) if(!($html = (string)$html)) return array(); // Nothing to do. - if(preg_match('/(?P(?P\]*?)?\>)(?P.*?)(?P\<\/head\>))/is', $html, $head_frag)) + if(preg_match('/(?P(?P\]*?)?\>)(?P.*?)(?P\<\/head\>))/is', $html, $head_frag)) return $this->remove_numeric_keys_deep($head_frag); return array(); @@ -1365,20 +1524,25 @@ protected function get_footer_scripts_frag($html) } /** - * Cleans up self-closing HTML tag lines. + * Construct a checksum for an array of tag fragments. * * @since 140417 Initial release. * - * @param string $html Self-closing HTML tag lines. + * @note This routine purposely excludes any "exclusions" from the checksum. + * All that's important here is an exclusion's position in the array, + * not its fragmentation; it's excluded anyway. * - * @return string Cleaned self-closing HTML tag lines. + * @param array $tag_frags Array of tag fragments. + * + * @return string MD5 checksum. */ - protected function cleanup_self_closing_html_tag_lines($html) + protected function get_tag_frags_checksum(array $tag_frags) { - if(!($html = (string)$html)) - return $html; // Nothing to do. + foreach($tag_frags as &$_frag) // Exclude exclusions. + $_frag = ($_frag['exclude']) ? array('exclude' => TRUE) : $_frag; + unset($_frag); // A little housekeeping. - return trim(preg_replace('/\>\s*?'."[\r\n]+".'\s*\\n<", $html)); + return md5(serialize($tag_frags)); } /********************************************************************************************************/ @@ -1444,7 +1608,7 @@ protected function compress_html($html) if(!($html = (string)$html)) return $html; // Nothing to do. - $static =& static::$cache[__FUNCTION__]; + $static =& static::$static[__FUNCTION__]; if(!isset($static['preservations'], $static['compressions'], $static['compress_with'])) { @@ -1551,7 +1715,7 @@ protected function compress_css($css) if(!($css = (string)$css)) return $css; // Nothing to do. - $static =& static::$cache[__FUNCTION__]; + $static =& static::$static[__FUNCTION__]; if(!isset($static['replace'], $static['with'], $static['colors'])) { @@ -2019,6 +2183,23 @@ protected function esc_refs($string, $times = 1) return $this->esc_refs_deep((string)$string, $times); } + /** + * Cleans up self-closing HTML tag lines. + * + * @since 140417 Initial release. + * + * @param string $html Self-closing HTML tag lines. + * + * @return string Cleaned self-closing HTML tag lines. + */ + protected function cleanup_self_closing_html_tag_lines($html) + { + if(!($html = (string)$html)) + return $html; // Nothing to do. + + return trim(preg_replace('/\>\s*?'."[\r\n]+".'\s*\\n<", $html)); + } + /********************************************************************************************************/ /* @@ -2073,8 +2254,8 @@ protected function cache_dir($type, $checksum = '', $base_only = FALSE) $cache_key = $type.$checksum.(integer)$base_only; - if(isset($this->icache[__FUNCTION__.'_'.$cache_key])) - return $this->icache[__FUNCTION__.'_'.$cache_key]; + if(isset($this->cache[__FUNCTION__.'_'.$cache_key])) + return $this->cache[__FUNCTION__.'_'.$cache_key]; if(!empty($this->options[__FUNCTION__.'_'.$type])) $basedir = $this->n_dir_seps($this->options[__FUNCTION__.'_'.$type]); @@ -2102,7 +2283,7 @@ protected function cache_dir($type, $checksum = '', $base_only = FALSE) if(!is_readable($dir) || !is_writable($dir)) // Must have this directory; and it MUST be readable/writable. throw new \exception(sprintf('Cache directory not readable/writable: `%1$s`. Failed on `%2$s`.', $basedir, $dir)); - return ($this->icache[__FUNCTION__.'_'.$cache_key] = $dir); + return ($this->cache[__FUNCTION__.'_'.$cache_key] = $dir); } /** @@ -2134,8 +2315,8 @@ protected function cache_dir_url($type, $checksum = '', $base_only = FALSE) $cache_key = $type.$checksum.(integer)$base_only; - if(isset($this->icache[__FUNCTION__.'_'.$cache_key])) - return $this->icache[__FUNCTION__.'_'.$cache_key]; + if(isset($this->cache[__FUNCTION__.'_'.$cache_key])) + return $this->cache[__FUNCTION__.'_'.$cache_key]; $basedir = $this->cache_dir($type, '', TRUE); @@ -2160,7 +2341,7 @@ protected function cache_dir_url($type, $checksum = '', $base_only = FALSE) $url .= '/'.trim(preg_replace('/[^a-z0-9\-]/i', '-', $this->current_url_host()), '-'); $url .= ($checksum) ? '/'.implode('/', str_split($checksum)) : ''; } - return ($this->icache[__FUNCTION__.'_'.$cache_key] = $url); + return ($this->cache[__FUNCTION__.'_'.$cache_key] = $url); } /** @@ -2362,22 +2543,22 @@ protected function n_dir_seps($dir_file, $allow_trailing_slash = FALSE) */ protected function current_url_ssl() { - if(isset(static::$cache[__FUNCTION__])) - return static::$cache[__FUNCTION__]; + if(isset(static::$static[__FUNCTION__])) + return static::$static[__FUNCTION__]; if(!empty($_SERVER['SERVER_PORT'])) if($_SERVER['SERVER_PORT'] === '443') - return (static::$cache[__FUNCTION__] = TRUE); + return (static::$static[__FUNCTION__] = TRUE); if(!empty($_SERVER['HTTPS'])) if($_SERVER['HTTPS'] === '1' || strcasecmp($_SERVER['HTTPS'], 'on') === 0) - return (static::$cache[__FUNCTION__] = TRUE); + return (static::$static[__FUNCTION__] = TRUE); if(!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) if(strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0) - return (static::$cache[__FUNCTION__] = TRUE); + return (static::$static[__FUNCTION__] = TRUE); - return (static::$cache[__FUNCTION__] = FALSE); + return (static::$static[__FUNCTION__] = FALSE); } /** @@ -2391,16 +2572,16 @@ protected function current_url_ssl() */ protected function current_url_scheme() { - if(isset(static::$cache[__FUNCTION__])) - return static::$cache[__FUNCTION__]; + if(isset(static::$static[__FUNCTION__])) + return static::$static[__FUNCTION__]; if(!empty($this->options[__FUNCTION__])) // Defined explicity? - return (static::$cache[__FUNCTION__] = $this->n_url_scheme($this->options[__FUNCTION__])); + return (static::$static[__FUNCTION__] = $this->n_url_scheme($this->options[__FUNCTION__])); if(!empty($_SERVER['REQUEST_SCHEME'])) - return (static::$cache[__FUNCTION__] = $this->n_url_scheme($_SERVER['REQUEST_SCHEME'])); + return (static::$static[__FUNCTION__] = $this->n_url_scheme($_SERVER['REQUEST_SCHEME'])); - return (static::$cache[__FUNCTION__] = ($this->current_url_ssl()) ? 'https' : 'http'); + return (static::$static[__FUNCTION__] = ($this->current_url_ssl()) ? 'https' : 'http'); } /** @@ -2414,16 +2595,16 @@ protected function current_url_scheme() */ protected function current_url_host() { - if(isset(static::$cache[__FUNCTION__])) - return static::$cache[__FUNCTION__]; + if(isset(static::$static[__FUNCTION__])) + return static::$static[__FUNCTION__]; if(!empty($this->options[__FUNCTION__])) // Defined explicity? - return (static::$cache[__FUNCTION__] = $this->n_url_host($this->options[__FUNCTION__])); + return (static::$static[__FUNCTION__] = $this->n_url_host($this->options[__FUNCTION__])); if(empty($_SERVER['HTTP_HOST'])) throw new \exception('Missing required `$_SERVER[\'HTTP_HOST\']`.'); - return (static::$cache[__FUNCTION__] = $this->n_url_host($_SERVER['HTTP_HOST'])); + return (static::$static[__FUNCTION__] = $this->n_url_host($_SERVER['HTTP_HOST'])); } /** @@ -2437,16 +2618,16 @@ protected function current_url_host() */ protected function current_url_uri() { - if(isset(static::$cache[__FUNCTION__])) - return static::$cache[__FUNCTION__]; + if(isset(static::$static[__FUNCTION__])) + return static::$static[__FUNCTION__]; if(!empty($this->options[__FUNCTION__])) // Defined explicity? - return (static::$cache[__FUNCTION__] = $this->must_parse_uri($this->options[__FUNCTION__])); + return (static::$static[__FUNCTION__] = $this->must_parse_uri($this->options[__FUNCTION__])); if(empty($_SERVER['REQUEST_URI'])) throw new \exception('Missing required `$_SERVER[\'REQUEST_URI\']`.'); - return (static::$cache[__FUNCTION__] = $this->must_parse_uri($_SERVER['REQUEST_URI'])); + return (static::$static[__FUNCTION__] = $this->must_parse_uri($_SERVER['REQUEST_URI'])); } /** @@ -2458,14 +2639,14 @@ protected function current_url_uri() */ protected function current_url() { - if(isset(static::$cache[__FUNCTION__])) - return static::$cache[__FUNCTION__]; + if(isset(static::$static[__FUNCTION__])) + return static::$static[__FUNCTION__]; $url = $this->current_url_scheme().'://'; $url .= $this->current_url_host(); $url .= $this->current_url_uri(); - return (static::$cache[__FUNCTION__] = $url); + return (static::$static[__FUNCTION__] = $url); } /**