1<?php 2 3namespace Drupal\update; 4 5use Drupal\Core\Config\ConfigFactoryInterface; 6use Drupal\Core\DependencyInjection\DependencySerializationTrait; 7use Drupal\Core\Extension\ModuleExtensionList; 8use Drupal\Core\Extension\ModuleHandlerInterface; 9use Drupal\Core\Extension\ThemeHandlerInterface; 10use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; 11use Drupal\Core\StringTranslation\TranslationInterface; 12use Drupal\Core\StringTranslation\StringTranslationTrait; 13use Drupal\Core\Utility\ProjectInfo; 14 15/** 16 * Default implementation of UpdateManagerInterface. 17 */ 18class UpdateManager implements UpdateManagerInterface { 19 use DependencySerializationTrait; 20 use StringTranslationTrait; 21 22 /** 23 * The update settings 24 * 25 * @var \Drupal\Core\Config\Config 26 */ 27 protected $updateSettings; 28 29 /** 30 * Module Handler Service. 31 * 32 * @var \Drupal\Core\Extension\ModuleHandlerInterface 33 */ 34 protected $moduleHandler; 35 36 /** 37 * Update Processor Service. 38 * 39 * @var \Drupal\update\UpdateProcessorInterface 40 */ 41 protected $updateProcessor; 42 43 /** 44 * An array of installed and enabled projects. 45 * 46 * @var array 47 */ 48 protected $projects; 49 50 /** 51 * The key/value store. 52 * 53 * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface 54 */ 55 protected $keyValueStore; 56 57 /** 58 * Update available releases key/value store. 59 * 60 * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface 61 */ 62 protected $availableReleasesTempStore; 63 64 /** 65 * The theme handler. 66 * 67 * @var \Drupal\Core\Extension\ThemeHandlerInterface 68 */ 69 protected $themeHandler; 70 71 /** 72 * The module extension list. 73 * 74 * @var \Drupal\Core\Extension\ModuleExtensionList 75 */ 76 protected $moduleExtensionList; 77 78 /** 79 * Constructs a UpdateManager. 80 * 81 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory 82 * The config factory. 83 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler 84 * The Module Handler service 85 * @param \Drupal\update\UpdateProcessorInterface $update_processor 86 * The Update Processor service. 87 * @param \Drupal\Core\StringTranslation\TranslationInterface $translation 88 * The translation service. 89 * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_expirable_factory 90 * The expirable key/value factory. 91 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler 92 * The theme handler. 93 * @param \Drupal\Core\Extension\ModuleExtensionList|null $extension_list_module 94 * The module extension list. This is left optional for BC reasons, but the 95 * optional usage is deprecated and will become required in Drupal 9.0.0. 96 */ 97 public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, UpdateProcessorInterface $update_processor, TranslationInterface $translation, KeyValueFactoryInterface $key_value_expirable_factory, ThemeHandlerInterface $theme_handler, ModuleExtensionList $extension_list_module = NULL) { 98 $this->updateSettings = $config_factory->get('update.settings'); 99 $this->moduleHandler = $module_handler; 100 $this->updateProcessor = $update_processor; 101 $this->stringTranslation = $translation; 102 $this->keyValueStore = $key_value_expirable_factory->get('update'); 103 $this->themeHandler = $theme_handler; 104 $this->availableReleasesTempStore = $key_value_expirable_factory->get('update_available_releases'); 105 $this->projects = []; 106 if ($extension_list_module === NULL) { 107 @trigger_error('Invoking the UpdateManager constructor without the module extension list parameter is deprecated in Drupal 8.8.0 and will no longer be supported in Drupal 9.0.0. The extension list parameter is now required in the ConfigImporter constructor. See https://www.drupal.org/node/2943918', E_USER_DEPRECATED); 108 $extension_list_module = \Drupal::service('extension.list.module'); 109 } 110 $this->moduleExtensionList = $extension_list_module; 111 } 112 113 /** 114 * {@inheritdoc} 115 */ 116 public function refreshUpdateData() { 117 118 // Since we're fetching new available update data, we want to clear 119 // of both the projects we care about, and the current update status of the 120 // site. We do *not* want to clear the cache of available releases just yet, 121 // since that data (even if it's stale) can be useful during 122 // \Drupal\update\UpdateManager::getProjects(); for example, to modules 123 // that implement hook_system_info_alter() such as cvs_deploy. 124 $this->keyValueStore->delete('update_project_projects'); 125 $this->keyValueStore->delete('update_project_data'); 126 127 $projects = $this->getProjects(); 128 129 // Now that we have the list of projects, we should also clear the available 130 // release data, since even if we fail to fetch new data, we need to clear 131 // out the stale data at this point. 132 $this->availableReleasesTempStore->deleteAll(); 133 134 foreach ($projects as $project) { 135 $this->updateProcessor->createFetchTask($project); 136 } 137 } 138 139 /** 140 * {@inheritdoc} 141 */ 142 public function getProjects() { 143 if (empty($this->projects)) { 144 // Retrieve the projects from storage, if present. 145 $this->projects = $this->projectStorage('update_project_projects'); 146 if (empty($this->projects)) { 147 // Still empty, so we have to rebuild. 148 $module_data = $this->moduleExtensionList->reset()->getList(); 149 $theme_data = $this->themeHandler->rebuildThemeData(); 150 $project_info = new ProjectInfo(); 151 $project_info->processInfoList($this->projects, $module_data, 'module', TRUE); 152 $project_info->processInfoList($this->projects, $theme_data, 'theme', TRUE); 153 if ($this->updateSettings->get('check.disabled_extensions')) { 154 $project_info->processInfoList($this->projects, $module_data, 'module', FALSE); 155 $project_info->processInfoList($this->projects, $theme_data, 'theme', FALSE); 156 } 157 // Allow other modules to alter projects before fetching and comparing. 158 $this->moduleHandler->alter('update_projects', $this->projects); 159 // Store the site's project data for at most 1 hour. 160 $this->keyValueStore->setWithExpire('update_project_projects', $this->projects, 3600); 161 } 162 } 163 return $this->projects; 164 } 165 166 /** 167 * {@inheritdoc} 168 */ 169 public function projectStorage($key) { 170 $projects = []; 171 172 // On certain paths, we should clear the data and recompute the projects for 173 // update status of the site to avoid presenting stale information. 174 $route_names = [ 175 'update.theme_update', 176 'system.modules_list', 177 'system.theme_install', 178 'update.module_update', 179 'update.module_install', 180 'update.status', 181 'update.report_update', 182 'update.report_install', 183 'update.settings', 184 'system.status', 185 'update.manual_status', 186 'update.confirmation_page', 187 'system.themes_page', 188 ]; 189 if (in_array(\Drupal::routeMatch()->getRouteName(), $route_names)) { 190 $this->keyValueStore->delete($key); 191 } 192 else { 193 $projects = $this->keyValueStore->get($key, []); 194 } 195 return $projects; 196 } 197 198 /** 199 * {@inheritdoc} 200 */ 201 public function fetchDataBatch(&$context) { 202 if (empty($context['sandbox']['max'])) { 203 $context['finished'] = 0; 204 $context['sandbox']['max'] = $this->updateProcessor->numberOfQueueItems(); 205 $context['sandbox']['progress'] = 0; 206 $context['message'] = $this->t('Checking available update data ...'); 207 $context['results']['updated'] = 0; 208 $context['results']['failures'] = 0; 209 $context['results']['processed'] = 0; 210 } 211 212 // Grab another item from the fetch queue. 213 for ($i = 0; $i < 5; $i++) { 214 if ($item = $this->updateProcessor->claimQueueItem()) { 215 if ($this->updateProcessor->processFetchTask($item->data)) { 216 $context['results']['updated']++; 217 $context['message'] = $this->t('Checked available update data for %title.', ['%title' => $item->data['info']['name']]); 218 } 219 else { 220 $context['message'] = $this->t('Failed to check available update data for %title.', ['%title' => $item->data['info']['name']]); 221 $context['results']['failures']++; 222 } 223 $context['sandbox']['progress']++; 224 $context['results']['processed']++; 225 $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; 226 $this->updateProcessor->deleteQueueItem($item); 227 } 228 else { 229 // If the queue is currently empty, we're done. It's possible that 230 // another thread might have added new fetch tasks while we were 231 // processing this batch. In that case, the usual 'finished' math could 232 // get confused, since we'd end up processing more tasks that we thought 233 // we had when we started and initialized 'max' with numberOfItems(). By 234 // forcing 'finished' to be exactly 1 here, we ensure that batch 235 // processing is terminated. 236 $context['finished'] = 1; 237 return; 238 } 239 } 240 } 241 242} 243