Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

564
web/core/includes/batch.inc Normal file
View File

@ -0,0 +1,564 @@
<?php
/**
* @file
* Batch processing API for processes to run in multiple HTTP requests.
*
* Note that batches are usually invoked by form submissions, which is
* why the core interaction functions of the batch processing API live in
* form.inc.
*
* @see form.inc
* @see batch_set()
* @see batch_process()
* @see batch_get()
*/
use Drupal\Component\Utility\Timer;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Batch\Percentage;
use Drupal\Core\Form\FormState;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Renders the batch processing page based on the current state of the batch.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @see _batch_shutdown()
*/
function _batch_page(Request $request) {
$batch = &batch_get();
if (!($request_id = $request->query->get('id'))) {
return FALSE;
}
// Retrieve the current state of the batch.
if (!$batch) {
$batch = \Drupal::service('batch.storage')->load($request_id);
if (!$batch) {
\Drupal::messenger()->addError(t('No batch with ID @batch exists.', ['@batch' => $request_id]));
throw new NotFoundHttpException(sprintf('No batch with ID %s exists.', $request_id));
}
}
// We need to store the updated batch information in the batch storage after
// processing the batch. In order for the error page to work correctly this
// needs to be done even in case of a PHP fatal error in which case the end of
// this function is never reached. Therefore we register a shutdown function
// to handle this case. Because with FastCGI and fastcgi_finish_request()
// shutdown functions are called after the HTTP connection is closed, updating
// the batch information in a shutdown function would lead to race conditions
// between consecutive requests if the batch processing continues. In case of
// a fatal error the processing stops anyway, so it works even with FastCGI.
// However, we must ensure to only update in the shutdown phase in this
// particular case we track whether the batch information still needs to be
// updated.
// @see _batch_shutdown()
// @see \Symfony\Component\HttpFoundation\Response::send()
drupal_register_shutdown_function('_batch_shutdown');
_batch_needs_update(TRUE);
$build = [];
// Add batch-specific libraries.
foreach ($batch['sets'] as $batch_set) {
if (isset($batch_set['library'])) {
foreach ($batch_set['library'] as $library) {
$build['#attached']['library'][] = $library;
}
}
}
$response = FALSE;
$op = $request->query->get('op', '');
switch ($op) {
case 'start':
case 'do_nojs':
// Display the full progress page on startup and on each additional
// non-JavaScript iteration.
$current_set = _batch_current_set();
$build['#title'] = $current_set['title'];
$build['content'] = _batch_progress_page();
$response = $build;
break;
case 'do':
// JavaScript-based progress page callback.
$response = _batch_do();
break;
case 'finished':
// _batch_finished() returns a RedirectResponse.
$response = _batch_finished();
break;
}
if ($batch) {
\Drupal::service('batch.storage')->update($batch);
}
_batch_needs_update(FALSE);
return $response;
}
/**
* Checks whether the batch information needs to be updated in the storage.
*
* @param bool $new_value
* (optional) A new value to set.
*
* @return bool
* TRUE if the batch information needs to be updated; FALSE otherwise.
*/
function _batch_needs_update($new_value = NULL) {
$needs_update = &drupal_static(__FUNCTION__, FALSE);
if (isset($new_value)) {
$needs_update = $new_value;
}
return $needs_update;
}
/**
* Does one execution pass with JavaScript and returns progress to the browser.
*
* @see _batch_progress_page_js()
* @see _batch_process()
*/
function _batch_do() {
// Perform actual processing.
[$percentage, $message, $label] = _batch_process();
return new JsonResponse(['status' => TRUE, 'percentage' => $percentage, 'message' => $message, 'label' => $label]);
}
/**
* Outputs a batch processing page.
*
* @see _batch_process()
*/
function _batch_progress_page(): array {
$batch = &batch_get();
$current_set = _batch_current_set();
$new_op = 'do_nojs';
if (!isset($batch['running'])) {
// This is the first page so we return some output immediately.
$percentage = 0;
$message = $current_set['init_message'];
$label = '';
$batch['running'] = TRUE;
}
else {
// This is one of the later requests; do some processing first.
// Error handling: if PHP dies due to a fatal error (e.g. a nonexistent
// function), it will output whatever is in the output buffer, followed by
// the error message.
ob_start();
$fallback = $current_set['error_message'] . '<br />' . $batch['error_message'];
// We strip the end of the page using a marker in the template, so any
// additional HTML output by PHP shows up inside the page rather than below
// it. While this causes invalid HTML, the same would be true if we didn't,
// as content is not allowed to appear after </html> anyway.
$bare_html_page_renderer = \Drupal::service('bare_html_page_renderer');
$response = $bare_html_page_renderer->renderBarePage(['#markup' => $fallback], $current_set['title'], 'maintenance_page', [
'#show_messages' => FALSE,
]);
// Just use the content of the response.
$fallback = $response->getContent();
[$fallback] = explode('<!--partial-->', $fallback);
print $fallback;
// Perform actual processing.
[$percentage, $message, $label] = _batch_process();
if ($percentage == 100) {
$new_op = 'finished';
}
// PHP did not die; remove the fallback output.
ob_end_clean();
}
// Merge required query parameters for batch processing into those provided by
// batch_set() or hook_batch_alter().
$query_options = $batch['url']->getOption('query');
$query_options['id'] = $batch['id'];
$query_options['op'] = $new_op;
$batch['url']->setOption('query', $query_options);
$url = $batch['url']->toString(TRUE)->getGeneratedUrl();
$build = [
'#theme' => 'progress_bar',
'#percent' => $percentage,
'#message' => ['#markup' => $message],
'#label' => $label,
'#attached' => [
'html_head' => [
[
[
// Redirect through a 'Refresh' meta tag if JavaScript is disabled.
'#tag' => 'meta',
'#noscript' => TRUE,
'#attributes' => [
'http-equiv' => 'Refresh',
'content' => '0; URL=' . $url,
],
],
'batch_progress_meta_refresh',
],
],
// Adds JavaScript code and settings for clients where JavaScript is
// enabled.
'drupalSettings' => [
'batch' => [
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => $url,
],
],
'library' => [
'core/drupal.batch',
],
],
];
return $build;
}
/**
* Processes sets in a batch.
*
* If the batch was marked for progressive execution (default), this executes as
* many operations in batch sets until an execution time of 1 second has been
* exceeded. It will continue with the next operation of the same batch set in
* the next request.
*
* @return array
* An array containing a completion value (in percent) and a status message.
*/
function _batch_process() {
$batch = &batch_get();
$current_set = &_batch_current_set();
// Indicate that this batch set needs to be initialized.
$set_changed = TRUE;
$task_message = '';
// If this batch was marked for progressive execution (e.g. forms submitted by
// \Drupal::formBuilder()->submitForm(), initialize a timer to determine
// whether we need to proceed with the same batch phase when a processing time
// of 1 second has been exceeded.
if ($batch['progressive']) {
Timer::start('batch_processing');
}
if (empty($current_set['start'])) {
$current_set['start'] = microtime(TRUE);
}
$queue = _batch_queue($current_set);
while (!$current_set['success']) {
// If this is the first time we iterate this batch set in the current
// request, we check if it requires an additional file for functions
// definitions.
if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
include_once \Drupal::root() . '/' . $current_set['file'];
}
$task_message = '';
// Assume a single pass operation and set the completion level to 1 by
// default.
$finished = 1;
if ($item = $queue->claimItem()) {
[$callback, $args] = $item->data;
// Build the 'context' array and execute the function call.
$batch_context = [
'sandbox' => &$current_set['sandbox'],
'results' => &$current_set['results'],
'finished' => &$finished,
'message' => &$task_message,
];
call_user_func_array($callback, array_merge($args, [&$batch_context]));
if ($finished >= 1) {
// Make sure this step is not counted twice when computing $current.
$finished = 0;
// Remove the processed operation and clear the sandbox.
$queue->deleteItem($item);
$current_set['count']--;
$current_set['sandbox'] = [];
}
}
// When all operations in the current batch set are completed, browse
// through the remaining sets, marking them 'successfully processed'
// along the way, until we find a set that contains operations.
// _batch_next_set() executes form submit handlers stored in 'control'
// sets (see \Drupal::service('form_submitter')), which can in turn add new
// sets to the batch.
$set_changed = FALSE;
$old_set = $current_set;
while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
$current_set = &_batch_current_set();
$current_set['start'] = microtime(TRUE);
$set_changed = TRUE;
}
// At this point, either $current_set contains operations that need to be
// processed or all sets have been completed.
$queue = _batch_queue($current_set);
// If we are in progressive mode, break processing after 1 second.
if ($batch['progressive'] && Timer::read('batch_processing') > 1000) {
// Record elapsed wall clock time.
$current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
break;
}
}
if ($batch['progressive']) {
// Gather progress information.
// Reporting 100% progress will cause the whole batch to be considered
// processed. If processing was paused right after moving to a new set,
// we have to use the info from the new (unprocessed) set.
if ($set_changed && isset($current_set['queue'])) {
// Processing will continue with a fresh batch set.
$remaining = $current_set['count'];
$total = $current_set['total'];
$progress_message = $current_set['init_message'];
$task_message = '';
}
else {
// Processing will continue with the current batch set.
$remaining = $old_set['count'] ?? 0;
$total = $old_set['total'] ?? 0;
$progress_message = $old_set['progress_message'] ?? '';
}
// Total progress is the number of operations that have fully run plus the
// completion level of the current operation.
$current = $total - $remaining + ($finished ?? 0);
$percentage = _batch_api_percentage($total, $current);
$elapsed = $current_set['elapsed'] ?? 0;
$values = [
'@remaining' => $remaining,
'@total' => $total,
'@current' => floor($current),
'@percentage' => $percentage,
'@elapsed' => \Drupal::service('date.formatter')->formatInterval((int) ($elapsed / 1000)),
// If possible, estimate remaining processing time.
'@estimate' => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval((int) (($elapsed * ($total - $current) / $current) / 1000)) : '-',
];
$message = strtr($progress_message, $values);
return [$percentage, $message, $task_message];
}
else {
// If we are not in progressive mode, the entire batch has been processed.
return _batch_finished();
}
}
/**
* Formats the percent completion for a batch set.
*
* @param int $total
* The total number of operations.
* @param int|float $current
* The number of the current operation. This may be a floating point number
* rather than an integer in the case of a multi-step operation that is not
* yet complete; in that case, the fractional part of $current represents the
* fraction of the operation that has been completed.
*
* @return string
* The properly formatted percentage, as a string. We output percentages
* using the correct number of decimal places so that we never print "100%"
* until we are finished, but we also never print more decimal places than
* are meaningful.
*
* @see _batch_process()
*/
function _batch_api_percentage($total, $current) {
return Percentage::format($total, $current);
}
/**
* Returns the batch set being currently processed.
*/
function &_batch_current_set() {
$batch = &batch_get();
return $batch['sets'][$batch['current_set']];
}
/**
* Retrieves the next set in a batch.
*
* If there is a subsequent set in this batch, assign it as the new set to
* process and execute its form submit handler (if defined), which may add
* further sets to this batch.
*
* @return true|null
* TRUE if a subsequent set was found in the batch; no value will be returned
* if no subsequent set was found.
*/
function _batch_next_set() {
$batch = &batch_get();
$set_indexes = array_keys($batch['sets']);
$current_set_index_key = array_search($batch['current_set'], $set_indexes);
if (isset($set_indexes[$current_set_index_key + 1])) {
$batch['current_set'] = $set_indexes[$current_set_index_key + 1];
$current_set = &_batch_current_set();
if (isset($current_set['form_submit']) && ($callback = $current_set['form_submit']) && is_callable($callback)) {
// We use our stored copies of $form and $form_state to account for
// possible alterations by previous form submit handlers.
$complete_form = &$batch['form_state']->getCompleteForm();
call_user_func_array($callback, [&$complete_form, &$batch['form_state']]);
}
return TRUE;
}
}
/**
* Ends the batch processing.
*
* Call the 'finished' callback of each batch set to allow custom handling of
* the results and resolve page redirection.
*/
function _batch_finished() {
$batch = &batch_get();
$batch_finished_redirect = NULL;
// Execute the 'finished' callbacks for each batch set, if defined.
foreach ($batch['sets'] as $batch_set) {
if (isset($batch_set['finished'])) {
// Check if the set requires an additional file for function definitions.
if (isset($batch_set['file']) && is_file($batch_set['file'])) {
include_once \Drupal::root() . '/' . $batch_set['file'];
}
if (is_callable($batch_set['finished'])) {
$queue = _batch_queue($batch_set);
$operations = $queue->getAllItems();
$batch_set_result = call_user_func_array($batch_set['finished'], [$batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval((int) ($batch_set['elapsed'] / 1000))]);
// If a batch 'finished' callback requested a redirect after the batch
// is complete, save that for later use. If more than one batch set
// returned a redirect, the last one is used.
if ($batch_set_result instanceof RedirectResponse) {
$batch_finished_redirect = $batch_set_result;
}
}
}
}
// Clean up the batch table and unset the static $batch variable.
if ($batch['progressive']) {
\Drupal::service('batch.storage')->delete($batch['id']);
foreach ($batch['sets'] as $batch_set) {
if ($queue = _batch_queue($batch_set)) {
$queue->deleteQueue();
}
}
// Clean-up the session. Not needed for CLI updates.
$session = \Drupal::request()->getSession();
$batches = $session->get('batches', []);
unset($batches[$batch['id']]);
if (empty($batches)) {
$session->remove('batches');
}
else {
$session->set('batches', $batches);
}
}
$_batch = $batch;
$batch = NULL;
// Redirect if needed.
if ($_batch['progressive']) {
// Revert the 'destination' that was saved in batch_process().
if (isset($_batch['destination'])) {
\Drupal::request()->query->set('destination', $_batch['destination']);
}
// Determine the target path to redirect to. If a batch 'finished' callback
// returned a redirect response object, use that. Otherwise, fall back on
// the form redirection.
if (isset($batch_finished_redirect)) {
return $batch_finished_redirect;
}
elseif (!isset($_batch['form_state'])) {
$_batch['form_state'] = new FormState();
}
if ($_batch['form_state']->getRedirect() === NULL) {
$redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
// Any path with a scheme does not correspond to a route.
if (!$redirect instanceof Url) {
$options = UrlHelper::parse($redirect);
if (parse_url($options['path'], PHP_URL_SCHEME)) {
$redirect = Url::fromUri($options['path'], $options);
}
else {
$redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
if (!$redirect) {
// Stay on the same page if the redirect was invalid.
$redirect = Url::fromRoute('<current>');
}
$redirect->setOptions($options);
}
}
$_batch['form_state']->setRedirectUrl($redirect);
}
// Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
// the redirection logic.
$redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
if (is_object($redirect)) {
return $redirect;
}
// If no redirection happened, redirect to the originating page. In case the
// form needs to be rebuilt, save the final $form_state for
// \Drupal\Core\Form\FormBuilderInterface::buildForm().
if ($_batch['form_state']->isRebuilding()) {
$session = \Drupal::request()->getSession();
$session->set('batch_form_state', $_batch['form_state']);
}
$callback = $_batch['redirect_callback'];
$_batch['source_url']->mergeOptions(['query' => ['op' => 'finish', 'id' => $_batch['id']]]);
if (is_callable($callback)) {
$callback($_batch['source_url'], $_batch['source_url']->getOption('query'));
}
elseif ($callback === NULL) {
// Default to RedirectResponse objects when nothing specified.
return new RedirectResponse($_batch['source_url']->setAbsolute()->toString());
}
}
}
/**
* Shutdown function: Stores the current batch data for the next request.
*
* @see _batch_page()
* @see drupal_register_shutdown_function()
*/
function _batch_shutdown(): void {
if (($batch = batch_get()) && _batch_needs_update()) {
\Drupal::service('batch.storage')->update($batch);
}
}

