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) {