annotate core/modules/update/src/UpdateProcessor.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 4c8ae668cc8c
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\update;
Chris@0 4
Chris@0 5 use Drupal\Component\Utility\Crypt;
Chris@0 6 use Drupal\Core\Config\ConfigFactoryInterface;
Chris@0 7 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
Chris@0 8 use Drupal\Core\State\StateInterface;
Chris@0 9 use Drupal\Core\PrivateKey;
Chris@0 10 use Drupal\Core\Queue\QueueFactory;
Chris@0 11
Chris@0 12 /**
Chris@0 13 * Process project update information.
Chris@0 14 */
Chris@0 15 class UpdateProcessor implements UpdateProcessorInterface {
Chris@0 16
Chris@0 17 /**
Chris@0 18 * The update settings
Chris@0 19 *
Chris@0 20 * @var \Drupal\Core\Config\Config
Chris@0 21 */
Chris@0 22 protected $updateSettings;
Chris@0 23
Chris@0 24 /**
Chris@0 25 * The UpdateFetcher service.
Chris@0 26 *
Chris@0 27 * @var \Drupal\update\UpdateFetcherInterface
Chris@0 28 */
Chris@0 29 protected $updateFetcher;
Chris@0 30
Chris@0 31 /**
Chris@0 32 * The update fetch queue.
Chris@0 33 *
Chris@0 34 * @var \Drupal\Core\Queue\QueueInterface
Chris@0 35 */
Chris@0 36 protected $fetchQueue;
Chris@0 37
Chris@0 38 /**
Chris@0 39 * Update key/value store
Chris@0 40 *
Chris@0 41 * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
Chris@0 42 */
Chris@0 43 protected $tempStore;
Chris@0 44
Chris@0 45 /**
Chris@0 46 * Update Fetch Task Store
Chris@0 47 *
Chris@0 48 * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
Chris@0 49 */
Chris@0 50 protected $fetchTaskStore;
Chris@0 51
Chris@0 52 /**
Chris@0 53 * Update available releases store
Chris@0 54 *
Chris@0 55 * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
Chris@0 56 */
Chris@0 57 protected $availableReleasesTempStore;
Chris@0 58
Chris@0 59 /**
Chris@0 60 * Array of release history URLs that we have failed to fetch
Chris@0 61 *
Chris@0 62 * @var array
Chris@0 63 */
Chris@0 64 protected $failed;
Chris@0 65
Chris@0 66 /**
Chris@0 67 * The state service.
Chris@0 68 *
Chris@0 69 * @var \Drupal\Core\State\StateInterface
Chris@0 70 */
Chris@0 71 protected $stateStore;
Chris@0 72
Chris@0 73 /**
Chris@0 74 * The private key.
Chris@0 75 *
Chris@0 76 * @var \Drupal\Core\PrivateKey
Chris@0 77 */
Chris@0 78 protected $privateKey;
Chris@0 79
Chris@0 80 /**
Chris@0 81 * Constructs a UpdateProcessor.
Chris@0 82 *
Chris@0 83 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
Chris@0 84 * The config factory.
Chris@0 85 * @param \Drupal\Core\Queue\QueueFactory $queue_factory
Chris@0 86 * The queue factory
Chris@0 87 * @param \Drupal\update\UpdateFetcherInterface $update_fetcher
Chris@0 88 * The update fetcher service
Chris@0 89 * @param \Drupal\Core\State\StateInterface $state_store
Chris@0 90 * The state service.
Chris@0 91 * @param \Drupal\Core\PrivateKey $private_key
Chris@0 92 * The private key factory service.
Chris@0 93 * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
Chris@0 94 * The key/value factory.
Chris@0 95 * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_expirable_factory
Chris@0 96 * The expirable key/value factory.
Chris@0 97 */
Chris@0 98 public function __construct(ConfigFactoryInterface $config_factory, QueueFactory $queue_factory, UpdateFetcherInterface $update_fetcher, StateInterface $state_store, PrivateKey $private_key, KeyValueFactoryInterface $key_value_factory, KeyValueFactoryInterface $key_value_expirable_factory) {
Chris@0 99 $this->updateFetcher = $update_fetcher;
Chris@0 100 $this->updateSettings = $config_factory->get('update.settings');
Chris@0 101 $this->fetchQueue = $queue_factory->get('update_fetch_tasks');
Chris@0 102 $this->tempStore = $key_value_expirable_factory->get('update');
Chris@0 103 $this->fetchTaskStore = $key_value_factory->get('update_fetch_task');
Chris@0 104 $this->availableReleasesTempStore = $key_value_expirable_factory->get('update_available_releases');
Chris@0 105 $this->stateStore = $state_store;
Chris@0 106 $this->privateKey = $private_key;
Chris@0 107 $this->fetchTasks = [];
Chris@0 108 $this->failed = [];
Chris@0 109 }
Chris@0 110
Chris@0 111 /**
Chris@0 112 * {@inheritdoc}
Chris@0 113 */
Chris@0 114 public function createFetchTask($project) {
Chris@0 115 if (empty($this->fetchTasks)) {
Chris@0 116 $this->fetchTasks = $this->fetchTaskStore->getAll();
Chris@0 117 }
Chris@0 118 if (empty($this->fetchTasks[$project['name']])) {
Chris@0 119 $this->fetchQueue->createItem($project);
Chris@0 120 $this->fetchTaskStore->set($project['name'], $project);
Chris@0 121 $this->fetchTasks[$project['name']] = REQUEST_TIME;
Chris@0 122 }
Chris@0 123 }
Chris@0 124
Chris@0 125 /**
Chris@0 126 * {@inheritdoc}
Chris@0 127 */
Chris@0 128 public function fetchData() {
Chris@0 129 $end = time() + $this->updateSettings->get('fetch.timeout');
Chris@0 130 while (time() < $end && ($item = $this->fetchQueue->claimItem())) {
Chris@0 131 $this->processFetchTask($item->data);
Chris@0 132 $this->fetchQueue->deleteItem($item);
Chris@0 133 }
Chris@0 134 }
Chris@0 135
Chris@0 136 /**
Chris@0 137 * {@inheritdoc}
Chris@0 138 */
Chris@0 139 public function processFetchTask($project) {
Chris@0 140 global $base_url;
Chris@0 141
Chris@0 142 // This can be in the middle of a long-running batch, so REQUEST_TIME won't
Chris@0 143 // necessarily be valid.
Chris@0 144 $request_time_difference = time() - REQUEST_TIME;
Chris@0 145 if (empty($this->failed)) {
Chris@0 146 // If we have valid data about release history XML servers that we have
Chris@0 147 // failed to fetch from on previous attempts, load that.
Chris@0 148 $this->failed = $this->tempStore->get('fetch_failures');
Chris@0 149 }
Chris@0 150
Chris@0 151 $max_fetch_attempts = $this->updateSettings->get('fetch.max_attempts');
Chris@0 152
Chris@0 153 $success = FALSE;
Chris@0 154 $available = [];
Chris@0 155 $site_key = Crypt::hmacBase64($base_url, $this->privateKey->get());
Chris@0 156 $fetch_url_base = $this->updateFetcher->getFetchBaseUrl($project);
Chris@0 157 $project_name = $project['name'];
Chris@0 158
Chris@0 159 if (empty($this->failed[$fetch_url_base]) || $this->failed[$fetch_url_base] < $max_fetch_attempts) {
Chris@0 160 $data = $this->updateFetcher->fetchProjectData($project, $site_key);
Chris@0 161 }
Chris@0 162 if (!empty($data)) {
Chris@0 163 $available = $this->parseXml($data);
Chris@0 164 // @todo: Purge release data we don't need. See
Chris@0 165 // https://www.drupal.org/node/238950.
Chris@0 166 if (!empty($available)) {
Chris@0 167 // Only if we fetched and parsed something sane do we return success.
Chris@0 168 $success = TRUE;
Chris@0 169 }
Chris@0 170 }
Chris@0 171 else {
Chris@0 172 $available['project_status'] = 'not-fetched';
Chris@0 173 if (empty($this->failed[$fetch_url_base])) {
Chris@0 174 $this->failed[$fetch_url_base] = 1;
Chris@0 175 }
Chris@0 176 else {
Chris@0 177 $this->failed[$fetch_url_base]++;
Chris@0 178 }
Chris@0 179 }
Chris@0 180
Chris@0 181 $frequency = $this->updateSettings->get('check.interval_days');
Chris@0 182 $available['last_fetch'] = REQUEST_TIME + $request_time_difference;
Chris@0 183 $this->availableReleasesTempStore->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency));
Chris@0 184
Chris@0 185 // Stash the $this->failed data back in the DB for the next 5 minutes.
Chris@0 186 $this->tempStore->setWithExpire('fetch_failures', $this->failed, $request_time_difference + (60 * 5));
Chris@0 187
Chris@0 188 // Whether this worked or not, we did just (try to) check for updates.
Chris@0 189 $this->stateStore->set('update.last_check', REQUEST_TIME + $request_time_difference);
Chris@0 190
Chris@0 191 // Now that we processed the fetch task for this project, clear out the
Chris@0 192 // record for this task so we're willing to fetch again.
Chris@0 193 $this->fetchTaskStore->delete($project_name);
Chris@0 194
Chris@0 195 return $success;
Chris@0 196 }
Chris@0 197
Chris@0 198 /**
Chris@0 199 * Parses the XML of the Drupal release history info files.
Chris@0 200 *
Chris@0 201 * @param string $raw_xml
Chris@0 202 * A raw XML string of available release data for a given project.
Chris@0 203 *
Chris@0 204 * @return array
Chris@0 205 * Array of parsed data about releases for a given project, or NULL if there
Chris@0 206 * was an error parsing the string.
Chris@0 207 */
Chris@0 208 protected function parseXml($raw_xml) {
Chris@0 209 try {
Chris@0 210 $xml = new \SimpleXMLElement($raw_xml);
Chris@0 211 }
Chris@0 212 catch (\Exception $e) {
Chris@0 213 // SimpleXMLElement::__construct produces an E_WARNING error message for
Chris@0 214 // each error found in the XML data and throws an exception if errors
Chris@0 215 // were detected. Catch any exception and return failure (NULL).
Chris@0 216 return NULL;
Chris@0 217 }
Chris@0 218 // If there is no valid project data, the XML is invalid, so return failure.
Chris@0 219 if (!isset($xml->short_name)) {
Chris@0 220 return NULL;
Chris@0 221 }
Chris@0 222 $data = [];
Chris@0 223 foreach ($xml as $k => $v) {
Chris@0 224 $data[$k] = (string) $v;
Chris@0 225 }
Chris@0 226 $data['releases'] = [];
Chris@0 227 if (isset($xml->releases)) {
Chris@0 228 foreach ($xml->releases->children() as $release) {
Chris@0 229 $version = (string) $release->version;
Chris@0 230 $data['releases'][$version] = [];
Chris@0 231 foreach ($release->children() as $k => $v) {
Chris@0 232 $data['releases'][$version][$k] = (string) $v;
Chris@0 233 }
Chris@0 234 $data['releases'][$version]['terms'] = [];
Chris@0 235 if ($release->terms) {
Chris@0 236 foreach ($release->terms->children() as $term) {
Chris@0 237 if (!isset($data['releases'][$version]['terms'][(string) $term->name])) {
Chris@0 238 $data['releases'][$version]['terms'][(string) $term->name] = [];
Chris@0 239 }
Chris@0 240 $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value;
Chris@0 241 }
Chris@0 242 }
Chris@0 243 }
Chris@0 244 }
Chris@0 245 return $data;
Chris@0 246 }
Chris@0 247
Chris@0 248 /**
Chris@0 249 * {@inheritdoc}
Chris@0 250 */
Chris@0 251 public function numberOfQueueItems() {
Chris@0 252 return $this->fetchQueue->numberOfItems();
Chris@0 253 }
Chris@0 254
Chris@0 255 /**
Chris@0 256 * {@inheritdoc}
Chris@0 257 */
Chris@0 258 public function claimQueueItem() {
Chris@0 259 return $this->fetchQueue->claimItem();
Chris@0 260 }
Chris@0 261
Chris@0 262 /**
Chris@0 263 * {@inheritdoc}
Chris@0 264 */
Chris@0 265 public function deleteQueueItem($item) {
Chris@0 266 return $this->fetchQueue->deleteItem($item);
Chris@0 267 }
Chris@0 268
Chris@0 269 }