View File

@ -0,0 +1,511 @@
<?php
/**
* @file
* Functions that need to be loaded on every Drupal request.
*/
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Test\TestDatabase;
use Drupal\Core\Utility\Error;
use Drupal\Core\StringTranslation\TranslatableMarkup;
// cspell:ignore htkey
/**
* Error reporting level: display no errors.
*/
const ERROR_REPORTING_HIDE = 'hide';
/**
* Error reporting level: display errors and warnings.
*/
const ERROR_REPORTING_DISPLAY_SOME = 'some';
/**
* Error reporting level: display all messages.
*/
const ERROR_REPORTING_DISPLAY_ALL = 'all';
/**
* Error reporting level: display all messages, plus backtrace information.
*/
const ERROR_REPORTING_DISPLAY_VERBOSE = 'verbose';
/**
* The maximum number of characters in a module or theme name.
*/
const DRUPAL_EXTENSION_NAME_MAX_LENGTH = 50;
/**
* Defines the root directory of the Drupal installation.
*
* This strips two levels of directories off the current directory.
*/
define('DRUPAL_ROOT', dirname(__DIR__, 2));
/**
* Translates a string to the current language or to a given language.
*
* In order for strings to be localized, make them available in one of the ways
* supported by the @link i18n Localization API. @endlink When possible, use
* the \Drupal\Core\StringTranslation\StringTranslationTrait $this->t().
* Otherwise create a new \Drupal\Core\StringTranslation\TranslatableMarkup
* object directly.
*
* See \Drupal\Core\StringTranslation\TranslatableMarkup::__construct() for
* important security information and usage guidelines.
*
* @param string $string
* A string containing the English text to translate.
* @param array $args
* (optional) An associative array of replacements to make after translation.
* Based on the first character of the key, the value is escaped and/or
* themed. See
* \Drupal\Component\Render\FormattableMarkup::placeholderFormat() for
* details.
* @param array $options
* (optional) An associative array of additional options, with the following
* elements:
* - 'langcode' (defaults to the current language): A language code, to
* translate to a language other than what is used to display the page.
* - 'context' (defaults to the empty context): The context the source string
* belongs to. See the @link i18n Internationalization topic @endlink for
* more information about string contexts.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* An object that, when cast to a string, returns the translated string.
*
* @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat()
* @see \Drupal\Core\StringTranslation\StringTranslationTrait::t()
* @see \Drupal\Core\StringTranslation\TranslatableMarkup::__construct()
*
* @ingroup sanitization
*/
function t($string, array $args = [], array $options = []) {
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
return new TranslatableMarkup($string, $args, $options);
}
/**
* Provides custom PHP error handling.
*
* @param int $error_level
* The level of the error raised.
* @param string $message
* The error message.
* @param string $filename
* (optional) The filename that the error was raised in.
* @param int $line
* (optional) The line number the error was raised at.
*/
function _drupal_error_handler($error_level, $message, $filename = NULL, $line = NULL): void {
require_once __DIR__ . '/errors.inc';
_drupal_error_handler_real($error_level, $message, $filename, $line);
}
/**
* Provides custom PHP exception handling.
*
* Uncaught exceptions are those not enclosed in a try/catch block. They are
* always fatal: the execution of the script will stop as soon as the exception
* handler exits.
*
* @param \Exception|\Throwable $exception
* The exception object that was thrown.
*/
function _drupal_exception_handler($exception): void {
require_once __DIR__ . '/errors.inc';
try {
// Log the message to the watchdog and return an error page to the user.
_drupal_log_error(Error::decodeException($exception), TRUE);
}
// Catch \Throwable, which covers both Error and Exception throwables.
catch (\Throwable $error) {
_drupal_exception_handler_additional($exception, $error);
}
}
/**
* Displays any additional errors caught while handling an exception.
*
* @param \Exception|\Throwable $exception
* The first exception object that was thrown.
* @param \Exception|\Throwable $exception2
* The second exception object that was thrown.
*/
function _drupal_exception_handler_additional($exception, $exception2): void {
// Another uncaught exception was thrown while handling the first one.
// If we are displaying errors, then do so with no possibility of a further
// uncaught exception being thrown.
if (error_displayable()) {
print '<h1>Additional uncaught exception thrown while handling exception.</h1>';
print '<h2>Original</h2><p>' . Error::renderExceptionSafe($exception) . '</p>';
print '<h2>Additional</h2><p>' . Error::renderExceptionSafe($exception2) . '</p><hr />';
}
}
/**
* Returns the test prefix if this is an internal request from a test.
*
* @param string $new_prefix
* Internal use only. A new prefix to be stored.
*
* @return string|false
* Either the simpletest prefix (the string "simpletest" followed by any
* number of digits) or FALSE if the user agent does not contain a valid
* HMAC and timestamp.
*/
function drupal_valid_test_ua($new_prefix = NULL) {
static $test_prefix;
if (isset($new_prefix)) {
$test_prefix = $new_prefix;
}
if (isset($test_prefix)) {
return $test_prefix;
}
// Unless the below User-Agent and HMAC validation succeeds, we are not in
// a test environment.
$test_prefix = FALSE;
// A valid test request will contain a hashed and salted authentication code.
// Check if this code is present in a cookie or custom user agent string.
$http_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? NULL;
$user_agent = $_COOKIE['SIMPLETEST_USER_AGENT'] ?? $http_user_agent;
if (isset($user_agent) && preg_match("/^simple(\w+\d+):(.+):(.+):(.+)$/", $user_agent, $matches)) {
[, $prefix, $time, $salt, $hmac] = $matches;
$check_string = $prefix . ':' . $time . ':' . $salt;
// Read the hash salt prepared by drupal_generate_test_ua().
// This function is called before settings.php is read and Drupal's error
// handlers are set up. While Drupal's error handling may be properly
// configured on production sites, the server's PHP error_reporting may not.
// Ensure that no information leaks on production sites.
$test_db = new TestDatabase($prefix);
$key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';
if (!is_readable($key_file) || is_dir($key_file)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit;
}
$private_key = file_get_contents($key_file);
// The string from drupal_generate_test_ua() is 74 bytes long. If we don't
// have it, tests cannot be allowed.
if (empty($private_key) || strlen($private_key) < 74) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit;
}
// The file properties add more entropy not easily accessible to others.
$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
$time_diff = time() - $time;
$test_hmac = Crypt::hmacBase64($check_string, $key);
// Since we are making a local request a 600 second time window is allowed,
// and the HMAC must match.
if ($time_diff >= 0 && $time_diff <= 600 && hash_equals($test_hmac, $hmac)) {
$test_prefix = $prefix;
}
else {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden (SIMPLETEST_USER_AGENT invalid)');
exit;
}
}
return $test_prefix;
}
/**
* Generates a user agent string with a HMAC and timestamp for tests.
*
* @return string|null
* The user agent, or NULL on failure.
*/
function drupal_generate_test_ua($prefix) {
static $key, $last_prefix;
if (!isset($key) || $last_prefix != $prefix) {
$last_prefix = $prefix;
$test_db = new TestDatabase($prefix);
$key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';
// When issuing an outbound HTTP client request from within an inbound test
// request, then the outbound request has to use the same User-Agent header
// as the inbound request. A newly generated private key for the same test
// prefix would invalidate all subsequent inbound requests.
// @see \Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware
if (DRUPAL_TEST_IN_CHILD_SITE && $parent_prefix = drupal_valid_test_ua()) {
if ($parent_prefix != $prefix) {
throw new \RuntimeException("Malformed User-Agent: Expected '$parent_prefix' but got '$prefix'.");
}
// If the file is not readable, a PHP warning is expected in this case.
$private_key = file_get_contents($key_file);
}
else {
// Generate and save a new hash salt for a test run.
// Consumed by drupal_valid_test_ua() before settings.php is loaded.
$private_key = Crypt::randomBytesBase64(55);
if (!@file_put_contents($key_file, $private_key)) {
return NULL;
}
}
// The file properties add more entropy not easily accessible to others.
$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
}
// Generate a moderately secure HMAC based on the database credentials.
$salt = uniqid('', TRUE);
$check_string = $prefix . ':' . time() . ':' . $salt;
return 'simple' . $check_string . ':' . Crypt::hmacBase64($check_string, $key);
}
/**
* Enables use of the theme system without requiring database access.
*
* Loads and initializes the theme system for site installs, updates and when
* the site is in maintenance mode. This also applies when the database fails.
*
* @see _drupal_maintenance_theme()
*/
function drupal_maintenance_theme(): void {
require_once __DIR__ . '/theme.maintenance.inc';
_drupal_maintenance_theme();
}
/**
* Provides central static variable storage.
*
* All functions requiring a static variable to persist or cache data within
* a single page request are encouraged to use this function unless it is
* absolutely certain that the static variable will not need to be reset during
* the page request. By centralizing static variable storage through this
* function, other functions can rely on a consistent API for resetting any
* other function's static variables.
*
* Example:
* @code
* function example_list($field = 'default') {
* $examples = &drupal_static(__FUNCTION__);
* if (!isset($examples)) {
* // If this function is being called for the first time after a reset,
* // query the database and execute any other code needed to retrieve
* // information.
* ...
* }
* if (!isset($examples[$field])) {
* // If this function is being called for the first time for a particular
* // index field, then execute code needed to index the information already
* // available in $examples by the desired field.
* ...
* }
* // Subsequent invocations of this function for a particular index field
* // skip the above two code blocks and quickly return the already indexed
* // information.
* return $examples[$field];
* }
* function examples_admin_overview() {
* // When building the content for the overview page, make sure to get
* // completely fresh information.
* drupal_static_reset('example_list');
* ...
* }
* @endcode
*
* In a few cases, a function can have certainty that there is no legitimate
* use-case for resetting that function's static variable. This is rare,
* because when writing a function, it's hard to forecast all the situations in
* which it will be used. A guideline is that if a function's static variable
* does not depend on any information outside of the function that might change
* during a single page request, then it's ok to use the "static" keyword
* instead of the drupal_static() function.
*
* Example:
* @code
* function my_module_log_stream_handle($new_handle = NULL) {
* static $handle;
* if (isset($new_handle)) {
* $handle = $new_handle;
* }
* return $handle;
* }
* @endcode
*
* In a few cases, a function needs a resettable static variable, but the
* function is called many times (100+) during a single page request, so
* every microsecond of execution time that can be removed from the function
* counts. These functions can use a more cumbersome, but faster variant of
* calling drupal_static(). It works by storing the reference returned by
* drupal_static() in the calling function's own static variable, thereby
* removing the need to call drupal_static() for each iteration of the function.
* Conceptually, it replaces:
* @code
* $foo = &drupal_static(__FUNCTION__);
* @endcode
* with:
* @code
* // Unfortunately, this does not work.
* static $foo = &drupal_static(__FUNCTION__);
* @endcode
* However, the above line of code does not work, because PHP only allows static
* variables to be initialized by literal values, and does not allow static
* variables to be assigned to references.
* - http://php.net/manual/language.variables.scope.php#language.variables.scope.static
* - http://php.net/manual/language.variables.scope.php#language.variables.scope.references
* The example below shows the syntax needed to work around both limitations.
* For benchmarks and more information, see https://www.drupal.org/node/619666.
*
* Example:
* @code
* function example_default_format_type() {
* // Use the advanced drupal_static() pattern, since this is called very often.
* static $drupal_static_fast;
* if (!isset($drupal_static_fast)) {
* $drupal_static_fast['format_type'] = &drupal_static(__FUNCTION__);
* }
* $format_type = &$drupal_static_fast['format_type'];
* ...
* }
* @endcode
*
* @param string|null $name
* Globally unique name for the variable. For a function with only one static,
* variable, the function name (e.g. via the PHP magic __FUNCTION__ constant)
* is recommended. For a function with multiple static variables add a
* distinguishing suffix to the function name for each one.
* @param mixed|null $default_value
* Optional default value.
* @param bool $reset
* TRUE to reset one or all variables(s). This parameter is only used
* internally and should not be passed in; use drupal_static_reset() instead.
* (This function's return value should not be used when TRUE is passed in.)
*
* @return mixed
* Returns a variable by reference.
*
* @see drupal_static_reset()
*/
function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
static $data = [], $defaults = [];
if (isset($name)) {
// Check if we're dealing with a previously defined static variable.
if (\array_key_exists($name, $data)) {
// Both $data[$name] and (implicitly) $defaults[$name] statics exist.
if ($reset) {
// Reset pre-existing static variable to its default value.
$data[$name] = $defaults[$name];
}
}
else {
// Neither $data[$name] nor $defaults[$name] static variables exist.
if ($reset) {
// Reset was called before any value for $name was set, so we should
// not set anything ($default_value is not reliable in this case). As
// the function returns a reference, we must still return a variable.
// (Code using $reset does not use the return value).
return $data;
}
// First call with new non-NULL $name. Initialize a new static variable.
$defaults[$name] = $data[$name] = $default_value;
}
// Return a reference to the named variable.
return $data[$name];
}
else {
// Reset all: ($name == NULL). This needs to be done one at a time so that
// references returned by earlier invocations of drupal_static() also get
// reset.
foreach ($defaults as $name => $value) {
$data[$name] = $value;
}
// As the function returns a reference, we must still return a variable.
return $data;
}
}
/**
* Resets one or all centrally stored static variable(s).
*
* @param string $name
* Name of the static variable to reset. Omit to reset all variables.
* Resetting all variables should only be used, for example, for running
* unit tests with a clean environment.
*/
function drupal_static_reset($name = NULL): void {
drupal_static($name, NULL, TRUE);
}
/**
* Registers a function for execution on shutdown.
*
* Wrapper for register_shutdown_function() that catches thrown exceptions to
* avoid "Exception thrown without a stack frame in Unknown".
*
* @param callable $callback
* The shutdown function to register.
* @param mixed ...$args
* Additional arguments to pass to the shutdown function.
*
* @return array
* Array of shutdown functions to be executed.
*
* @see register_shutdown_function()
* @ingroup php_wrappers
*/
function &drupal_register_shutdown_function($callback = NULL, ...$args) {
// We cannot use drupal_static() here because the static cache is reset during
// batch processing, which breaks batch handling.
static $callbacks = [];
if (isset($callback)) {
// Only register the internal shutdown function once.
if (empty($callbacks)) {
register_shutdown_function('_drupal_shutdown_function');
}
// Save callback and arguments
$callbacks[] = ['callback' => $callback, 'arguments' => $args];
}
return $callbacks;
}
/**
* Executes registered shutdown functions.
*/
function _drupal_shutdown_function(): void {
$callbacks = &drupal_register_shutdown_function();
// Set the CWD to DRUPAL_ROOT as it is not guaranteed to be the same as it
// was in the normal context of execution.
chdir(DRUPAL_ROOT);
try {
reset($callbacks);
// Do not use foreach() here because it is possible that the callback will
// add to the $callbacks array via drupal_register_shutdown_function().
while ($callback = current($callbacks)) {
call_user_func_array($callback['callback'], $callback['arguments']);
next($callbacks);
}
}
// Catch \Throwable, which covers both Error and Exception throwables.
catch (\Throwable $error) {
_drupal_shutdown_function_handle_exception($error);
}
}
/**
* Displays and logs any errors that may happen during shutdown.
*
* @param \Exception|\Throwable $exception
* The exception object that was thrown.
*
* @see _drupal_shutdown_function()
*/
function _drupal_shutdown_function_handle_exception($exception): void {
// If using PHP-FPM then fastcgi_finish_request() will have been fired
// preventing further output to the browser.
if (!function_exists('fastcgi_finish_request')) {
// If we are displaying errors, then do so with no possibility of a
// further uncaught exception being thrown.
require_once __DIR__ . '/errors.inc';
if (error_displayable()) {
print '<h1>Uncaught exception thrown in shutdown function.</h1>';
print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />';
}
}
error_log($exception);
}

