diff --git a/includes/common.inc b/includes/common.inc index 8575844..b7588b4 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -3120,7 +3120,12 @@ function drupal_aggregate_css(&$css_groups) { // the group's data property to the file path of the aggregate file. case 'file': if ($group['preprocess'] && $preprocess_css) { - $css_groups[$key]['data'] = drupal_build_css_cache($group['items']); + $uri = drupal_build_css_cache($group['items']); + // Only include the file if was written successfully. Errors are + // logged using watchdog. + if ($uri) { + $css_groups[$key]['data'] = $uri; + } } break; // Aggregate all inline CSS content into the group's data property. @@ -3383,68 +3388,122 @@ function drupal_pre_render_styles($elements) { * The URI of the CSS cache file, or FALSE if the file could not be saved. */ function drupal_build_css_cache($css) { - $data = ''; $uri = ''; $map = variable_get('drupal_css_cache_files', array()); $key = hash('sha256', serialize($css)); if (isset($map[$key])) { $uri = $map[$key]; + if (file_exists($uri)) { + return $uri; + } } - if (empty($uri) || !file_exists($uri)) { - // Build aggregate CSS file. - foreach ($css as $stylesheet) { - // Only 'file' stylesheets can be aggregated. - if ($stylesheet['type'] == 'file') { - $contents = drupal_load_stylesheet($stylesheet['data'], TRUE); - - // Build the base URL of this CSS file: start with the full URL. - $css_base_url = file_create_url($stylesheet['data']); - // Move to the parent. - $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/')); - // Simplify to a relative URL if the stylesheet URL starts with the - // base URL of the website. - if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) { - $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root'])); - } + $lock_name = 'css_cache:' . $key; + $lock_attempts = 0; + while ($lock_attempts++ < 5) { + $lock_acquired = lock_acquire($lock_name); + + // Reload the CSS map directly from the database. + $variables = variable_initialize(); + $map = isset($variables['drupal_css_cache_files']) ? $variables['drupal_css_cache_files'] : array(); - _drupal_build_css_path(NULL, $css_base_url . '/'); - // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths. - $data .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents); + if ($lock_acquired) { + // We got the lock, but the CSS file has changed since we started trying + // to rebuild it, so another process did the work. Release the lock and + // return the new URI. + if (isset($map[$key]) && $map[$key] != $uri) { + lock_release($lock_name); + return $map[$key]; + } + break; + } + else { + // We didn't get the lock, so check if there's a file we can serve. + if (isset($map[$key]) && file_exists($map[$key])) { + return $map[$key]; } + + // No file and no lock, so try again. + lock_wait($lock_name); } + } - // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, - // @import rules must proceed any other style, so we move those to the top. - $regexp = '/@import[^;]+;/i'; - preg_match_all($regexp, $data, $matches); - $data = preg_replace($regexp, '', $data); - $data = implode('', $matches[0]) . $data; - - // Prefix filename to prevent blocking by firewalls which reject files - // starting with "ad*". - $filename = 'css_' . drupal_hash_base64($data) . '.css'; - // Create the css/ within the files folder. - $csspath = 'public://css'; - $uri = $csspath . '/' . $filename; - // Create the CSS file. - file_prepare_directory($csspath, FILE_CREATE_DIRECTORY); - if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) { - return FALSE; - } - // If CSS gzip compression is enabled, clean URLs are enabled (which means - // that rewrite rules are working) and the zlib extension is available then - // create a gzipped version of this file. This file is served conditionally - // to browsers that accept gzip using .htaccess rules. - if (variable_get('css_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { - if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) { - return FALSE; + // We got the lock, or there's no aggregated CSS file for this $key. + $data = ''; + foreach ($css as $stylesheet) { + // Only 'file' stylesheets can be aggregated. + if ($stylesheet['type'] == 'file') { + $contents = drupal_load_stylesheet($stylesheet['data'], TRUE); + + // Build the base URL of this CSS file: start with the full URL. + $css_base_url = file_create_url($stylesheet['data']); + // Move to the parent. + $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/')); + // Simplify to a relative URL if the stylesheet URL starts with the + // base URL of the website. + if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) { + $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root'])); } + + _drupal_build_css_path(NULL, $css_base_url . '/'); + // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths. + $data .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents); + } + } + + // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, + // @import rules must proceed any other style, so we move those to the top. + $regexp = '/@import[^;]+;/i'; + preg_match_all($regexp, $data, $matches); + $data = preg_replace($regexp, '', $data); + $data = implode('', $matches[0]) . $data; + + // Create the css/ directory within the files folder. + $csspath = 'public://css'; + file_prepare_directory($csspath, FILE_CREATE_DIRECTORY); + + // Prefix filename to prevent blocking by firewalls which reject files + // starting with "ad*", and create the CSS file. + $filename = 'css_' . drupal_hash_base64($data) . '.css'; + $old_uri = $uri; + $uri = $csspath . '/' . $filename; + if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) { + // The write failed, so release the lock and return an old URI + // if there is one. + lock_release($lock_name); + return $old_uri && file_exists($old_uri) ? $old_uri : FALSE; + } + + // Save the updated map atomically. + $lock_attempts = 0; + while (!lock_acquire('css_cache') && $lock_attempts++ < 5) { + lock_wait('css_cache'); + } + + // Be sure we have the latest version of the css map. + $variables = variable_initialize(); + $map = isset($variables['drupal_css_cache_files']) ? $variables['drupal_css_cache_files'] : array(); + $map[$key] = $uri; + variable_set('drupal_css_cache_files', $map); + lock_release('css_cache'); + + // Release the lock now, because the file is written to disk and map + // updated, so we've prevented a potential request stampede. + lock_release($lock_name); + + // If CSS gzip compression is enabled, clean URLs are enabled (which + // means that rewrite rules are working) and the zlib extension is + // available then create a gzipped version of this file. This file is + // served conditionally to browsers that accept gzip using .htaccess + // rules. + if (variable_get('css_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { + if (!file_exists($uri . '.gz')) { + // Ignore an error saving the file. Requests will fall back + // to the non-gzip copy of the file we have already written. + file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE); } - // Save the updated map. - $map[$key] = $uri; - variable_set('drupal_css_cache_files', $map); } + return $uri; } @@ -3609,7 +3668,12 @@ function _drupal_load_stylesheet($matches) { * Deletes old cached CSS files. */ function drupal_clear_css_cache() { + $lock_attempts = 0; + while (!lock_acquire('css_cache') && $lock_attempts++ < 5) { + lock_wait('css_cache'); + } variable_del('drupal_css_cache_files'); + lock_release('css_cache'); file_scan_directory('public://css', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); } @@ -4742,45 +4806,101 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro * The URI of the cache file, or FALSE if the file could not be saved. */ function drupal_build_js_cache($files) { - $contents = ''; $uri = ''; $map = variable_get('drupal_js_cache_files', array()); $key = hash('sha256', serialize($files)); if (isset($map[$key])) { $uri = $map[$key]; + if (file_exists($uri)) { + return $uri; + } } - if (empty($uri) || !file_exists($uri)) { - // Build aggregate JS file. - foreach ($files as $path => $info) { - if ($info['preprocess']) { - // Append a ';' and a newline after each JS file to prevent them from running together. - $contents .= file_get_contents($path) . ";\n"; + $lock_name = 'js_cache:' . $key; + $lock_attempts = 0; + while ($lock_attempts++ < 5) { + $lock_acquired = lock_acquire($lock_name); + + // Reload the javacript map directly from the database. + $variables = variable_initialize(); + $map = isset($variables['drupal_js_cache_files']) ? $variables['drupal_js_cache_files'] : array(); + + if ($lock_acquired) { + // We got the lock, but the javacript file has changed since we started + // trying to rebuild it, so another process did the work. Release the lock and + // return the new URI. + if (isset($map[$key]) && $map[$key] != $uri) { + lock_release($lock_name); + return $map[$key]; } + break; } - // Prefix filename to prevent blocking by firewalls which reject files - // starting with "ad*". - $filename = 'js_' . drupal_hash_base64($contents) . '.js'; - // Create the js/ within the files folder. - $jspath = 'public://js'; - $uri = $jspath . '/' . $filename; - // Create the JS file. - file_prepare_directory($jspath, FILE_CREATE_DIRECTORY); - if (!file_exists($uri) && !file_unmanaged_save_data($contents, $uri, FILE_EXISTS_REPLACE)) { - return FALSE; - } - // If JS gzip compression is enabled, clean URLs are enabled (which means - // that rewrite rules are working) and the zlib extension is available then - // create a gzipped version of this file. This file is served conditionally - // to browsers that accept gzip using .htaccess rules. - if (variable_get('js_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { - if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($contents, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) { - return FALSE; + else { + // We didn't get the lock, so check if there's a file we can serve. + if (isset($map[$key]) && file_exists($map[$key])) { + return $map[$key]; } + + // No file and no lock, so try again. + lock_wait($lock_name); + } + } + + // We got the lock, or there's no aggregated javascript file for this $key. + $data = ''; + foreach ($files as $path => $info) { + if ($info['preprocess']) { + // Append a ';' and a newline after each JS file to prevent them from running together. + $data .= file_get_contents($path) . ";\n"; } - $map[$key] = $uri; - variable_set('drupal_js_cache_files', $map); } + + // Create the js/ within the files folder. + $jspath = 'public://js'; + file_prepare_directory($jspath, FILE_CREATE_DIRECTORY); + + // Prefix filename to prevent blocking by firewalls which reject files + // starting with "ad*", and create the javascript file. + $filename = 'js_' . drupal_hash_base64($data) . '.js'; + $old_uri = $uri; + $uri = $jspath . '/' . $filename; + if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) { + // The write failed, so release the lock and return an old URI + // if there is one. + lock_release($lock_name); + return $old_uri && file_exists($old_uri) ? $old_uri : FALSE; + } + + // Atomically save the updated map. + $lock_attempts = 0; + while (!lock_acquire('js_cache') && $lock_attempts++ < 5) { + lock_wait('js_cache'); + } + + // Be sure we have the latest version of the javascript map. + $variables = variable_initialize(); + $map = isset($variables['drupal_js_cache_files']) ? $variables['drupal_js_cache_files'] : array(); + $map[$key] = $uri; + variable_set('drupal_js_cache_files', $map); + lock_release('js_cache'); + + // Release the lock now, because the file is written to disk and map + // updated, so we've prevented a potential request stampede. + lock_release($lock_name); + + // If JS gzip compression is enabled, clean URLs are enabled (which + // means that rewrite rules are working) and the zlib extension is + // available then create a gzipped version of this file. This file is + // served conditionally to browsers that accept gzip using .htaccess + // rules. + if (variable_get('js_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { + if (!file_exists($uri . '.gz')) { + // Ignore an error saving the file. Requests will fall back + // to the non-gzip copy of the file we have already written. + file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE); + } + } + return $uri; } @@ -4788,8 +4908,13 @@ function drupal_build_js_cache($files) { * Deletes old cached JavaScript files and variables. */ function drupal_clear_js_cache() { + $lock_attempts = 0; + while (!lock_acquire('js_cache') && $lock_attempts++ < 5) { + lock_wait('js_cache'); + } variable_del('javascript_parsed'); variable_del('drupal_js_cache_files'); + lock_release('js_cache'); file_scan_directory('public://js', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); }