Chris@0: updateFetcher = $update_fetcher; Chris@0: $this->updateSettings = $config_factory->get('update.settings'); Chris@0: $this->fetchQueue = $queue_factory->get('update_fetch_tasks'); Chris@0: $this->tempStore = $key_value_expirable_factory->get('update'); Chris@0: $this->fetchTaskStore = $key_value_factory->get('update_fetch_task'); Chris@0: $this->availableReleasesTempStore = $key_value_expirable_factory->get('update_available_releases'); Chris@0: $this->stateStore = $state_store; Chris@0: $this->privateKey = $private_key; Chris@0: $this->fetchTasks = []; Chris@0: $this->failed = []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function createFetchTask($project) { Chris@0: if (empty($this->fetchTasks)) { Chris@0: $this->fetchTasks = $this->fetchTaskStore->getAll(); Chris@0: } Chris@0: if (empty($this->fetchTasks[$project['name']])) { Chris@0: $this->fetchQueue->createItem($project); Chris@0: $this->fetchTaskStore->set($project['name'], $project); Chris@0: $this->fetchTasks[$project['name']] = REQUEST_TIME; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function fetchData() { Chris@0: $end = time() + $this->updateSettings->get('fetch.timeout'); Chris@0: while (time() < $end && ($item = $this->fetchQueue->claimItem())) { Chris@0: $this->processFetchTask($item->data); Chris@0: $this->fetchQueue->deleteItem($item); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function processFetchTask($project) { Chris@0: global $base_url; Chris@0: Chris@0: // This can be in the middle of a long-running batch, so REQUEST_TIME won't Chris@0: // necessarily be valid. Chris@0: $request_time_difference = time() - REQUEST_TIME; Chris@0: if (empty($this->failed)) { Chris@0: // If we have valid data about release history XML servers that we have Chris@0: // failed to fetch from on previous attempts, load that. Chris@0: $this->failed = $this->tempStore->get('fetch_failures'); Chris@0: } Chris@0: Chris@0: $max_fetch_attempts = $this->updateSettings->get('fetch.max_attempts'); Chris@0: Chris@0: $success = FALSE; Chris@0: $available = []; Chris@0: $site_key = Crypt::hmacBase64($base_url, $this->privateKey->get()); Chris@0: $fetch_url_base = $this->updateFetcher->getFetchBaseUrl($project); Chris@0: $project_name = $project['name']; Chris@0: Chris@0: if (empty($this->failed[$fetch_url_base]) || $this->failed[$fetch_url_base] < $max_fetch_attempts) { Chris@0: $data = $this->updateFetcher->fetchProjectData($project, $site_key); Chris@0: } Chris@0: if (!empty($data)) { Chris@0: $available = $this->parseXml($data); Chris@0: // @todo: Purge release data we don't need. See Chris@0: // https://www.drupal.org/node/238950. Chris@0: if (!empty($available)) { Chris@0: // Only if we fetched and parsed something sane do we return success. Chris@0: $success = TRUE; Chris@0: } Chris@0: } Chris@0: else { Chris@0: $available['project_status'] = 'not-fetched'; Chris@0: if (empty($this->failed[$fetch_url_base])) { Chris@0: $this->failed[$fetch_url_base] = 1; Chris@0: } Chris@0: else { Chris@0: $this->failed[$fetch_url_base]++; Chris@0: } Chris@0: } Chris@0: Chris@0: $frequency = $this->updateSettings->get('check.interval_days'); Chris@0: $available['last_fetch'] = REQUEST_TIME + $request_time_difference; Chris@0: $this->availableReleasesTempStore->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency)); Chris@0: Chris@0: // Stash the $this->failed data back in the DB for the next 5 minutes. Chris@0: $this->tempStore->setWithExpire('fetch_failures', $this->failed, $request_time_difference + (60 * 5)); Chris@0: Chris@0: // Whether this worked or not, we did just (try to) check for updates. Chris@0: $this->stateStore->set('update.last_check', REQUEST_TIME + $request_time_difference); Chris@0: Chris@0: // Now that we processed the fetch task for this project, clear out the Chris@0: // record for this task so we're willing to fetch again. Chris@0: $this->fetchTaskStore->delete($project_name); Chris@0: Chris@0: return $success; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses the XML of the Drupal release history info files. Chris@0: * Chris@0: * @param string $raw_xml Chris@0: * A raw XML string of available release data for a given project. Chris@0: * Chris@0: * @return array Chris@0: * Array of parsed data about releases for a given project, or NULL if there Chris@0: * was an error parsing the string. Chris@0: */ Chris@0: protected function parseXml($raw_xml) { Chris@0: try { Chris@0: $xml = new \SimpleXMLElement($raw_xml); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // SimpleXMLElement::__construct produces an E_WARNING error message for Chris@0: // each error found in the XML data and throws an exception if errors Chris@0: // were detected. Catch any exception and return failure (NULL). Chris@0: return NULL; Chris@0: } Chris@0: // If there is no valid project data, the XML is invalid, so return failure. Chris@0: if (!isset($xml->short_name)) { Chris@0: return NULL; Chris@0: } Chris@0: $data = []; Chris@0: foreach ($xml as $k => $v) { Chris@0: $data[$k] = (string) $v; Chris@0: } Chris@0: $data['releases'] = []; Chris@0: if (isset($xml->releases)) { Chris@0: foreach ($xml->releases->children() as $release) { Chris@0: $version = (string) $release->version; Chris@0: $data['releases'][$version] = []; Chris@0: foreach ($release->children() as $k => $v) { Chris@0: $data['releases'][$version][$k] = (string) $v; Chris@0: } Chris@0: $data['releases'][$version]['terms'] = []; Chris@0: if ($release->terms) { Chris@0: foreach ($release->terms->children() as $term) { Chris@0: if (!isset($data['releases'][$version]['terms'][(string) $term->name])) { Chris@0: $data['releases'][$version]['terms'][(string) $term->name] = []; Chris@0: } Chris@0: $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function numberOfQueueItems() { Chris@0: return $this->fetchQueue->numberOfItems(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function claimQueueItem() { Chris@0: return $this->fetchQueue->claimItem(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function deleteQueueItem($item) { Chris@0: return $this->fetchQueue->deleteItem($item); Chris@0: } Chris@0: Chris@0: }