Index: update.php =================================================================== RCS file: /cvs/drupal/drupal/update.php,v retrieving revision 1.211 diff -u -F^f -r1.211 update.php --- update.php 25 Dec 2006 21:22:03 -0000 1.211 +++ update.php 28 Mar 2007 04:03:39 -0000 @@ -322,7 +322,7 @@ function update_selection_page() { drupal_set_title('Drupal database update'); // Prevent browser from using cached drupal.js or update.js - drupal_add_js('misc/update.js', 'core', 'header', FALSE, TRUE); + drupal_add_js('misc/update.js', 'core', 'header', FALSE, FALSE); $output .= drupal_get_form('update_script_selection_form'); return $output; @@ -402,9 +402,9 @@ function update_update_page() { } function update_progress_page() { - // Prevent browser from using cached drupal.js or update.js - drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE); - drupal_add_js('misc/update.js', 'core', 'header', FALSE, TRUE); + // Prevent browser from using cached progress.js or update.js + drupal_add_js('misc/progress.js', 'core', 'header', FALSE, FALSE); + drupal_add_js('misc/update.js', 'core', 'header', FALSE, FALSE); drupal_set_title('Updating'); $output = '
'; @@ -446,6 +446,7 @@ function update_do_updates() { cache_clear_all('*', 'cache_menu', TRUE); cache_clear_all('*', 'cache_filter', TRUE); drupal_clear_css_cache(); + drupal_clear_js_cache(); } return array($percentage, isset($update['module']) ? 'Updating '. $update['module'] .' module' : 'Updating complete'); Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.611.2.3 diff -u -F^f -r1.611.2.3 common.inc --- includes/common.inc 23 Mar 2007 20:49:11 -0000 1.611.2.3 +++ includes/common.inc 28 Mar 2007 04:03:41 -0000 @@ -1593,24 +1593,28 @@ function drupal_clear_css_cache() { * (optional) If set to FALSE, the JavaScript file is loaded anew on every page * call, that means, it is not cached. Defaults to TRUE. Used only when $type * references a JavaScript file. + * @param $preprocess + * (optional) Should this JS file be aggregated if this + * feature has been turned on under the performance section? + * @param $pack + * (optional) Should this JS be packed (e.g., compressed)? * @return * If the first parameter is NULL, the JavaScript array that has been built so * far for $scope is returned. */ -function drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer = FALSE, $cache = TRUE) { - if (!is_null($data)) { - _drupal_add_js('misc/jquery.js', 'core', 'header', FALSE, $cache); - _drupal_add_js('misc/drupal.js', 'core', 'header', FALSE, $cache); - } - return _drupal_add_js($data, $type, $scope, $defer, $cache); -} - -/** - * Helper function for drupal_add_js(). - */ -function _drupal_add_js($data, $type, $scope, $defer, $cache) { +function drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer = FALSE, $cache = TRUE, $preprocess = TRUE, $pack = TRUE) { static $javascript = array(); + // Add jquery.js and drupal.js the first time a Javascript file is added. + if ($data && empty($javascript)) { + $javascript['header'] = array( + 'core' => array( + 'misc/jquery.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE, 'pack' => FALSE), + 'misc/drupal.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE, 'pack' => TRUE), + ), + 'module' => array(), 'theme' => array(), 'setting' => array(), 'inline' => array(), + ); + } if (!isset($javascript[$scope])) { $javascript[$scope] = array('core' => array(), 'module' => array(), 'theme' => array(), 'setting' => array(), 'inline' => array()); } @@ -1619,7 +1623,7 @@ function _drupal_add_js($data, $type, $s $javascript[$scope][$type] = array(); } - if (!is_null($data)) { + if (isset($data)) { switch ($type) { case 'setting': $javascript[$scope][$type][] = $data; @@ -1628,7 +1632,8 @@ function _drupal_add_js($data, $type, $s $javascript[$scope][$type][] = array('code' => $data, 'defer' => $defer); break; default: - $javascript[$scope][$type][$data] = array('cache' => $cache, 'defer' => $defer); + // If cache is FALSE, don't preprocess the JS file. + $javascript[$scope][$type][$data] = array('cache' => $cache, 'defer' => $defer, 'preprocess' => (!$cache ? FALSE : $preprocess), 'pack' => $pack); } } @@ -1651,13 +1656,25 @@ function _drupal_add_js($data, $type, $s * @return * All JavaScript code segments and includes for the scope as HTML tags. */ -function drupal_get_js($scope = 'header', $javascript = NULL) { - $output = ''; - if (is_null($javascript)) { +function drupal_get_js($scope = 'header', $javascript = NULL) { + if (!isset($javascript)) { $javascript = drupal_add_js(NULL, NULL, $scope); } + if (count($javascript) < 1) { + return ''; + } + + $output = ''; + $preprocessed = ''; + $no_preprocess = array('core' => '', 'module' => '', 'theme' => ''); + $files = array(); + $preprocess_js = variable_get('preprocess_js', FALSE); + $directory = file_directory_path(); + $is_writable = is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC); + foreach ($javascript as $type => $data) { + if (!$data) continue; switch ($type) { @@ -1670,16 +1687,82 @@ function drupal_get_js($scope = 'header' } break; default: + // If JS preprocessing is off, we still need to output the scripts. + // Additionally, go through any remaining scripts if JS preprocessing is on and output the non-cached ones. foreach ($data as $path => $info) { - $output .= '\n"; + if (!$info['preprocess'] || !$is_writable || !$preprocess_js) { + $no_preprocess[$info['type']] .= '\n"; + } + else { + $files[$path] = $info; + } } } + } + + // Aggregate any remaining JS files that haven't already been output. + if ($is_writable && $preprocess_js && count($files) > 0) { + $filename = md5(serialize($files)) .'.js'; + $preprocess_file = drupal_build_js_cache($files, $filename); + $preprocessed .= ''. "\n"; } + // Keep the order of JS files consistent as some are preprocessed and others are not. + $output .= $preprocessed . implode('', $no_preprocess); + return $output; } /** + * Aggregate JS files, putting them in the files directory. + * + * @param $files + * An array of JS files to aggregate and compress into one file. + * @param $filename + * The name of the aggregate JS file. + * @return + * The name of the JS file. + */ +function drupal_build_js_cache($files, $filename) { + $contents = ''; + $to_pack = ''; + + // Create the js/ within the files folder. + $jspath = file_create_path('js'); + file_check_directory($jspath, FILE_CREATE_DIRECTORY); + + if (!file_exists($jspath .'/'. $filename)) { + // Build aggregate JS file. + foreach ($files as $path => $info) { + if ($info['preprocess']) { + if ($info['pack']) { + $to_pack .= file_get_contents($path); + } + else { + $contents .= file_get_contents($path); + } + } + } + + // Pack JS files. + include_once './includes/js-packer.inc'; + $contents .= $to_pack;//drupal_pack_js($to_pack); + + // Create the JS file. + file_save_data($contents, $jspath .'/'. $filename, FILE_EXISTS_REPLACE); + } + + return $jspath .'/'. $filename; +} + +/** + * Delete all cached JS files. + */ +function drupal_clear_js_cache() { + file_scan_directory(file_create_path('js'), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); +} + +/** * Converts a PHP variable into its Javascript equivalent. * * We use HTML-safe strings, i.e. with <, > and & escaped. Index: includes/js-packer.inc =================================================================== RCS file: includes/js-packer.inc diff -N includes/js-packer.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/js-packer.inc 28 Mar 2007 04:03:41 -0000 @@ -0,0 +1,519 @@ + '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; +} + +/** + * Helper function for _packer_apply(). + */ +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 '\\'; + } +} + +/** + * Helper function for _packer_apply(). + */ +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); + } +} + +/** + * Helper function for _packer_apply(). + * + * Performs replacements for the multi-regexp. + */ +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; + } + } +} + +/** + * Helper function for _packer_replacement(). + */ +function _packer_backreferences($match, $offset, $data) { + $replacement = $data['replacement']; + $i = $data['length']; + while ($i) { + $replacement = str_replace('$'.$i--, $match[$offset + $i], $replacement); + } + return $replacement; +} + +/** + * Helper function for _packer_replacement(). + */ +function _packer_replace_encoded($match, $offset, $data) { + return $data[$match[$offset]]; +} + Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.440.2.9 diff -u -F^f -r1.440.2.9 system.module --- modules/system/system.module 13 Mar 2007 13:43:22 -0000 1.440.2.9 +++ modules/system/system.module 28 Mar 2007 04:03:43 -0000 @@ -229,7 +229,7 @@ function system_menu($may_cache) { $items[] = array( 'path' => 'admin/settings/performance', 'title' => t('Performance'), - 'description' => t('Enable or disable page caching for anonymous users, and enable or disable CSS preprocessor.'), + 'description' => t('Enable or disable page caching for anonymous users and set CSS and JS bandwith optimization options.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('system_performance_settings')); $items[] = array( @@ -697,14 +697,24 @@ function system_performance_settings() { $form['bandwidth_optimizations']['preprocess_css'] = array( '#type' => 'radios', '#title' => t('Aggregate and compress CSS files'), - '#default_value' => variable_get('preprocess_css', FALSE) && $is_writable, + '#default_value' => variable_get('preprocess_css', 0) && $is_writable, '#disabled' => !$is_writable, '#options' => array(t('Disabled'), t('Enabled')), '#description' => t("Some Drupal modules include their own CSS files. When these modules are enabled, each module's CSS file adds an additional HTTP request to the page, which can increase the load time of each page. These HTTP requests can also slightly increase server load. It is recommended to only turn this option on when your site is in production, as it can interfere with theme development. This option is disabled if you have not set up your files directory, or if your download method is set to private."), ); + + $form['bandwidth_optimizations']['preprocess_js'] = array( + '#type' => 'radios', + '#title' => t('Aggregate JS files'), + '#default_value' => variable_get('preprocess_js', 0) && $is_writable, + '#disabled' => !$is_writable, + '#options' => array(t('Disabled'), t('Enabled')), + '#description' => t("Some Drupal modules include their own JS files. When these modules are enabled, each module's JS file adds an additional HTTP request to the page, which can increase the load time of each page. These HTTP requests can also slightly increase server load. It is recommended to only turn this option on when your site is in production, as it can interfere with module development. This option is disabled if you have not set up your files directory, or if your download method is set to private."), + ); $form['#submit']['system_settings_form_submit'] = array(); $form['#submit']['drupal_clear_css_cache'] = array(); + $form['#submit']['drupal_clear_js_cache'] = array(); return system_settings_form($form); } @@ -1516,6 +1526,7 @@ function system_modules_submit($form_id, } drupal_clear_css_cache(); + drupal_clear_js_cache(); return 'admin/build/modules'; }