View File

@ -0,0 +1,530 @@
<?php
/**
* @file
* Common functions that many Drupal modules will need to reference.
*
* The functions that are critical and need to be available even when serving
* a cached page are instead located in bootstrap.inc.
*/
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheTagsPurgeInterface;
use Drupal\Core\DrupalKernel;
/**
* @defgroup php_wrappers PHP wrapper functions
* @{
* Functions that are wrappers or custom implementations of PHP functions.
*
* Certain PHP functions should not be used in Drupal. Instead, Drupal's
* replacement functions should be used.
*
* For example, for improved or more secure UTF8-handling, or RFC-compliant
* handling of URLs in Drupal.
*
* For ease of use and memorizing, all these wrapper functions use the same name
* as the original PHP function, but prefixed with "drupal_". Beware, however,
* that not all wrapper functions support the same arguments as the original
* functions.
*
* You should always use these wrapper functions in your code.
*
* Wrong:
* @code
* $my_substring = substr($original_string, 0, 5);
* @endcode
*
* Correct:
* @code
* $my_substring = mb_substr($original_string, 0, 5);
* @endcode
*
* @}
*/
/**
* Return status for saving which involved creating a new item.
*/
const SAVED_NEW = 1;
/**
* Return status for saving which involved an update to an existing item.
*/
const SAVED_UPDATED = 2;
/**
* Return status for saving which deleted an existing item.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0.
* There is no replacement.
*
* @see https://www.drupal.org/node/3328750
*/
const SAVED_DELETED = 3;
/**
* The default aggregation group for CSS files added to the page.
*/
const CSS_AGGREGATE_DEFAULT = 0;
/**
* The default aggregation group for theme CSS files added to the page.
*/
const CSS_AGGREGATE_THEME = 100;
/**
* The default weight for CSS rules that style HTML elements ("base" styles).
*/
const CSS_BASE = -200;
/**
* The default weight for CSS rules that layout a page.
*/
const CSS_LAYOUT = -100;
/**
* The default weight for CSS rules that style design components.
*
* This applies to the associated states and themes of the component.
*/
const CSS_COMPONENT = 0;
/**
* The default weight for rules that style states not included with components.
*/
const CSS_STATE = 100;
/**
* The default weight for rules that style themes not included with components.
*/
const CSS_THEME = 200;
/**
* The default group for JavaScript settings added to the page.
*/
const JS_SETTING = -200;
/**
* The default group for JavaScript and jQuery libraries added to the page.
*/
const JS_LIBRARY = -100;
/**
* The default group for module JavaScript code added to the page.
*/
const JS_DEFAULT = 0;
/**
* The default group for theme JavaScript code added to the page.
*/
const JS_THEME = 100;
/**
* Returns the base URL path (i.e., directory) of the Drupal installation.
*
* Function base_path() adds a "/" to the beginning and end of the returned path
* if the path is not empty. At the very least, this will return "/".
*
* Examples:
* - http://example.com returns "/" because the path is empty.
* - http://example.com/drupal/folder returns "/drupal/folder/".
*/
function base_path() {
return $GLOBALS['base_path'];
}
/**
* Assists in attaching the tableDrag JavaScript behavior to a themed table.
*
* Draggable tables should be used wherever an outline or list of sortable items
* needs to be arranged by an end-user. Draggable tables are very flexible and
* can manipulate the value of form elements placed within individual columns.
*
* To set up a table to use drag and drop in place of weight select-lists or in
* place of a form that contains parent relationships, the form must be themed
* into a table. The table must have an ID attribute set and it
* may be set as follows:
* @code
* $table = [
* '#type' => 'table',
* '#header' => $header,
* '#rows' => $rows,
* '#attributes' => [
* 'id' => 'my-module-table',
* ],
* ];
* return \Drupal::service('renderer')->render($table);
* @endcode
*
* In the theme function for the form, a special class must be added to each
* form element within the same column, "grouping" them together.
*
* In a situation where a single weight column is being sorted in the table, the
* classes could be added like this (in the theme function):
* @code
* $form['my_elements'][$delta]['weight']['#attributes']['class'] = ['my-elements-weight'];
* @endcode
*
* Each row of the table must also have a class of "draggable" in order to
* enable the drag handles:
* @code
* $row = [...];
* $rows[] = [
* 'data' => $row,
* 'class' => ['draggable'],
* ];
* @endcode
*
* When tree relationships are present, the two additional classes
* 'tabledrag-leaf' and 'tabledrag-root' can be used to refine the behavior:
* - Rows with the 'tabledrag-leaf' class cannot have child rows.
* - Rows with the 'tabledrag-root' class cannot be nested under a parent row.
*
* Calling drupal_attach_tabledrag() would then be written as such:
* @code
* drupal_attach_tabledrag('my-module-table', [
* 'action' => 'order',
* 'relationship' => 'sibling',
* 'group' => 'my-elements-weight',
* ];
* @endcode
*
* In a more complex case where there are several groups in one column (such as
* the block regions on the admin/structure/block page), a separate subgroup
* class must also be added to differentiate the groups.
* @code
* $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = ['my-elements-weight', 'my-elements-weight-' . $region];
* @endcode
*
* The 'group' option is still 'my-element-weight', and the additional
* 'subgroup' option will be passed in as 'my-elements-weight-' . $region. This
* also means that you'll need to call drupal_attach_tabledrag() once for every
* region added.
*
* @code
* foreach ($regions as $region) {
* drupal_attach_tabledrag('my-module-table', [
* 'action' => 'order',
* 'relationship' => 'sibling',
* 'group' => 'my-elements-weight',
* 'subgroup' => 'my-elements-weight-' . $region,
* ]);
* }
* @endcode
*
* In a situation where tree relationships are present, adding multiple
* subgroups is not necessary, because the table will contain indentations that
* provide enough information about the sibling and parent relationships. See
* MenuForm::BuildOverviewForm for an example creating a table
* containing parent relationships.
*
* @param array $element
* A form element to attach the tableDrag behavior to.
* @param array $options
* These options are used to generate JavaScript settings necessary to
* configure the tableDrag behavior appropriately for this particular table.
* An associative array containing the following keys:
* - 'table_id': String containing the target table's id attribute.
* If the table does not have an id, one will need to be set,
* such as <table id="my-module-table">.
* - 'action': String describing the action to be done on the form item.
* Either 'match' 'depth', or 'order':
* - 'match' is typically used for parent relationships.
* - 'order' is typically used to set weights on other form elements with
* the same group.
* - 'depth' updates the target element with the current indentation.
* - 'relationship': String describing where the "action" option
* should be performed. Either 'parent', 'sibling', 'group', or 'self':
* - 'parent' will only look for fields up the tree.
* - 'sibling' will look for fields in the same group in rows above and
* below it.
* - 'self' affects the dragged row itself.
* - 'group' affects the dragged row, plus any children below it (the entire
* dragged group).
* - 'group': A class name applied on all related form elements for this
* action.
* - 'subgroup': (optional) If the group has several subgroups within it, this
* string should contain the class name identifying fields in the same
* subgroup.
* - 'source': (optional) If the $action is 'match', this string should
* contain the classname identifying what field will be used as the source
* value when matching the value in $subgroup.
* - 'hidden': (optional) The column containing the field elements may be
* entirely hidden from view dynamically when the JavaScript is loaded. Set
* to FALSE if the column should not be hidden.
* - 'limit': (optional) Limit the maximum amount of parenting in this table.
*
* @see MenuForm::BuildOverviewForm()
*/
function drupal_attach_tabledrag(&$element, array $options): void {
// Add default values to elements.
$options = $options + [
'subgroup' => NULL,
'source' => NULL,
'hidden' => TRUE,
'limit' => 0,
];
$group = $options['group'];
$tabledrag_id = &drupal_static(__FUNCTION__);
$tabledrag_id = (!isset($tabledrag_id)) ? 0 : $tabledrag_id + 1;
// If a subgroup or source isn't set, assume it is the same as the group.
$target = $options['subgroup'] ?? $group;
$source = $options['source'] ?? $target;
$element['#attached']['drupalSettings']['tableDrag'][$options['table_id']][$group][$tabledrag_id] = [
'target' => $target,
'source' => $source,
'relationship' => $options['relationship'],
'action' => $options['action'],
'hidden' => $options['hidden'],
'limit' => $options['limit'],
];
$element['#attached']['library'][] = 'core/drupal.tabledrag';
}
/**
* Hides an element from later rendering.
*
* The first time render() or RenderInterface::render() is called on an element
* tree, as each element in the tree is rendered, it is marked with a #printed
* flag and the rendered children of the element are cached. Subsequent calls to
* render() or RenderInterface::render() will not traverse the child tree of
* this element again: they will just use the cached children. So if you want to
* hide an element, be sure to call hide() on the element before its parent tree
* is rendered for the first time, as it will have no effect on subsequent
* renderings of the parent tree.
*
* @param array $element
* The element to be hidden.
*
* @return array
* The element.
*
* @see \Drupal\Core\Render\RendererInterface
* @see render()
* @see show()
*/
function hide(&$element) {
$element['#printed'] = TRUE;
return $element;
}
/**
* Shows a hidden element for later rendering.
*
* You can also use render($element), which shows the element while rendering
* it.
*
* The first time render() or RenderInterface::render() is called on an element
* tree, as each element in the tree is rendered, it is marked with a #printed
* flag and the rendered children of the element are cached. Subsequent calls to
* render() or RenderInterface::render() will not traverse the child tree of
* this element again: they will just use the cached children. So if you want to
* show an element, be sure to call show() on the element before its parent tree
* is rendered for the first time, as it will have no effect on subsequent
* renderings of the parent tree.
*
* @param array $element
* The element to be shown.
*
* @return array
* The element.
*
* @see \Drupal\Core\Render\RendererInterface
* @see render()
* @see hide()
*/
function show(&$element) {
$element['#printed'] = FALSE;
return $element;
}
/**
* Re-initializes the entire system.
*
* To re-initialize the system, the container is rebuilt, all persistent caches
* are flushed, all variables are reset and all data structures are rebuilt.
*
* At times, it is necessary to re-initialize the entire system to account for
* changed or new code. This function:
* - Rebuilds the container if $kernel is not passed in.
* - Clears all persistent caches:
* - The bootstrap cache bin containing base system, module system, and theme
* system information.
* - The common 'default' cache bin containing arbitrary caches.
* - The page cache.
* - The URL alias path cache.
* - Resets all static variables that have been defined via drupal_static().
* - Clears asset (JS/CSS) file caches.
* - Updates the system with latest information about extensions (modules and
* themes).
* - Updates the bootstrap flag for modules implementing bootstrap_hooks().
* - Rebuilds the full database schema information (invoking hook_schema()).
* - Rebuilds data structures of all modules (invoking hook_rebuild()). In
* core this means
* - blocks, node types, date formats and actions are synchronized with the
* database
* - The 'active' status of fields is refreshed.
* - Rebuilds the menu router.
*
* It's discouraged to call this during a regular page request.
* If you call this function in tests, every code afterwards should use the new
* container.
*
* This means the entire system is reset so all caches and static variables are
* effectively empty. After that is guaranteed, information about the currently
* active code is updated, and rebuild operations are successively called in
* order to synchronize the active system according to the current information
* defined in code.
*
* All modules need to ensure that all of their caches are flushed when
* hook_cache_flush() is invoked; any previously known information must no
* longer exist. All following hook_rebuild() operations must be based on fresh
* and current system data. All modules must be able to rely on this contract.
*
* This function also resets the theme, which means it is not initialized
* anymore and all previously added JavaScript and CSS is gone. Normally, this
* function is called as an end-of-POST-request operation that is followed by a
* redirect, so this effect is not visible. Since the full reset is the whole
* point of this function, callers need to take care for backing up all needed
* variables and properly restoring or re-initializing them on their own. For
* convenience, this function automatically re-initializes the maintenance theme
* if it was initialized before.
*
* @param \Drupal\Core\DrupalKernel|array $kernel
* (optional) The Drupal Kernel. It is the caller's responsibility to rebuild
* the container if this is passed in. Sometimes drupal_flush_all_caches is
* used as a batch operation so $kernel will be an array, in this instance it
* will be treated as if it is NULL.
*
* @see \Drupal\Core\Cache\CacheHelper::getBins()
* @see hook_cache_flush()
* @see hook_rebuild()
*
* @todo Try to clear page/JS/CSS caches last, so cached pages can still be
* served during this possibly long-running operation. (Conflict on bootstrap
* cache though.)
* @todo Add a global lock to ensure that caches are not primed in concurrent
* requests.
*/
function drupal_flush_all_caches($kernel = NULL): void {
// This is executed based on old/previously known information if $kernel is
// not passed in, which is sufficient, since new extensions cannot have any
// primed caches yet.
$module_handler = \Drupal::moduleHandler();
// Flush all persistent caches.
$module_handler->invokeAll('cache_flush');
// Purge cache tags immediately before flushing cache bins. If a cache tag is
// invalidated between the tags being purged and cache bins are flushed, then
// it will be included in the checksum of any new cache items, but still valid
// because the tag was written before the creation of cache item.
if (($invalidator = \Drupal::service('cache_tags.invalidator')) && ($invalidator instanceof CacheTagsPurgeInterface)) {
$invalidator->purge();
}
foreach (Cache::getBins() as $cache_backend) {
$cache_backend->deleteAll();
}
// Flush asset file caches.
\Drupal::service('asset.css.collection_optimizer')->deleteAll();
\Drupal::service('asset.js.collection_optimizer')->deleteAll();
\Drupal::service('asset.query_string')->reset();
// Reset all static caches.
drupal_static_reset();
// Wipe the Twig PHP Storage cache.
\Drupal::service('twig')->invalidate();
// Rebuild profile, profile, theme_engine and theme data.
\Drupal::service('extension.list.profile')->reset();
\Drupal::service('extension.list.theme_engine')->reset();
\Drupal::service('theme_handler')->refreshInfo();
// In case the active theme gets requested later in the same request we need
// to reset the theme manager.
\Drupal::theme()->resetActiveTheme();
if (!$kernel instanceof DrupalKernel) {
$kernel = \Drupal::service('kernel');
$kernel->invalidateContainer();
$kernel->rebuildContainer();
}
// Rebuild module data that is stored in state.
\Drupal::service('extension.list.module')->reset();
// Reload modules.
\Drupal::moduleHandler()->reload();
// Rebuild all information based on new module data.
\Drupal::moduleHandler()->invokeAll('rebuild');
// Clear all plugin caches.
\Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
// Rebuild the menu router based on all rebuilt data.
// Important: This rebuild must happen last, so the menu router is guaranteed
// to be based on up to date information.
\Drupal::service('router.builder')->rebuild();
// Re-initialize the maintenance theme, if the current request attempted to
// use it. Unlike regular usages of this function, the installer and update
// scripts need to flush all caches during GET requests/page building.
if (function_exists('_drupal_maintenance_theme')) {
\Drupal::theme()->resetActiveTheme();
drupal_maintenance_theme();
}
}
/**
* Assembles the Drupal Updater registry.
*
* An Updater is a class that knows how to update various parts of the Drupal
* file system, for example to update modules that have newer releases, or to
* install a new theme.
*
* @return array
* The Drupal Updater class registry.
*
* @see \Drupal\Core\Updater\Updater
* @see hook_updater_info()
* @see hook_updater_info_alter()
*/
function drupal_get_updaters() {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', E_USER_DEPRECATED);
$updaters = &drupal_static(__FUNCTION__);
if (!isset($updaters)) {
$updaters = \Drupal::moduleHandler()->invokeAllDeprecated('There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', 'updater_info');
\Drupal::moduleHandler()->alterDeprecated('There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', 'updater_info', $updaters);
uasort($updaters, [SortArray::class, 'sortByWeightElement']);
}
return $updaters;
}
/**
* Assembles the Drupal FileTransfer registry.
*
* @return array
* The Drupal FileTransfer class registry.
*
* @see \Drupal\Core\FileTransfer\FileTransfer
* @see hook_filetransfer_info()
* @see hook_filetransfer_info_alter()
*/
function drupal_get_filetransfer_info() {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', E_USER_DEPRECATED);
$info = &drupal_static(__FUNCTION__);
if (!isset($info)) {
$info = \Drupal::moduleHandler()->invokeAllDeprecated('There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', 'filetransfer_info');
\Drupal::moduleHandler()->alterDeprecated('There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', 'filetransfer_info', $info);
uasort($info, [SortArray::class, 'sortByWeightElement']);
}
return $info;
}

