Backup of db before drupal security update
[yaffs-website] / web / core / modules / update / src / UpdateProcessor.php
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 }