Mercurial > hg > isophonics-drupal-site
diff core/modules/update/src/UpdateProcessor.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/modules/update/src/UpdateProcessor.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,269 @@ +<?php + +namespace Drupal\update; + +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\Core\State\StateInterface; +use Drupal\Core\PrivateKey; +use Drupal\Core\Queue\QueueFactory; + +/** + * Process project update information. + */ +class UpdateProcessor implements UpdateProcessorInterface { + + /** + * The update settings + * + * @var \Drupal\Core\Config\Config + */ + protected $updateSettings; + + /** + * The UpdateFetcher service. + * + * @var \Drupal\update\UpdateFetcherInterface + */ + protected $updateFetcher; + + /** + * The update fetch queue. + * + * @var \Drupal\Core\Queue\QueueInterface + */ + protected $fetchQueue; + + /** + * Update key/value store + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + protected $tempStore; + + /** + * Update Fetch Task Store + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $fetchTaskStore; + + /** + * Update available releases store + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + protected $availableReleasesTempStore; + + /** + * Array of release history URLs that we have failed to fetch + * + * @var array + */ + protected $failed; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $stateStore; + + /** + * The private key. + * + * @var \Drupal\Core\PrivateKey + */ + protected $privateKey; + + /** + * Constructs a UpdateProcessor. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Queue\QueueFactory $queue_factory + * The queue factory + * @param \Drupal\update\UpdateFetcherInterface $update_fetcher + * The update fetcher service + * @param \Drupal\Core\State\StateInterface $state_store + * The state service. + * @param \Drupal\Core\PrivateKey $private_key + * The private key factory service. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory + * The key/value factory. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_expirable_factory + * The expirable key/value factory. + */ + 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) { + $this->updateFetcher = $update_fetcher; + $this->updateSettings = $config_factory->get('update.settings'); + $this->fetchQueue = $queue_factory->get('update_fetch_tasks'); + $this->tempStore = $key_value_expirable_factory->get('update'); + $this->fetchTaskStore = $key_value_factory->get('update_fetch_task'); + $this->availableReleasesTempStore = $key_value_expirable_factory->get('update_available_releases'); + $this->stateStore = $state_store; + $this->privateKey = $private_key; + $this->fetchTasks = []; + $this->failed = []; + } + + /** + * {@inheritdoc} + */ + public function createFetchTask($project) { + if (empty($this->fetchTasks)) { + $this->fetchTasks = $this->fetchTaskStore->getAll(); + } + if (empty($this->fetchTasks[$project['name']])) { + $this->fetchQueue->createItem($project); + $this->fetchTaskStore->set($project['name'], $project); + $this->fetchTasks[$project['name']] = REQUEST_TIME; + } + } + + /** + * {@inheritdoc} + */ + public function fetchData() { + $end = time() + $this->updateSettings->get('fetch.timeout'); + while (time() < $end && ($item = $this->fetchQueue->claimItem())) { + $this->processFetchTask($item->data); + $this->fetchQueue->deleteItem($item); + } + } + + /** + * {@inheritdoc} + */ + public function processFetchTask($project) { + global $base_url; + + // This can be in the middle of a long-running batch, so REQUEST_TIME won't + // necessarily be valid. + $request_time_difference = time() - REQUEST_TIME; + if (empty($this->failed)) { + // If we have valid data about release history XML servers that we have + // failed to fetch from on previous attempts, load that. + $this->failed = $this->tempStore->get('fetch_failures'); + } + + $max_fetch_attempts = $this->updateSettings->get('fetch.max_attempts'); + + $success = FALSE; + $available = []; + $site_key = Crypt::hmacBase64($base_url, $this->privateKey->get()); + $fetch_url_base = $this->updateFetcher->getFetchBaseUrl($project); + $project_name = $project['name']; + + if (empty($this->failed[$fetch_url_base]) || $this->failed[$fetch_url_base] < $max_fetch_attempts) { + $data = $this->updateFetcher->fetchProjectData($project, $site_key); + } + if (!empty($data)) { + $available = $this->parseXml($data); + // @todo: Purge release data we don't need. See + // https://www.drupal.org/node/238950. + if (!empty($available)) { + // Only if we fetched and parsed something sane do we return success. + $success = TRUE; + } + } + else { + $available['project_status'] = 'not-fetched'; + if (empty($this->failed[$fetch_url_base])) { + $this->failed[$fetch_url_base] = 1; + } + else { + $this->failed[$fetch_url_base]++; + } + } + + $frequency = $this->updateSettings->get('check.interval_days'); + $available['last_fetch'] = REQUEST_TIME + $request_time_difference; + $this->availableReleasesTempStore->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency)); + + // Stash the $this->failed data back in the DB for the next 5 minutes. + $this->tempStore->setWithExpire('fetch_failures', $this->failed, $request_time_difference + (60 * 5)); + + // Whether this worked or not, we did just (try to) check for updates. + $this->stateStore->set('update.last_check', REQUEST_TIME + $request_time_difference); + + // Now that we processed the fetch task for this project, clear out the + // record for this task so we're willing to fetch again. + $this->fetchTaskStore->delete($project_name); + + return $success; + } + + /** + * Parses the XML of the Drupal release history info files. + * + * @param string $raw_xml + * A raw XML string of available release data for a given project. + * + * @return array + * Array of parsed data about releases for a given project, or NULL if there + * was an error parsing the string. + */ + protected function parseXml($raw_xml) { + try { + $xml = new \SimpleXMLElement($raw_xml); + } + catch (\Exception $e) { + // SimpleXMLElement::__construct produces an E_WARNING error message for + // each error found in the XML data and throws an exception if errors + // were detected. Catch any exception and return failure (NULL). + return NULL; + } + // If there is no valid project data, the XML is invalid, so return failure. + if (!isset($xml->short_name)) { + return NULL; + } + $data = []; + foreach ($xml as $k => $v) { + $data[$k] = (string) $v; + } + $data['releases'] = []; + if (isset($xml->releases)) { + foreach ($xml->releases->children() as $release) { + $version = (string) $release->version; + $data['releases'][$version] = []; + foreach ($release->children() as $k => $v) { + $data['releases'][$version][$k] = (string) $v; + } + $data['releases'][$version]['terms'] = []; + if ($release->terms) { + foreach ($release->terms->children() as $term) { + if (!isset($data['releases'][$version]['terms'][(string) $term->name])) { + $data['releases'][$version]['terms'][(string) $term->name] = []; + } + $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value; + } + } + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function numberOfQueueItems() { + return $this->fetchQueue->numberOfItems(); + } + + /** + * {@inheritdoc} + */ + public function claimQueueItem() { + return $this->fetchQueue->claimItem(); + } + + /** + * {@inheritdoc} + */ + public function deleteQueueItem($item) { + return $this->fetchQueue->deleteItem($item); + } + +}