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