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:
- See PHP OpenSSL requirements in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.
- Check your local system logs for additional error messages.
');
+
+ \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:- See PHP OpenSSL requirements in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.
- Check your local system logs for additional error messages.
');
+
+ $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:- See PHP OpenSSL requirements in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.
- Check');
+ $dblog_url = Url::fromRoute('dblog.overview', [], ['query' => ['type' => ['update']]]);
+ $this->assertRaw(Link::fromTextAndUrl('your local system logs', $dblog_url)->toString());
+ $this->assertRaw(' for additional error messages.
');
+
+ $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