diff -u b/includes/bootstrap.inc b/includes/bootstrap.inc --- b/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -838,7 +838,7 @@ // The $files static variable will hold the locations of all requested files, // and any file that gets added during the request will have been checked // with a file_exists() at some point. - static $files = array(), $dirs = array(), $files_scanned = array(); + static $files = array(); // Profiles are a special case: they have a fixed location and naming. if ($type == 'profile') { @@ -850,192 +850,222 @@ } if (!empty($filename) && file_exists($filename)) { + // Prime the static cache with the provided filename. $files[$type][$name] = $filename; } elseif (isset($files[$type][$name])) { - // This item had already been found earlier in the request. Do nothing. + // This item had already been found earlier in the request, either in this + // same function or by the static cache having been primed elsewhere, such + // as in system_list(). Do nothing. } else { - // Look for the location listed in the {system} table. - // Verify that we have an active database connection, before querying - // the database. This is required because this function is called both - // before we have a database connection (i.e. during installation) and - // when a database connection fails. - try { - $database_file_exists = NULL; - if (function_exists('db_query')) { - $file = db_query("SELECT filename FROM {system} WHERE name = :name AND type = :type", array(':name' => $name, ':type' => $type))->fetchField(); - if ($file !== FALSE) { - if (file_exists(DRUPAL_ROOT . '/' . $file)) { - $files[$type][$name] = $file; - $database_file_exists = TRUE; - } - else { - // Flag that the filename in the database does not exist for the - // requested item, so we can figure out if it has moved. - $database_file_exists = FALSE; + $system_filepaths = &drupal_static('system_filepaths'); + if (!isset($system_filepaths[$type][$name])) { + // Look for the file name listed in the {system} table. + // Verify that we have an active database connection, before querying + // the database. This is required because this function is called both + // before we have a database connection (i.e. during installation) and + // when a database connection fails. + try { + if (function_exists('db_query')) { + $file = db_query("SELECT filename FROM {system} WHERE name = :name AND type = :type", array( + ':name' => $name, + ':type' => $type + ))->fetchField(); + if ($file !== FALSE) { + $system_filepaths[$type][$name] = $file; + if (file_exists(DRUPAL_ROOT . '/' . $file)) { + $files[$type][$name] = $file; + } } } } - } - catch (Exception $e) { - // The database table may not exist because Drupal is not yet installed, - // or the database might be down. We have a fallback for this case so we - // hide the error completely. + catch (Exception $e) { + // The database table may not exist because Drupal is not yet installed, + // or the database might be down. We have a fallback for this case so we + // hide the error completely. + } } // Fallback to searching the filesystem if the database could not find the - // file or the filename returned by the database is not found. We cache - // missing and moved files in order to prevent unnecessary file scans. + // file or the filename returned by the database is not found. if (!isset($files[$type][$name])) { - // We have a consistent directory naming: modules, themes... - $dir = $type . 's'; - if ($type == 'theme_engine') { - $dir = 'themes/engines'; - $extension = 'engine'; - } - elseif ($type == 'theme') { - $extension = 'info'; - } - else { - $extension = $type; - } - - // This static variable will hold all missing and moved files, in order - // to prevent unnecessary file scans. It is an associative array with as - // keys the file type and name, and as value the boolean TRUE (if the - // module is missing) or a string (if the module has moved from the - // location listed in the {system} table) with the location that was - // found in a file scan. - $missing = &drupal_static('drupal_get_filename:missing'); - if (!isset($missing)) { - $missing = array(); - } - // Get the missing and moved files from persistent cache, if available. - if (!isset($missing['#cache_merge_done'])) { - try { - if (function_exists('cache_get')) { - $cache = cache_get('drupal_get_filename:missing', 'cache_bootstrap'); - if (!empty($cache->data)) { - // Merge the changes already done in the current request - // (including the setting of missing records to NULL) into the - // values saved in persistent cache. - $missing = drupal_array_merge_deep($cache->data, $missing); - // Set a flag so we know that we've already done a merge with - // the values in persistent cache. - $missing['#cache_merge_done'] = TRUE; - } - } - } - catch (Exception $e) { - // Hide the error. - } - } - - // Check whether this file had previously moved from its location in - // the {system} table, and the new location found in a file scan had - // been cached. - if (isset($missing[$type][$name]) && is_string($missing[$type][$name])) { - // This file has moved and the result of a file scan had been cached. - if (file_exists($missing[$type][$name])) { - // We found the file at the cached location. - $files[$type][$name] = $missing[$type][$name]; - } - else { - // File is not available anymore at the cached location. Remove the - // missing/moved file record so a new file scan will be performed. - $missing[$type][$name] = NULL; - $missing['#write_cache'] = TRUE; - } - } + $files[$type][$name] = _drupal_get_filename_fallback($type, $name); + } + } - // Check if we had already scanned this directory/extension combination, - // or if this file had been marked as missing/moved. - if (!isset($dirs[$dir][$extension]) && !isset($missing[$type][$name])) { - // Log that we have now scanned this directory/extension combination - // into a static variable to prevent unnecessary scans. - $dirs[$dir][$extension] = TRUE; - if (!function_exists('drupal_system_listing')) { - require_once DRUPAL_ROOT . '/includes/common.inc'; - } - // Scan the appropriate directories for all files with the requested - // extension, not just the file we are currently looking for. This - // prevents unnecessary scans from being repeated when this function is - // called more than once in the same page request. - $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0); - foreach ($matches as $matched_name => $file) { - // Log the locations found in the file scan into a static variable. - $files_scanned[$type][$matched_name] = $file->uri; - } - } + if (isset($files[$type][$name])) { + // We found a file. + // Clean up the item from the file scan cache, in case the item had been + // marked as missing or moved. + $file_scans = &drupal_static('_drupal_get_filename_fallback'); + if (isset($file_scans[$type][$name])) { + $file_scans[$type][$name] = NULL; + $file_scans['#write_cache'] = TRUE; + } + return $files[$type][$name]; + } +} - // If current file had been found in a file scan earlier on in this - // request, use the new location that had been found in the file scan. - if (isset($files_scanned[$type][$name])) { - // Use the location from the file scan earlier on in this request. - $files[$type][$name] = $files_scanned[$type][$name]; - // Check whether the current file was listed as being in another - // location in the {system} table. - if (isset($database_file_exists) && $database_file_exists === FALSE) { - // This file has moved. Cache its new location into the missing and - // moved files list. - $missing[$type][$name] = $files_scanned[$type][$name]; - // Make sure our change to the missing and moved files list will be - // written to persistent cache. - $missing['#write_cache'] = TRUE; - trigger_error(format_string('The following @type has moved on the file system: @name. Clearing caches may help fix this. For more information, see the documentation page.', array('@type' => $type, '@name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING); +/** + * Helper function for drupal_get_filename(). + * + * This triggers a file scan, caching any missing or moved files. If an item + * had been marked as missing or moved in a previous file scan, no new file + * scans will be performed. + * + * @param string $type + * The type of the item (theme, theme_engine, module, profile). + * @param string $name + * The name of the item for which the filename is requested. + * + * @return string + * The filename of the requested item or NULL if the item is not found. + */ +function _drupal_get_filename_fallback($type, $name) { + // This static variable will hold all missing and moved files, in order + // to prevent unnecessary file scans. It is an associative array with as + // keys the file type and name, and as value the boolean FALSE if the + // module is missing, or a string with location found in a file scan if + // the module has moved from the location listed in the {system} table. + $file_scans = &drupal_static('_drupal_get_filename_fallback'); + if (!isset($file_scans)) { + $file_scans = array(); + } + // Get the missing and moved files from persistent cache, if available. + if (!isset($file_scans['#cache_merge_done'])) { + try { + if (function_exists('cache_get')) { + $cache = cache_get('_drupal_get_filename_fallback', 'cache_bootstrap'); + if (!empty($cache->data)) { + // Merge the changes already done in the current request + // (including the setting of missing records to NULL) into the + // values saved in persistent cache. + $file_scans = drupal_array_merge_deep($cache->data, $file_scans); + // Set a flag so we know that we've already done a merge with + // the values in persistent cache. + $file_scans['#cache_merge_done'] = TRUE; } } } + catch (Exception $e) { + // Hide the error. + } } - if (isset($files[$type][$name])) { - // The requested file exists. - // Check if the file had previously been marked as missing or moved. - if (isset($missing[$type][$name]) && ($missing[$type][$name] === TRUE || (is_string($missing[$type][$name]) && $missing[$type][$name] != $files[$type][$name]))) { - // Previously missing/moved file has reappeared. - $missing[$type][$name] = NULL; - $missing['#write_cache'] = TRUE; + // Check whether this file had previously moved from its location in the + // {system} table. + if (isset($file_scans[$type][$name]) && is_string($file_scans[$type][$name]) && file_exists($file_scans[$type][$name])) { + // We found the file at the cached location. + return $file_scans[$type][$name]; + } + + // Unless the module had been marked as missing, perform a file scan. + if (!(isset($file_scans[$type][$name]) && $file_scans[$type][$name] === FALSE)) { + $filename_from_file_scan = _drupal_get_filename_perform_file_scan($type, $name); + } + if (isset($filename_from_file_scan)) { + // Check whether the current file was listed as being in another + // location in the {system} table. + $system_filepaths = &drupal_static('system_filepaths'); + if (isset($system_filepaths[$type][$name]) && $system_filepaths[$type][$name] != $filename_from_file_scan) { + // This file has moved. Cache its new location into the missing and + // moved files list. + $file_scans[$type][$name] = $filename_from_file_scan[$type][$name]; + // Make sure our change to the missing and moved files list will be + // written to persistent cache. + $file_scans['#write_cache'] = TRUE; + trigger_error(format_string('The following %type has moved on the file system: %name. In order to fix this, clear caches or put the file back in its original location. For more information, see the documentation page.', array('%type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING); } - return $files[$type][$name]; + return $filename_from_file_scan; } else { - // The requested file does not exist in any location. - if (!isset($missing[$type][$name])) { - // Mark the file as missing. - $missing[$type][$name] = TRUE; - // Make sure our change to the missing files list will be written to - // persistent cache. - // This cache will be cleared on module/theme rebuild. - $missing['#write_cache'] = TRUE; + trigger_error(format_string('The following %type is missing from the file system: %name. In order to fix this, put the file back in its original location or uninstall the module. For more information, see the documentation page.', array('%type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING); + // Make sure our change to the missing files list will be written to + // persistent cache. + if (!isset($file_scans[$type][$name])) { + $file_scans['#write_cache'] = TRUE; } - // Trigger a user-level warning on every request for the missing file. - trigger_error(format_string('The following @type is missing from the file system: @name. For more information, see the documentation page.', array('@type' => $type, '@name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING); + // Mark the file as missing. + $file_scans[$type][$name] = FALSE; } } /** - * Writes the missing and moved files to persistent cache. + * Helper function for drupal_get_filename(). + * + * Performs a file scan. * - * @param array $missing - * The array of missing and moved files. + * @param $type + * The type of the item (theme, theme_engine, module, profile). + * @param $name + * The name of the item for which the filename is requested. + * + * @return + * The filename of the requested item or NULL if the item is not found. + */ +function _drupal_get_filename_perform_file_scan($type, $name) { + static $dirs = array(), $files = array(); + + // We have a consistent directory naming: modules, themes... + $dir = $type . 's'; + if ($type == 'theme_engine') { + $dir = 'themes/engines'; + $extension = 'engine'; + } + elseif ($type == 'theme') { + $extension = 'info'; + } + else { + $extension = $type; + } + + // Check if we had already scanned this directory/extension combination, + // or if this file had been marked as missing/moved. + if (!isset($dirs[$dir][$extension])) { + // Log that we have now scanned this directory/extension combination + // into a static variable to prevent unnecessary scans. + $dirs[$dir][$extension] = TRUE; + if (!function_exists('drupal_system_listing')) { + require_once DRUPAL_ROOT . '/includes/common.inc'; + } + // Scan the appropriate directories for all files with the requested + // extension, not just the file we are currently looking for. This + // prevents unnecessary scans from being repeated when this function is + // called more than once in the same page request. + $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0); + foreach ($matches as $matched_name => $file) { + // Log the locations found in the file scan into a static variable. + $files[$type][$matched_name] = $file->uri; + } + } + + // If current file had been found in a file scan earlier on in this + // request, use the new location that had been found in the file scan. + if (isset($files[$type][$name])) { + return $files[$type][$name]; + } +} + +/** + * Writes the missing and moved files to persistent cache. */ -function drupal_missing_write_cache() { +function drupal_file_scan_write_cache() { // Only write to cache if we are fully bootstrapped. if (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL) { return; } - $missing = &drupal_static('drupal_get_filename:missing'); - if (isset($missing['#write_cache'])) { + $file_scans = &drupal_static('_drupal_get_filename_fallback'); + if (isset($file_scans['#write_cache'])) { // Merge the newly found out missing and moved file data with // the previously existing data, if we hadn't done so yet. - if (!isset($missing['#cache_merge_done'])) { - $cache = cache_get('drupal_get_filename:missing', 'cache_bootstrap'); + if (!isset($file_scans['#cache_merge_done'])) { + $cache = cache_get('_drupal_get_filename_fallback', 'cache_bootstrap'); if (isset($cache->data)) { - $missing = drupal_array_merge_deep($cache->data, $missing); + $file_scans = drupal_array_merge_deep($cache->data, $file_scans); } } - $missing['#write_cache'] = NULL; - cache_set('drupal_get_filename:missing', $missing, 'cache_bootstrap'); + $file_scans['#write_cache'] = NULL; + cache_set('_drupal_get_filename_fallback', $file_scans, 'cache_bootstrap'); } } diff -u b/includes/common.inc b/includes/common.inc --- b/includes/common.inc +++ b/includes/common.inc @@ -2751,7 +2751,7 @@ _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE); drupal_cache_system_paths(); module_implements_write_cache(); - drupal_missing_write_cache(); + drupal_file_scan_write_cache(); system_run_automated_cron(); } diff -u b/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test --- b/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -432,11 +432,15 @@ restore_error_handler(); // Get the missing records static from drupal_get_filename(). - $missing = &drupal_static('drupal_get_filename:missing'); + $file_scans = &drupal_static('_drupal_get_filename_fallback'); // Searching for an item that does not exist creates a static record in // drupal_get_filename(). - $this->assertTrue($missing['module'][$non_existing_module], 'Searching for an item that does not exist creates a static record in drupal_get_filename().'); + $this->assertTrue($file_scans['module'][$non_existing_module] === FALSE, 'Searching for an item that does not exist creates a static record in drupal_get_filename().'); + drupal_install_schema('system'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_get_filename_fallback', 'cache_bootstrap'); + $this->assertTrue($cache->data['module'][$non_existing_module] === FALSE, 'File scan results are correctly saved in persistent cache.'); } /** diff -u b/modules/system/system.module b/modules/system/system.module --- b/modules/system/system.module +++ b/modules/system/system.module @@ -2366,8 +2366,8 @@ */ function _system_rebuild_module_data() { // Clean up the bootstrap "missing files" cache when rebuilding module data. - drupal_static_reset('drupal_get_filename:missing'); - cache_clear_all('drupal_get_filename:missing', 'cache'); + drupal_static_reset('_drupal_get_filename_fallback'); + cache_clear_all('_drupal_get_filename_fallback', 'cache_bootstrap'); // Find modules $modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0); @@ -2506,8 +2506,8 @@ */ function _system_rebuild_theme_data() { // Clean up the bootstrap "missing files" cache when rebuilding theme data. - drupal_static_reset('drupal_get_filename:missing'); - cache_clear_all('drupal_get_filename:missing', 'cache'); + drupal_static_reset('_drupal_get_filename_fallback'); + cache_clear_all('_drupal_get_filename_fallback', 'cache_bootstrap'); // Find themes $themes = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info$/', 'themes'); only in patch2: unchanged: --- a/includes/module.inc +++ b/includes/module.inc @@ -121,6 +121,7 @@ function module_list($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE */ function system_list($type) { $lists = &drupal_static(__FUNCTION__); + $system_filepaths = &drupal_static('system_filepaths'); // For bootstrap modules, attempt to fetch the list from cache if possible. // if not fetch only the required information to fire bootstrap hooks @@ -208,9 +209,12 @@ function system_list($type) { } cache_set('system_list', $lists, 'cache_bootstrap'); } - // To avoid a separate database lookup for the filepath, prime the - // drupal_get_filename() static cache with all enabled modules and themes. foreach ($lists['filepaths'] as $item) { + // Save the file paths from the database into a static variable so we + // can determine later if the files have moved. + $system_filepaths[$item['type']][$item['name']] = $item['filepath']; + // To avoid a separate database lookup for the filepath, prime the + // drupal_get_filename() static cache with all enabled modules and themes. drupal_get_filename($item['type'], $item['name'], $item['filepath']); } }