diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index b66ae17..366abfb 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,252 @@ 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])) {
+ 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 @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') {
+ trigger_error(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;
+ }
+}
+
+/**
+ * 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/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) {