View File

@ -0,0 +1,376 @@
<?php
/**
* @file
* Functions for error handling.
*/
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Render\Markup;
use Drupal\Core\Utility\Error;
use Symfony\Component\HttpFoundation\Response;
/**
* Maps PHP error constants to watchdog severity levels.
*
* The error constants are documented at
* http://php.net/manual/errorfunc.constants.php
*
* @ingroup logging_severity_levels
*/
function drupal_error_levels(): array {
$types = [
E_ERROR => ['Error', RfcLogLevel::ERROR],
E_WARNING => ['Warning', RfcLogLevel::WARNING],
E_PARSE => ['Parse error', RfcLogLevel::ERROR],
E_NOTICE => ['Notice', RfcLogLevel::NOTICE],
E_CORE_ERROR => ['Core error', RfcLogLevel::ERROR],
E_CORE_WARNING => ['Core warning', RfcLogLevel::WARNING],
E_COMPILE_ERROR => ['Compile error', RfcLogLevel::ERROR],
E_COMPILE_WARNING => ['Compile warning', RfcLogLevel::WARNING],
E_USER_ERROR => ['User error', RfcLogLevel::ERROR],
E_USER_WARNING => ['User warning', RfcLogLevel::WARNING],
E_USER_NOTICE => ['User notice', RfcLogLevel::NOTICE],
E_RECOVERABLE_ERROR => ['Recoverable fatal error', RfcLogLevel::ERROR],
E_DEPRECATED => ['Deprecated function', RfcLogLevel::DEBUG],
E_USER_DEPRECATED => ['User deprecated function', RfcLogLevel::DEBUG],
];
return $types;
}
/**
* Provides custom PHP error handling.
*
* @param int $error_level
* The level of the error raised.
* @param string $message
* The error message.
* @param string $filename
* The filename that the error was raised in.
* @param int $line
* The line number the error was raised at.
*/
function _drupal_error_handler_real($error_level, $message, $filename, $line): void {
if ($error_level & error_reporting()) {
$types = drupal_error_levels();
[$severity_msg, $severity_level] = $types[$error_level];
$backtrace = debug_backtrace();
$caller = Error::getLastCaller($backtrace);
// We treat recoverable errors as fatal.
$recoverable = $error_level == E_RECOVERABLE_ERROR;
// As __toString() methods must not throw exceptions (recoverable errors)
// in PHP, we allow them to trigger a fatal error by emitting a user error
// using trigger_error().
$to_string = $error_level == E_USER_ERROR && str_ends_with($caller['function'], '__toString()');
_drupal_log_error([
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
// The standard PHP error handler considers that the error messages
// are HTML. We mimic this behavior here.
'@message' => Markup::create(Xss::filterAdmin($message)),
'%function' => $caller['function'],
'%file' => $caller['file'],
'%line' => $caller['line'],
'severity_level' => $severity_level,
'backtrace' => $backtrace,
'@backtrace_string' => (new \Exception())->getTraceAsString(),
'exception' => NULL,
], $recoverable || $to_string);
}
// If the site is a test site then fail for user deprecations so they can be
// caught by the deprecation error handler.
elseif (defined('DRUPAL_TEST_IN_CHILD_SITE') && DRUPAL_TEST_IN_CHILD_SITE && $error_level === E_USER_DEPRECATED) {
static $seen = [];
if (array_search($message, $seen, TRUE) === FALSE) {
// Only report each deprecation once. Too many headers can break some
// Chrome and web driver testing.
$seen[] = $message;
$backtrace = debug_backtrace();
$caller = Error::getLastCaller($backtrace);
_drupal_error_header(
Markup::create(Xss::filterAdmin($message)),
'User deprecated function',
$caller['function'],
$caller['file'],
$caller['line']
);
}
}
}
/**
* Determines whether an error should be displayed.
*
* When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
* all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
* will be examined to determine if it should be displayed.
*
* @param array $error
* Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
*
* @return bool
* TRUE if an error should be displayed.
*/
function error_displayable($error = NULL) {
if (defined('MAINTENANCE_MODE')) {
return TRUE;
}
$error_level = _drupal_get_error_level();
if ($error_level == ERROR_REPORTING_DISPLAY_ALL || $error_level == ERROR_REPORTING_DISPLAY_VERBOSE) {
return TRUE;
}
if ($error_level == ERROR_REPORTING_DISPLAY_SOME && isset($error)) {
return $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning';
}
return FALSE;
}
/**
* Logs a PHP error or exception and displays an error page in fatal cases.
*
* @param array $error
* An array with the following keys: %type, @message, %function, %file, %line,
* @backtrace_string, severity_level, backtrace, and exception. All the
* parameters are plain-text, with the exception of @message, which needs to
* be an HTML string, backtrace, which is a standard PHP backtrace, and
* exception, which is the exception object (or NULL if the error is not an
* exception).
* @param bool $fatal
* TRUE for:
* - An exception is thrown and not caught by something else.
* - A recoverable fatal error, which is a fatal error.
* Non-recoverable fatal errors cannot be logged by Drupal.
*/
function _drupal_log_error($error, $fatal = FALSE): void {
$is_installer = InstallerKernel::installationAttempted() && \Drupal::hasContainer();
// Backtrace, exception and 'severity_level' are not valid replacement values
// for t().
$backtrace = $error['backtrace'];
$exception = $error['exception'];
$severity = $error['severity_level'];
unset($error['backtrace'], $error['exception'], $error['severity_level']);
// When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers.
if (defined('DRUPAL_TEST_IN_CHILD_SITE') && DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
_drupal_error_header($error['@message'], $error['%type'], $error['%function'], $error['%file'], $error['%line']);
}
$response = new Response();
// Only call the logger if there is a logger factory available. This can occur
// if there is an error while rebuilding the container or during the
// installer.
if (\Drupal::hasService('logger.factory')) {
try {
// Provide the PHP backtrace and exception to logger implementations. Add
// 'severity_level' to the context to maintain BC and allow logging
// implementations to use it.
\Drupal::logger('php')->log($severity, '%type: @message in %function (line %line of %file) @backtrace_string.', $error + ['backtrace' => $backtrace, 'exception' => $exception, 'severity_level' => $severity]);
}
catch (\Throwable) {
// We can't log, for example because the database connection is not
// available. At least try to log to PHP error log.
error_log(strtr('Failed to log error: ' . Error::DEFAULT_ERROR_MESSAGE . ' @backtrace_string', $error));
}
}
// Log fatal errors, so developers can find and debug them.
if ($fatal) {
error_log(sprintf('%s: %s in %s on line %d %s', $error['%type'], $error['@message'], $error['%file'], $error['%line'], $error['@backtrace_string']));
}
if (PHP_SAPI === 'cli') {
if ($fatal) {
// When called from CLI, simply output a plain text message.
// Should not translate the string to avoid errors producing more errors.
$response->setContent(html_entity_decode(strip_tags(new FormattableMarkup(Error::DEFAULT_ERROR_MESSAGE, $error))) . "\n");
$response->send();
exit(1);
}
}
if (\Drupal::hasRequest() && \Drupal::request()->isXmlHttpRequest()) {
if ($fatal) {
if (error_displayable($error)) {
// When called from JavaScript, simply output the error message. Should
// not translate the string to avoid errors producing more errors.
$response->setContent(new FormattableMarkup(Error::DEFAULT_ERROR_MESSAGE, $error));
$response->send();
}
exit;
}
}
else {
// Display the message if the current error reporting level allows this type
// of message to be displayed, and unconditionally in update.php.
$message = '';
$class = NULL;
if (error_displayable($error)) {
$class = 'error';
// If error type is 'User notice' then treat it as debug information
// instead of an error message.
if ($error['%type'] == 'User notice') {
$error['%type'] = 'Debug';
$class = 'status';
}
// Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
// in the message. This also prevents full path disclosure, see
// https://owasp.org/www-community/attacks/Full_Path_Disclosure.
try {
$root = \Drupal::root();
}
catch (\Throwable) {
$root = realpath(dirname(__DIR__, 2));
}
if (str_starts_with($error['%file'], $root)) {
$error['%file'] = substr($error['%file'], strlen($root) + 1);
}
// Check if verbose error reporting is on.
$error_level = _drupal_get_error_level();
if ($error_level != ERROR_REPORTING_DISPLAY_VERBOSE) {
// Without verbose logging, use a simple message.
// We use \Drupal\Component\Render\FormattableMarkup directly here,
// rather than use t() since we are in the middle of error handling, and
// we don't want t() to cause further errors.
$message = new FormattableMarkup(Error::DEFAULT_ERROR_MESSAGE, $error);
}
else {
// With verbose logging, we will also include a backtrace.
// First trace is the error itself, already contained in the message.
array_shift($backtrace);
// Strip arguments from the backtrace.
$error['@backtrace'] = Error::formatBacktrace(array_map(function ($trace) {
unset($trace['args']);
return $trace;
}, $backtrace));
$message = new FormattableMarkup('<details class="error-with-backtrace"><summary>' . Error::DEFAULT_ERROR_MESSAGE . '</summary><pre class="backtrace">@backtrace</pre></details>', $error);
}
}
if ($fatal) {
// We fallback to a maintenance page at this point, because the page
// generation itself can generate errors.
// Should not translate the string to avoid errors producing more errors.
$message = 'The website encountered an unexpected error. Try again later.<br />' . $message;
if ($is_installer) {
// install_display_output() prints the output and ends script execution.
$output = [
'#title' => 'Error',
'#markup' => $message,
];
try {
install_display_output($output, $GLOBALS['install_state']);
exit;
}
catch (\Throwable) {
// The maintenance page failed, so fall back to a plain error message.
}
}
$response->setContent($message);
$response->setStatusCode(500, '500 Service unavailable (with message)');
$response->send();
// An exception must halt script execution.
exit;
}
if ($message) {
if (\Drupal::hasService('session')) {
// Message display is dependent on sessions being available.
\Drupal::messenger()->addMessage($message, $class, TRUE);
}
else {
print $message;
}
}
}
}
/**
* Returns the current error level.
*
* This function should only be used to get the current error level prior to the
* kernel being booted or before Drupal is installed. In all other situations
* the following code is preferred:
* @code
* \Drupal::config('system.logging')->get('error_level');
* @endcode
*
* @return string
* The current error level.
*/
function _drupal_get_error_level() {
// Raise the error level to maximum for the installer, so users are able to
// file proper bug reports for installer errors. The returned value is
// different to the one below, because the installer actually has a
// 'config.factory' service, which reads the default 'error_level' value from
// System module's default configuration and the default value is not verbose.
// @see error_displayable()
if (InstallerKernel::installationAttempted()) {
return ERROR_REPORTING_DISPLAY_VERBOSE;
}
$error_level = NULL;
// Try to get the error level configuration from database. If this fails,
// for example if the database connection is not there, try to read it from
// settings.php.
try {
$error_level = \Drupal::config('system.logging')->get('error_level');
}
catch (\Exception) {
$error_level = $GLOBALS['config']['system.logging']['error_level'] ?? ERROR_REPORTING_HIDE;
}
// If there is no container or if it has no config.factory service, we are
// possibly in an edge-case error situation while trying to serve a regular
// request on a public site, so use the non-verbose default value.
return $error_level ?: ERROR_REPORTING_DISPLAY_ALL;
}
/**
* Adds error information to headers so that tests can access it.
*
* @param string $message
* The error message.
* @param string $type
* The type of error.
* @param string $function
* The function that emitted the error.
* @param string $file
* The file that emitted the error.
* @param int $line
* The line number in file that emitted the error.
*/
function _drupal_error_header($message, $type, $function, $file, $line): void {
// $number does not use drupal_static as it should not be reset
// as it uniquely identifies each PHP error.
static $number = 0;
$assertion = [
$message,
$type,
[
'function' => $function,
'file' => $file,
'line' => $line,
],
];
// For non-fatal errors (e.g. PHP notices) _drupal_log_error can be called
// multiple times per request. In that case the response is typically
// generated outside of the error handler, e.g., in a controller. As a
// result it is not possible to use a Response object here but instead the
// headers need to be emitted directly.
header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
$number++;
}

