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