comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
1 <?php
2
3 namespace Drupal\update;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Config\ConfigFactoryInterface;
7 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
8 use Drupal\Core\State\StateInterface;
9 use Drupal\Core\PrivateKey;
10 use Drupal\Core\Queue\QueueFactory;
11
12 /**
13 * Process project update information.
14 */
15 class UpdateProcessor implements UpdateProcessorInterface {
16
17 /**
18 * The update settings
19 *
20 * @var \Drupal\Core\Config\Config
21 */
22 protected $updateSettings;
23
24 /**
25 * The UpdateFetcher service.
26 *
27 * @var \Drupal\update\UpdateFetcherInterface
28 */
29 protected $updateFetcher;
30
31 /**
32 * The update fetch queue.
33 *
34 * @var \Drupal\Core\Queue\QueueInterface
35 */
36 protected $fetchQueue;
37
38 /**
39 * Update key/value store
40 *
41 * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
42 */
43 protected $tempStore;
44
45 /**
46 * Update Fetch Task Store
47 *
48 * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
49 */
50 protected $fetchTaskStore;
51
52 /**
53 * Update available releases store
54 *
55 * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
56 */
57 protected $availableReleasesTempStore;
58
59 /**
60 * Array of release history URLs that we have failed to fetch
61 *
62 * @var array
63 */
64 protected $failed;
65
66 /**
67 * The state service.
68 *
69 * @var \Drupal\Core\State\StateInterface
70 */
71 protected $stateStore;
72
73 /**
74 * The private key.
75 *
76 * @var \Drupal\Core\PrivateKey
77 */
78 protected $privateKey;
79
80 /**
81 * Constructs a UpdateProcessor.
82 *
83 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
84 * The config factory.
85 * @param \Drupal\Core\Queue\QueueFactory $queue_factory
86 * The queue factory
87 * @param \Drupal\update\UpdateFetcherInterface $update_fetcher
88 * The update fetcher service
89 * @param \Drupal\Core\State\StateInterface $state_store
90 * The state service.
91 * @param \Drupal\Core\PrivateKey $private_key
92 * The private key factory service.
93 * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
94 * The key/value factory.
95 * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_expirable_factory
96 * The expirable key/value factory.
97 */
98 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) {
99 $this->updateFetcher = $update_fetcher;
100 $this->updateSettings = $config_factory->get('update.settings');
101 $this->fetchQueue = $queue_factory->get('update_fetch_tasks');
102 $this->tempStore = $key_value_expirable_factory->get('update');
103 $this->fetchTaskStore = $key_value_factory->get('update_fetch_task');
104 $this->availableReleasesTempStore = $key_value_expirable_factory->get('update_available_releases');
105 $this->stateStore = $state_store;
106 $this->privateKey = $private_key;
107 $this->fetchTasks = [];
108 $this->failed = [];
109 }
110
111 /**
112 * {@inheritdoc}
113 */
114 public function createFetchTask($project) {
115 if (empty($this->fetchTasks)) {
116 $this->fetchTasks = $this->fetchTaskStore->getAll();
117 }
118 if (empty($this->fetchTasks[$project['name']])) {
119 $this->fetchQueue->createItem($project);
120 $this->fetchTaskStore->set($project['name'], $project);
121 $this->fetchTasks[$project['name']] = REQUEST_TIME;
122 }
123 }
124
125 /**
126 * {@inheritdoc}
127 */
128 public function fetchData() {
129 $end = time() + $this->updateSettings->get('fetch.timeout');
130 while (time() < $end && ($item = $this->fetchQueue->claimItem())) {
131 $this->processFetchTask($item->data);
132 $this->fetchQueue->deleteItem($item);
133 }
134 }
135
136 /**
137 * {@inheritdoc}
138 */
139 public function processFetchTask($project) {
140 global $base_url;
141
142 // This can be in the middle of a long-running batch, so REQUEST_TIME won't
143 // necessarily be valid.
144 $request_time_difference = time() - REQUEST_TIME;
145 if (empty($this->failed)) {
146 // If we have valid data about release history XML servers that we have
147 // failed to fetch from on previous attempts, load that.
148 $this->failed = $this->tempStore->get('fetch_failures');
149 }
150
151 $max_fetch_attempts = $this->updateSettings->get('fetch.max_attempts');
152
153 $success = FALSE;
154 $available = [];
155 $site_key = Crypt::hmacBase64($base_url, $this->privateKey->get());
156 $fetch_url_base = $this->updateFetcher->getFetchBaseUrl($project);
157 $project_name = $project['name'];
158
159 if (empty($this->failed[$fetch_url_base]) || $this->failed[$fetch_url_base] < $max_fetch_attempts) {
160 $data = $this->updateFetcher->fetchProjectData($project, $site_key);
161 }
162 if (!empty($data)) {
163 $available = $this->parseXml($data);
164 // @todo: Purge release data we don't need. See
165 // https://www.drupal.org/node/238950.
166 if (!empty($available)) {
167 // Only if we fetched and parsed something sane do we return success.
168 $success = TRUE;
169 }
170 }
171 else {
172 $available['project_status'] = 'not-fetched';
173 if (empty($this->failed[$fetch_url_base])) {
174 $this->failed[$fetch_url_base] = 1;
175 }
176 else {
177 $this->failed[$fetch_url_base]++;
178 }
179 }
180
181 $frequency = $this->updateSettings->get('check.interval_days');
182 $available['last_fetch'] = REQUEST_TIME + $request_time_difference;
183 $this->availableReleasesTempStore->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency));
184
185 // Stash the $this->failed data back in the DB for the next 5 minutes.
186 $this->tempStore->setWithExpire('fetch_failures', $this->failed, $request_time_difference + (60 * 5));
187
188 // Whether this worked or not, we did just (try to) check for updates.
189 $this->stateStore->set('update.last_check', REQUEST_TIME + $request_time_difference);
190
191 // Now that we processed the fetch task for this project, clear out the
192 // record for this task so we're willing to fetch again.
193 $this->fetchTaskStore->delete($project_name);
194
195 return $success;
196 }
197
198 /**
199 * Parses the XML of the Drupal release history info files.
200 *
201 * @param string $raw_xml
202 * A raw XML string of available release data for a given project.
203 *
204 * @return array
205 * Array of parsed data about releases for a given project, or NULL if there
206 * was an error parsing the string.
207 */
208 protected function parseXml($raw_xml) {
209 try {
210 $xml = new \SimpleXMLElement($raw_xml);
211 }
212 catch (\Exception $e) {
213 // SimpleXMLElement::__construct produces an E_WARNING error message for
214 // each error found in the XML data and throws an exception if errors
215 // were detected. Catch any exception and return failure (NULL).
216 return NULL;
217 }
218 // If there is no valid project data, the XML is invalid, so return failure.
219 if (!isset($xml->short_name)) {
220 return NULL;
221 }
222 $data = [];
223 foreach ($xml as $k => $v) {
224 $data[$k] = (string) $v;
225 }
226 $data['releases'] = [];
227 if (isset($xml->releases)) {
228 foreach ($xml->releases->children() as $release) {
229 $version = (string) $release->version;
230 $data['releases'][$version] = [];
231 foreach ($release->children() as $k => $v) {
232 $data['releases'][$version][$k] = (string) $v;
233 }
234 $data['releases'][$version]['terms'] = [];
235 if ($release->terms) {
236 foreach ($release->terms->children() as $term) {
237 if (!isset($data['releases'][$version]['terms'][(string) $term->name])) {
238 $data['releases'][$version]['terms'][(string) $term->name] = [];
239 }
240 $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value;
241 }
242 }
243 }
244 }
245 return $data;
246 }
247
248 /**
249 * {@inheritdoc}
250 */
251 public function numberOfQueueItems() {
252 return $this->fetchQueue->numberOfItems();
253 }
254
255 /**
256 * {@inheritdoc}
257 */
258 public function claimQueueItem() {
259 return $this->fetchQueue->claimItem();
260 }
261
262 /**
263 * {@inheritdoc}
264 */
265 public function deleteQueueItem($item) {
266 return $this->fetchQueue->deleteItem($item);
267 }
268
269 }