Commit cc1892d4 authored by alexpott's avatar alexpott

Issue #1987888 by zengenuity, kim.pepper, disasm, vijaycs85, JB13, dawehner:...

Issue #1987888 by zengenuity, kim.pepper, disasm, vijaycs85, JB13, dawehner: Convert update_manual_status() to a new style controller.
parent 19fa29c3
......@@ -9,6 +9,7 @@
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\update\UpdateManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -23,14 +24,25 @@ class UpdateController implements ContainerInjectionInterface {
*/
protected $moduleHandler;
/**
* Update manager service.
*
* @var \Drupal\update\UpdateManagerInterface
*/
protected $updateManager;
/**
* Constructs update status data.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* Module Handler Service.
*
* @param \Drupal\update\UpdateManagerInterface $update_manager
* Update Manager Service.
*/
public function __construct(ModuleHandlerInterface $module_handler) {
public function __construct(ModuleHandlerInterface $module_handler, UpdateManagerInterface $update_manager) {
$this->moduleHandler = $module_handler;
$this->updateManager = $update_manager;
}
/**
......@@ -38,7 +50,8 @@ public function __construct(ModuleHandlerInterface $module_handler) {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler')
$container->get('module_handler'),
$container->get('update.manager')
);
}
......@@ -63,11 +76,21 @@ public function updateStatus() {
}
/**
* @todo Remove update_manual_status().
* Manually checks the update status without the use of cron.
*/
public function updateStatusManually() {
module_load_include('fetch.inc', 'update');
return update_manual_status();
$this->updateManager->refreshUpdateData();
$batch = array(
'operations' => array(
array(array($this->updateManager, 'fetchDataBatch'), array()),
),
'finished' => 'update_fetch_data_finished',
'title' => t('Checking available update data'),
'progress_message' => t('Trying to check available update data ...'),
'error_message' => t('Error checking available update data.'),
);
batch_set($batch);
return batch_process('admin/reports/updates');
}
}
......@@ -14,7 +14,7 @@
/**
* Fetches project information from remote locations.
*/
class UpdateFetcher {
class UpdateFetcher implements UpdateFetcherInterface {
/**
* URL to check for updates, if a given project doesn't define its own.
......@@ -28,6 +28,13 @@ class UpdateFetcher {
*/
protected $fetchUrl;
/**
* The update settings
*
* @var \Drupal\Core\Config\Config
*/
protected $updateSettings;
/**
* The HTTP client to fetch the feed data with.
*
......@@ -46,18 +53,11 @@ class UpdateFetcher {
public function __construct(ConfigFactory $config_factory, ClientInterface $http_client) {
$this->fetchUrl = $config_factory->get('update.settings')->get('fetch.url');
$this->httpClient = $http_client;
$this->updateSettings = $config_factory->get('update.settings');
}
/**
* Retrieves the project information.
*
* @param array $project
* The array of project information from update_get_projects().
* @param string $site_key
* (optional) The anonymous site key hash. Defaults to an empty string.
*
* @return string
* The project information fetched as string. Empty string upon failure.
* {@inheritdoc}
*/
public function fetchProjectData(array $project, $site_key = '') {
$url = $this->buildFetchUrl($project, $site_key);
......@@ -75,23 +75,7 @@ public function fetchProjectData(array $project, $site_key = '') {
}
/**
* Generates the URL to fetch information about project updates.
*
* This figures out the right URL to use, based on the project's .info.yml file
* and the global defaults. Appends optional query arguments when the site is
* configured to report usage stats.
*
* @param array $project
* The array of project information from update_get_projects().
* @param string $site_key
* (optional) The anonymous site key hash. Defaults to an empty string.
*
* @return string
* The URL for fetching information about updates to the specified project.
*
* @see update_fetch_data()
* @see _update_process_fetch_task()
* @see update_get_projects()
* {@inheritdoc}
*/
public function buildFetchUrl(array $project, $site_key = '') {
$name = $project['name'];
......@@ -121,17 +105,7 @@ public function buildFetchUrl(array $project, $site_key = '') {
}
/**
* Returns the base of the URL to fetch available update data for a project.
*
* @param array $project
* The array of project information from update_get_projects().
*
* @return string
* The base of the URL used for fetching available update data. This does
* not include the path elements to specify a particular project, version,
* site_key, etc.
*
* @see \Drupal\update\UpdateFetcher::getFetchBaseUrl()
* {@inheritdoc}
*/
public function getFetchBaseUrl($project) {
if (isset($project['info']['project status url'])) {
......
<?php
/**
* @file
* Contains \Drupal\update\UpdateFetcherInterface.
*/
namespace Drupal\update;
/**
* Fetches project information from remote locations.
*/
interface UpdateFetcherInterface {
/**
* Returns the base of the URL to fetch available update data for a project.
*
* @param array $project
* The array of project information from update_get_projects().
*
* @return string
* The base of the URL used for fetching available update data. This does
* not include the path elements to specify a particular project, version,
* site_key, etc.
*/
public function getFetchBaseUrl($project);
/**
* Retrieves the project information.
*
* @param array $project
* The array of project information from update_get_projects().
* @param string $site_key
* (optional) The anonymous site key hash. Defaults to an empty string.
*
* @return string
* The project information fetched as string. Empty string upon failure.
*/
public function fetchProjectData(array $project, $site_key = '');
/**
* Generates the URL to fetch information about project updates.
*
* This figures out the right URL to use, based on the project's .info.yml
* file and the global defaults. Appends optional query arguments when the
* site is configured to report usage stats.
*
* @param array $project
* The array of project information from update_get_projects().
* @param string $site_key
* (optional) The anonymous site key hash. Defaults to an empty string.
*
* @return string
* The URL for fetching information about updates to the specified project.
*
* @see \Drupal\update\UpdateProcessor::fetchData()
* @see \Drupal\update\UpdateProcessor::processFetchTask()
* @see \Drupal\update\UpdateManager::getProjects()
*/
public function buildFetchUrl(array $project, $site_key = '');
}
<?php
/**
* @file
* Contains \Drupal\update\UpdateManager.
*/
namespace Drupal\update;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\ProjectInfo;
/**
* Default implementation of UpdateManagerInterface.
*/
class UpdateManager implements UpdateManagerInterface {
/**
* The update settings
*
* @var \Drupal\Core\Config\Config
*/
protected $updateSettings;
/**
* Module Handler Service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Update Processor Service.
*
* @var \Drupal\update\UpdateProcessorInterface
*/
protected $updateProcessor;
/**
* An array of installed and enabled projects.
*
* @var array
*/
protected $projects;
/**
* The key/value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueStore;
/**
* Update available releases key/value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $availableReleasesTempStore;
/**
* The translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translation;
/**
* Constructs a UpdateManager.
*
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The Module Handler service
* @param \Drupal\update\UpdateProcessorInterface $update_processor
* The Update Processor service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The translation service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_expirable_factory
* The expirable key/value factory.
*/
public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, UpdateProcessorInterface $update_processor, TranslationInterface $translation, KeyValueFactoryInterface $key_value_expirable_factory) {
$this->updateSettings = $config_factory->get('update.settings');
$this->moduleHandler = $module_handler;
$this->updateProcessor = $update_processor;
$this->translation = $translation;
$this->keyValueStore = $key_value_expirable_factory->get('update');
$this->availableReleasesTempStore = $key_value_expirable_factory->get('update_available_releases');
$this->projects = array();
}
/**
* {@inheritdoc}
*/
public function refreshUpdateData() {
// Since we're fetching new available update data, we want to clear
// of both the projects we care about, and the current update status of the
// site. We do *not* want to clear the cache of available releases just yet,
// since that data (even if it's stale) can be useful during
// update_get_projects(); for example, to modules that implement
// hook_system_info_alter() such as cvs_deploy.
$this->keyValueStore->delete('update_project_projects');
$this->keyValueStore->delete('update_project_data');
$projects = $this->getProjects();
// Now that we have the list of projects, we should also clear the available
// release data, since even if we fail to fetch new data, we need to clear
// out the stale data at this point.
$this->availableReleasesTempStore->deleteAll();
foreach ($projects as $project) {
$this->updateProcessor->createFetchTask($project);
}
}
/**
* {@inheritdoc}
*/
public function getProjects() {
if (empty($this->projects)) {
// Retrieve the projects from storage, if present.
$this->projects = $this->projectStorage('update_project_projects');
if (empty($this->projects)) {
// Still empty, so we have to rebuild.
$module_data = system_rebuild_module_data();
$theme_data = system_rebuild_theme_data();
$project_info = new ProjectInfo();
$project_info->processInfoList($this->projects, $module_data, 'module', TRUE);
$project_info->processInfoList($this->projects, $theme_data, 'theme', TRUE);
if ($this->updateSettings->get('check.disabled_extensions')) {
$project_info->processInfoList($this->projects, $module_data, 'module', FALSE);
$project_info->processInfoList($this->projects, $theme_data, 'theme', FALSE);
}
// Allow other modules to alter projects before fetching and comparing.
$this->moduleHandler->alter('update_projects', $this->projects);
// Store the site's project data for at most 1 hour.
$this->keyValueStore->setWithExpire('update_project_projects', $this->projects, 3600);
}
}
return $this->projects;
}
/**
* {@inheritdoc}
*/
public function projectStorage($key) {
$projects = array();
// On certain paths, we should clear the data and recompute the projects for
// update status of the site to avoid presenting stale information.
$paths = array(
'admin/modules',
'admin/modules/update',
'admin/appearance',
'admin/appearance/update',
'admin/reports',
'admin/reports/updates',
'admin/reports/updates/update',
'admin/reports/status',
'admin/reports/updates/check',
);
if (in_array(current_path(), $paths)) {
$this->keyValueStore->delete($key);
}
else {
$projects = $this->keyValueStore->get($key);
}
return $projects;
}
/**
* {@inheritdoc}
*/
public function fetchDataBatch(&$context) {
if (empty($context['sandbox']['max'])) {
$context['finished'] = 0;
$context['sandbox']['max'] = $this->updateProcessor->numberOfQueueItems();
$context['sandbox']['progress'] = 0;
$context['message'] = $this->t('Checking available update data ...');
$context['results']['updated'] = 0;
$context['results']['failures'] = 0;
$context['results']['processed'] = 0;
}
// Grab another item from the fetch queue.
for ($i = 0; $i < 5; $i++) {
if ($item = $this->updateProcessor->claimQueueItem()) {
if ($this->updateProcessor->processFetchTask($item->data)) {
$context['results']['updated']++;
$context['message'] = $this->t('Checked available update data for %title.', array('%title' => $item->data['info']['name']));
}
else {
$context['message'] = $this->t('Failed to check available update data for %title.', array('%title' => $item->data['info']['name']));
$context['results']['failures']++;
}
$context['sandbox']['progress']++;
$context['results']['processed']++;
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
$this->updateProcessor->deleteQueueItem($item);
}
else {
// If the queue is currently empty, we're done. It's possible that
// another thread might have added new fetch tasks while we were
// processing this batch. In that case, the usual 'finished' math could
// get confused, since we'd end up processing more tasks that we thought
// we had when we started and initialized 'max' with numberOfItems(). By
// forcing 'finished' to be exactly 1 here, we ensure that batch
// processing is terminated.
$context['finished'] = 1;
return;
}
}
}
/**
* Translates a string to the current language or to a given language.
*
* See the t() documentation for details.
*/
protected function t($string, array $args = array(), array $options = array()) {
return $this->translation->translate($string, $args, $options);
}
}
<?php
/**
* @file
* Contains \Drupal\update\UpdateManagerInterface.
*/
namespace Drupal\update;
/**
* Manages project update information.
*/
interface UpdateManagerInterface {
/**
* Fetches an array of installed and enabled projects.
*
* This is only responsible for generating an array of projects (taking into
* account projects that include more than one module or theme). Other
* information like the specific version and install type (official release,
* dev snapshot, etc) is handled later in update_process_project_info() since
* that logic is only required when preparing the status report, not for
* fetching the available release data.
*
* This array is fairly expensive to construct, since it involves a lot of
* disk I/O, so we store the results. However, since this is not the data
* about available updates fetched from the network, it is acceptable to
* invalidate it somewhat quickly. If we keep this data for very long, site
* administrators are more likely to see incorrect results if they upgrade to
* a newer version of a module or theme but do not visit certain pages that
* automatically clear this data.
*
* @return array
* An associative array of currently enabled projects keyed by the
* machine-readable project short name. Each project contains:
* - name: The machine-readable project short name.
* - info: An array with values from the main .info.yml file for this
* project.
* - name: The human-readable name of the project.
* - package: The package that the project is grouped under.
* - version: The version of the project.
* - project: The Drupal.org project name.
* - datestamp: The date stamp of the project's main .info.yml file.
* - _info_file_ctime: The maximum file change time for all of the
* .info.yml
* files included in this project.
* - datestamp: The date stamp when the project was released, if known.
* - includes: An associative array containing all projects included with
* this project, keyed by the machine-readable short name with the
* human-readable name as value.
* - project_type: The type of project. Allowed values are 'module' and
* 'theme'.
* - project_status: This indicates if the project is enabled and will
* always be TRUE, as the function only returns enabled projects.
* - sub_themes: If the project is a theme it contains an associative array
* of all sub-themes.
* - base_themes: If the project is a theme it contains an associative array
* of all base-themes.
*
* @see update_process_project_info()
* @see update_calculate_project_data()
* @see \Drupal\update\UpdateManager::projectStorage()
*/
public function getProjects();
/**
* Processes a step in batch for fetching available update data.
*
* @param array $context
* Reference to an array used for Batch API storage.
*/
public function fetchDataBatch(&$context);
/**
* Clears out all the available update data and initiates re-fetching.
*/
public function refreshUpdateData();
/**
* Retrieves update storage data or empties it.
*
* Two very expensive arrays computed by this module are the list of all
* installed modules and themes (and .info.yml data, project associations,
* etc), and the current status of the site relative to the currently
* available releases. These two arrays are stored and used whenever possible.
* The data is cleared whenever the administrator visits the status report,
* available updates report, or the module or theme administration pages,
* since we should always recompute the most current values on any of those
* pages.
*
* Note: while both of these arrays are expensive to compute (in terms of disk
* I/O and some fairly heavy CPU processing), neither of these is the actual
* data about available updates that we have to fetch over the network from
* updates.drupal.org. That information is stored in the
* 'update_available_releases' collection -- it needs to persist longer than 1
* hour and never get invalidated just by visiting a page on the site.
*
* @param string $key
* The key of data to return. Valid options are 'update_project_data' and
* 'update_project_projects'.
*
* @return array
* The stored value of the $projects array generated by
* update_calculate_project_data() or update_get_projects(), or an empty
* array when the storage is cleared.
*/
public function projectStorage($key);
}
<?php
/**
* @file
* Contains \Drupal\update\UpdateProcessor.
*/
namespace Drupal\update;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\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\KeyValueStore\StateInterface
*/
protected $stateStore;
/**
* The private key.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* Constructs a UpdateProcessor.
*
* @param \Drupal\Core\Config\ConfigFactory $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\KeyValueStore\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(ConfigFactory $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 = array();
$this->failed = array();
}
/**
* {@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 = array();
$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);
}