'replace_encoded', 'data' => $encoded, ), )); $script = _packer_apply($script, $regexps); return array($script, empty($script) ? NULL : $keywords); } /** * Analyzes a piece of JavaScript for replaceable tokens and sorts them by frequency. * * Helper function for drupal_pack_js(). */ function _packer_analyze($script, $regexp) { // Retrieve all words in the script. $all = array(); preg_match_all($regexp, $script, $all); // List of words sorted by frequency $_sorted = array(); // Dictionary of word to encoding $_encoded = array(); // Instances of protected words $_protected = array(); // Simulate the JavaScript behaviour of global match $all = $all[0]; if (!empty($all)) { $unsorted = array(); // "protected" words (dictionary of word to "word") $protected = array(); // Dictionary of character code to encoding (e.g. 256 to ff) $value = array(); // Word counts $count = array(); // Count word occurrences for sorting $i = count($all); $j = 0; do { --$i; $word = '$' . $all[$i]; if (!isset($count[$word])) { $count[$word] = 0; $unsorted[$j] = $word; $values[$j] = _packer_encode62($j); $protected['$'. $values[$j]] = $j++; } $count[$word]++; } while ($i > 0); // Prepare to sort the word list. // We protect words that are also used as codes by assigning them a code // equal to themselves. // // e.g. if "do" falls within our encoding range // then we store keywords["do"] = "do"; // This avoids problems when decoding $i = count($unsorted); do { $word = $unsorted[--$i]; if (isset($protected[$word])) { $_sorted[$protected[$word]] = substr($word, 1); $_protected[$protected[$word]] = true; $count[$word] = 0; } } while ($i); // Sort the words by frequency _packer_sort_words(NULL, NULL, $count); usort($unsorted, '_packer_sort_words'); $j = 0; // Because there are "protected" words in the list // we must add the sorted words around them do { if (!isset($_sorted[$i])) { $_sorted[$i] = substr($unsorted[$j++], 1); } $_encoded[$_sorted[$i]] = $values[$i]; } while (++$i < count($unsorted)); } return array( 'sorted' => $_sorted, 'encoded' => $_encoded, 'protected' => $_protected); } /** * Helper function _packer_analyse(). */ function _packer_sort_words($match1, $match2, $count = NULL) { static $_count; if (isset($count)) { $_count = $count; } return $_count[$match2] - $_count[$match1]; } /** * Helper function for JS packing. * * Encodes a character into base62, using case-sensitive alphanumerics. */ function _packer_encode62($char_code) { $res = ''; if ($char_code >= 62) { $res = _packer_encode62((int)($char_code / 62)); } $char_code = $char_code % 62; if ($char_code > 35) { return $res . chr($char_code + 29); } else { return $res . base_convert($char_code, 10, 36); } } /** * Helper function for _packer_apply. * * Escapes dollar signs in a string, for use as regexp. */ function _packer_safe_regexp($string) { return '/'. preg_replace('/\$/', '\\\$', $string) .'/'; } /** * Helper function for _packer_analyse. * * Protect JavaScript code so it can be embedded in a single quoted string. */ function _packer_escape_code($script) { return preg_replace('/([\\\\\'])/', '\\\$1', $script); } /** * Helper function for _packer_apply. * * Replace a matched variable name with a shorter ones. */ function _packer_replace_name($match) { $length = strlen($match[2]); $start = $length - max($length - strlen($match[3]), 0); return substr($match[1], $start, $length) . $match[4]; } /** * Build the boot function used for loading and decoding. */ function _packer_bootstrap($packed, $keywords) { // For processing reasons, JS variables below are prefixed with dollar signs. // These are removed later. // JS version of _packer_encode62 $js_encode62 = 'function($char_code) { return ($char_code < _encoding ? \'\' : arguments.callee(parseInt($char_code / _encoding))) + (($char_code = $char_code % _encoding) > 35 ? String.fromCharCode($char_code + 29) : $char_code.toString(36)); }'; // Fast decoding routine. $js_decode_body = ' if (!\'\'.replace(/^/, String)) { // Decode all the values we need while ($count--) { $decode[$encode($count)] = $keywords[$count] || $encode($count); } // Global replacement function $keywords = [function ($encoded) {return $decode[$encoded]}]; // Generic match $encode = function () {return \'\\\\\\w+\'}; // Reset the loop counter - we are now doing a global replace $count = 1; } '; // Bootstrap code. The data from this packing routine is passed to this // function when decoded on the client side. Can not have a semi-colon at the end. $js_unpack = 'function($packed, $ascii, $count, $keywords, $encode, $decode) { while ($count--) { if ($keywords[$count]) { $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]); } } return $packed; }'; $_encode = _packer_safe_regexp('$encode\\($count\\)'); // Prepare the packed script $packed = "'" . _packer_escape_code($packed) . "'"; // Base for encoding $ascii = 62; // Number of words contained in the script $count = count($keywords['sorted']); // List of words contained in the script foreach ($keywords['protected'] as $i => $value) { $keywords['sorted'][$i] = ''; } // JS code to decode the keywords string to an array. ksort($keywords['sorted']); $keywords = "'" . implode('|',$keywords['sorted']) . "'.split('|')"; $encode = $js_encode62; $encode = preg_replace('/_encoding/', '$ascii', $encode); $encode = preg_replace('/arguments\\.callee/', '$encode', $encode); // Code snippet to speed up decoding // Create the decoder $decode = $js_decode_body; // Perform the encoding inline for lower ascii values // Special case: when $count == 0 there are no keywords. I want to keep // the basic shape of the unpacking funcion so i'll frig the code... if ($count == 0) { $decode = preg_replace(_packer_safe_regexp('($count)\\s*=\\s*1'), '$1=0', $decode, 1); } // Boot function $unpack = $js_unpack; // Insert the decoder $unpack = preg_replace('/\{/', '{'. $decode .';', $unpack, 1); $unpack = str_replace('"', "'", $unpack); $unpack = preg_replace('/\{/', '{$encode='. $encode .';', $unpack, 1); // Compact the boot snippet too. $unpack = preg_replace_callback('/((\\x24+)([a-zA-Z$_]+))(\\d*)/', '_packer_replace_name', $unpack); $unpack = _packer_basic_compression($unpack); // Arguments $params = array($packed, $ascii, $count, $keywords); $params[] = 0; $params[] = '{}'; $params = implode(',', $params); // the whole thing return 'eval(' . $unpack . '(' . $params . "))\n"; } /** * Multi-regexp replacements. * * Allows you to perform multiple regular expression replacements at once, * without overlapping matches. * * @param $script * The text to modify. * @param $regexps * An array of replacement instructions, each being a tuple with values: * - A stand-alone regular expression without modifiers (slash-delimited) * - A replacement expression, which may include placeholders. * @param $escape * Whether to ignore slash-escaped characters for matching. This allows you * to match e.g. quote-delimited strings with /'[^']+'/ without having to * worry about \'. Otherwise, you'd have to mess with look-aheads and * look-behinds to match these. */ function _packer_apply($script, $regexps, $escape = FALSE) { $_regexps = array(); // Process all regexps foreach ($regexps as $regexp) { list($expression, $replacement) = $regexp; // Count the number of matching groups (including the whole). $length = 1 + preg_match_all('/(? 'backreferences', 'data' => array( 'replacement' => $replacement, 'length' => $length, ) ); } } } // Store the modified expression. if (!empty($expression)) { $_regexps[] = array($expression, $replacement, $length); } else { $_regexps[] = array('/^$/', $replacement, $length); } } // Execute the global replacement // Build one mega-regexp out of the smaller ones. $regexp = '/'; foreach ($_regexps as $_regexp) { list($expression) = $_regexp; $regexp .= '(' . substr($expression, 1, -1) . ')|'; } $regexp = substr($regexp, 0, -1) . '/'; // In order to simplify the regexps that look e.g. for quoted strings, we // remove all escaped characters (such as \' or \") from the data. Then, we // put them back as they were. if ($escape) { // Remove escaped characters $script = preg_replace_callback( '/\\\\(.)' .'/', '_packer_escape_char', $script ); $escaped = _packer_escape_char(NULL, TRUE); } _packer_replacement(NULL, $_regexps, $escape); $script = preg_replace_callback( $regexp, '_packer_replacement', $script ); if ($escape) { // Restore escaped characters _packer_unescape_char(NULL, $escaped); $script = preg_replace_callback( '/\\\\' .'/', '_packer_unescape_char', $script ); // We only delete portions of data afterwards to ensure the escaped character // replacements don't go out of sync. We mark all sections to delete with // ASCII 01 bytes. $script = preg_replace('/\\x01[^\\x01]*\\x01/', '', $script); } return $script; } function _packer_escape_char($match, $return = FALSE) { // Build array of escaped characters that were removed. static $_escaped = array(); if ($return) { $escaped = $_escaped; $_escaped = array(); return $escaped; } else { $_escaped[] = $match[1]; return '\\'; } } function _packer_unescape_char($match, $escaped = NULL) { // Store array of escaped characters to insert back. static $_escaped, $i; if ($escaped) { $_escaped = $escaped; $i = 0; } else { return '\\'. array_shift($_escaped); } } // this is the global replace function (it's quite complicated) function _packer_replacement($arguments, $regexps = NULL, $escape = NULL) { // Cache regexps static $_regexps, $_escape; if (isset($regexps)) { $_regexps = $regexps; } if (isset($escape)) { $_escape = $escape; } if (empty($arguments)) { return ''; } $i = 1; $j = 0; // Loop through the regexps while (isset($_regexps[$j])) { list($expression, $replacement, $length) = $_regexps[$j++];; // Do we have a result? if (isset($arguments[$i]) && ($arguments[$i] != '')) { if (is_array($replacement) && isset($replacement['fn'])) { return call_user_func('_packer_'. $replacement['fn'], $arguments, $i, $replacement['data']); } elseif (is_int($replacement)) { return $arguments[$replacement + $i]; } else { $delete = !$escape || strpos($arguments[$i], '\\') === FALSE ? '' : "\x01" . $arguments[$i] . "\x01"; return $delete . $replacement; } // skip over references to sub-expressions } else { $i += $length; } } } function _packer_backreferences($match, $offset, $data) { $replacement = $data['replacement']; $i = $data['length']; while ($i) { $replacement = str_replace('$'.$i--, $match[$offset + $i], $replacement); } return $replacement; } function _packer_replace_encoded($match, $offset, $data) { return $data[$match[$offset]]; }