diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index b2f2b04..bf59d6b 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -828,14 +828,25 @@ function drupal_settings_initialize() { * @param $filename * The filename of the item if it is to be set explicitly rather * than by consulting the database. + * @param bool $trigger_error + * Whether to trigger an error when a file is missing / moved. Defaults to + * TRUE, but we may want to set this to FALSE if we merely want to check + * whether an item is installed. * * @return * The filename of the requested item or NULL if the item is not found. */ -function drupal_get_filename($type, $name, $filename = NULL) { +function drupal_get_filename($type, $name, $filename = NULL, $trigger_error = TRUE) { // 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. + // We can be sure that any file listed in this static variable actually + // exists as all additions have gone through a file_exists() check. + static $files = array(); + + // This flag may hold a string that indicates where the filename was retrieved + // from. + $filename_retrieved_from = ''; // Profiles are a special case: they have a fixed location and naming. if ($type == 'profile') { @@ -847,68 +858,288 @@ function drupal_get_filename($type, $name, $filename = NULL) { } if (!empty($filename) && file_exists($filename)) { + // Prime the static cache with the provided filename. $files[$type][$name] = $filename; + $filename_retrieved_from = 'priming'; } elseif (isset($files[$type][$name])) { - // nothing + // This item had already been found earlier in the request, either through + // priming of the static cache (i.e. in system_list()), through a lookup + // in the {system} table, or through a file scan (cached or not). + // 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 filename 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 { + $system_query_success = FALSE; 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; + $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; + $filename_retrieved_from = 'system_table'; + } } + $system_query_success = TRUE; } } 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 file returned by the database is not found. + // the database might be down, or we may have done a non-database cache + // flush while $conf['page_cache_without_database'] = TRUE and + // $conf['page_cache_invoke_hooks'] = TRUE. We have a fallback for these + // cases so we hide the error completely. + // We also hide the trigger_error() "moved or newly introduced" warning + // in this case as we can't be sure if it's correct. + } + if (!$system_query_success) { + // We can't tell if the error message will be valid if the query to the + // {system} table failed, so skip triggering of errors. + $trigger_error = FALSE; + } + // Fall back to searching the filesystem if the database could not find the + // file or the file does not exist at the path returned by the database. 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; - } + $files[$type][$name] = _drupal_get_filename_fallback($type, $name, $trigger_error); + } + } - if (!isset($dirs[$dir][$extension])) { - $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) { - $files[$type][$matched_name] = $file->uri; + if (isset($files[$type][$name])) { + // We found a file. + // Get the static file scan list. This list may be incomplete as there + // may be further files listed in persistent cache, but we skip loading that + // as a performance optimization, as the persistent cache record will + // be cleared on every system_rebuild_module_data() anyway. + $file_scans = &drupal_static('_drupal_get_filename_fallback'); + // If this file had previously been marked as missing, clean up the entry + // from the file scan cache. + $missing_reappeared = isset($file_scans[$type][$name]) && $file_scans[$type][$name] === FALSE; + // If this file had previously been marked as moved, and reappeared + // at the location listed in the system table or in the primed static, + // clean up the entry from the file scan cache. + $moved_reappeared = isset($file_scans[$type][$name]) && is_string($file_scans[$type][$name]) && ($filename_retrieved_from == 'system_table' || $filename_retrieved_from == 'priming'); + if ($missing_reappeared || $moved_reappeared) { + $file_scans[$type][$name] = NULL; + $file_scans['#write_cache'] = TRUE; + } + return $files[$type][$name]; + } +} + +/** + * Helper function for drupal_get_filename(). + * + * This function looks for the requested file by triggering a file scan, + * caching the new location if the file has moved and caching the miss + * if the file is missing. If a file had been marked as missing or moved + * in a previous file scan, no new file scan 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. + * @param bool $trigger_error + * Whether to create an error when a file is missing or moved. + * + * @return string + * The filename of the requested item or NULL if the item is not found. + */ +function _drupal_get_filename_fallback($type, $name, $trigger_error) { + // 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 type and name of the item, and as value: + // - the boolean FALSE if the item is missing + // - a string with location found in a file scan if the module/theme 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 list of files marked as missing and moved during earlier file scans + // from the 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 to the file scan cache done in the current + // request (including the setting of previously set records to NULL) + // with the values that had been stored in the persistent cache. + $file_scans = drupal_array_merge_deep($cache->data, $file_scans); + // Set a flag so we remember that we've done a merge with the values + // stored in the persistent cache. } + $file_scans['#cache_merge_done'] = TRUE; + } + } + catch (Exception $e) { + // Hide the error. + } + } + + // Check whether this file had moved from the path listed in the {system} + // table during a previous file scan. + if (isset($file_scans[$type][$name]) && is_string($file_scans[$type][$name]) && file_exists($file_scans[$type][$name])) { + // The file exists at the location cached in a previous file scan. + $filename_from_file_scan = $file_scans[$type][$name]; + } + // Unless the module had been marked as missing, perform a file scan. + elseif (!(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)) { + // The file has either moved from the location in the {system} table + // or is not yet listed in the {system} table. + // Make sure our change to the file scan cache will be written to the + // persistent cache. + if (!isset($file_scans[$type][$name]) || isset($file_scans[$type][$name]) && $file_scans[$type][$name] != $filename_from_file_scan) { + $file_scans[$type][$name] = $filename_from_file_scan; + if (!$trigger_error) { + // If we skip error triggering, do not write the found file location to + // persistent cache. + $file_scans['#write_cache'] = TRUE; } } + // Create a user-level warning about the moved / recently introduced file. + if ($trigger_error) { + _drupal_get_filename_fallback_trigger_error($type, $name, 'moved'); + } + return $filename_from_file_scan; + } + else { + // Make sure our change to the file scan cache will be written to + // the persistent cache. + if (!(isset($file_scans[$type][$name]) && $file_scans[$type][$name] === FALSE)) { + $file_scans['#write_cache'] = TRUE; + } + // Mark the file as missing. + $file_scans[$type][$name] = FALSE; + // Create a user-level warning about the missing file. + if ($trigger_error) { + _drupal_get_filename_fallback_trigger_error($type, $name, 'missing'); + } + } +} + +/** + * Helper function for _drupal_get_filename_fallback(). + * + * Creates a user-level warning when a missing or moved file is detected in + * _drupal_get_filename_fallback(). + * + * @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. + * @param $error_type + * The type of the error ('missing' or 'moved'). + */ +function _drupal_get_filename_fallback_trigger_error($type, $name, $error_type) { + // Make sure we only show any missing / moved file error only once per + // request. + static $errors_triggered = array(); + if (empty($errors_triggered[$type][$name][$error_type])) { + if ($error_type == 'missing') { + 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); + } + elseif ($error_type == 'moved') { + trigger_error(format_string('The following @type has moved or has recently appeared within 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); + } + $errors_triggered[$type][$name][$error_type] = TRUE; + } +} + +/** + * Helper function for drupal_get_filename(). + * + * Performs a file scan. + * + * @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. + if (!isset($dirs[$dir][$extension])) { + // Log that we have now scanned this directory/extension combination + // into a static variable so as to prevent unnecessary file 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 file scan cache to the persistent cache. + * + * This cache stores all files marked as missing or moved after a file scan + * to prevent unnecessary file scans in subsequent requests. This cache is + * cleared in system_list_reset() (i.e. after a module/theme rebuild). + */ +function drupal_file_scan_write_cache() { + // Only write to the persistent cache if we are fully bootstrapped. + if (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL) { + return; + } + $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 had not done so yet. + if (!isset($file_scans['#cache_merge_done'])) { + $cache = cache_get('_drupal_get_filename_fallback', 'cache_bootstrap'); + if (isset($cache->data)) { + $file_scans = drupal_array_merge_deep($cache->data, $file_scans); + } + } + $file_scans['#write_cache'] = NULL; + cache_set('_drupal_get_filename_fallback', $file_scans, 'cache_bootstrap'); + } +} + +/** * Loads the persistent variable table. * * The variable table is composed of values that have been saved in the table diff --git a/includes/common.inc b/includes/common.inc index ceac115..414dab0 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_file_scan_write_cache(); system_run_automated_cron(); } diff --git a/includes/module.inc b/includes/module.inc index 076992c..101f5ca 100644 --- a/includes/module.inc +++ b/includes/module.inc @@ -208,9 +208,10 @@ 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) { + // To avoid a separate database lookup for the file path, prime the + // drupal_get_filename() static cache with all enabled modules and all + // themes. drupal_get_filename($item['type'], $item['name'], $item['filepath']); } } @@ -227,6 +228,11 @@ function system_list_reset() { drupal_static_reset('list_themes'); cache_clear_all('bootstrap_modules', 'cache_bootstrap'); cache_clear_all('system_list', 'cache_bootstrap'); + + // Clean up the bootstrap file scan cache. + drupal_static_reset('_drupal_get_filename_fallback'); + drupal_static_reset('system_filepaths'); + cache_clear_all('_drupal_get_filename_fallback', 'cache_bootstrap'); } /** diff --git a/includes/update.inc b/includes/update.inc index a17161c..4f12db8 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..a2e6c6e 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -371,7 +371,11 @@ 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)) { + // Pass FALSE as fourth argument so no error gets created for + // the missing file. + $found_module = drupal_get_filename('module', $module, NULL, FALSE); + + if (!$found_module) { continue 2; } } diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index ece1cd9..9088023 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -371,7 +371,7 @@ class HookBootExitTestCase extends DrupalWebTestCase { /** * Test drupal_get_filename()'s availability. */ -class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { +class BootstrapGetFilenameTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -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,40 @@ 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 and moved files static variable. + $file_scans = &drupal_static('_drupal_get_filename_fallback'); + + // Searching for an item that does not exist creates a record in the static + // variable. + $this->assertTrue($file_scans['module'][$non_existing_module] === FALSE, 'Searching for an item that does not exist creates a record in the missing and moved files static variable.'); + 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.'); + } + + /** + * 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.updater.inc b/modules/system/system.updater.inc index a14d788..2a32c4b 100644 --- a/modules/system/system.updater.inc +++ b/modules/system/system.updater.inc @@ -24,7 +24,7 @@ class ModuleUpdater extends Updater implements DrupalUpdaterInterface { * found on your system, and if there was a copy in sites/all, we'd see it. */ public function getInstallDirectory() { - if ($relative_path = drupal_get_path('module', $this->name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('module', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -34,7 +34,7 @@ class ModuleUpdater extends Updater implements DrupalUpdaterInterface { } public function isInstalled() { - return (bool) drupal_get_path('module', $this->name); + return (bool) drupal_get_filename('module', $this->name, NULL, FALSE); } public static function canUpdateDirectory($directory) { @@ -109,7 +109,7 @@ class ThemeUpdater extends Updater implements DrupalUpdaterInterface { * found on your system, and if there was a copy in sites/all, we'd see it. */ public function getInstallDirectory() { - if ($relative_path = drupal_get_path('theme', $this->name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('theme', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -119,7 +119,7 @@ class ThemeUpdater extends Updater implements DrupalUpdaterInterface { } public function isInstalled() { - return (bool) drupal_get_path('theme', $this->name); + return (bool) drupal_get_filename('theme', $this->name, NULL, FALSE); } static function canUpdateDirectory($directory) {