diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php index f43915b..8000241 100644 --- a/core/assets/scaffold/files/default.settings.php +++ b/core/assets/scaffold/files/default.settings.php @@ -308,6 +308,20 @@ $settings['update_free_access'] = FALSE; /** + * Fallback to HTTP for Update Manager. + * + * If your Drupal site fails to connect to updates.drupal.org using HTTPS to + * fetch Drupal core, module and theme update status, you may uncomment this + * setting and set it to TRUE to allow an insecure fallback to HTTP. Note that + * doing so will open your site up to a potential man-in-the-middle attack. You + * should instead attempt to resolve the issues before enabling this option. + * @see https://www.drupal.org/docs/system-requirements/php-requirements#openssl + * @see https://en.wikipedia.org/wiki/Man-in-the-middle_attack + * @see \Drupal\update\UpdateFetcher + */ +# $settings['update_fetch_with_http_fallback'] = TRUE; + +/** * External access proxy settings: * * If your site must access the Internet via a web proxy then you can enter the diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php index beab94a..5918895 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php @@ -47270,7 +47270,7 @@ 'name' => 'update', 'type' => 'module', 'owner' => '', - 'status' => '0', + 'status' => '1', 'throttle' => '0', 'bootstrap' => '0', 'schema_version' => '-1', diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php index 3935407..6436b28 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php @@ -29,6 +29,7 @@ class Upgrade6Test extends MigrateUpgradeExecuteTestBase { 'book', 'forum', 'statistics', + 'update', ]; /** @@ -166,6 +167,7 @@ protected function getAvailablePaths() { 'System', 'Taxonomy', 'Text', + 'Update status', 'Upload', 'User', 'User Reference', diff --git a/core/modules/update/config/schema/update.source.schema.yml b/core/modules/update/config/schema/update.source.schema.yml new file mode 100644 index 0000000..02777ac --- /dev/null +++ b/core/modules/update/config/schema/update.source.schema.yml @@ -0,0 +1,12 @@ +# Schema for the migration source plugins. + +migrate.source.update_settings: + type: migrate_source_sql + label: 'Drupal update settings' + mapping: + variables: + type: sequence + label: 'Variables' + sequence: + type: string + label: 'Variable' diff --git a/core/modules/update/migrations/update_settings.yml b/core/modules/update/migrations/update_settings.yml index 4bd4a2b..6525a30 100644 --- a/core/modules/update/migrations/update_settings.yml +++ b/core/modules/update/migrations/update_settings.yml @@ -5,7 +5,7 @@ migration_tags: - Drupal 7 - Configuration source: - plugin: variable + plugin: update_settings variables: - update_max_fetch_attempts - update_fetch_url diff --git a/core/modules/update/src/Controller/UpdateController.php b/core/modules/update/src/Controller/UpdateController.php index 6a8ec6a..bf0f168 100644 --- a/core/modules/update/src/Controller/UpdateController.php +++ b/core/modules/update/src/Controller/UpdateController.php @@ -2,9 +2,11 @@ namespace Drupal\update\Controller; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Render\RendererInterface; +use Drupal\update\UpdateFetcherInterface; use Drupal\update\UpdateManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Controller\ControllerBase; /** * Controller routines for update routes. @@ -19,13 +21,27 @@ class UpdateController extends ControllerBase { protected $updateManager; /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Constructs update status data. * * @param \Drupal\update\UpdateManagerInterface $update_manager * Update Manager Service. + * @param \Drupal\Core\Render\RendererInterface|null $renderer + * The renderer. */ - public function __construct(UpdateManagerInterface $update_manager) { + public function __construct(UpdateManagerInterface $update_manager, RendererInterface $renderer = NULL) { $this->updateManager = $update_manager; + if (is_null($renderer)) { + @trigger_error('The renderer service should be passed to UpdateController::__construct() since 9.1.0. This will be required in Drupal 10.0.0. See https://www.drupal.org/node/3179315', E_USER_DEPRECATED); + $renderer = \Drupal::service('renderer'); + } + $this->renderer = $renderer; } /** @@ -33,7 +49,8 @@ public function __construct(UpdateManagerInterface $update_manager) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('update.manager') + $container->get('update.manager'), + $container->get('renderer') ); } @@ -50,6 +67,20 @@ public function updateStatus() { if ($available = update_get_available(TRUE)) { $this->moduleHandler()->loadInclude('update', 'compare.inc'); $build['#data'] = update_calculate_project_data($available); + + // @todo Consider using 'fetch_failures' from the 'update' collection + // in the key_value_expire service for this? + $fetch_failed = FALSE; + foreach ($build['#data'] as $project) { + if ($project['status'] === UpdateFetcherInterface::NOT_FETCHED) { + $fetch_failed = TRUE; + break; + } + } + if ($fetch_failed) { + $message = ['#theme' => 'update_fetch_error_message']; + $this->messenger()->addError($this->renderer->renderPlain($message)); + } } return $build; } diff --git a/core/modules/update/src/Form/UpdateManagerUpdate.php b/core/modules/update/src/Form/UpdateManagerUpdate.php index 6e23903..aeabd99 100644 --- a/core/modules/update/src/Form/UpdateManagerUpdate.php +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php @@ -104,7 +104,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['project_downloads'] = ['#tree' => TRUE]; $this->moduleHandler->loadInclude('update', 'inc', 'update.compare'); $project_data = update_calculate_project_data($available); + + $fetch_failed = FALSE; foreach ($project_data as $name => $project) { + if ($project['status'] === UpdateFetcherInterface::NOT_FETCHED) { + $fetch_failed = TRUE; + } + // Filter out projects which are up to date already. if ($project['status'] == UpdateManagerInterface::CURRENT) { continue; @@ -245,6 +251,11 @@ public function buildForm(array $form, FormStateInterface $form_state) { } } + if ($fetch_failed) { + $message = ['#theme' => 'update_fetch_error_message']; + $this->messenger()->addError(\Drupal::service('renderer')->renderPlain($message)); + } + if (empty($projects)) { $form['message'] = [ '#markup' => $this->t('All of your projects are up to date.'), diff --git a/core/modules/update/src/Plugin/migrate/source/UpdateSettings.php b/core/modules/update/src/Plugin/migrate/source/UpdateSettings.php new file mode 100644 index 0000000..845baf5 --- /dev/null +++ b/core/modules/update/src/Plugin/migrate/source/UpdateSettings.php @@ -0,0 +1,28 @@ +fetchUrl = $config_factory->get('update.settings')->get('fetch.url'); $this->httpClient = $http_client; $this->updateSettings = $config_factory->get('update.settings'); + if (is_null($settings)) { + @trigger_error('The settings service should be passed to UpdateFetcherr::__construct() since 9.1.0. This will be required in Drupal 10.0.0. See https://www.drupal.org/node/3179315', E_USER_DEPRECATED); + $settings = \Drupal::service('settings'); + } + $this->withHttpFallback = $settings->get('update_fetch_with_http_fallback', FALSE); } /** @@ -59,6 +74,26 @@ public function __construct(ConfigFactoryInterface $config_factory, ClientInterf */ public function fetchProjectData(array $project, $site_key = '') { $url = $this->buildFetchUrl($project, $site_key); + return $this->doRequest($url, ['headers' => ['Accept' => 'text/xml']], $this->withHttpFallback); + } + + /** + * Applies a GET request with a possible HTTP fallback. + * + * This method falls back to HTTP in case there was some certificate + * problem. + * + * @param string $url + * The URL. + * @param array $options + * The guzzle client options. + * @param bool $with_http_fallback + * Should the function fall back to HTTP. + * + * @return string + * The body of the HTTP(S) request, or an empty string on failure. + */ + protected function doRequest(string $url, array $options, bool $with_http_fallback) : string { $data = ''; try { $data = (string) $this->httpClient @@ -67,6 +102,10 @@ public function fetchProjectData(array $project, $site_key = '') { } catch (RequestException $exception) { watchdog_exception('update', $exception); + if ($with_http_fallback && strpos($url, "http://") === FALSE) { + $url = str_replace('https://', 'http://', $url); + return $this->doRequest($url, $options, FALSE); + } } return $data; } diff --git a/core/modules/update/templates/update-fetch-error-message.html.twig b/core/modules/update/templates/update-fetch-error-message.html.twig new file mode 100644 index 0000000..fd4a967 --- /dev/null +++ b/core/modules/update/templates/update-fetch-error-message.html.twig @@ -0,0 +1,14 @@ +{# +/** + * @file + * Default theme implementation for the message when fetching data fails. + * + * Available variables: + * - error_message: A render array containing the appropriate error message. + * + * @see template_preprocess_update_fetch_error_message() + * + * @ingroup themeable + */ +#} +{{ error_message }} diff --git a/core/modules/update/tests/src/Kernel/Migrate/d6/MigrateUpdateConfigsTest.php b/core/modules/update/tests/src/Kernel/Migrate/d6/MigrateUpdateConfigsTest.php index 7132546..cea75f1 100644 --- a/core/modules/update/tests/src/Kernel/Migrate/d6/MigrateUpdateConfigsTest.php +++ b/core/modules/update/tests/src/Kernel/Migrate/d6/MigrateUpdateConfigsTest.php @@ -33,7 +33,7 @@ protected function setUp(): void { public function testUpdateSettings() { $config = $this->config('update.settings'); $this->assertIdentical(2, $config->get('fetch.max_attempts')); - $this->assertIdentical('http://updates.drupal.org/release-history', $config->get('fetch.url')); + $this->assertIdentical('https://updates.drupal.org/release-history', $config->get('fetch.url')); $this->assertIdentical('all', $config->get('notification.threshold')); $this->assertIdentical([], $config->get('notification.emails')); $this->assertIdentical(7, $config->get('check.interval_days')); diff --git a/core/modules/update/tests/src/Kernel/UpdateReportTest.php b/core/modules/update/tests/src/Kernel/UpdateReportTest.php index 69aab60..522c89f 100644 --- a/core/modules/update/tests/src/Kernel/UpdateReportTest.php +++ b/core/modules/update/tests/src/Kernel/UpdateReportTest.php @@ -2,7 +2,10 @@ namespace Drupal\Tests\update\Kernel; +use Drupal\Core\Link; +use Drupal\Core\Url; use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\user\Traits\UserCreationTrait; /** * Tests update report functionality. @@ -12,6 +15,8 @@ */ class UpdateReportTest extends KernelTestBase { + use UserCreationTrait; + /** * {@inheritdoc} */ @@ -54,4 +59,72 @@ public function providerTemplatePreprocessUpdateReport() { ]; } + /** + * Tests the error message when failing to fetch data without dblog enabled. + * + * @see template_preprocess_update_fetch_error_message() + */ + public function testTemplatePreprocessUpdateFetchErrorMessageNoDblog() { + $build = [ + '#theme' => 'update_fetch_error_message', + ]; + $this->render($build); + $this->assertRaw('Failed to fetch available update data:'); + + \Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.report'); + $variables = []; + template_preprocess_update_fetch_error_message($variables); + $this->assertArrayHasKey('error_message', $variables); + $this->assertEquals('Failed to fetch available update data:', $variables['error_message']['message']['#markup']); + $this->assertArrayHasKey('documentation_link', $variables['error_message']['items']['#items']); + $this->assertArrayHasKey('logs', $variables['error_message']['items']['#items']); + $this->assertArrayNotHasKey('dblog', $variables['error_message']['items']['#items']); + } + + /** + * Tests the error message when failing to fetch data with dblog enabled. + * + * @see template_preprocess_update_fetch_error_message() + */ + public function testTemplatePreprocessUpdateFetchErrorMessageWithDblog() { + \Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.report'); + + $this->enableModules(['dblog', 'user']); + $this->installEntitySchema('user'); + + // First, try as a normal user that can't access dblog. + $this->setUpCurrentUser(); + + $build = [ + '#theme' => 'update_fetch_error_message', + ]; + $this->render($build); + $this->assertRaw('Failed to fetch available update data:'); + + $variables = []; + template_preprocess_update_fetch_error_message($variables); + $this->assertArrayHasKey('error_message', $variables); + $this->assertEquals('Failed to fetch available update data:', $variables['error_message']['message']['#markup']); + $this->assertArrayHasKey('documentation_link', $variables['error_message']['items']['#items']); + $this->assertArrayHasKey('logs', $variables['error_message']['items']['#items']); + $this->assertArrayNotHasKey('dblog', $variables['error_message']['items']['#items']); + + // Now, try as an admin that can access dblog. + $this->setUpCurrentUser([], ['access content', 'access site reports']); + + $this->render($build); + $this->assertRaw('Failed to fetch available update data:'); + + $variables = []; + template_preprocess_update_fetch_error_message($variables); + $this->assertArrayHasKey('error_message', $variables); + $this->assertEquals('Failed to fetch available update data:', $variables['error_message']['message']['#markup']); + $this->assertArrayHasKey('documentation_link', $variables['error_message']['items']['#items']); + $this->assertArrayNotHasKey('logs', $variables['error_message']['items']['#items']); + $this->assertArrayHasKey('dblog', $variables['error_message']['items']['#items']); + } + } diff --git a/core/modules/update/tests/src/Unit/UpdateFetcherTest.php b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php index e0a5692..0a68971 100644 --- a/core/modules/update/tests/src/Unit/UpdateFetcherTest.php +++ b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php @@ -2,12 +2,20 @@ namespace Drupal\Tests\update\Unit; +use Drupal\Core\Site\Settings; use Drupal\Tests\UnitTestCase; use Drupal\update\UpdateFetcher; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Response; /** * Tests update functionality unrelated to the database. * + * @coversDefaultClass \Drupal\update\UpdateFetcher + * * @group update */ class UpdateFetcherTest extends UnitTestCase { @@ -20,12 +28,51 @@ class UpdateFetcherTest extends UnitTestCase { protected $updateFetcher; /** + * History of requests/responses. + * + * @var array + */ + protected $history = []; + + /** + * Mock HTTP client. + * + * @var \GuzzleHttp\ClientInterface + */ + protected $mockHttpClient; + + /** + * Mock config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $mockConfigFactory; + + /** + * A test project to fetch with. + * + * @var array + */ + protected $testProject; + + /** * {@inheritdoc} */ protected function setUp(): void { - $config_factory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]); - $http_client_mock = $this->createMock('\GuzzleHttp\ClientInterface'); - $this->updateFetcher = new UpdateFetcher($config_factory, $http_client_mock); + parent::setUp(); + $this->mockConfigFactory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]); + $this->mockHttpClient = $this->createMock('\GuzzleHttp\ClientInterface'); + $settings = new Settings([]); + $this->updateFetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings); + $this->testProject = [ + 'name' => 'update_test', + 'project_type' => '', + 'info' => [ + 'version' => '', + 'project status url' => 'https://www.example.com', + ], + 'includes' => ['module1' => 'Module 1', 'module2' => 'Module 2'], + ]; } /** @@ -97,4 +144,94 @@ public function providerTestUpdateBuildFetchUrl() { return $data; } + /** + * Mocks the HTTP client. + * + * @param GuzzleHttp\Psr7\Response ...$responses + * Variable number of Response objects that the mocked client should return. + */ + protected function mockClient(Response ...$responses) { + // Create a mock and queue responses. + $mock_handler = new MockHandler($responses); + $handler_stack = HandlerStack::create($mock_handler); + $history = Middleware::history($this->history); + $handler_stack->push($history); + $this->mockHttpClient = new Client(['handler' => $handler_stack]); + } + + /** + * @covers ::doRequest + * @covers ::fetchProjectData + */ + public function testUpdateFetcherNoFallback() { + // First, try without the HTTP fallback setting, and HTTPS mocked to fail. + $settings = new Settings([]); + $this->mockClient( + new Response('500', [], 'https failed'), + ); + $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings); + + $data = $update_fetcher->fetchProjectData($this->testProject, ''); + // There should only be one request / response pair. + $this->assertCount(1, $this->history); + $request = $this->history[0]['request']; + $this->assertNotEmpty($request); + // It should have only been an https request. + $this->assertEquals('https', $request->getUri()->getScheme()); + // And it should have failed. + $response = $this->history[0]['response']; + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEmpty($data); + } + + /** + * @covers ::doRequest + * @covers ::fetchProjectData + */ + public function testUpdateFetcherHttpFallback() { + $settings = new Settings(['update_fetch_with_http_fallback' => TRUE]); + $this->mockClient( + new Response('500', [], 'https failed'), + new Response('200', [], 'http worked'), + ); + $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings); + + $data = $update_fetcher->fetchProjectData($this->testProject, ''); + + // There should be two request / response pairs. + $this->assertCount(2, $this->history); + + // The first should have been https and should have failed. + $first_try = $this->history[0]; + $this->assertNotEmpty($first_try); + $this->assertEquals('https', $first_try['request']->getUri()->getScheme()); + $this->assertEquals(500, $first_try['response']->getStatusCode()); + + // The second should have been the http fallback and should have worked. + $second_try = $this->history[1]; + $this->assertNotEmpty($second_try); + $this->assertEquals('http', $second_try['request']->getUri()->getScheme()); + $this->assertEquals(200, $second_try['response']->getStatusCode()); + // Although this is a bogus mocked response, it's what fetchProjectData() + // should return in this case. + $this->assertEquals('http worked', $data); + } + +} + +namespace Drupal\update; +use Drupal\Core\Logger\RfcLogLevel; +/** + * Mock version of watchdog_exception(). + * + * This is unfortunate, but UpdateFetcher::doRequest() calls + * watchdog_exception(), which is defined in includes/bootstrap.inc, which + * cannot be included for Unit tests or we break unit test encapsulation. + * Therefore, we define our own copy in the \Drupal\update namespace that + * doesn't do anything (we're not trying to test the exception logging). + * + * @todo Remove in https://www.drupal.org/project/drupal/issues/2932518 + */ +function watchdog_exception($type, \Exception $exception, $message = NULL, $variables = [], $severity = RfcLogLevel::ERROR, $link = NULL) { + // No-op. } diff --git a/core/modules/update/update.module b/core/modules/update/update.module index d777a06..d610796 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -161,6 +161,11 @@ function update_theme() { 'variables' => ['version' => NULL, 'title' => NULL, 'attributes' => []], 'file' => 'update.report.inc', ], + 'update_fetch_error_message' => [ + 'file' => 'update.report.inc', + 'render element' => 'element', + 'variables' => ['error_message' => []], + ], ]; } diff --git a/core/modules/update/update.report.inc b/core/modules/update/update.report.inc index 172860f..7e65011 100644 --- a/core/modules/update/update.report.inc +++ b/core/modules/update/update.report.inc @@ -334,3 +334,34 @@ function template_preprocess_update_project_status(&$variables) { '#title' => $text, ]; } + +/** + * Prepares variables for update fetch error message templates. + * + * Default template: update-fetch-error-message.html.twig. + * + * @param array $variables + * An associative array of template variables. + */ +function template_preprocess_update_fetch_error_message(&$variables): void { + $variables['error_message'] = [ + 'message' => [ + '#markup' => t('Failed to fetch available update data:'), + ], + 'items' => [ + '#theme' => 'item_list', + '#items' => [ + 'documentation_link' => t('See PHP OpenSSL requirements in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.', ['@url' => 'https://www.drupal.org/node/3170647']), + ], + ], + ]; + if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) { + $options = ['query' => ['type' => ['update']]]; + $dblog_url = Url::fromRoute('dblog.overview', [], $options); + $variables['error_message']['items']['#items']['dblog'] = t('Check your local system logs for additional error messages.', ['@url' => $dblog_url->toString()]); + } + else { + $variables['error_message']['items']['#items']['logs'] = t('Check your local system logs for additional error messages.'); + } + +} diff --git a/core/modules/update/update.services.yml b/core/modules/update/update.services.yml index 197e3d5..df32844 100644 --- a/core/modules/update/update.services.yml +++ b/core/modules/update/update.services.yml @@ -12,7 +12,7 @@ services: arguments: ['@config.factory', '@queue', '@update.fetcher', '@state', '@private_key', '@keyvalue', '@keyvalue.expirable'] update.fetcher: class: Drupal\update\UpdateFetcher - arguments: ['@config.factory', '@http_client'] + arguments: ['@config.factory', '@http_client', '@settings'] update.root: class: SplString factory: ['@update.root.factory', 'get'] diff --git a/core/themes/stable/templates/admin/update-fetch-error-message.html.twig b/core/themes/stable/templates/admin/update-fetch-error-message.html.twig new file mode 100644 index 0000000..fd4a967 --- /dev/null +++ b/core/themes/stable/templates/admin/update-fetch-error-message.html.twig @@ -0,0 +1,14 @@ +{# +/** + * @file + * Default theme implementation for the message when fetching data fails. + * + * Available variables: + * - error_message: A render array containing the appropriate error message. + * + * @see template_preprocess_update_fetch_error_message() + * + * @ingroup themeable + */ +#} +{{ error_message }} diff --git a/core/themes/stable9/templates/admin/update-fetch-error-message.html.twig b/core/themes/stable9/templates/admin/update-fetch-error-message.html.twig new file mode 100644 index 0000000..fd4a967 --- /dev/null +++ b/core/themes/stable9/templates/admin/update-fetch-error-message.html.twig @@ -0,0 +1,14 @@ +{# +/** + * @file + * Default theme implementation for the message when fetching data fails. + * + * Available variables: + * - error_message: A render array containing the appropriate error message. + * + * @see template_preprocess_update_fetch_error_message() + * + * @ingroup themeable + */ +#} +{{ error_message }} diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index f43915b..8000241 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -308,6 +308,20 @@ $settings['update_free_access'] = FALSE; /** + * Fallback to HTTP for Update Manager. + * + * If your Drupal site fails to connect to updates.drupal.org using HTTPS to + * fetch Drupal core, module and theme update status, you may uncomment this + * setting and set it to TRUE to allow an insecure fallback to HTTP. Note that + * doing so will open your site up to a potential man-in-the-middle attack. You + * should instead attempt to resolve the issues before enabling this option. + * @see https://www.drupal.org/docs/system-requirements/php-requirements#openssl + * @see https://en.wikipedia.org/wiki/Man-in-the-middle_attack + * @see \Drupal\update\UpdateFetcher + */ +# $settings['update_fetch_with_http_fallback'] = TRUE; + +/** * External access proxy settings: * * If your site must access the Internet via a web proxy then you can enter the