UpdateProcessor.php

Same filename and directory in other branches
  1. 9 core/modules/update/src/UpdateProcessor.php
  2. 10 core/modules/update/src/UpdateProcessor.php
  3. 11.x core/modules/update/src/UpdateProcessor.php

Namespace

Drupal\update

File

core/modules/update/src/UpdateProcessor.php

View source
<?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');
        if ($this->fetchQueue
            ->numberOfItems()) {
            // Delete any stored project data as that needs refreshing when
            // update_calculate_project_data() is called.
            $this->tempStore
                ->delete('update_project_data');
        }
        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);
    }

}

Classes

Title Deprecated Summary
UpdateProcessor Process project update information.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.