Backup of db before drupal security update
[yaffs-website] / web / core / modules / update / update.compare.inc
1 <?php
2
3 /**
4  * @file
5  * Code required only when comparing available updates to existing data.
6  */
7
8 /**
9  * Determines version and type information for currently installed projects.
10  *
11  * Processes the list of projects on the system to figure out the currently
12  * installed versions, and other information that is required before we can
13  * compare against the available releases to produce the status report.
14  *
15  * @param $projects
16  *   Array of project information from
17  *   \Drupal\Update\UpdateManager::getProjects().
18  */
19 function update_process_project_info(&$projects) {
20   foreach ($projects as $key => $project) {
21     // Assume an official release until we see otherwise.
22     $install_type = 'official';
23
24     $info = $project['info'];
25
26     if (isset($info['version'])) {
27       // Check for development snapshots
28       if (preg_match('@(dev|HEAD)@', $info['version'])) {
29         $install_type = 'dev';
30       }
31
32       // Figure out what the currently installed major version is. We need
33       // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
34       // (e.g. "5.1", major = 5) version strings.
35       $matches = [];
36       if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
37         $info['major'] = $matches[2];
38       }
39       elseif (!isset($info['major'])) {
40         // This would only happen for version strings that don't follow the
41         // drupal.org convention. We let contribs define "major" in their
42         // .info.yml in this case, and only if that's missing would we hit this.
43         $info['major'] = -1;
44       }
45     }
46     else {
47       // No version info available at all.
48       $install_type = 'unknown';
49       $info['version'] = t('Unknown');
50       $info['major'] = -1;
51     }
52
53     // Finally, save the results we care about into the $projects array.
54     $projects[$key]['existing_version'] = $info['version'];
55     $projects[$key]['existing_major'] = $info['major'];
56     $projects[$key]['install_type'] = $install_type;
57   }
58 }
59
60 /**
61  * Calculates the current update status of all projects on the site.
62  *
63  * The results of this function are expensive to compute, especially on sites
64  * with lots of modules or themes, since it involves a lot of comparisons and
65  * other operations. Therefore, we store the results. However, since this is not
66  * the data about available updates fetched from the network, it is ok to
67  * invalidate it somewhat quickly. If we keep this data for very long, site
68  * administrators are more likely to see incorrect results if they upgrade to a
69  * newer version of a module or theme but do not visit certain pages that
70  * automatically clear this.
71  *
72  * @param array $available
73  *   Data about available project releases.
74  *
75  * @return
76  *   An array of installed projects with current update status information.
77  *
78  * @see update_get_available()
79  * @see \Drupal\Update\UpdateManager::getProjects()
80  * @see update_process_project_info()
81  * @see \Drupal\update\UpdateManagerInterface::projectStorage()
82  */
83 function update_calculate_project_data($available) {
84   // Retrieve the projects from storage, if present.
85   $projects = \Drupal::service('update.manager')->projectStorage('update_project_data');
86   // If $projects is empty, then the data must be rebuilt.
87   // Otherwise, return the data and skip the rest of the function.
88   if (!empty($projects)) {
89     return $projects;
90   }
91   $projects = \Drupal::service('update.manager')->getProjects();
92   update_process_project_info($projects);
93   foreach ($projects as $project => $project_info) {
94     if (isset($available[$project])) {
95       update_calculate_project_update_status($projects[$project], $available[$project]);
96     }
97     else {
98       $projects[$project]['status'] = UPDATE_UNKNOWN;
99       $projects[$project]['reason'] = t('No available releases found');
100     }
101   }
102   // Give other modules a chance to alter the status (for example, to allow a
103   // contrib module to provide fine-grained settings to ignore specific
104   // projects or releases).
105   \Drupal::moduleHandler()->alter('update_status', $projects);
106
107   // Store the site's update status for at most 1 hour.
108   \Drupal::keyValueExpirable('update')->setWithExpire('update_project_data', $projects, 3600);
109   return $projects;
110 }
111
112 /**
113  * Calculates the current update status of a specific project.
114  *
115  * This function is the heart of the update status feature. For each project it
116  * is invoked with, it first checks if the project has been flagged with a
117  * special status like "unsupported" or "insecure", or if the project node
118  * itself has been unpublished. In any of those cases, the project is marked
119  * with an error and the next project is considered.
120  *
121  * If the project itself is valid, the function decides what major release
122  * series to consider. The project defines what the currently supported major
123  * versions are for each version of core, so the first step is to make sure the
124  * current version is still supported. If so, that's the target version. If the
125  * current version is unsupported, the project maintainer's recommended major
126  * version is used. There's also a check to make sure that this function never
127  * recommends an earlier release than the currently installed major version.
128  *
129  * Given a target major version, the available releases are scanned looking for
130  * the specific release to recommend (avoiding beta releases and development
131  * snapshots if possible). For the target major version, the highest patch level
132  * is found. If there is a release at that patch level with no extra ("beta",
133  * etc.), then the release at that patch level with the most recent release date
134  * is recommended. If every release at that patch level has extra (only betas),
135  * then the latest release from the previous patch level is recommended. For
136  * example:
137  *
138  * - 1.6-bugfix <-- recommended version because 1.6 already exists.
139  * - 1.6
140  *
141  * or
142  *
143  * - 1.6-beta
144  * - 1.5 <-- recommended version because no 1.6 exists.
145  * - 1.4
146  *
147  * Also, the latest release from the same major version is looked for, even beta
148  * releases, to display to the user as the "Latest version" option.
149  * Additionally, the latest official release from any higher major versions that
150  * have been released is searched for to provide a set of "Also available"
151  * options.
152  *
153  * Finally, and most importantly, the release history continues to be scanned
154  * until the currently installed release is reached, searching for anything
155  * marked as a security update. If any security updates have been found between
156  * the recommended release and the installed version, all of the releases that
157  * included a security fix are recorded so that the site administrator can be
158  * warned their site is insecure, and links pointing to the release notes for
159  * each security update can be included (which, in turn, will link to the
160  * official security announcements for each vulnerability).
161  *
162  * This function relies on the fact that the .xml release history data comes
163  * sorted based on major version and patch level, then finally by release date
164  * if there are multiple releases such as betas from the same major.patch
165  * version (e.g., 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development
166  * snapshots for a given major version are always listed last.
167  *
168  * @param $project_data
169  *   An array containing information about a specific project.
170  * @param $available
171  *   Data about available project releases of a specific project.
172  */
173 function update_calculate_project_update_status(&$project_data, $available) {
174   foreach (['title', 'link'] as $attribute) {
175     if (!isset($project_data[$attribute]) && isset($available[$attribute])) {
176       $project_data[$attribute] = $available[$attribute];
177     }
178   }
179
180   // If the project status is marked as something bad, there's nothing else
181   // to consider.
182   if (isset($available['project_status'])) {
183     switch ($available['project_status']) {
184       case 'insecure':
185         $project_data['status'] = UPDATE_NOT_SECURE;
186         if (empty($project_data['extra'])) {
187           $project_data['extra'] = [];
188         }
189         $project_data['extra'][] = [
190           'label' => t('Project not secure'),
191           'data' => t('This project has been labeled insecure by the Drupal security team, and is no longer available for download. Immediately disabling everything included by this project is strongly recommended!'),
192         ];
193         break;
194       case 'unpublished':
195       case 'revoked':
196         $project_data['status'] = UPDATE_REVOKED;
197         if (empty($project_data['extra'])) {
198           $project_data['extra'] = [];
199         }
200         $project_data['extra'][] = [
201           'label' => t('Project revoked'),
202           'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
203         ];
204         break;
205       case 'unsupported':
206         $project_data['status'] = UPDATE_NOT_SUPPORTED;
207         if (empty($project_data['extra'])) {
208           $project_data['extra'] = [];
209         }
210         $project_data['extra'][] = [
211           'label' => t('Project not supported'),
212           'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
213         ];
214         break;
215       case 'not-fetched':
216         $project_data['status'] = UPDATE_NOT_FETCHED;
217         $project_data['reason'] = t('Failed to get available update data.');
218         break;
219
220       default:
221         // Assume anything else (e.g. 'published') is valid and we should
222         // perform the rest of the logic in this function.
223         break;
224     }
225   }
226
227   if (!empty($project_data['status'])) {
228     // We already know the status for this project, so there's nothing else to
229     // compute. Record the project status into $project_data and we're done.
230     $project_data['project_status'] = $available['project_status'];
231     return;
232   }
233
234   // Figure out the target major version.
235   $existing_major = $project_data['existing_major'];
236   $supported_majors = [];
237   if (isset($available['supported_majors'])) {
238     $supported_majors = explode(',', $available['supported_majors']);
239   }
240   elseif (isset($available['default_major'])) {
241     // Older release history XML file without supported or recommended.
242     $supported_majors[] = $available['default_major'];
243   }
244
245   if (in_array($existing_major, $supported_majors)) {
246     // Still supported, stay at the current major version.
247     $target_major = $existing_major;
248   }
249   elseif (isset($available['recommended_major'])) {
250     // Since 'recommended_major' is defined, we know this is the new XML
251     // format. Therefore, we know the current release is unsupported since
252     // its major version was not in the 'supported_majors' list. We should
253     // find the best release from the recommended major version.
254     $target_major = $available['recommended_major'];
255     $project_data['status'] = UPDATE_NOT_SUPPORTED;
256   }
257   elseif (isset($available['default_major'])) {
258     // Older release history XML file without recommended, so recommend
259     // the currently defined "default_major" version.
260     $target_major = $available['default_major'];
261   }
262   else {
263     // Malformed XML file? Stick with the current version.
264     $target_major = $existing_major;
265   }
266
267   // Make sure we never tell the admin to downgrade. If we recommended an
268   // earlier version than the one they're running, they'd face an
269   // impossible data migration problem, since Drupal never supports a DB
270   // downgrade path. In the unfortunate case that what they're running is
271   // unsupported, and there's nothing newer for them to upgrade to, we
272   // can't print out a "Recommended version", but just have to tell them
273   // what they have is unsupported and let them figure it out.
274   $target_major = max($existing_major, $target_major);
275
276   $release_patch_changed = '';
277   $patch = '';
278
279   // If the project is marked as UPDATE_FETCH_PENDING, it means that the
280   // data we currently have (if any) is stale, and we've got a task queued
281   // up to (re)fetch the data. In that case, we mark it as such, merge in
282   // whatever data we have (e.g. project title and link), and move on.
283   if (!empty($available['fetch_status']) && $available['fetch_status'] == UPDATE_FETCH_PENDING) {
284     $project_data['status'] = UPDATE_FETCH_PENDING;
285     $project_data['reason'] = t('No available update data');
286     $project_data['fetch_status'] = $available['fetch_status'];
287     return;
288   }
289
290   // Defend ourselves from XML history files that contain no releases.
291   if (empty($available['releases'])) {
292     $project_data['status'] = UPDATE_UNKNOWN;
293     $project_data['reason'] = t('No available releases found');
294     return;
295   }
296   foreach ($available['releases'] as $version => $release) {
297     // First, if this is the existing release, check a few conditions.
298     if ($project_data['existing_version'] === $version) {
299       if (isset($release['terms']['Release type']) &&
300           in_array('Insecure', $release['terms']['Release type'])) {
301         $project_data['status'] = UPDATE_NOT_SECURE;
302       }
303       elseif ($release['status'] == 'unpublished') {
304         $project_data['status'] = UPDATE_REVOKED;
305         if (empty($project_data['extra'])) {
306           $project_data['extra'] = [];
307         }
308         $project_data['extra'][] = [
309           'class' => ['release-revoked'],
310           'label' => t('Release revoked'),
311           'data' => t('Your currently installed release has been revoked, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
312         ];
313       }
314       elseif (isset($release['terms']['Release type']) &&
315               in_array('Unsupported', $release['terms']['Release type'])) {
316         $project_data['status'] = UPDATE_NOT_SUPPORTED;
317         if (empty($project_data['extra'])) {
318           $project_data['extra'] = [];
319         }
320         $project_data['extra'][] = [
321           'class' => ['release-not-supported'],
322           'label' => t('Release not supported'),
323           'data' => t('Your currently installed release is now unsupported, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
324         ];
325       }
326     }
327
328     // Otherwise, ignore unpublished, insecure, or unsupported releases.
329     if ($release['status'] == 'unpublished' ||
330         (isset($release['terms']['Release type']) &&
331          (in_array('Insecure', $release['terms']['Release type']) ||
332           in_array('Unsupported', $release['terms']['Release type'])))) {
333       continue;
334     }
335
336     // See if this is a higher major version than our target and yet still
337     // supported. If so, record it as an "Also available" release.
338     // Note: Some projects have a HEAD release from CVS days, which could
339     // be one of those being compared. They would not have version_major
340     // set, so we must call isset first.
341     if (isset($release['version_major']) && $release['version_major'] > $target_major) {
342       if (in_array($release['version_major'], $supported_majors)) {
343         if (!isset($project_data['also'])) {
344           $project_data['also'] = [];
345         }
346         if (!isset($project_data['also'][$release['version_major']])) {
347           $project_data['also'][$release['version_major']] = $version;
348           $project_data['releases'][$version] = $release;
349         }
350       }
351       // Otherwise, this release can't matter to us, since it's neither
352       // from the release series we're currently using nor the recommended
353       // release. We don't even care about security updates for this
354       // branch, since if a project maintainer puts out a security release
355       // at a higher major version and not at the lower major version,
356       // they must remove the lower version from the supported major
357       // versions at the same time, in which case we won't hit this code.
358       continue;
359     }
360
361     // Look for the 'latest version' if we haven't found it yet. Latest is
362     // defined as the most recent version for the target major version.
363     if (!isset($project_data['latest_version'])
364         && $release['version_major'] == $target_major) {
365       $project_data['latest_version'] = $version;
366       $project_data['releases'][$version] = $release;
367     }
368
369     // Look for the development snapshot release for this branch.
370     if (!isset($project_data['dev_version'])
371         && $release['version_major'] == $target_major
372         && isset($release['version_extra'])
373         && $release['version_extra'] == 'dev') {
374       $project_data['dev_version'] = $version;
375       $project_data['releases'][$version] = $release;
376     }
377
378     // Look for the 'recommended' version if we haven't found it yet (see
379     // phpdoc at the top of this function for the definition).
380     if (!isset($project_data['recommended'])
381         && $release['version_major'] == $target_major
382         && isset($release['version_patch'])) {
383       if ($patch != $release['version_patch']) {
384         $patch = $release['version_patch'];
385         $release_patch_changed = $release;
386       }
387       if (empty($release['version_extra']) && $patch == $release['version_patch']) {
388         $project_data['recommended'] = $release_patch_changed['version'];
389         $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed;
390       }
391     }
392
393     // Stop searching once we hit the currently installed version.
394     if ($project_data['existing_version'] === $version) {
395       break;
396     }
397
398     // If we're running a dev snapshot and have a timestamp, stop
399     // searching for security updates once we hit an official release
400     // older than what we've got. Allow 100 seconds of leeway to handle
401     // differences between the datestamp in the .info.yml file and the
402     // timestamp of the tarball itself (which are usually off by 1 or 2
403     // seconds) so that we don't flag that as a new release.
404     if ($project_data['install_type'] == 'dev') {
405       if (empty($project_data['datestamp'])) {
406         // We don't have current timestamp info, so we can't know.
407         continue;
408       }
409       elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) {
410         // We're newer than this, so we can skip it.
411         continue;
412       }
413     }
414
415     // See if this release is a security update.
416     if (isset($release['terms']['Release type'])
417         && in_array('Security update', $release['terms']['Release type'])) {
418       $project_data['security updates'][] = $release;
419     }
420   }
421
422   // If we were unable to find a recommended version, then make the latest
423   // version the recommended version if possible.
424   if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) {
425     $project_data['recommended'] = $project_data['latest_version'];
426   }
427
428   //
429   // Check to see if we need an update or not.
430   //
431
432   if (!empty($project_data['security updates'])) {
433     // If we found security updates, that always trumps any other status.
434     $project_data['status'] = UPDATE_NOT_SECURE;
435   }
436
437   if (isset($project_data['status'])) {
438     // If we already know the status, we're done.
439     return;
440   }
441
442   // If we don't know what to recommend, there's nothing we can report.
443   // Bail out early.
444   if (!isset($project_data['recommended'])) {
445     $project_data['status'] = UPDATE_UNKNOWN;
446     $project_data['reason'] = t('No available releases found');
447     return;
448   }
449
450   // If we're running a dev snapshot, compare the date of the dev snapshot
451   // with the latest official version, and record the absolute latest in
452   // 'latest_dev' so we can correctly decide if there's a newer release
453   // than our current snapshot.
454   if ($project_data['install_type'] == 'dev') {
455     if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) {
456       $project_data['latest_dev'] = $project_data['dev_version'];
457     }
458     else {
459       $project_data['latest_dev'] = $project_data['latest_version'];
460     }
461   }
462
463   // Figure out the status, based on what we've seen and the install type.
464   switch ($project_data['install_type']) {
465     case 'official':
466       if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) {
467         $project_data['status'] = UPDATE_CURRENT;
468       }
469       else {
470         $project_data['status'] = UPDATE_NOT_CURRENT;
471       }
472       break;
473
474     case 'dev':
475       $latest = $available['releases'][$project_data['latest_dev']];
476       if (empty($project_data['datestamp'])) {
477         $project_data['status'] = UPDATE_NOT_CHECKED;
478         $project_data['reason'] = t('Unknown release date');
479       }
480       elseif (($project_data['datestamp'] + 100 > $latest['date'])) {
481         $project_data['status'] = UPDATE_CURRENT;
482       }
483       else {
484         $project_data['status'] = UPDATE_NOT_CURRENT;
485       }
486       break;
487
488     default:
489       $project_data['status'] = UPDATE_UNKNOWN;
490       $project_data['reason'] = t('Invalid info');
491   }
492 }