diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 847bfc3..4396889 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -835,7 +835,10 @@ function drupal_settings_initialize() { function drupal_get_filename($type, $name, $filename = NULL) { // The location of files will not change during the request, so do not use // drupal_static(). - static $files = array(), $dirs = array(); + // 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(); // Profiles are a special case: they have a fixed location and naming. if ($type == 'profile') { @@ -850,18 +853,28 @@ function drupal_get_filename($type, $name, $filename = NULL) { $files[$type][$name] = $filename; } elseif (isset($files[$type][$name])) { - // nothing + // This item had already been found earlier in the request. Do nothing. } - // 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. 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 && file_exists(DRUPAL_ROOT . '/' . $file)) { - $files[$type][$name] = $file; + 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; + } } } } @@ -871,7 +884,8 @@ function drupal_get_filename($type, $name, $filename = NULL) { // hide the error completely. } // Fallback to searching the filesystem if the database could not find the - // file or the file returned by the database is not found. + // file or the filename returned by the database is not found. We cache + // missing and moved files in order to prevent unnecessary file scans. if (!isset($files[$type][$name])) { // We have a consistent directory naming: modules, themes... $dir = $type . 's'; @@ -886,7 +900,59 @@ function drupal_get_filename($type, $name, $filename = NULL) { $extension = $type; } - if (!isset($dirs[$dir][$extension])) { + // 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; + } + } + + // 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'; @@ -897,15 +963,80 @@ function drupal_get_filename($type, $name, $filename = NULL) { // 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) { - $files[$type][$matched_name] = $file->uri; + // Log the locations found in the file scan into a static variable. + $files_scanned[$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_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); } } } } 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; + } return $files[$type][$name]; } + 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 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); + } +} + +/** + * Writes the missing and moved files to persistent cache. + * + * @param array $missing + * The array of missing and moved files. + */ +function drupal_missing_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'])) { + // 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($cache->data)) { + $missing = drupal_array_merge_deep($cache->data, $missing); + } + } + $missing['#write_cache'] = NULL; + cache_set('drupal_get_filename:missing', $missing, 'cache_bootstrap'); + } } /** diff --git a/includes/common.inc b/includes/common.inc index cd30145..ffb35a4 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2751,6 +2751,7 @@ function drupal_page_footer() { _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE); drupal_cache_system_paths(); module_implements_write_cache(); + drupal_missing_write_cache(); system_run_automated_cron(); } diff --git a/includes/update.inc b/includes/update.inc index a17161c..c23519d 100644 --- a/includes/update.inc +++ b/includes/update.inc @@ -795,6 +795,14 @@ function update_fix_d7_requirements() { function update_fix_d7_install_profile() { $profile = drupal_get_profile(); + // 'Default' profile has been renamed to 'Standard' in D7. + // We change the profile here to prevent a broken record in the system table. + // @see system_update_7049() + if ($profile == 'default') { + $profile = 'standard'; + variable_set('install_profile', $profile); + } + $results = db_select('system', 's') ->fields('s', array('name', 'schema_version')) ->condition('name', $profile) diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index 91f0f90..73cd14f 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -371,7 +371,18 @@ function simpletest_test_get_all() { // If this test class requires a non-existing module, skip it. if (!empty($info['dependencies'])) { foreach ($info['dependencies'] as $module) { - if (!drupal_get_filename('module', $module)) { + + // Searching for an item that does not exist triggers an PHP + // error. Set a custom error handler so we can ignore the file + // not found error. + set_error_handler('_simpletest_file_not_found_error_handler'); + + $found_module = drupal_get_filename('module', $module); + + // Restore the original error handler. + restore_error_handler(); + + if (!$found_module) { continue 2; } } @@ -395,6 +406,16 @@ function simpletest_test_get_all() { return $groups; } +/** + * Skips handling of "file not found" errors. + */ +function _simpletest_file_not_found_error_handler($error_level, $message, $filename, $line, $context) { + if (strstr($message, 'is missing from the file system:')) { + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); +} + /* * Register a simple class loader that can find D8-style PSR-0 test classes. * diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index ece1cd9..71bcaff 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -382,6 +382,15 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { } /** + * Whether the filename test triggered the right error. + * + * Used by BootstrapGetFilenameTestCase::testDrupalGetFilename(). + * + * @var boolean + */ + protected $getFilenameTestTriggeredError = FALSE; + + /** * Test that drupal_get_filename() works correctly when the file is not found in the database. */ function testDrupalGetFilename() { @@ -410,6 +419,36 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { // automatically check there for 'script' files, just as it does for (e.g.) // 'module' files in modules. $this->assertIdentical(drupal_get_filename('script', 'test'), 'scripts/test.script', t('Retrieve test script location.')); + + // Generate a non-existing module name. + $non_existing_module = $this->randomName(); + + // Searching for an item that does not exist returns NULL. + // Set a custom error handler so we can ignore the file not found error. + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for an item that does not exist returns NULL.'); + $this->assertTrue($this->getFilenameTestTriggeredError, 'Searching for an item that does not exist triggers an error.'); + // Restore the original error handler. + restore_error_handler(); + + // Get the missing records static from drupal_get_filename(). + $missing = &drupal_static('drupal_get_filename:missing'); + + // 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().'); + } + + /** + * Skips handling of "file not found" errors. + */ + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + // Skip error handling if this is a "file not found" error. + if (strstr($message, 'is missing from the file system:')) { + $this->getFilenameTestTriggeredError = TRUE; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); } } diff --git a/modules/system/system.module b/modules/system/system.module index 8fc517f..10e672e 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -2365,6 +2365,10 @@ function system_get_info($type, $name = NULL) { * An associative array of module information. */ 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'); + // Find modules $modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0); @@ -2501,6 +2505,10 @@ function _system_update_bootstrap_status() { * An associative array of themes information. */ 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'); + // Find themes $themes = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info$/', 'themes'); // Allow modules to add further themes.