diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index b66ae17..acfff8e 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -828,14 +828,21 @@ 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 or has unexpectedly + * moved. This defaults to TRUE, but can be set to FALSE by calling code that + * merely wants to check whether an item exists in the filesystem. * * @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 $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. // The location of files will not change during the request, so do not use // drupal_static(). - static $files = array(), $dirs = array(); + static $files = array(); // Profiles are a special case: they have a fixed location and naming. if ($type == 'profile') { @@ -847,64 +854,290 @@ 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; } elseif (isset($files[$type][$name])) { - // nothing + // This item had already been found earlier in the request, either through + // priming of the static cache (for example, 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 doing so, since this function is + // called both before we have a database connection (i.e. during + // installation) and when a database connection fails. + $database_unavailable = TRUE; 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 && file_exists(DRUPAL_ROOT . '/' . $file)) { $files[$type][$name] = $file; } + $database_unavailable = FALSE; } } 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. + // 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. } - // Fallback to searching the filesystem if the database could not find the - // file or the file returned by the database is not found. + // 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, $database_unavailable); + } + } - 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])) { + return $files[$type][$name]; + } +} + +/** + * Performs a cached file system scan as a fallback when searching for a file. + * + * 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 in a previous + * file scan, or if it has been marked as moved and is still in the last known + * location, 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 trigger an error when a file is missing or has unexpectedly + * moved. + * @param bool $database_unavailable + * Whether this function is being called because the Drupal database could + * not be queried for the file's location. + * + * @return + * The filename of the requested item or NULL if the item is not found. + * + * @see drupal_get_filename() + */ +function _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable) { + $file_scans = &_drupal_file_scan_cache(); + $filename = NULL; + + // If the cache indicates that the item is missing, or we can verify that the + // item exists in the location the cache says it exists in, use that. + if (isset($file_scans[$type][$name]) && ($file_scans[$type][$name] === FALSE || file_exists($file_scans[$type][$name]))) { + $filename = $file_scans[$type][$name]; + } + // Otherwise, perform a new file scan to find the item. + else { + $filename = _drupal_get_filename_perform_file_scan($type, $name); + // Update the static cache, and mark the persistent cache for updating at + // the end of the page request. See drupal_file_scan_write_cache(). + $file_scans[$type][$name] = $filename; + $file_scans['#write_cache'] = TRUE; + } + + // If requested, trigger a user-level warning about the missing or + // unexpectedly moved file. If the database was unavailable, do not trigger a + // warning in the latter case, though, since if the {system} table could not + // be queried there is no way to know if the location found here was + // "unexpected" or not. + if ($trigger_error) { + $error_type = $filename === FALSE ? 'missing' : 'moved'; + if ($error_type == 'missing' || !$database_unavailable) { + _drupal_get_filename_fallback_trigger_error($type, $name, $error_type); + } + } + + // The cache stores FALSE for files that aren't found (to be able to + // distinguish them from files that have not yet been searched for), but + // drupal_get_filename() expects NULL for these instead, so convert to NULL + // before returning. + if ($filename === FALSE) { + $filename = NULL; + } + return $filename; +} + +/** + * Returns the current list of cached file system scan results. + * + * @return + * An associative array tracking the most recent file scan results for all + * files that have had scans performed. The keys are the type and name of the + * item that was searched for, and the values can be either: + * - Boolean FALSE if the item was not found in the file system. + * - A string pointing to the location where the item was found. + */ +function &_drupal_file_scan_cache() { + $file_scans = &drupal_static(__FUNCTION__, array()); + + // The file scan results are stored in a persistent cache (in addition to the + // static cache) but because this function can be called before the + // persistent cache is available, we must merge any items that were found + // earlier in the page request into the results from the persistent cache. + if (!isset($file_scans['#cache_merge_done'])) { + try { + if (function_exists('cache_get')) { + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + if (!empty($cache->data)) { + // File scan results from the current request should take precedence + // over the results from the persistent cache, since they are newer. + $file_scans = drupal_array_merge_deep($cache->data, $file_scans); } + // Set a flag to indicate that the persistent cache does not need to be + // merged again. + $file_scans['#cache_merge_done'] = TRUE; } } + catch (Exception $e) { + // Hide the error. + } } - if (isset($files[$type][$name])) { - return $files[$type][$name]; + return $file_scans; +} + +/** + * Performs a file system scan to search for a system resource. + * + * @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 FALSE if the item is not found. + * + * @see drupal_get_filename() + * @see _drupal_get_filename_fallback() + */ +function _drupal_get_filename_perform_file_scan($type, $name) { + // The location of files will not change during the request, so do not use + // drupal_static(). + 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; + } + } + + // Return the results of the file system scan, or FALSE to indicate the file + // was not found. + return isset($files[$type][$name]) ? $files[$type][$name] : FALSE; +} + +/** + * Triggers a user-level warning for missing or unexpectedly 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. + * @param $error_type + * The type of the error ('missing' or 'moved'). + * + * @see drupal_get_filename() + * @see _drupal_get_filename_fallback() + */ +function _drupal_get_filename_fallback_trigger_error($type, $name, $error_type) { + // Make sure we only show any missing or moved file errors only once per + // request. + static $errors_triggered = array(); + if (empty($errors_triggered[$type][$name][$error_type])) { + // Use _drupal_trigger_error_with_delayed_logging() here since these are + // triggered during low-level operations that cannot necessarily be + // interrupted by a watchdog() call. + if ($error_type == 'missing') { + _drupal_trigger_error_with_delayed_logging(format_string('The following @type is missing from the file system: %name. In order to fix this, put the @type 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); + } + elseif ($error_type == 'moved') { + _drupal_trigger_error_with_delayed_logging(format_string('The following @type has moved within the file system: %name. In order to fix this, clear caches or put the @type 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; + } +} + +/** + * Invokes trigger_error() with logging delayed until the end of the request. + * + * This is an alternative to PHP's trigger_error() function which can be used + * during low-level Drupal core operations that need to avoid being interrupted + * by a watchdog() call. + * + * Normally, Drupal's error handler calls watchdog() in response to a + * trigger_error() call. However, this invokes hook_watchdog() which can run + * arbitrary code. If the trigger_error() happens in the middle of an + * operation such as a rebuild operation which should not be interrupted by + * arbitrary code, that could potentially break or trigger the rebuild again. + * This function protects against that by delaying the watchdog() call until + * the end of the current page request. + * + * This is an internal function which should only be called by low-level Drupal + * core functions. It may be removed in a future Drupal 7 release. + * + * @param string $error_msg + * The error message to trigger. As with trigger_error() itself, this is + * limited to 1024 bytes; additional characters beyond that will be removed. + * @param int $error_type + * (optional) The type of error. This should be one of the E_USER family of + * constants. As with trigger_error() itself, this defaults to E_USER_NOTICE + * if not provided. + * + * @see _drupal_log_error() + */ +function _drupal_trigger_error_with_delayed_logging($error_msg, $error_type = E_USER_NOTICE) { + $delay_logging = &drupal_static(__FUNCTION__, FALSE); + $delay_logging = TRUE; + trigger_error($error_msg, $error_type); + $delay_logging = FALSE; +} + +/** + * 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 requested, and if we know that any + // data previously in the cache was successfully loaded and merged in by + // _drupal_file_scan_cache(). + $file_scans = &_drupal_file_scan_cache(); + if (isset($file_scans['#write_cache']) && isset($file_scans['#cache_merge_done'])) { + unset($file_scans['#write_cache']); + cache_set('_drupal_file_scan_cache', $file_scans, 'cache_bootstrap'); } } diff --git a/includes/common.inc b/includes/common.inc index 717f568..1cba3f0 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2770,6 +2770,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/errors.inc b/includes/errors.inc index a9b7b5b..7393148 100644 --- a/includes/errors.inc +++ b/includes/errors.inc @@ -199,7 +199,16 @@ function _drupal_log_error($error, $fatal = FALSE) { $number++; } - watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + // Log the error immediately, unless this is a non-fatal error which has been + // triggered via drupal_trigger_error_with_delayed_logging(); in that case + // trigger it in a shutdown function. Fatal errors are always triggered + // immediately since for a fatal error the page request will end here anyway. + if (!$fatal && drupal_static('_drupal_trigger_error_with_delayed_logging')) { + drupal_register_shutdown_function('watchdog', 'php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + } + else { + watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + } if ($fatal) { drupal_add_http_header('Status', '500 Service unavailable (with message)'); diff --git a/includes/module.inc b/includes/module.inc index 1f9252e..2e25108 100644 --- a/includes/module.inc +++ b/includes/module.inc @@ -227,6 +227,10 @@ 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_file_scan_cache'); + cache_clear_all('_drupal_file_scan_cache', 'cache_bootstrap'); } /** diff --git a/includes/update.inc b/includes/update.inc index 35a73c3..2167db7 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 29a20bb..cf83047 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -374,7 +374,10 @@ 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 3fbec37..16ac171 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -388,13 +388,20 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { public static function getInfo() { return array( - 'name' => 'Get filename test', - 'description' => 'Test that drupal_get_filename() works correctly when the file is not found in the database.', + 'name' => 'Get filename test (without the system table)', + 'description' => 'Test that drupal_get_filename() works correctly when the database is not available.', 'group' => 'Bootstrap', ); } /** + * The last file-related error message triggered by the filename test. + * + * Used by BootstrapGetFilenameTestCase::testDrupalGetFilename(). + */ + protected $getFilenameTestTriggeredError; + + /** * Test that drupal_get_filename() works correctly when the file is not found in the database. */ function testDrupalGetFilename() { @@ -423,6 +430,203 @@ 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.')); + + // When searching for a module that does not exist, drupal_get_filename() + // should return NULL and trigger an appropriate error message. + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $non_existing_module = $this->randomName(); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for an item that does not exist triggers the correct error.'); + restore_error_handler(); + + // Check that the result is stored in the file system scan cache. + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files static variable.'); + + // Performing the search again in the same request still should not find + // the file, but the error message should not be repeated (therefore we do + // not override the error handler here). + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL during the second search.'); + } + + /** + * 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 (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { + $this->getFilenameTestTriggeredError = $message; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); + } +} + +/** + * Test drupal_get_filename() in the context of a full Drupal installation. + */ +class BootstrapGetFilenameWebTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Get filename test (full installation)', + 'description' => 'Test that drupal_get_filename() works correctly in the context of a full Drupal installation.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * The last file-related error message triggered by the filename test. + * + * Used by BootstrapGetFilenameWebTestCase::testDrupalGetFilename(). + */ + protected $getFilenameTestTriggeredError; + + /** + * Test that drupal_get_filename() works correctly with a full Drupal site. + */ + function testDrupalGetFilename() { + // Search for a module that exists in the file system and the {system} + // table and make sure that it is found. + $this->assertIdentical(drupal_get_filename('module', 'node'), 'modules/node/node.module', 'Module found at expected location.'); + + // Search for a module that does not exist in either the file system or the + // {system} table. Make sure that an appropriate error is triggered and + // that the module winds up in the static and persistent cache. + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $non_existing_module = $this->randomName(); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for a module that does not exist triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files persistent cache.'); + + // Simulate moving a module to a location that does not match the location + // in the {system} table and perform similar tests as above. + db_update('system') + ->fields(array('filename' => 'modules/simpletest/tests/fake_location/module_test.module')) + ->condition('name', 'module_test') + ->condition('type', 'module') + ->execute(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertIdentical(drupal_get_filename('module', 'module_test'), 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved finds the module at its new location.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module has moved within the file system: %name', array('%name' => 'module_test'))) === 0, 'Searching for a module that has moved triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module']['module_test'], 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module']['module_test'], 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved creates a record in the missing and moved files persistent cache.'); + + // Simulate a module that exists in the {system} table but does not exist + // in the file system and perform similar tests as above. + $non_existing_module = $this->randomName(); + db_update('system') + ->fields(array('name' => $non_existing_module)) + ->condition('name', 'module_test') + ->condition('type', 'module') + ->execute(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that exists in the system table but not in the file system returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for a module that exists in the system table but not in the file system triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that exists in the system table but not in the file system creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module'][$non_existing_module], FALSE, 'Searching for a module that exists in the system table but not in the file system creates a record in the missing and moved files persistent cache.'); + + // Simulate a module that exists in the file system but not in the {system} + // table and perform similar tests as above. + db_delete('system') + ->condition('name', 'common_test') + ->condition('type', 'module') + ->execute(); + system_list_reset(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertIdentical(drupal_get_filename('module', 'common_test'), 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table finds the module at its actual location.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module has moved within the file system: %name', array('%name' => 'common_test'))) === 0, 'Searching for a module that does not exist in the system table triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module']['common_test'], 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module']['common_test'], 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table creates a record in the missing and moved files 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 (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { + $this->getFilenameTestTriggeredError = $message; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); + } + + /** + * Test that watchdog messages about missing files are correctly recorded. + */ + public function testWatchdog() { + // Search for a module that does not exist in either the file system or the + // {system} table. Make sure that an appropriate warning is recorded in the + // logs. + $non_existing_module = $this->randomName(); + $query_parameters = array( + ':type' => 'php', + ':severity' => WATCHDOG_WARNING, + ); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchField(), 0, 'No warning message appears in the logs before searching for a module that does not exist.'); + // Trigger the drupal_get_filename() call. This must be done via a request + // to a separate URL since the watchdog() will happen in a shutdown + // function, and so that SimpleTest can be told to ignore (and not fail as + // a result of) the expected PHP warnings generated during this process. + variable_set('system_test_drupal_get_filename_test_module_name', $non_existing_module); + $this->drupalGet('system-test/drupal-get-filename'); + $message_variables = db_query('SELECT variables FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchCol(); + $this->assertEqual(count($message_variables), 1, 'A single warning message appears in the logs after searching for a module that does not exist.'); + $variables = reset($message_variables); + $variables = unserialize($variables); + $this->assertTrue(isset($variables['!message']) && strpos($variables['!message'], format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) !== FALSE, 'The warning message that appears in the logs after searching for a module that does not exist contains the expected text.'); + } + + /** + * Test that drupal_get_filename() does not break recursive rebuilds. + */ + public function testRecursiveRebuilds() { + // Ensure that the drupal_get_filename() call due to a missing module does + // not break the data returned by an attempted recursive rebuild. The code + // path which is tested is as follows: + // - Call drupal_get_schema(). + // - Within a hook_schema() implementation, trigger a drupal_get_filename() + // search for a nonexistent module. + // - In the watchdog() call that results from that, trigger + // drupal_get_schema() again. + // Without some kind of recursion protection, this could cause the second + // drupal_get_schema() call to return incomplete results. This test ensures + // that does not happen. + $non_existing_module = $this->randomName(); + variable_set('system_test_drupal_get_filename_test_module_name', $non_existing_module); + $this->drupalGet('system-test/drupal-get-filename-with-schema-rebuild'); + $original_drupal_get_schema_tables = variable_get('system_test_drupal_get_filename_with_schema_rebuild_original_tables'); + $final_drupal_get_schema_tables = variable_get('system_test_drupal_get_filename_with_schema_rebuild_final_tables'); + $this->assertTrue(!empty($original_drupal_get_schema_tables)); + $this->assertTrue(!empty($final_drupal_get_schema_tables)); + $this->assertEqual($original_drupal_get_schema_tables, $final_drupal_get_schema_tables); } } diff --git a/modules/simpletest/tests/system_test.install b/modules/simpletest/tests/system_test.install new file mode 100644 index 0000000..18d4af9 --- /dev/null +++ b/modules/simpletest/tests/system_test.install @@ -0,0 +1,22 @@ + MENU_CALLBACK, ); + $items['system-test/drupal-get-filename'] = array( + 'title' => 'Test drupal_get_filename()', + 'page callback' => 'system_test_drupal_get_filename', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/drupal-get-filename-with-schema-rebuild'] = array( + 'title' => 'Test drupal_get_filename() with a schema rebuild', + 'page callback' => 'system_test_drupal_get_filename_with_schema_rebuild', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -482,3 +496,76 @@ function system_test_request_destination() { // information. exit; } + +/** + * Page callback to run drupal_get_filename() on a particular module. + */ +function system_test_drupal_get_filename() { + // Prevent SimpleTest from failing as a result of the expected PHP warnings + // this function causes. Any warnings will be recorded in the database logs + // for examination by the tests. + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + drupal_get_filename('module', $module_name); + + return ''; +} + +/** + * Page callback to run drupal_get_filename() and do a schema rebuild. + */ +function system_test_drupal_get_filename_with_schema_rebuild() { + // Prevent SimpleTest from failing as a result of the expected PHP warnings + // this function causes. + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + // Record the original database tables from drupal_get_schema(). + variable_set('system_test_drupal_get_filename_with_schema_rebuild_original_tables', array_keys(drupal_get_schema(NULL, TRUE))); + + // Trigger system_test_schema() and system_test_watchdog() to perform an + // attempted recursive rebuild when drupal_get_schema() is called. See + // BootstrapGetFilenameWebTestCase::testRecursiveRebuilds(). + variable_set('system_test_drupal_get_filename_attempt_recursive_rebuild', TRUE); + drupal_get_schema(NULL, TRUE); + + return ''; +} + +/** + * Implements hook_watchdog(). + */ +function system_test_watchdog($log_entry) { + // If an attempted recursive schema rebuild has been triggered by + // system_test_drupal_get_filename_with_schema_rebuild(), perform the rebuild + // in response to the missing file message triggered by system_test_schema(). + if (!variable_get('system_test_drupal_get_filename_attempt_recursive_rebuild')) { + return; + } + if ($log_entry['type'] != 'php' || $log_entry['severity'] != WATCHDOG_WARNING) { + return; + } + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + if (!isset($log_entry['variables']['!message']) || strpos($log_entry['variables']['!message'], format_string('The following module is missing from the file system: %name', array('%name' => $module_name))) === FALSE) { + return; + } + variable_set('system_test_drupal_get_filename_with_schema_rebuild_final_tables', array_keys(drupal_get_schema())); +} + +/** + * Implements hook_module_implements_alter(). + */ +function system_test_module_implements_alter(&$implementations, $hook) { + // For BootstrapGetFilenameWebTestCase::testRecursiveRebuilds() to work + // correctly, this module's hook_schema() implementation cannot be either the + // first implementation (since that would trigger a potential recursive + // rebuild before anything is in the drupal_get_schema() cache) or the last + // implementation (since that would trigger a potential recursive rebuild + // after the cache is already complete). So put it somewhere in the middle. + if ($hook == 'schema') { + $group = $implementations['system_test']; + unset($implementations['system_test']); + $count = count($implementations); + $implementations = array_merge(array_slice($implementations, 0, $count / 2, TRUE), array('system_test' => $group), array_slice($implementations, $count / 2, NULL, TRUE)); + } +} 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) {