diff --git a/core/modules/update/lib/Drupal/update/Controller/UpdateController.php b/core/modules/update/lib/Drupal/update/Controller/UpdateController.php index 6816466..6081361 100644 --- a/core/modules/update/lib/Drupal/update/Controller/UpdateController.php +++ b/core/modules/update/lib/Drupal/update/Controller/UpdateController.php @@ -9,6 +9,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\update\UpdateManager; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -24,13 +25,24 @@ class UpdateController implements ContainerInjectionInterface { protected $moduleHandler; /** + * Update manager service. + * + * @var \Drupal\update\UpdateManager + */ + protected $updateManager; + + /** * Constructs update status data. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * Module Handler Service. + * + * @param \Drupal\update\UpdateManager $update_manager + * Update Manager Service. */ - public function __construct(ModuleHandlerInterface $module_handler) { + public function __construct(ModuleHandlerInterface $module_handler, UpdateManager $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'); } } diff --git a/core/modules/update/lib/Drupal/update/UpdateFetcher.php b/core/modules/update/lib/Drupal/update/UpdateFetcher.php index 6893210..f1600a7 100644 --- a/core/modules/update/lib/Drupal/update/UpdateFetcher.php +++ b/core/modules/update/lib/Drupal/update/UpdateFetcher.php @@ -29,6 +29,13 @@ class UpdateFetcher { protected $fetchUrl; /** + * The update settings + * + * @var \Drupal\Core\Config\Config + */ + protected $updateSettings; + + /** * The HTTP client to fetch the feed data with. * * @var \Guzzle\Http\ClientInterface @@ -46,6 +53,7 @@ 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'); } /** @@ -89,9 +97,9 @@ public function fetchProjectData(array $project, $site_key = '') { * @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() + * @see \Drupal\update\UpdateProcessor::fetchData() + * @see \Drupal\update\UpdateProcessor::processFetchTask() + * @see \Drupal\update\UpdateManager::getProjects() */ public function buildFetchUrl(array $project, $site_key = '') { $name = $project['name']; @@ -130,8 +138,6 @@ public function buildFetchUrl(array $project, $site_key = '') { * 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() */ public function getFetchBaseUrl($project) { if (isset($project['info']['project status url'])) { diff --git a/core/modules/update/lib/Drupal/update/UpdateManager.php b/core/modules/update/lib/Drupal/update/UpdateManager.php new file mode 100644 index 0000000..6129e76 --- /dev/null +++ b/core/modules/update/lib/Drupal/update/UpdateManager.php @@ -0,0 +1,298 @@ +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(); + } + + /** + * Clears out all the available update data and initiates re-fetching. + */ + 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); + } + } + + /** + * 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 + * 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() { + 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; + } + + /** + * 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 $key + * The key of data to return. Valid options are 'update_project_data' and + * 'update_project_projects'. + * + * @return + * 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) { + $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; + } + + /** + * Batch callback: Processes a step in batch for fetching available update data. + * + * @param $context + * Reference to an array used for Batch API storage. + */ + 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); + } + +} diff --git a/core/modules/update/lib/Drupal/update/UpdateProcessor.php b/core/modules/update/lib/Drupal/update/UpdateProcessor.php new file mode 100644 index 0000000..0237fda --- /dev/null +++ b/core/modules/update/lib/Drupal/update/UpdateProcessor.php @@ -0,0 +1,311 @@ +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(); + } + + /** + * Adds a task to the queue for fetching release history data for a project. + * + * We only create a new fetch task if there's no task already in the queue for + * this particular project (based on 'update_fetch_task' key-value collection). + * + * @param array $project + * Associative array of information about a project as created by + * update_get_projects(), including keys such as 'name' (short name), and the + * 'info' array with data from a .info.yml file for the project. + * + * @see \Drupal\update\UpdateManager::getProjects() + * @see update_get_available() + * @see \Drupal\update\UpdateManager::refreshUpdateData() + * @see \Drupal\update\UpdateProcessor::fetchData() + * @see \Drupal\update\UpdateProcessor::processFetchTask() + */ + 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; + } + } + + /** + * Attempts to drain the queue of tasks for release history data to fetch. + */ + 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); + } + } + + /** + * Processes a task to fetch available update data for a single project. + * + * Once the release history XML data is downloaded, it is parsed and saved in + * an entry just for that project. + * + * @param array $project + * Associative array of information about the project to fetch data for. + * + * @return bool + * TRUE if we fetched parsable XML, otherwise FALSE. + */ + 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); + } + if (!empty($data)) { + $available = $this->parseXml($data); + // @todo: Purge release data we don't need (http://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 = array(); + foreach ($xml as $k => $v) { + $data[$k] = (string) $v; + } + $data['releases'] = array(); + if (isset($xml->releases)) { + foreach ($xml->releases->children() as $release) { + $version = (string) $release->version; + $data['releases'][$version] = array(); + foreach ($release->children() as $k => $v) { + $data['releases'][$version][$k] = (string) $v; + } + $data['releases'][$version]['terms'] = array(); + 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] = array(); + } + $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value; + } + } + } + } + return $data; + } + + /** + * Retrieves the number of items in the update fetch queue. + * + * @return int + * An integer estimate of the number of items in the queue. + * + * @see \Drupal\Core\Queue\QueueInterface::numberOfItems() + */ + public function numberOfQueueItems() { + return $this->fetchQueue->numberOfItems(); + } + + /** + * Claims an item in the update fetch queue for processing. + * + * @return + * On success we return an item object. If the queue is unable to claim an + * item it returns false. + * + * @see \Drupal\Core\Queue\QueueInterface::claimItem() + */ + public function claimQueueItem() { + return $this->fetchQueue->claimItem(); + } + + /** + * Deletes a finished item from the update fetch queue. + * + * @param $item + * The item returned by \Drupal\Core\Queue\QueueInterface::claimItem(). + * + * @see \Drupal\Core\Queue\QueueInterface::deleteItem() + */ + public function deleteQueueItem($item) { + return $this->fetchQueue->deleteItem($item); + } + +} diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc index 15446a3..492e650 100644 --- a/core/modules/update/update.compare.inc +++ b/core/modules/update/update.compare.inc @@ -5,8 +5,6 @@ * Code required only when comparing available updates to existing data. */ -use Drupal\Core\Utility\ProjectInfo; - /** * Fetches an array of installed and enabled projects. * @@ -50,33 +48,13 @@ * - 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 update_project_storage() + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateManager::getProjects() + * + * @see \Drupal\update\UpdateManager::getProjects() */ function update_get_projects() { - $projects = &drupal_static(__FUNCTION__, array()); - if (empty($projects)) { - // Retrieve the projects from storage, if present. - $projects = update_project_storage('update_project_projects'); - if (empty($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($projects, $module_data, 'module', TRUE); - $project_info->processInfoList($projects, $theme_data, 'theme', TRUE); - if (\Drupal::config('update.settings')->get('check.disabled_extensions')) { - $project_info->processInfoList($projects, $module_data, 'module', FALSE); - $project_info->processInfoList($projects, $theme_data, 'theme', FALSE); - } - // Allow other modules to alter projects before fetching and comparing. - drupal_alter('update_projects', $projects); - // Store the site's project data for at most 1 hour. - \Drupal::keyValueExpirable('update')->setWithExpire('update_project_projects', $projects, 3600); - } - } - return $projects; + return \Drupal::service('update.manager')->getProjects(); } /** @@ -593,28 +571,12 @@ function update_calculate_project_update_status(&$project_data, $available) { * 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. + * + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateManager::projectStorage() + * + * @see \Drupal\update\UpdateManager::projectStorage() */ function update_project_storage($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)) { - \Drupal::keyValueExpirable('update')->delete($key); - } - else { - $projects = \Drupal::keyValueExpirable('update')->get($key); - } - return $projects; + return \Drupal::service('update.manager')->projectStorage($key); } diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc index 20eb0b5..be7d84d 100644 --- a/core/modules/update/update.fetch.inc +++ b/core/modules/update/update.fetch.inc @@ -5,118 +5,31 @@ * Code required only when fetching information about available updates. */ -use Drupal\Component\Utility\Crypt; -use Drupal\update\UpdateFetcher; - -/** - * Page callback: Checks for updates and displays the update status report. - * - * Manually checks the update status without the use of cron. - * - * @see update_menu() - * - * @deprecated Use \Drupal\update\Controller\UpdateController::updateStatusManually() - */ -function update_manual_status() { - _update_refresh(); - $batch = array( - 'operations' => array( - array('update_fetch_data_batch', 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.'), - 'file' => drupal_get_path('module', 'update') . '/update.fetch.inc', - ); - batch_set($batch); - return batch_process('admin/reports/updates'); -} - /** * Batch callback: Processes a step in batch for fetching available update data. * * @param $context * Reference to an array used for Batch API storage. - */ -function update_fetch_data_batch(&$context) { - $queue = \Drupal::queue('update_fetch_tasks'); - if (empty($context['sandbox']['max'])) { - $context['finished'] = 0; - $context['sandbox']['max'] = $queue->numberOfItems(); - $context['sandbox']['progress'] = 0; - $context['message'] = 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 = $queue->claimItem()) { - if (_update_process_fetch_task($item->data)) { - $context['results']['updated']++; - $context['message'] = t('Checked available update data for %title.', array('%title' => $item->data['info']['name'])); - } - else { - $context['message'] = 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']; - $queue->deleteItem($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; - } - } -} - -/** - * Batch callback: Performs actions when all fetch tasks have been completed. * - * @param $success - * TRUE if the batch operation was successful; FALSE if there were errors. - * @param $results - * An associative array of results from the batch operation, including the key - * 'updated' which holds the total number of projects we fetched available - * update data for. + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateManager::fetchDataBatch() + * + * @see \Drupal\update\UpdateManager::fetchDataBatch() */ -function update_fetch_data_finished($success, $results) { - if ($success) { - if (!empty($results)) { - if (!empty($results['updated'])) { - drupal_set_message(format_plural($results['updated'], 'Checked available update data for one project.', 'Checked available update data for @count projects.')); - } - if (!empty($results['failures'])) { - drupal_set_message(format_plural($results['failures'], 'Failed to get available update data for one project.', 'Failed to get available update data for @count projects.'), 'error'); - } - } - } - else { - drupal_set_message(t('An error occurred trying to get available update data.'), 'error'); - } +function update_fetch_data_batch(&$context) { + \Drupal::service('update.manager')->fetchDataBatch($context); } /** * Attempts to drain the queue of tasks for release history data to fetch. + * + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateFetcher::fetchData() + * + * @see \Drupal\update\UpdateFetcher::fetchData() */ function _update_fetch_data() { - $queue = \Drupal::queue('update_fetch_tasks'); - $end = time() + \Drupal::config('update.settings')->get('fetch.timeout'); - while (time() < $end && ($item = $queue->claimItem())) { - _update_process_fetch_task($item->data); - $queue->deleteItem($item); - } + \Drupal::service('update.processor')->fetchData(); } /** @@ -130,93 +43,26 @@ function _update_fetch_data() { * * @return * TRUE if we fetched parsable XML, otherwise FALSE. + * + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateFetcher::processFetchTask() + * + * @see \Drupal\update\UpdateFetcher::processFetchTask() */ function _update_process_fetch_task($project) { - global $base_url; - $update_config = \Drupal::config('update.settings'); - $fail = &drupal_static(__FUNCTION__, array()); - // 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($fail)) { - // If we have valid data about release history XML servers that we have - // failed to fetch from on previous attempts, load that. - $fail = \Drupal::keyValueExpirable('update')->get('fetch_failures'); - } - - $max_fetch_attempts = $update_config->get('fetch.max_attempts'); - - $success = FALSE; - $available = array(); - $site_key = Crypt::hmacBase64($base_url, \Drupal::service('private_key')->get()); - $update_fetcher = new UpdateFetcher(\Drupal::service('config.factory'), \Drupal::service('http_default_client')); - $fetch_url_base = $update_fetcher->getFetchBaseUrl($project); - $project_name = $project['name']; - - if (empty($fail[$fetch_url_base]) || $fail[$fetch_url_base] < $max_fetch_attempts) { - $data = $update_fetcher->fetchProjectData($project, $site_key); - } - - if (!empty($data)) { - $available = update_parse_xml($data); - // @todo: Purge release data we don't need (http://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($fail[$fetch_url_base])) { - $fail[$fetch_url_base] = 1; - } - else { - $fail[$fetch_url_base]++; - } - } - - $frequency = $update_config->get('check.interval_days'); - $available['last_fetch'] = REQUEST_TIME + $request_time_difference; - \Drupal::keyValueExpirable('update_available_releases')->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency)); - - // Stash the $fail data back in the DB for the next 5 minutes. - \Drupal::keyValueExpirable('update')->setWithExpire('fetch_failures', $fail, $request_time_difference + (60 * 5)); - - // Whether this worked or not, we did just (try to) check for updates. - \Drupal::state()->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. - \Drupal::keyValue('update_fetch_task')->delete($project_name); - - return $success; + return \Drupal::service('update.processor')->processFetchTask($project); } /** * Clears out all the available update data and initiates re-fetching. + * + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateManager::refreshUpdateData() + * + * @see \Drupal\update\UpdateManager::refreshUpdateData() */ function _update_refresh() { - module_load_include('inc', 'update', 'update.compare'); - - // 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. - \Drupal::keyValueExpirable('update')->delete('update_project_projects'); - \Drupal::keyValueExpirable('update')->delete('update_project_data'); - - $projects = update_get_projects(); - - // 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. - \Drupal::keyValueExpirable('update_available_releases')->deleteAll(); - - foreach ($projects as $project) { - update_create_fetch_task($project); - } + \Drupal::service('update.manager')->refreshUpdateData(); } /** @@ -230,23 +76,13 @@ function _update_refresh() { * update_get_projects(), including keys such as 'name' (short name), and the * 'info' array with data from a .info.yml file for the project. * - * @see update_get_projects() - * @see update_get_available() - * @see update_refresh() - * @see update_fetch_data() - * @see _update_process_fetch_task() + * @deprecated as of Drupal 8.0. Use + * \Drupal\update\UpdateFetcher::createFetchTask() + * + * @see \Drupal\update\UpdateFetcher::createFetchTask() */ function _update_create_fetch_task($project) { - $fetch_tasks = &drupal_static(__FUNCTION__, array()); - if (empty($fetch_tasks)) { - $fetch_tasks = \Drupal::keyValue('update_fetch_task')->getAll(); - } - if (empty($fetch_tasks[$project['name']])) { - $queue = \Drupal::queue('update_fetch_tasks'); - $queue->createItem($project); - \Drupal::keyValue('update_fetch_task')->set($project['name'], $project); - $fetch_tasks[$project['name']] = REQUEST_TIME; - } + \Drupal::service('update.processor')->createFetchTask($project); } /** diff --git a/core/modules/update/update.module b/core/modules/update/update.module index af8143b..550bdfb 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -332,12 +332,12 @@ function update_get_available($refresh = FALSE) { // Grab whatever data we currently have. $available = \Drupal::keyValueExpirable('update_available_releases')->getAll(); - - $projects = update_get_projects(); + $projects = \Drupal::service('update.manager')->getProjects(); foreach ($projects as $key => $project) { // If there's no data at all, we clearly need to fetch some. if (empty($available[$key])) { - update_create_fetch_task($project); + //update_create_fetch_task($project); + \Drupal::service('update.processor')->createFetchTask($project); $needs_refresh = TRUE; continue; } @@ -360,7 +360,7 @@ function update_get_available($refresh = FALSE) { // If we think this project needs to fetch, actually create the task now // and remember that we think we're missing some data. if (!empty($available[$key]['fetch_status']) && $available[$key]['fetch_status'] == UPDATE_FETCH_PENDING) { - update_create_fetch_task($project); + \Drupal::service('update.processor')->createFetchTask($project); $needs_refresh = TRUE; } } @@ -411,6 +411,32 @@ function update_fetch_data() { } /** + * Batch callback: Performs actions when all fetch tasks have been completed. + * + * @param $success + * TRUE if the batch operation was successful; FALSE if there were errors. + * @param $results + * An associative array of results from the batch operation, including the key + * 'updated' which holds the total number of projects we fetched available + * update data for. + */ +function update_fetch_data_finished($success, $results) { + if ($success) { + if (!empty($results)) { + if (!empty($results['updated'])) { + drupal_set_message(format_plural($results['updated'], 'Checked available update data for one project.', 'Checked available update data for @count projects.')); + } + if (!empty($results['failures'])) { + drupal_set_message(format_plural($results['failures'], 'Failed to get available update data for one project.', 'Failed to get available update data for @count projects.'), 'error'); + } + } + } + else { + drupal_set_message(t('An error occurred trying to get available update data.'), 'error'); + } +} + +/** * Implements hook_mail(). * * Constructs the e-mail notification message when the site is out of date. diff --git a/core/modules/update/update.services.yml b/core/modules/update/update.services.yml index 7008182..4146766 100644 --- a/core/modules/update/update.services.yml +++ b/core/modules/update/update.services.yml @@ -4,3 +4,12 @@ services: arguments: ['@settings'] tags: - { name: access_check, applies_to: _access_update_manager } + update.manager: + class: Drupal\update\UpdateManager + arguments: ['@config.factory', '@module_handler', '@update.processor', '@string_translation', '@keyvalue.expirable'] + update.processor: + class: Drupal\update\UpdateProcessor + arguments: ['@config.factory', '@queue', '@update.fetcher', '@state', '@private_key', '@keyvalue', '@keyvalue.expirable'] + update.fetcher: + class: Drupal\update\UpdateFetcher + arguments: ['@config.factory', '@http_default_client']