1048
web/core/includes/form.inc Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,869 @@
<?php
/**
* @file
* API functions for installing modules and themes.
*/
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Database\Database;
use Drupal\Core\Extension\Dependency;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\InstallRequirementsInterface;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Installer\InstallerKernel;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Requirement severity -- Informational message only.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
* \Drupal\Core\Extension\Requirement\RequirementSeverity::Info instead.
*
* @see https://www.drupal.org/node/3410939
*/
const REQUIREMENT_INFO = -1;
/**
* Requirement severity -- Requirement successfully met.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
* \Drupal\Core\Extension\Requirement\RequirementSeverity::OK instead.
*
* @see https://www.drupal.org/node/3410939
*/
const REQUIREMENT_OK = 0;
/**
* Requirement severity -- Warning condition; proceed but flag warning.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
* \Drupal\Core\Extension\Requirement\RequirementSeverity::Warning instead.
*
* @see https://www.drupal.org/node/3410939
*/
const REQUIREMENT_WARNING = 1;
/**
* Requirement severity -- Error condition; abort installation.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
* \Drupal\Core\Extension\Requirement\RequirementSeverity::Error instead.
*
* @see https://www.drupal.org/node/3410939
*/
const REQUIREMENT_ERROR = 2;
/**
* File permission check -- File exists.
*/
const FILE_EXIST = 1;
/**
* File permission check -- File is readable.
*/
const FILE_READABLE = 2;
/**
* File permission check -- File is writable.
*/
const FILE_WRITABLE = 4;
/**
* File permission check -- File is executable.
*/
const FILE_EXECUTABLE = 8;
/**
* File permission check -- File does not exist.
*/
const FILE_NOT_EXIST = 16;
/**
* File permission check -- File is not readable.
*/
const FILE_NOT_READABLE = 32;
/**
* File permission check -- File is not writable.
*/
const FILE_NOT_WRITABLE = 64;
/**
* File permission check -- File is not executable.
*/
const FILE_NOT_EXECUTABLE = 128;
/**
* Loads .install files for installed modules to initialize the update system.
*/
function drupal_load_updates(): void {
/** @var \Drupal\Core\Extension\ModuleExtensionList $extension_list_module */
$extension_list_module = \Drupal::service('extension.list.module');
foreach (\Drupal::service('update.update_hook_registry')->getAllInstalledVersions() as $module => $schema_version) {
if ($extension_list_module->exists($module) && !$extension_list_module->checkIncompatibility($module)) {
if ($schema_version > -1) {
\Drupal::moduleHandler()->loadInclude($module, 'install');
}
}
}
}
/**
* Loads the installation profile, extracting its defined distribution name.
*
* @return string
* The distribution name defined in the profile's .info.yml file. Defaults to
* "Drupal" if none is explicitly provided by the installation profile.
*
* @see install_profile_info()
*/
function drupal_install_profile_distribution_name() {
// During installation, the profile information is stored in the global
// installation state (it might not be saved anywhere yet).
$info = [];
if (InstallerKernel::installationAttempted()) {
global $install_state;
if (isset($install_state['profile_info'])) {
$info = $install_state['profile_info'];
}
}
// At all other times, we load the profile via standard methods.
elseif ($profile = \Drupal::installProfile()) {
$info = \Drupal::service('extension.list.profile')->getExtensionInfo($profile);
}
return $info['distribution']['name'] ?? 'Drupal';
}
/**
* Loads the installation profile, extracting its defined version.
*
* @return string
* Distribution version defined in the profile's .info.yml file.
* Defaults to \Drupal::VERSION if no version is explicitly provided by the
* installation profile.
*
* @see install_profile_info()
*/
function drupal_install_profile_distribution_version() {
// During installation, the profile information is stored in the global
// installation state (it might not be saved anywhere yet).
if (InstallerKernel::installationAttempted()) {
global $install_state;
return $install_state['profile_info']['version'] ?? \Drupal::VERSION;
}
// At all other times, we load the profile via standard methods.
else {
$profile = \Drupal::installProfile();
$info = \Drupal::service('extension.list.profile')->getExtensionInfo($profile);
return $info['version'];
}
}
/**
* Verifies that all dependencies are met for a given installation profile.
*
* @param array $install_state
* An array of information about the current installation state.
*
* @return array
* The list of modules to install.
*
* @todo https://www.drupal.org/i/3005959 Rework this method as it is not only
* about profiles.
*/
function drupal_verify_profile($install_state): array {
$profile = $install_state['parameters']['profile'];
if ($profile === FALSE) {
return [];
}
$info = $install_state['profile_info'];
// Get the list of available modules for the selected installation profile.
$listing = new ExtensionDiscovery(\Drupal::root());
$present_modules = [];
foreach ($listing->scan('module') as $present_module) {
$present_modules[] = $present_module->getName();
}
// The installation profile is also a module, which needs to be installed
// after all the other dependencies have been installed.
$present_modules[] = $profile;
// Verify that all of the profile's required modules are present.
$missing_modules = array_diff($info['install'], $present_modules);
$requirements = [];
if ($missing_modules) {
$build = [
'#theme' => 'item_list',
'#context' => ['list_style' => 'comma-list'],
];
foreach ($missing_modules as $module) {
$build['#items'][] = ['#markup' => '<span class="admin-missing">' . Unicode::ucfirst($module) . '</span>'];
}
$modules_list = \Drupal::service('renderer')->renderInIsolation($build);
$requirements['required_modules'] = [
'title' => t('Required modules'),
'value' => t('Required modules not found.'),
'severity' => RequirementSeverity::Error,
'description' => t('The following modules are required but were not found. Move them into the appropriate modules subdirectory, such as <em>/modules</em>. Missing modules: @modules', ['@modules' => $modules_list]),
];
}
return $requirements;
}
/**
* Installs the system module.
*
* Separated from the installation of other modules so core system
* functions can be made available while other modules are installed.
*
* @param array $install_state
* An array of information about the current installation state. This is used
* to set the default language.
*/
function drupal_install_system($install_state): void {
// Remove the service provider of the early installer.
unset($GLOBALS['conf']['container_service_providers']['InstallerServiceProvider']);
// Add the normal installer service provider.
$GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\NormalInstallerServiceProvider';
// Get the existing request.
$request = \Drupal::request();
// Reboot into a full production environment to continue the installation.
/** @var \Drupal\Core\Installer\InstallerKernel $kernel */
$kernel = \Drupal::service('kernel');
$kernel->shutdown();
// Have installer rebuild from the disk, rather then building from scratch.
$kernel->rebuildContainer();
// Reboot the kernel with new container.
$kernel->boot();
$kernel->preHandle($request);
// Ensure our request includes the session if appropriate.
if (PHP_SAPI !== 'cli') {
$request->setSession($kernel->getContainer()->get('session'));
}
// Before having installed the system module and being able to do a module
// rebuild, prime the \Drupal\Core\Extension\ModuleExtensionList static cache
// with the module's location.
// @todo Try to install system as any other module, see
// https://www.drupal.org/node/2719315.
\Drupal::service('extension.list.module')->setPathname('system', 'core/modules/system/system.info.yml');
// Install base system configuration.
\Drupal::service('config.installer')->installDefaultConfig('core', 'core');
// Store the installation profile in configuration to populate the
// 'install_profile' container parameter.
$config = \Drupal::configFactory()->getEditable('core.extension');
if ($install_state['parameters']['profile'] === FALSE) {
$config->clear('profile');
}
else {
$config->set('profile', $install_state['parameters']['profile']);
}
$config->save();
$connection = Database::getConnection();
$provider = $connection->getProvider();
// When the database driver is provided by a module, then install that module.
// This module must be installed before any other module, as it must be able
// to override any call to hook_schema() or any "backend_overridable" service.
// In edge cases, a driver module may extend from another driver module (for
// instance, a module to provide backward compatibility with a database
// version no longer supported by core). In order for the extended classes to
// be autoloadable, the extending module should list the extended module in
// its dependencies, and here the dependencies will be installed as well.
if ($provider !== 'core') {
$autoload = $connection->getConnectionOptions()['autoload'] ?? '';
if (str_contains($autoload, 'src/Driver/Database/')) {
$kernel->getContainer()->get('module_installer')->install([$provider], TRUE);
}
}
// Install System module.
$kernel->getContainer()->get('module_installer')->install(['system'], FALSE);
// Ensure default language is saved.
if (isset($install_state['parameters']['langcode'])) {
\Drupal::configFactory()->getEditable('system.site')
->set('langcode', (string) $install_state['parameters']['langcode'])
->set('default_langcode', (string) $install_state['parameters']['langcode'])
->save(TRUE);
}
}
/**
* Verifies the state of the specified file.
*
* @param string $file
* The file to check for.
* @param int $mask
* An optional bitmask created from various FILE_* constants.
* @param string|null $type
* The type of file. Can be file (default), dir, or link.
* @param bool $auto_fix
* (optional) Determines whether to attempt fixing the permissions according
* to the provided $mask. Defaults to TRUE.
*
* @return bool
* TRUE on success or FALSE on failure. A message is set for the latter.
*/
function drupal_verify_install_file($file, $mask = NULL, $type = 'file', $auto_fix = TRUE) {
$return = TRUE;
// Check for files that shouldn't be there.
if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) {
return FALSE;
}
// Verify that the file is the type of file it is supposed to be.
if (isset($type) && file_exists($file)) {
$check = 'is_' . $type;
if (!function_exists($check) || !$check($file)) {
$return = FALSE;
}
}
// Verify file permissions.
if (isset($mask)) {
$masks = [FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
foreach ($masks as $current_mask) {
if ($mask & $current_mask) {
switch ($current_mask) {
case FILE_EXIST:
if (!file_exists($file)) {
if ($type == 'dir' && $auto_fix) {
drupal_install_mkdir($file, $mask);
}
if (!file_exists($file)) {
$return = FALSE;
}
}
break;
case FILE_READABLE:
if (!is_readable($file)) {
$return = FALSE;
}
break;
case FILE_WRITABLE:
if (!is_writable($file)) {
$return = FALSE;
}
break;
case FILE_EXECUTABLE:
if (!is_executable($file)) {
$return = FALSE;
}
break;
case FILE_NOT_READABLE:
if (is_readable($file)) {
$return = FALSE;
}
break;
case FILE_NOT_WRITABLE:
if (is_writable($file)) {
$return = FALSE;
}
break;
case FILE_NOT_EXECUTABLE:
if (is_executable($file)) {
$return = FALSE;
}
break;
}
}
}
}
if (!$return && $auto_fix) {
return drupal_install_fix_file($file, $mask);
}
return $return;
}
/**
* Creates a directory with the specified permissions.
*
* @param string $file
* The name of the directory to create.
* @param int $mask
* The permissions of the directory to create.
* @param bool $message
* (optional) Whether to output messages. Defaults to TRUE.
*
* @return bool
* TRUE/FALSE whether or not the directory was successfully created.
*/
function drupal_install_mkdir($file, $mask, $message = TRUE) {
$mod = 0;
$masks = [FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
foreach ($masks as $m) {
if ($mask & $m) {
switch ($m) {
case FILE_READABLE:
$mod |= 0444;
break;
case FILE_WRITABLE:
$mod |= 0222;
break;
case FILE_EXECUTABLE:
$mod |= 0111;
break;
}
}
}
if (@\Drupal::service('file_system')->mkdir($file, $mod)) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Attempts to fix file permissions.
*
* The general approach here is that, because we do not know the security
* setup of the webserver, we apply our permission changes to all three
* digits of the file permission (i.e. user, group and all).
*
* To ensure that the values behave as expected (and numbers don't carry
* from one digit to the next) we do the calculation on the octal value
* using bitwise operations. This lets us remove, for example, 0222 from
* 0700 and get the correct value of 0500.
*
* @param string $file
* The name of the file with permissions to fix.
* @param int $mask
* The desired permissions for the file.
* @param bool $message
* (optional) Whether to output messages. Defaults to TRUE.
*
* @return bool
* TRUE/FALSE whether or not we were able to fix the file's permissions.
*/
function drupal_install_fix_file($file, $mask, $message = TRUE) {
// If $file does not exist, fileperms() issues a PHP warning.
if (!file_exists($file)) {
return FALSE;
}
$mod = fileperms($file) & 0777;
$masks = [FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
// FILE_READABLE, FILE_WRITABLE, and FILE_EXECUTABLE permission strings
// can theoretically be 0400, 0200, and 0100 respectively, but to be safe
// we set all three access types in case the administrator intends to
// change the owner of settings.php after installation.
foreach ($masks as $m) {
if ($mask & $m) {
switch ($m) {
case FILE_READABLE:
if (!is_readable($file)) {
$mod |= 0444;
}
break;
case FILE_WRITABLE:
if (!is_writable($file)) {
$mod |= 0222;
}
break;
case FILE_EXECUTABLE:
if (!is_executable($file)) {
$mod |= 0111;
}
break;
case FILE_NOT_READABLE:
if (is_readable($file)) {
$mod &= ~0444;
}
break;
case FILE_NOT_WRITABLE:
if (is_writable($file)) {
$mod &= ~0222;
}
break;
case FILE_NOT_EXECUTABLE:
if (is_executable($file)) {
$mod &= ~0111;
}
break;
}
}
}
// chmod() will work if the web server is running as owner of the file.
if (@chmod($file, $mod)) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Sends the user to a different installer page.
*
* This issues an on-site HTTP redirect. Messages (and errors) are erased.
*
* @param string $path
* An installer path.
*/
function install_goto($path): void {
global $base_url;
$headers = [
// Not a permanent redirect.
'Cache-Control' => 'no-cache',
];
$response = new RedirectResponse($base_url . '/' . $path, 302, $headers);
$response->send();
}
/**
* Returns the URL of the current script, with modified query parameters.
*
* This function can be called by low-level scripts (such as install.php and
* update.php) and returns the URL of the current script. Existing query
* parameters are preserved by default, but new ones can optionally be merged
* in.
*
* This function is used when the script must maintain certain query parameters
* over multiple page requests in order to work correctly. In such cases (for
* example, update.php, which requires the 'continue=1' parameter to remain in
* the URL throughout the update process if there are any requirement warnings
* that need to be bypassed), using this function to generate the URL for links
* to the next steps of the script ensures that the links will work correctly.
*
* @param array $query
* (optional) An array of query parameters to merge in to the existing ones.
*
* @return string
* The URL of the current script, with query parameters modified by the
* passed-in $query. The URL is not sanitized, so it still needs to be run
* through \Drupal\Component\Utility\UrlHelper::filterBadProtocol() if it will
* be used as an HTML attribute value.
*
* @see drupal_requirements_url()
* @see Drupal\Component\Utility\UrlHelper::filterBadProtocol()
*/
function drupal_current_script_url($query = []) {
$uri = $_SERVER['SCRIPT_NAME'];
$query = array_merge(UrlHelper::filterQueryParameters(\Drupal::request()->query->all()), $query);
if (!empty($query)) {
$uri .= '?' . UrlHelper::buildQuery($query);
}
return $uri;
}
/**
* Returns a URL for proceeding to the next page after a requirements problem.
*
* This function can be called by low-level scripts (such as install.php and
* update.php) and returns a URL that can be used to attempt to proceed to the
* next step of the script.
*
* @param int|\Drupal\Core\Extension\Requirement\RequirementSeverity $severity
* The severity of the requirements problem, as returned by
* drupal_requirements_severity().
*
* @return string
* A URL for attempting to proceed to the next step of the script. The URL is
* not sanitized, so it still needs to be run through
* \Drupal\Component\Utility\UrlHelper::filterBadProtocol() if it will be used
* as an HTML attribute value.
*
* @see drupal_current_script_url()
* @see \Drupal\Component\Utility\UrlHelper::filterBadProtocol()
*/
function drupal_requirements_url(/* int|RequirementSeverity */ $severity): string {
if (!$severity instanceof RequirementSeverity) {
@trigger_error('Passing a type other than ' . RequirementSeverity::class . ' to ' . __FUNCTION__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a ' . RequirementSeverity::class . ' enum instead. See https://www.drupal.org/node/3410939', E_USER_DEPRECATED);
$severity = RequirementSeverity::from($severity);
}
if (is_null($severity)) {
$severity = RequirementSeverity::Info;
}
$query = [];
// If there are no errors, only warnings, append 'continue=1' to the URL so
// the user can bypass this screen on the next page load.
if ($severity === RequirementSeverity::Warning) {
$query['continue'] = 1;
}
return drupal_current_script_url($query);
}
/**
* Checks an installation profile's requirements.
*
* @param string $profile
* Name of installation profile to check.
*
* @return array
* Array of the installation profile's requirements.
*/
function drupal_check_profile($profile): array {
$info = install_profile_info($profile);
// Collect requirement testing results.
$requirements = [];
// Performs an ExtensionDiscovery scan as the system module is unavailable and
// we don't yet know where all the modules are located.
// @todo Remove as part of https://www.drupal.org/node/2186491
$drupal_root = \Drupal::root();
$module_list = (new ExtensionDiscovery($drupal_root))->scan('module');
foreach ($info['install'] as $module) {
// If the module is in the module list we know it exists and we can continue
// including and registering it.
// @see \Drupal\Core\Extension\ExtensionDiscovery::scanDirectory()
if (isset($module_list[$module])) {
$function = $module . '_requirements';
$module_path = $module_list[$module]->getPath();
$install_file = "$drupal_root/$module_path/$module.install";
if (is_file($install_file)) {
require_once $install_file;
}
if (function_exists($function)) {
$requirements = array_merge($requirements, $function('install'));
}
$requirements = array_merge($requirements, install_check_class_requirements($module_list[$module]));
}
}
// Add the profile requirements.
$function = $profile . '_requirements';
if (function_exists($function)) {
$requirements = array_merge($requirements, $function('install'));
}
$extension = \Drupal::service('extension.list.profile')->get($profile);
$requirements = array_merge($requirements, install_check_class_requirements($extension));
return $requirements;
}
/**
* Extracts the highest severity from the requirements array.
*
* @param array $requirements
* An array of requirements, in the same format as is returned by
* hook_requirements().
*
* @return int
* The highest severity in the array.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
* \Drupal\Core\Extension\Requirement\RequirementSeverity::getMaxSeverity()
* instead.
*
* @see https://www.drupal.org/node/3410939
*/
function drupal_requirements_severity(&$requirements) {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use ' . RequirementSeverity::class . '::maxSeverityFromRequirements() instead. See https://www.drupal.org/node/3410939', E_USER_DEPRECATED);
return RequirementSeverity::maxSeverityFromRequirements($requirements)->value;
}
/**
* Checks a module's requirements.
*
* @param string $module
* Machine name of module to check.
*
* @return bool
* TRUE or FALSE, depending on whether the requirements are met.
*/
function drupal_check_module($module) {
/** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */
$module_list = \Drupal::service('extension.list.module');
$extension = $module_list->get($module);
$file = \Drupal::root() . '/' . $extension->getPath() . "/$module.install";
if (is_file($file)) {
require_once $file;
}
// Check requirements
$requirements = \Drupal::moduleHandler()->invoke($module, 'requirements', ['install']) ?? [];
$requirements = array_merge($requirements, install_check_class_requirements($extension));
if (!empty($requirements) && RequirementSeverity::maxSeverityFromRequirements($requirements) === RequirementSeverity::Error) {
// Print any error messages
foreach ($requirements as $requirement) {
if (isset($requirement['severity']) && $requirement['severity'] === RequirementSeverity::Error) {
$message = $requirement['description'];
if (isset($requirement['value']) && $requirement['value']) {
$message = t('@requirements_message (Currently using @item version @version)', ['@requirements_message' => $requirement['description'], '@item' => $requirement['title'], '@version' => $requirement['value']]);
}
\Drupal::messenger()->addError($message);
}
}
return FALSE;
}
return TRUE;
}
/**
* Retrieves information about an installation profile from its .info.yml file.
*
* The information stored in a profile .info.yml file is similar to that stored
* in a normal Drupal module .info.yml file. For example:
* - name: The real name of the installation profile for display purposes.
* - description: A brief description of the profile.
* - dependencies: An array of short names of other modules that this install
* profile requires.
* - install: An array of shortname of other modules to install that are not
* required by this install profile.
*
* Additional, less commonly-used information that can appear in a
* profile.info.yml file but not in a normal Drupal module .info.yml file
* includes:
*
* - distribution: Existence of this key denotes that the installation profile
* is intended to be the only eligible choice in a distribution and will be
* auto-selected during installation, whereas the installation profile
* selection screen will be skipped. If more than one distribution profile is
* found then the first one discovered will be selected.
* The following subproperties may be set:
* - name: The name of the distribution that is being installed, to be shown
* throughout the installation process. If omitted,
* drupal_install_profile_distribution_name() defaults to 'Drupal'.
* - install: Optional parameters to override the installer:
* - theme: The machine name of a theme to use in the installer instead of
* Drupal's default installer theme.
* - finish_url: A destination to visit after the installation of the
* distribution is finished
*
* Note that this function does an expensive file system scan to get info file
* information for dependencies. If you only need information from the info
* file itself, use
* \Drupal::service('extension.list.profile')->getExtensionInfo().
*
* Example of .info.yml file:
* @code
* name: Minimal
* description: Start fresh, with only a few modules enabled.
* install:
* - block
* - dblog
* @endcode
*
* @param string $profile
* Name of profile.
* @param string $langcode
* Language code (if any).
*
* @return array
* The info array.
*/
function install_profile_info($profile, $langcode = 'en') {
static $cache = [];
if (!isset($cache[$profile][$langcode])) {
// Set defaults for module info.
$defaults = [
'dependencies' => [],
'install' => [],
'themes' => ['stark'],
'description' => '',
'version' => NULL,
'hidden' => FALSE,
'php' => \Drupal::MINIMUM_PHP,
'config_install_path' => NULL,
];
$profile_path = \Drupal::service('extension.list.profile')->getPath($profile);
/** @var \Drupal\Core\Extension\InfoParserInterface $parser */
$parser = \Drupal::service('info_parser');
$info = $parser->parse("$profile_path/$profile.info.yml");
$info += $defaults;
$dependency_name_function = function ($dependency) {
return Dependency::createFromString($dependency)->getName();
};
// Convert dependencies in [project:module] format.
$info['dependencies'] = array_map($dependency_name_function, $info['dependencies']);
// Convert install key in [project:module] format.
$info['install'] = array_map($dependency_name_function, $info['install']);
// Get a list of core's required modules.
$required = [];
$listing = new ExtensionDiscovery(\Drupal::root());
$files = $listing->scan('module');
foreach ($files as $name => $file) {
$parsed = $parser->parse($file->getPathname());
if (!empty($parsed) && !empty($parsed['required']) && $parsed['required']) {
$required[] = $name;
}
}
$locale = !empty($langcode) && $langcode != 'en' ? ['locale'] : [];
// Merge dependencies, required modules and locale into install list and
// remove any duplicates.
$info['install'] = array_unique(array_merge($info['install'], $required, $info['dependencies'], $locale));
// If the profile has a config/sync directory use that to install drupal.
if (is_dir($profile_path . '/config/sync')) {
$info['config_install_path'] = $profile_path . '/config/sync';
}
$cache[$profile][$langcode] = $info;
}
return $cache[$profile][$langcode];
}
/**
* Checks a module or profile requirements.
*
* See \Drupal\Core\Extension\InstallRequirementsInterface for more information.
*
* @param \Drupal\Core\Extension $extension
* The extension to check install requirements for.
*
* @return array
* The requirements.
*
* @internal
*/
function install_check_class_requirements(Extension $extension): array {
$extension_path = $extension->getPath();
$extension_name = $extension->getName();
$dir = \Drupal::root() . "/$extension_path/src/Install/Requirements";
$requirements = [];
if (is_dir($dir)) {
$fileSystemIterator = new FilesystemIterator($dir);
foreach ($fileSystemIterator as $fileInfo) {
if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') {
$filename = $fileInfo->getFilename();
$requirements_path = $dir . '/' . $filename;
require_once $requirements_path;
$namespace = "Drupal\\$extension_name\\Install\\Requirements";
$class_name = $namespace . '\\' . $fileInfo->getBasename('.php');
if (class_exists($class_name) && class_implements($class_name, InstallRequirementsInterface::class)) {
$requirements = $class_name::getRequirements();
}
}
}
}
return $requirements;
}

View File

@ -0,0 +1,75 @@
<?php
/**
* @file
* API for loading and interacting with Drupal modules.
*/
/**
* Sets weight of a particular module.
*
* The weight of uninstalled modules cannot be changed.
*
* @param string $module
* The name of the module (without the .module extension).
* @param int $weight
* An integer representing the weight of the module.
*/
function module_set_weight($module, $weight): void {
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
if ($extension_config->get("module.$module") !== NULL) {
// Pre-cast the $weight to an integer so that we can save this without using
// schema. This is a performance improvement for module installation.
$extension_config
->set("module.$module", (int) $weight)
->set('module', module_config_sort($extension_config->get('module')))
->save(TRUE);
// Prepare the new module list, sorted by weight, including filenames.
// @see \Drupal\Core\Extension\ModuleInstaller::install()
$module_handler = \Drupal::moduleHandler();
$current_module_filenames = $module_handler->getModuleList();
$current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
$current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
$module_filenames = [];
foreach ($current_modules as $name => $weight) {
$module_filenames[$name] = $current_module_filenames[$name];
}
// Update the module list in the extension handler.
$module_handler->setModuleList($module_filenames);
return;
}
}
/**
* Sorts the configured list of enabled modules.
*
* The list of enabled modules is expected to be ordered by weight and name.
* The list is always sorted on write to avoid the overhead on read.
*
* @param array $data
* An array of module configuration data.
*
* @return array
* An array of module configuration data sorted by weight and name.
*/
function module_config_sort($data) {
// PHP array sorting functions such as uasort() do not work with both keys and
// values at the same time, so we achieve weight and name sorting by computing
// strings with both information concatenated (weight first, name second) and
// use that as a regular string sort reference list via array_multisort(),
// compound of "[sign-as-integer][padded-integer-weight][name]"; e.g., given
// two modules and weights (spaces added for clarity):
// - Block with weight -5: 0 0000000000000000005 block
// - Node with weight 0: 1 0000000000000000000 node
$sort = [];
foreach ($data as $name => $weight) {
// Prefix negative weights with 0, positive weights with 1.
// +/- signs cannot be used, since + (ASCII 43) is before - (ASCII 45).
$prefix = (int) ($weight >= 0);
// The maximum weight is PHP_INT_MAX, so pad all weights to 19 digits.
$sort[] = $prefix . sprintf('%019d', abs($weight)) . $name;
}
array_multisort($sort, SORT_STRING, $data);
return $data;
}

1566
web/core/includes/theme.inc Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,141 @@
<?php
/**
* @file
* Theming for maintenance pages.
*/
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Site\Settings;
/**
* Sets up the theming system for maintenance page.
*
* Used for site installs, updates and when the site is in maintenance mode.
* It also applies when the database is unavailable or bootstrap was not
* complete. Claro is always used for the initial install and update
* operations, but this can be overridden by setting a "maintenance_theme" key
* in the $settings variable in settings.php.
*/
function _drupal_maintenance_theme(): void {
// If the theme is already set, assume the others are set too, and do nothing.
if (\Drupal::theme()->hasActiveTheme()) {
return;
}
require_once __DIR__ . '/theme.inc';
require_once __DIR__ . '/common.inc';
require_once __DIR__ . '/module.inc';
// Install and update pages are treated differently to prevent theming
// overrides.
if (defined('MAINTENANCE_MODE') && (MAINTENANCE_MODE == 'install' || MAINTENANCE_MODE == 'update')) {
if (InstallerKernel::installationAttempted()) {
$custom_theme = $GLOBALS['install_state']['theme'];
}
else {
$custom_theme = Settings::get('maintenance_theme', 'claro');
}
}
else {
// Use the maintenance theme if specified, otherwise attempt to use the
// default site theme.
try {
$custom_theme = Settings::get('maintenance_theme', '');
if (!$custom_theme) {
$config = \Drupal::config('system.theme');
$custom_theme = $config->get('default');
}
}
catch (\Exception) {
// Whatever went wrong (often a database connection problem), we are
// about to fall back to a sensible theme so there is no need for special
// handling.
}
if (!$custom_theme) {
// We have been unable to identify the configured theme, so fall back to
// a safe default. Claro is reasonably user friendly and fairly generic.
$custom_theme = 'claro';
}
}
$themes = \Drupal::service('theme_handler')->listInfo();
// If no themes are installed yet, or if the requested custom theme is not
// installed, retrieve all available themes.
/** @var \Drupal\Core\Theme\ThemeInitialization $theme_init */
$theme_init = \Drupal::service('theme.initialization');
$theme_handler = \Drupal::service('theme_handler');
if (empty($themes) || !isset($themes[$custom_theme])) {
$themes = \Drupal::service('extension.list.theme')->getList();
$theme_handler->addTheme($themes[$custom_theme]);
}
// \Drupal\Core\Extension\ThemeHandlerInterface::listInfo() triggers a
// \Drupal\Core\Extension\ModuleHandler::alter() in maintenance mode, but we
// can't let themes alter the .info.yml data until we know a theme's base
// themes. So don't set active theme until after
// \Drupal\Core\Extension\ThemeHandlerInterface::listInfo() builds its cache.
$theme = $custom_theme;
// Find all our ancestor themes and put them in an array.
// @todo This is just a workaround. Find a better way how to handle themes
// on maintenance pages, see https://www.drupal.org/node/2322619.
// This code is basically a duplicate of
// \Drupal\Core\Theme\ThemeInitialization::getActiveThemeByName.
$base_themes = [];
$ancestor = $theme;
while ($ancestor && isset($themes[$ancestor]->base_theme)) {
$base_themes[] = $themes[$themes[$ancestor]->base_theme];
$ancestor = $themes[$ancestor]->base_theme;
if ($ancestor) {
// Ensure that the base theme is added and installed.
$theme_handler->addTheme($themes[$ancestor]);
}
}
\Drupal::theme()->setActiveTheme($theme_init->getActiveTheme($themes[$custom_theme], $base_themes));
// Prime the theme registry.
Drupal::service('theme.registry');
}
/**
* Prepares variables for authorize.php operation report templates.
*
* This report displays the results of an operation run via authorize.php.
*
* Default template: authorize-report.html.twig.
*
* @param array $variables
* An associative array containing:
* - messages: An array of result messages.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no
* replacement. Use composer to manage the code for your site.
*
* @see https://www.drupal.org/node/3522119
*/
function template_preprocess_authorize_report(&$variables): void {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3522119', E_USER_DEPRECATED);
$messages = [];
if (!empty($variables['messages'])) {
foreach ($variables['messages'] as $heading => $logs) {
$items = [];
foreach ($logs as $number => $log_message) {
if ($number === '#abort') {
continue;
}
$class = 'authorize-results__' . ($log_message['success'] ? 'success' : 'failure');
$items[] = [
'#wrapper_attributes' => ['class' => [$class]],
'#markup' => $log_message['message'],
];
}
$messages[] = [
'#theme' => 'item_list',
'#items' => $items,
'#title' => $heading,
];
}
}
$variables['messages'] = $messages;
}

View File

@ -0,0 +1,709 @@
<?php
/**
* @file
* Drupal database update API.
*
* This file contains functions to perform database updates for a Drupal
* installation. It is included and used extensively by update.php.
*/
use Drupal\Component\Graph\Graph;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Utility\Error;
use Drupal\Core\Update\EquivalentUpdate;
/**
* Returns whether the minimum schema requirement has been satisfied.
*
* @return array
* A requirements info array.
*/
function update_system_schema_requirements(): array {
$requirements = [];
$system_schema = \Drupal::service('update.update_hook_registry')->getInstalledVersion('system');
$requirements['minimum schema']['title'] = 'Minimum schema version';
if ($system_schema >= \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
$requirements['minimum schema'] += [
'value' => 'The installed schema version meets the minimum.',
'description' => 'Schema version: ' . $system_schema,
];
}
else {
$requirements['minimum schema'] += [
'value' => 'The installed schema version does not meet the minimum.',
'severity' => RequirementSeverity::Error,
'description' => 'Your system schema version is ' . $system_schema . '. Updating directly from a schema version prior to 8000 is not supported. You must upgrade your site to Drupal 8 first, see https://www.drupal.org/docs/8/upgrade.',
];
}
return $requirements;
}
/**
* Checks update requirements and reports errors and (optionally) warnings.
*/
function update_check_requirements() {
// Because this is one of the earliest points in the update process,
// detect and fix missing schema versions for modules here to ensure
// it runs on all update code paths.
_update_fix_missing_schema();
// Check requirements of all loaded modules.
$requirements = array_merge(
\Drupal::moduleHandler()->invokeAll('requirements', ['update']),
\Drupal::moduleHandler()->invokeAll('update_requirements')
);
\Drupal::moduleHandler()->alter('requirements', $requirements);
\Drupal::moduleHandler()->alter('update_requirements', $requirements);
$requirements += update_system_schema_requirements();
return $requirements;
}
/**
* Helper to detect and fix 'missing' schema information.
*
* Repairs the case where a module has no schema version recorded.
* This has to be done prior to updates being run, otherwise the update
* system would detect and attempt to run all historical updates for a
* module.
*
* @todo remove in a major version after
* https://www.drupal.org/project/drupal/issues/3130037 has been fixed.
*/
function _update_fix_missing_schema(): void {
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
$update_registry = \Drupal::service('update.update_hook_registry');
$versions = $update_registry->getAllInstalledVersions();
$module_handler = \Drupal::moduleHandler();
$enabled_modules = $module_handler->getModuleList();
foreach (array_keys($enabled_modules) as $module) {
// All modules should have a recorded schema version, but when they
// don't, detect and fix the problem.
if (!isset($versions[$module])) {
// Ensure the .install file is loaded.
$module_handler->loadInclude($module, 'install');
$all_updates = $update_registry->getAvailableUpdates($module);
// If the schema version of a module hasn't been recorded, we cannot
// know the actual schema version a module is at, because
// no updates will ever have been run on the site and it was not set
// correctly when the module was installed, so instead set it to
// the same as the last update. This means that updates will proceed
// again the next time the module is updated and a new update is
// added. Updates added in between the module being installed and the
// schema version being fixed here (if any have been added) will never
// be run, but we have no way to identify which updates these are.
if ($all_updates) {
$last_update = max($all_updates);
}
else {
$last_update = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
}
// If the module implements hook_update_last_removed() use the
// value of that if it's higher than the schema versions found so
// far.
$function = $module . '_update_last_removed';
if (function_exists($function) && ($last_removed = $function())) {
$last_update = max($last_update, $last_removed);
}
$update_registry->setInstalledVersion($module, $last_update);
$args = ['%module' => $module, '%last_update_hook' => $module . '_update_' . $last_update . '()'];
\Drupal::messenger()->addWarning(t('Schema information for module %module was missing from the database. You should manually review the module updates and your database to check if any updates have been skipped up to, and including, %last_update_hook.', $args));
\Drupal::logger('update')->warning('Schema information for module %module was missing from the database. You should manually review the module updates and your database to check if any updates have been skipped up to, and including, %last_update_hook.', $args);
}
}
}
/**
* Implements callback_batch_operation().
*
* Performs one update and stores the results for display on the results page.
*
* If an update function completes successfully, it should return a message
* as a string indicating success, for example:
* @code
* return t('New index added successfully.');
* @endcode
*
* Alternatively, it may return nothing. In that case, no message
* will be displayed at all.
*
* If it fails for whatever reason, it should throw an instance of
* Drupal\Core\Utility\UpdateException with an appropriate error message, for
* example:
* @code
* use Drupal\Core\Utility\UpdateException;
* throw new UpdateException('Description of what went wrong');
* @endcode
*
* If an exception is thrown, the current update and all updates that depend on
* it will be aborted. The schema version will not be updated in this case, and
* all the aborted updates will continue to appear on update.php as updates
* that have not yet been run.
*
* If an update function needs to be re-run as part of a batch process, it
* should accept the $sandbox array by reference as its first parameter
* and set the #finished property to the percentage completed that it is, as a
* fraction of 1.
*
* @param string $module
* The module whose update will be run.
* @param string $number
* The update number to run.
* @param array $dependency_map
* An array whose keys are the names of all update functions that will be
* performed during this batch process, and whose values are arrays of other
* update functions that each one depends on.
* @param array $context
* The batch context array.
*
* @see update_resolve_dependencies()
*/
function update_do_one($module, $number, $dependency_map, &$context): void {
$function = $module . '_update_' . $number;
// If this update was aborted in a previous step, or has a dependency that
// was aborted in a previous step, go no further.
if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, [$function]))) {
return;
}
$ret = [];
$equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate($module, $number);
if ($equivalent_update instanceof EquivalentUpdate) {
$ret['results']['query'] = $equivalent_update->toSkipMessage();
$ret['results']['success'] = TRUE;
$context['sandbox']['#finished'] = TRUE;
}
elseif (function_exists($function)) {
try {
$ret['results']['query'] = $function($context['sandbox']);
$ret['results']['success'] = TRUE;
}
// @todo We may want to do different error handling for different
// exception types, but for now we'll just log the exception and
// return the message for printing.
// @see https://www.drupal.org/node/2564311
catch (Exception $e) {
$variables = Error::decodeException($e);
\Drupal::logger('update')->error(Error::DEFAULT_ERROR_MESSAGE, $variables);
unset($variables['backtrace'], $variables['exception'], $variables['severity_level']);
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
$ret['#abort'] = ['success' => FALSE, 'query' => t(Error::DEFAULT_ERROR_MESSAGE, $variables)];
}
}
if (isset($context['sandbox']['#finished'])) {
$context['finished'] = $context['sandbox']['#finished'];
unset($context['sandbox']['#finished']);
}
if (!isset($context['results'][$module])) {
$context['results'][$module] = [];
}
if (!isset($context['results'][$module][$number])) {
$context['results'][$module][$number] = [];
}
$context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);
if (!empty($ret['#abort'])) {
// Record this function in the list of updates that were aborted.
$context['results']['#abort'][] = $function;
}
// Record the schema update if it was completed successfully.
if ($context['finished'] == 1 && empty($ret['#abort'])) {
\Drupal::service('update.update_hook_registry')->setInstalledVersion($module, $number);
}
$context['message'] = t('Updating @module', ['@module' => $module]);
}
/**
* Executes a single hook_post_update_NAME().
*
* @param string $function
* The function name, that should be executed.
* @param array $context
* The batch context array.
*/
function update_invoke_post_update($function, &$context): void {
$ret = [];
// If this update was aborted in a previous step, or has a dependency that was
// aborted in a previous step, go no further.
if (!empty($context['results']['#abort'])) {
return;
}
// Ensure extension post update code is loaded.
[$extension, $name] = explode('_post_update_', $function, 2);
\Drupal::service('update.post_update_registry')->getUpdateFunctions($extension);
if (function_exists($function)) {
try {
$ret['results']['query'] = $function($context['sandbox']);
$ret['results']['success'] = TRUE;
if (!isset($context['sandbox']['#finished']) || (isset($context['sandbox']['#finished']) && $context['sandbox']['#finished'] >= 1)) {
\Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]);
}
}
// @todo We may want to do different error handling for different exception
// types, but for now we'll just log the exception and return the message
// for printing.
// @see https://www.drupal.org/node/2564311
catch (Exception $e) {
$variables = Error::decodeException($e);
\Drupal::logger('update')->error(Error::DEFAULT_ERROR_MESSAGE, $variables);
unset($variables['backtrace'], $variables['exception'], $variables['severity_level']);
$ret['#abort'] = [
'success' => FALSE,
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
'query' => t(Error::DEFAULT_ERROR_MESSAGE, $variables),
];
}
}
if (isset($context['sandbox']['#finished'])) {
$context['finished'] = $context['sandbox']['#finished'];
unset($context['sandbox']['#finished']);
}
if (!isset($context['results'][$extension][$name])) {
$context['results'][$extension][$name] = [];
}
$context['results'][$extension][$name] = array_merge($context['results'][$extension][$name], $ret);
if (!empty($ret['#abort'])) {
// Record this function in the list of updates that were aborted.
$context['results']['#abort'][] = $function;
}
$context['message'] = t('Post updating @extension', ['@extension' => $extension]);
}
/**
* Returns a list of all the pending database updates.
*
* @return array
* An associative array keyed by module name which contains all information
* about database updates that need to be run, and any updates that are not
* going to proceed due to missing requirements. The system module will
* always be listed first.
*
* The subarray for each module can contain the following keys:
* - start: The starting update that is to be processed. If this does not
* exist then do not process any updates for this module as there are
* other requirements that need to be resolved.
* - warning: Any warnings about why this module can not be updated.
* - pending: An array of all the pending updates for the module including
* the update number and the description from source code comment for
* each update function. This array is keyed by the update number.
*/
function update_get_update_list(): array {
// Make sure that the system module is first in the list of updates.
$ret = ['system' => []];
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
$update_registry = \Drupal::service('update.update_hook_registry');
$modules = $update_registry->getAllInstalledVersions();
/** @var \Drupal\Core\Extension\ExtensionList $extension_list */
$extension_list = \Drupal::service('extension.list.module');
/** @var array $installed_module_info */
$installed_module_info = $extension_list->getAllInstalledInfo();
foreach ($modules as $module => $schema_version) {
// Skip uninstalled and incompatible modules.
try {
if ($schema_version == $update_registry::SCHEMA_UNINSTALLED || $extension_list->checkIncompatibility($module)) {
continue;
}
}
// It is possible that the system schema has orphaned entries, so the
// incompatibility checking might throw an exception.
catch (UnknownExtensionException) {
$args = [
'%name' => $module,
':url' => 'https://www.drupal.org/node/3137656',
];
\Drupal::messenger()->addWarning(t('Module %name has an entry in the system.schema key/value storage, but is missing from your site. <a href=":url">More information about this error</a>.', $args));
\Drupal::logger('system')->notice('Module %name has an entry in the system.schema key/value storage, but is missing from your site. <a href=":url">More information about this error</a>.', $args);
continue;
}
// There might be orphaned entries for modules that are in the filesystem
// but not installed. Also skip those, but warn site admins about it.
if (empty($installed_module_info[$module])) {
$args = [
'%name' => $module,
':url' => 'https://www.drupal.org/node/3137656',
];
\Drupal::messenger()->addWarning(t('Module %name has an entry in the system.schema key/value storage, but is not installed. <a href=":url">More information about this error</a>.', $args));
\Drupal::logger('system')->notice('Module %name has an entry in the system.schema key/value storage, but is not installed. <a href=":url">More information about this error</a>.', $args);
continue;
}
// Display a requirements error if the user somehow has a schema version
// from the previous Drupal major version.
if ($schema_version < \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
$ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. Its schema version is ' . $schema_version . ', which is from an earlier major release of Drupal. See the <a href="https://www.drupal.org/docs/upgrading-drupal">Upgrading Drupal guide</a>.';
continue;
}
// Otherwise, get the list of updates defined by this module.
$updates = $update_registry->getAvailableUpdates($module);
if ($updates) {
foreach ($updates as $update) {
if ($update == \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
$ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. It contains an update numbered as ' . \Drupal::CORE_MINIMUM_SCHEMA_VERSION . ' which is reserved for the earliest installation of a module in Drupal ' . \Drupal::CORE_COMPATIBILITY . ', before any updates. In order to update <em>' . $module . '</em> module, you will need to download a version of the module with valid updates.';
continue 2;
}
if ($update > $schema_version) {
// The description for an update comes from its Doxygen.
$func = new ReflectionFunction($module . '_update_' . $update);
$patterns = [
'/^\s*[\/*]*/',
'/(\n\s*\**)(.*)/',
'/\/$/',
'/^\s*/',
];
$replacements = ['', '$2', '', ''];
$description = preg_replace($patterns, $replacements, $func->getDocComment());
$ret[$module]['pending'][$update] = "$update - $description";
if (!isset($ret[$module]['start'])) {
$ret[$module]['start'] = $update;
}
}
}
if (!isset($ret[$module]['start']) && isset($ret[$module]['pending'])) {
$ret[$module]['start'] = $schema_version;
}
}
}
if (empty($ret['system'])) {
unset($ret['system']);
}
return $ret;
}
/**
* Resolves dependencies in a set of module updates, and orders them correctly.
*
* This function receives a list of requested module updates and determines an
* appropriate order to run them in such that all update dependencies are met.
* Any updates whose dependencies cannot be met are included in the returned
* array but have the key 'allowed' set to FALSE; the calling function should
* take responsibility for ensuring that these updates are ultimately not
* performed.
*
* In addition, the returned array also includes detailed information about the
* dependency chain for each update, as provided by the depth-first search
* algorithm in Drupal\Component\Graph\Graph::searchAndSort().
*
* @param array $starting_updates
* An array whose keys contain the names of modules with updates to be run
* and whose values contain the number of the first requested update for that
* module.
*
* @return array
* An array whose keys are the names of all update functions within the
* provided modules that would need to be run in order to fulfill the
* request, arranged in the order in which the update functions should be
* run. (This includes the provided starting update for each module and all
* subsequent updates that are available.) The values are themselves arrays
* containing all the keys provided by the
* Drupal\Component\Graph\Graph::searchAndSort() algorithm, which encode
* detailed information about the dependency chain for this update function
* (for example: 'paths', 'reverse_paths', 'weight', and 'component'), as
* well as the following additional keys:
* - 'allowed': A boolean which is TRUE when the update function's
* dependencies are met, and FALSE otherwise. Calling functions should
* inspect this value before running the update.
* - 'missing_dependencies': An array containing the names of any other
* update functions that are required by this one but that are unavailable
* to be run. This array will be empty when 'allowed' is TRUE.
* - 'module': The name of the module that this update function belongs to.
* - 'number': The number of this update function within that module.
*
* @see \Drupal\Component\Graph\Graph::searchAndSort()
*/
function update_resolve_dependencies($starting_updates) {
// Obtain a dependency graph for the requested update functions.
$update_functions = update_get_update_function_list($starting_updates);
$graph = update_build_dependency_graph($update_functions);
// Perform the depth-first search and sort on the results.
$graph_object = new Graph($graph);
$graph = $graph_object->searchAndSort();
uasort($graph, ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
foreach ($graph as $function => &$data) {
$module = $data['module'];
$number = $data['number'];
// If the update function is missing and has not yet been performed, mark
// it and everything that ultimately depends on it as disallowed.
if (update_is_missing($module, $number, $update_functions) && !update_already_performed($module, $number)) {
$data['allowed'] = FALSE;
foreach (array_keys($data['paths']) as $dependent) {
$graph[$dependent]['allowed'] = FALSE;
$graph[$dependent]['missing_dependencies'][] = $function;
}
}
elseif (!isset($data['allowed'])) {
$data['allowed'] = TRUE;
$data['missing_dependencies'] = [];
}
// Now that we have finished processing this function, remove it from the
// graph if it was not part of the original list. This ensures that we
// never try to run any updates that were not specifically requested.
if (!isset($update_functions[$module][$number])) {
unset($graph[$function]);
}
}
return $graph;
}
/**
* Returns an organized list of update functions for a set of modules.
*
* @param array $starting_updates
* An array whose keys contain the names of modules and whose values contain
* the number of the first requested update for that module.
*
* @return array
* An array containing all the update functions that should be run for each
* module, including the provided starting update and all subsequent updates
* that are available. The keys of the array contain the module names, and
* each value is an ordered array of update functions, keyed by the update
* number.
*
* @see update_resolve_dependencies()
*/
function update_get_update_function_list($starting_updates): array {
// Go through each module and find all updates that we need (including the
// first update that was requested and any updates that run after it).
$update_functions = [];
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
$update_registry = \Drupal::service('update.update_hook_registry');
foreach ($starting_updates as $module => $version) {
$update_functions[$module] = [];
$updates = $update_registry->getAvailableUpdates($module);
if ($updates) {
$max_version = max($updates);
if ($version <= $max_version) {
foreach ($updates as $update) {
if ($update >= $version) {
$update_functions[$module][$update] = $module . '_update_' . $update;
}
}
}
}
}
return $update_functions;
}
/**
* Constructs a graph which encodes the dependencies between module updates.
*
* This function returns an associative array which contains a "directed graph"
* representation of the dependencies between a provided list of update
* functions, as well as any outside update functions that they directly depend
* on but that were not in the provided list. The vertices of the graph
* represent the update functions themselves, and each edge represents a
* requirement that the first update function needs to run before the second.
* For example, consider this graph:
*
* system_update_8001 ---> system_update_8002 ---> system_update_8003
*
* Visually, this indicates that system_update_8001() must run before
* system_update_8002(), which in turn must run before system_update_8003().
*
* The function takes into account standard dependencies within each module, as
* shown above (i.e., the fact that each module's updates must run in numerical
* order), but also finds any cross-module dependencies that are defined by
* modules which implement hook_update_dependencies(), and builds them into the
* graph as well.
*
* @param array $update_functions
* An organized array of update functions, in the format returned by
* update_get_update_function_list().
*
* @return array
* A multidimensional array representing the dependency graph, suitable for
* passing in to Drupal\Component\Graph\Graph::searchAndSort(), but with extra
* information about each update function also included. Each array key
* contains the name of an update function, including all update functions
* from the provided list as well as any outside update functions which they
* directly depend on. Each value is an associative array containing the
* following keys:
* - 'edges': A representation of any other update functions that immediately
* depend on this one. See Drupal\Component\Graph\Graph::searchAndSort() for
* more details on the format.
* - 'module': The name of the module that this update function belongs to.
* - 'number': The number of this update function within that module.
*
* @see \Drupal\Component\Graph\Graph::searchAndSort()
* @see update_resolve_dependencies()
*/
function update_build_dependency_graph($update_functions): array {
// Initialize an array that will define a directed graph representing the
// dependencies between update functions.
$graph = [];
// Go through each update function and build an initial list of dependencies.
foreach ($update_functions as $module => $functions) {
$previous_function = NULL;
foreach ($functions as $number => $function) {
// Add an edge to the directed graph representing the fact that each
// update function in a given module must run after the update that
// numerically precedes it.
if ($previous_function) {
$graph[$previous_function]['edges'][$function] = TRUE;
}
$previous_function = $function;
// Define the module and update number associated with this function.
$graph[$function]['module'] = $module;
$graph[$function]['number'] = $number;
}
}
// Now add any explicit update dependencies declared by modules.
$update_dependencies = update_retrieve_dependencies();
foreach ($graph as $function => $data) {
if (!empty($update_dependencies[$data['module']][$data['number']])) {
foreach ($update_dependencies[$data['module']][$data['number']] as $module => $number) {
$dependency = $module . '_update_' . $number;
$graph[$dependency]['edges'][$function] = TRUE;
$graph[$dependency]['module'] = $module;
$graph[$dependency]['number'] = $number;
}
}
}
return $graph;
}
/**
* Determines if a module update is missing or unavailable.
*
* @param string $module
* The name of the module.
* @param int $number
* The number of the update within that module.
* @param array $update_functions
* An organized array of update functions, in the format returned by
* update_get_update_function_list(). This should represent all module
* updates that are requested to run at the time this function is called.
*
* @return bool
* TRUE if the provided module update is not installed or is not in the
* provided list of updates to run; FALSE otherwise.
*/
function update_is_missing($module, $number, $update_functions) {
return !isset($update_functions[$module][$number]) || !function_exists($update_functions[$module][$number]);
}
/**
* Determines if a module update has already been performed.
*
* @param string $module
* The name of the module.
* @param int $number
* The number of the update within that module.
*
* @return bool
* TRUE if the database schema indicates that the update has already been
* performed; FALSE otherwise.
*/
function update_already_performed($module, $number) {
return $number <= \Drupal::service('update.update_hook_registry')->getInstalledVersion($module);
}
/**
* Invokes hook_update_dependencies() in all installed modules.
*
* This function is similar to \Drupal::moduleHandler()->invokeAll(), with the
* main difference that it does not require that a module be enabled to invoke
* its hook, only that it be installed. This allows the update system to
* properly perform updates even on modules that are currently disabled.
*
* @return array
* An array of return values obtained by merging the results of the
* hook_update_dependencies() implementations in all installed modules.
*
* @see \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll()
* @see hook_update_dependencies()
*/
function update_retrieve_dependencies(): array {
$return = [];
/** @var \Drupal\Core\Extension\ModuleExtensionList */
$extension_list = \Drupal::service('extension.list.module');
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
$update_registry = \Drupal::service('update.update_hook_registry');
// Get a list of installed modules, arranged so that we invoke their hooks in
// the same order that \Drupal::moduleHandler()->invokeAll() does.
foreach ($update_registry->getAllInstalledVersions() as $module => $schema) {
// Skip modules that are entirely missing from the filesystem here, since
// loading .install file will call trigger_error() if invoked on a module
// that doesn't exist. There's no way to catch() that, so avoid it entirely.
// This can happen when there are orphaned entries in the system.schema k/v
// store for modules that have been removed from a site without first being
// cleanly uninstalled. We don't care here if the module has been installed
// or not, since we'll filter those out in update_get_update_list().
if ($schema == $update_registry::SCHEMA_UNINSTALLED || !$extension_list->exists($module)) {
// Nothing to upgrade.
continue;
}
$function = $module . '_update_dependencies';
// Ensure install file is loaded.
\Drupal::moduleHandler()->loadInclude($module, 'install');
if (function_exists($function)) {
$updated_dependencies = $function();
// Each implementation of hook_update_dependencies() returns a
// multidimensional, associative array containing some keys that
// represent module names (which are strings) and other keys that
// represent update function numbers (which are integers). We cannot use
// array_merge_recursive() to properly merge these results, since it
// treats strings and integers differently. Therefore, we have to
// explicitly loop through the expected array structure here and perform
// the merge manually.
if (isset($updated_dependencies) && is_array($updated_dependencies)) {
foreach ($updated_dependencies as $module_name => $module_data) {
foreach ($module_data as $update_version => $update_data) {
foreach ($update_data as $module_dependency => $update_dependency) {
// If there are redundant dependencies declared for the same
// update function (so that it is declared to depend on more than
// one update from a particular module), record the dependency on
// the highest numbered update here, since that automatically
// implies the previous ones. For example, if one module's
// implementation of hook_update_dependencies() required this
// ordering:
//
// system_update_8002 ---> user_update_8001
//
// but another module's implementation of the hook required this
// one:
//
// system_update_8003 ---> user_update_8001
//
// we record the second one, since system_update_8002() is always
// guaranteed to run before system_update_8003() anyway (within
// an individual module, updates are always run in numerical
// order).
if (!isset($return[$module_name][$update_version][$module_dependency]) || $update_dependency > $return[$module_name][$update_version][$module_dependency]) {
$return[$module_name][$update_version][$module_dependency] = $update_dependency;
}
}
}
}
}
}
}
return $return;
}

View File

@ -0,0 +1,50 @@
<?php
/**
* @file
* Miscellaneous functions.
*/
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
/**
* Rebuilds all caches even when Drupal itself does not work.
*
* @param \Composer\Autoload\ClassLoader $class_loader
* The class loader. Normally Composer's ClassLoader, as included by the
* front controller, but may also be decorated.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @see rebuild.php
*/
function drupal_rebuild($class_loader, Request $request): void {
// Remove Drupal's error and exception handlers; they rely on a working
// service container and other subsystems and will only cause a fatal error
// that hides the actual error.
restore_error_handler();
restore_exception_handler();
// Invalidate the container.
// Bootstrap up to where caches exist and clear them.
$kernel = new DrupalKernel('prod', $class_loader);
$kernel->setSitePath(DrupalKernel::findSitePath($request));
$kernel->invalidateContainer();
$kernel->boot();
$kernel->preHandle($request);
// Ensure our request includes the session if appropriate.
if (PHP_SAPI !== 'cli') {
$request->setSession($kernel->getContainer()->get('session'));
}
drupal_flush_all_caches($kernel);
// Disable recording of cached pages.
\Drupal::service('page_cache_kill_switch')->trigger();
// Restore Drupal's error and exception handlers.
// @see \Drupal\Core\DrupalKernel::boot()
set_error_handler('_drupal_error_handler');
set_exception_handler('_drupal_exception_handler');
}