Index: update.php =================================================================== RCS file: /cvs/drupal/drupal/update.php,v retrieving revision 1.211.2.2 diff -u -F^f -r1.211.2.2 update.php --- update.php 8 Apr 2007 00:54:04 -0000 1.211.2.2 +++ update.php 16 Oct 2007 13:11:18 -0000 @@ -405,8 +405,8 @@ 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); + 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 = '
'; @@ -448,6 +448,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.9 diff -u -F^f -r1.611.2.9 common.inc --- includes/common.inc 26 Jul 2007 19:16:45 -0000 1.611.2.9 +++ includes/common.inc 16 Oct 2007 13:11:19 -0000 @@ -1375,6 +1375,19 @@ } /** + * Resolves static file to a full URL pathe. + * If no static settings exist, uses base_path() + */ +function static_file($file_path, $absolute_url = FALSE, $files_synced = array()) { + $static_base = variable_get('static_file_base', NULL); + if (isset($static_base)) { + $full_path = $static_base . $file_path; + } else { + $static_func = variable_get('static_file_func', NULL); + if ($static_func) { + $full_path = call_user_func($static_func, $file_path, $absolute_url, $files_synced); + } else { + $full_path = ($absolute_url ? $GLOBALS['base_url'] .'/' : base_path()) . $file_path; + } + } + return check_plain($full_path); +} + +/** * Provide a substitute clone() function for PHP4. */ function drupal_clone($object) { @@ -1469,15 +1471,15 @@ // If a CSS file is not to be preprocessed and it's a module CSS file, it needs to *always* appear at the *top*, // regardless of whether preprocessing is on or off. if (!$preprocess && $type == 'module') { - $no_module_preprocess .= '' ."\n"; + $no_module_preprocess .= '' ."\n"; } // If a CSS file is not to be preprocessed and it's a theme CSS file, it needs to *always* appear at the *bottom*, // regardless of whether preprocessing is on or off. else if (!$preprocess && $type == 'theme') { - $no_theme_preprocess .= '' ."\n"; + $no_theme_preprocess .= '' ."\n"; } else { - $output .= '' ."\n"; + $output .= '' ."\n"; } } } @@ -1486,7 +1488,7 @@ if ($is_writable && $preprocess_css) { $filename = md5(serialize($types)) .'.css'; $preprocess_file = drupal_build_css_cache($types, $filename); - $output .= ''. "\n"; + $output .= ''. "\n"; } } @@ -1599,24 +1599,26 @@ 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? * @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) { 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), + 'misc/drupal.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => 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()); } @@ -1616,7 +1618,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; @@ -1620,7 +1622,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)); } } @@ -1643,13 +1646,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) { @@ -1662,16 +1677,302 @@ 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[$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. + // Make sure any inline or JS setting variables appear last after libraries have loaded. + $output = $preprocessed . implode('', $no_preprocess) . $output; + 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 = ''; + + // 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']) { + // Append a ';' after each JS file to prevent them from running together. + $contents .= _drupal_compress_js(file_get_contents($path). ';'); + } + } + + // Create the JS file. + file_save_data($contents, $jspath .'/'. $filename, FILE_EXISTS_REPLACE); + } + + return $jspath .'/'. $filename; +} + +/** + * Perform basic code compression for JavaScript. + * + * Helper function for drupal_pack_js(). + */ +function _drupal_compress_js($script) { + $regexps = array( + // Protect strings. + array('/\'[^\'\\n\\r]*\'/', '$0'), + array('/"[^"\\n\\r]*"/', '$0'), + // Remove comments. + array('/\\/\\/[^\\n\\r]*[\\n\\r]/', ''), + array('/\\/\\*[^*]*\\*+((?:[^\\/][^*]*\\*+)*)\\//', ''), + // Protect regular expressions + array('/\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)/', '$1'), + array('/[^\\w\\x24\\/\'"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?/', '$0'), + // Protect spaces between keywords and variables + array('/(?<=[A-Za-z0-9_$])\\s+(?=[A-Za-z0-9_$])/', ' '), + array('/([+\\-])\\s+([+\\-])/', '$1 $2'), + + // Protect newlines after close-braces + array('/}\\s+/', "}\n"), + + // Remove all other white-space + array('/\\s+/', ''), + ); + $script = _packer_apply($script, $regexps, TRUE); + + return $script; +} + +/** + * 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(). + * + * 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_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_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; +} + +/** + * 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/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.90.2.1 diff -u -r1.90.2.1 file.inc --- includes/file.inc 31 May 2007 05:48:58 -0000 1.90.2.1 +++ includes/file.inc 2 Jan 2008 11:29:15 -0000 @@ -33,7 +33,8 @@ } switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) { case FILE_DOWNLOADS_PUBLIC: - return $GLOBALS['base_url'] .'/'. file_directory_path() .'/'. str_replace('\\', '/', $path); + return static_file(file_directory_path() .'/'. str_replace('\\', '/', $path), TRUE); case FILE_DOWNLOADS_PRIVATE: return url('system/files/'. $path, NULL, NULL, TRUE); } Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.337.2.2 diff -u -r1.337.2.2 theme.inc --- includes/theme.inc 31 May 2007 05:52:42 -0000 1.337.2.2 +++ includes/theme.inc 2 Jan 2008 11:29:15 -0000 @@ -322,24 +323,24 @@ if ($settings['toggle_logo']) { if ($settings['default_logo']) { - $settings['logo'] = base_path() . dirname($theme_object->filename) .'/logo.png'; + $settings['logo'] = static_file(dirname($theme_object->filename).'/logo.png'); } elseif ($settings['logo_path']) { - $settings['logo'] = base_path() . $settings['logo_path']; + $settings['logo'] = static_file($settings['logo_path']); } } if ($settings['toggle_favicon']) { if ($settings['default_favicon']) { if (file_exists($favicon = dirname($theme_object->filename) .'/favicon.ico')) { - $settings['favicon'] = base_path() . $favicon; + $settings['favicon'] = static_file($favicon); } else { - $settings['favicon'] = base_path() . 'misc/favicon.ico'; + $settings['favicon'] = static_file('misc/favicon.ico'); } } elseif ($settings['favicon_path']) { - $settings['favicon'] = base_path() . $settings['favicon_path']; + $settings['favicon'] = static_file($settings['favicon_path']); } else { $settings['toggle_favicon'] = FALSE; @@ -613,7 +614,8 @@ function theme_image($path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE) { if (!$getsize || (is_file($path) && (list($width, $height, $type, $image_attributes) = @getimagesize($path)))) { $attributes = drupal_attributes($attributes); - $url = (url($path) == $path) ? $path : (base_path() . $path); + $url = (url($path) == $path) ? $path : static_file($path); return 'Drupal can automatically aggregate and compress external resources like CSS and JavaScript into a single cached file. This can help reduce both the size and number of requests made to your website. This in turn reduces the server load, the bandwidth used, and the average page loading time for your visitors.
These options are disabled if you have not set up your files directory, or if your download method is set to private.
') ); $directory = file_directory_path(); $is_writable = is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC); $form['bandwidth_optimizations']['preprocess_css'] = array( '#type' => 'radios', - '#title' => t('Aggregate and compress CSS files'), - '#default_value' => intval(variable_get('preprocess_css', FALSE) && $is_writable), + '#title' => t('Optimize CSS files'), + '#default_value' => intval(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."), + '#description' => t("This option can interfere with theme development. It is recommended to only turn this on when your site is complete."), ); + $form['bandwidth_optimizations']['preprocess_js'] = array( + '#type' => 'radios', + '#title' => t('Optimize JavaScript files'), + '#default_value' => intval(variable_get('preprocess_js', 0) && $is_writable), + '#disabled' => !$is_writable, + '#options' => array(t('Disabled'), t('Enabled')), + '#description' => t("This option can interfere with module development. It is recommended to only turn this on when your site is complete."), + ); $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); } @@ -1517,6 +1527,7 @@ function system_modules_submit($form_id, } drupal_clear_css_cache(); + drupal_clear_js_cache(); return 'admin/build/modules'; }