update.compare.inc 20.7 KB
Newer Older
1 2 3 4 5 6 7 8
<?php

/**
 * @file
 * Code required only when comparing available updates to existing data.
 */

/**
9 10 11
 * Determines version and type information for currently installed projects.
 *
 * Processes the list of projects on the system to figure out the currently
12 13 14 15
 * installed versions, and other information that is required before we can
 * compare against the available releases to produce the status report.
 *
 * @param $projects
16 17
 *   Array of project information from
 *   \Drupal\Update\UpdateManager::getProjects().
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
 */
function update_process_project_info(&$projects) {
  foreach ($projects as $key => $project) {
    // Assume an official release until we see otherwise.
    $install_type = 'official';

    $info = $project['info'];

    if (isset($info['version'])) {
      // Check for development snapshots
      if (preg_match('@(dev|HEAD)@', $info['version'])) {
        $install_type = 'dev';
      }

      // Figure out what the currently installed major version is. We need
      // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
      // (e.g. "5.1", major = 5) version strings.
      $matches = array();
      if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
        $info['major'] = $matches[2];
      }
      elseif (!isset($info['major'])) {
        // This would only happen for version strings that don't follow the
        // 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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
        $info['major'] = -1;
      }
    }
    else {
      // No version info available at all.
      $install_type = 'unknown';
      $info['version'] = t('Unknown');
      $info['major'] = -1;
    }

    // Finally, save the results we care about into the $projects array.
    $projects[$key]['existing_version'] = $info['version'];
    $projects[$key]['existing_major'] = $info['major'];
    $projects[$key]['install_type'] = $install_type;
  }
}

/**
61
 * Calculates the current update status of all projects on the site.
62
 *
63 64
 * The results of this function are expensive to compute, especially on sites
 * 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 67
 * the data about available updates fetched from the network, it is ok to
 * invalidate it somewhat quickly. If we keep this data for very long, site
68 69
 * administrators are more likely to see incorrect results if they upgrade to a
 * newer version of a module or theme but do not visit certain pages that
70
 * automatically clear this.
71 72 73 74
 *
 * @param array $available
 *   Data about available project releases.
 *
75 76 77
 * @return
 *   An array of installed projects with current update status information.
 *
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 87
  // If $projects is empty, then the data must be rebuilt.
  // Otherwise, return the data and skip the rest of the function.
88 89 90
  if (!empty($projects)) {
    return $projects;
  }
91
  $projects = \Drupal::service('update.manager')->getProjects();
92 93 94
  update_process_project_info($projects);
  foreach ($projects as $project => $project_info) {
    if (isset($available[$project])) {
95
      update_calculate_project_update_status($projects[$project], $available[$project]);
96 97 98 99 100 101 102 103 104
    }
    else {
      $projects[$project]['status'] = UPDATE_UNKNOWN;
      $projects[$project]['reason'] = t('No available releases found');
    }
  }
  // Give other modules a chance to alter the status (for example, to allow a
  // contrib module to provide fine-grained settings to ignore specific
  // 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 110 111 112
  return $projects;
}

/**
113
 * Calculates the current update status of a specific project.
114
 *
115 116
 * This function is the heart of the update status feature. For each project it
 * is invoked with, it first checks if the project has been flagged with a
117 118 119
 * special status like "unsupported" or "insecure", or if the project node
 * itself has been unpublished. In any of those cases, the project is marked
 * with an error and the next project is considered.
120 121 122
 *
 * If the project itself is valid, the function decides what major release
 * series to consider. The project defines what the currently supported major
123 124 125 126 127 128 129
 * versions are for each version of core, so the first step is to make sure the
 * current version is still supported. If so, that's the target version. If the
 * current version is unsupported, the project maintainer's recommended major
 * version is used. There's also a check to make sure that this function never
 * recommends an earlier release than the currently installed major version.
 *
 * Given a target major version, the available releases are scanned looking for
130
 * the specific release to recommend (avoiding beta releases and development
131 132 133 134 135 136
 * snapshots if possible). For the target major version, the highest patch level
 * is found. If there is a release at that patch level with no extra ("beta",
 * etc.), then the release at that patch level with the most recent release date
 * is recommended. If every release at that patch level has extra (only betas),
 * then the latest release from the previous patch level is recommended. For
 * example:
137
 *
138 139
 * - 1.6-bugfix <-- recommended version because 1.6 already exists.
 * - 1.6
140 141 142
 *
 * or
 *
143 144 145
 * - 1.6-beta
 * - 1.5 <-- recommended version because no 1.6 exists.
 * - 1.4
146
 *
147 148 149 150
 * Also, the latest release from the same major version is looked for, even beta
 * releases, to display to the user as the "Latest version" option.
 * Additionally, the latest official release from any higher major versions that
 * have been released is searched for to provide a set of "Also available"
151 152
 * options.
 *
153 154 155 156
 * Finally, and most importantly, the release history continues to be scanned
 * until the currently installed release is reached, searching for anything
 * marked as a security update. If any security updates have been found between
 * the recommended release and the installed version, all of the releases that
157 158 159 160 161 162 163 164
 * included a security fix are recorded so that the site administrator can be
 * warned their site is insecure, and links pointing to the release notes for
 * each security update can be included (which, in turn, will link to the
 * official security announcements for each vulnerability).
 *
 * This function relies on the fact that the .xml release history data comes
 * sorted based on major version and patch level, then finally by release date
 * 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 167
 * snapshots for a given major version are always listed last.
 *
168 169 170 171
 * @param $project_data
 *   An array containing information about a specific project.
 * @param $available
 *   Data about available project releases of a specific project.
172
 */
173
function update_calculate_project_update_status(&$project_data, $available) {
174 175 176 177
  foreach (array('title', 'link') as $attribute) {
    if (!isset($project_data[$attribute]) && isset($available[$attribute])) {
      $project_data[$attribute] = $available[$attribute];
    }
178
  }
179

180 181 182 183 184 185 186 187
  // If the project status is marked as something bad, there's nothing else
  // to consider.
  if (isset($available['project_status'])) {
    switch ($available['project_status']) {
      case 'insecure':
        $project_data['status'] = UPDATE_NOT_SECURE;
        if (empty($project_data['extra'])) {
          $project_data['extra'] = array();
188
        }
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
        $project_data['extra'][] = array(
          'label' => t('Project not secure'),
          '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!'),
        );
        break;
      case 'unpublished':
      case 'revoked':
        $project_data['status'] = UPDATE_REVOKED;
        if (empty($project_data['extra'])) {
          $project_data['extra'] = array();
        }
        $project_data['extra'][] = array(
          'label' => t('Project revoked'),
          'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
        );
        break;
      case 'unsupported':
        $project_data['status'] = UPDATE_NOT_SUPPORTED;
        if (empty($project_data['extra'])) {
          $project_data['extra'] = array();
        }
        $project_data['extra'][] = array(
          'label' => t('Project not supported'),
          'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
        );
        break;
      case 'not-fetched':
        $project_data['status'] = UPDATE_NOT_FETCHED;
        $project_data['reason'] = t('Failed to get available update data.');
        break;

      default:
        // Assume anything else (e.g. 'published') is valid and we should
        // perform the rest of the logic in this function.
        break;
    }
  }
226

227 228 229 230 231 232
  if (!empty($project_data['status'])) {
    // We already know the status for this project, so there's nothing else to
    // compute. Record the project status into $project_data and we're done.
    $project_data['project_status'] = $available['project_status'];
    return;
  }
233

234 235 236 237 238 239 240 241 242 243
  // Figure out the target major version.
  $existing_major = $project_data['existing_major'];
  $supported_majors = array();
  if (isset($available['supported_majors'])) {
    $supported_majors = explode(',', $available['supported_majors']);
  }
  elseif (isset($available['default_major'])) {
    // Older release history XML file without supported or recommended.
    $supported_majors[] = $available['default_major'];
  }
244

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
  if (in_array($existing_major, $supported_majors)) {
    // Still supported, stay at the current major version.
    $target_major = $existing_major;
  }
  elseif (isset($available['recommended_major'])) {
    // Since 'recommended_major' is defined, we know this is the new XML
    // format. Therefore, we know the current release is unsupported since
    // its major version was not in the 'supported_majors' list. We should
    // find the best release from the recommended major version.
    $target_major = $available['recommended_major'];
    $project_data['status'] = UPDATE_NOT_SUPPORTED;
  }
  elseif (isset($available['default_major'])) {
    // Older release history XML file without recommended, so recommend
    // the currently defined "default_major" version.
    $target_major = $available['default_major'];
  }
  else {
    // Malformed XML file? Stick with the current version.
    $target_major = $existing_major;
  }
266

267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
  // Make sure we never tell the admin to downgrade. If we recommended an
  // earlier version than the one they're running, they'd face an
  // impossible data migration problem, since Drupal never supports a DB
  // downgrade path. In the unfortunate case that what they're running is
  // unsupported, and there's nothing newer for them to upgrade to, we
  // can't print out a "Recommended version", but just have to tell them
  // what they have is unsupported and let them figure it out.
  $target_major = max($existing_major, $target_major);

  $release_patch_changed = '';
  $patch = '';

  // If the project is marked as UPDATE_FETCH_PENDING, it means that the
  // data we currently have (if any) is stale, and we've got a task queued
  // up to (re)fetch the data. In that case, we mark it as such, merge in
  // whatever data we have (e.g. project title and link), and move on.
  if (!empty($available['fetch_status']) && $available['fetch_status'] == UPDATE_FETCH_PENDING) {
    $project_data['status'] = UPDATE_FETCH_PENDING;
    $project_data['reason'] = t('No available update data');
    $project_data['fetch_status'] = $available['fetch_status'];
    return;
  }
289

290 291 292 293 294 295 296 297 298 299 300 301
  // Defend ourselves from XML history files that contain no releases.
  if (empty($available['releases'])) {
    $project_data['status'] = UPDATE_UNKNOWN;
    $project_data['reason'] = t('No available releases found');
    return;
  }
  foreach ($available['releases'] as $version => $release) {
    // First, if this is the existing release, check a few conditions.
    if ($project_data['existing_version'] === $version) {
      if (isset($release['terms']['Release type']) &&
          in_array('Insecure', $release['terms']['Release type'])) {
        $project_data['status'] = UPDATE_NOT_SECURE;
302
      }
303 304 305 306
      elseif ($release['status'] == 'unpublished') {
        $project_data['status'] = UPDATE_REVOKED;
        if (empty($project_data['extra'])) {
          $project_data['extra'] = array();
307
        }
308 309 310 311 312 313 314 315 316 317 318
        $project_data['extra'][] = array(
          'class' => array('release-revoked'),
          'label' => t('Release revoked'),
          '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!'),
        );
      }
      elseif (isset($release['terms']['Release type']) &&
              in_array('Unsupported', $release['terms']['Release type'])) {
        $project_data['status'] = UPDATE_NOT_SUPPORTED;
        if (empty($project_data['extra'])) {
          $project_data['extra'] = array();
319
        }
320 321 322 323 324 325 326
        $project_data['extra'][] = array(
          'class' => array('release-not-supported'),
          'label' => t('Release not supported'),
          '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!'),
        );
      }
    }
327

328 329 330 331 332 333 334
    // Otherwise, ignore unpublished, insecure, or unsupported releases.
    if ($release['status'] == 'unpublished' ||
        (isset($release['terms']['Release type']) &&
         (in_array('Insecure', $release['terms']['Release type']) ||
          in_array('Unsupported', $release['terms']['Release type'])))) {
      continue;
    }
335

336 337
    // See if this is a higher major version than our target and yet still
    // supported. If so, record it as an "Also available" release.
338 339 340 341
    // Note: Some projects have a HEAD release from CVS days, which could
    // be one of those being compared. They would not have version_major
    // set, so we must call isset first.
    if (isset($release['version_major']) && $release['version_major'] > $target_major) {
342 343 344
      if (in_array($release['version_major'], $supported_majors)) {
        if (!isset($project_data['also'])) {
          $project_data['also'] = array();
345
        }
346 347 348
        if (!isset($project_data['also'][$release['version_major']])) {
          $project_data['also'][$release['version_major']] = $version;
          $project_data['releases'][$version] = $release;
349
        }
350 351 352 353 354 355 356 357 358 359
      }
      // Otherwise, this release can't matter to us, since it's neither
      // from the release series we're currently using nor the recommended
      // release. We don't even care about security updates for this
      // branch, since if a project maintainer puts out a security release
      // at a higher major version and not at the lower major version,
      // they must remove the lower version from the supported major
      // versions at the same time, in which case we won't hit this code.
      continue;
    }
360

361 362 363 364 365 366 367
    // Look for the 'latest version' if we haven't found it yet. Latest is
    // defined as the most recent version for the target major version.
    if (!isset($project_data['latest_version'])
        && $release['version_major'] == $target_major) {
      $project_data['latest_version'] = $version;
      $project_data['releases'][$version] = $release;
    }
368

369 370 371 372 373 374 375 376
    // Look for the development snapshot release for this branch.
    if (!isset($project_data['dev_version'])
        && $release['version_major'] == $target_major
        && isset($release['version_extra'])
        && $release['version_extra'] == 'dev') {
      $project_data['dev_version'] = $version;
      $project_data['releases'][$version] = $release;
    }
377

378 379 380 381 382 383 384 385
    // Look for the 'recommended' version if we haven't found it yet (see
    // phpdoc at the top of this function for the definition).
    if (!isset($project_data['recommended'])
        && $release['version_major'] == $target_major
        && isset($release['version_patch'])) {
      if ($patch != $release['version_patch']) {
        $patch = $release['version_patch'];
        $release_patch_changed = $release;
386
      }
387 388 389
      if (empty($release['version_extra']) && $patch == $release['version_patch']) {
        $project_data['recommended'] = $release_patch_changed['version'];
        $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed;
390
      }
391
    }
392

393 394 395 396
    // Stop searching once we hit the currently installed version.
    if ($project_data['existing_version'] === $version) {
      break;
    }
397

398 399 400
    // If we're running a dev snapshot and have a timestamp, stop
    // searching for security updates once we hit an official release
    // 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 403 404 405 406
    // timestamp of the tarball itself (which are usually off by 1 or 2
    // seconds) so that we don't flag that as a new release.
    if ($project_data['install_type'] == 'dev') {
      if (empty($project_data['datestamp'])) {
        // We don't have current timestamp info, so we can't know.
407 408
        continue;
      }
409 410
      elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) {
        // We're newer than this, so we can skip it.
411 412
        continue;
      }
413
    }
414

415 416 417 418 419 420
    // See if this release is a security update.
    if (isset($release['terms']['Release type'])
        && in_array('Security update', $release['terms']['Release type'])) {
      $project_data['security updates'][] = $release;
    }
  }
421

422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
  // If we were unable to find a recommended version, then make the latest
  // version the recommended version if possible.
  if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) {
    $project_data['recommended'] = $project_data['latest_version'];
  }

  //
  // Check to see if we need an update or not.
  //

  if (!empty($project_data['security updates'])) {
    // If we found security updates, that always trumps any other status.
    $project_data['status'] = UPDATE_NOT_SECURE;
  }

  if (isset($project_data['status'])) {
    // If we already know the status, we're done.
    return;
  }

  // If we don't know what to recommend, there's nothing we can report.
  // Bail out early.
  if (!isset($project_data['recommended'])) {
    $project_data['status'] = UPDATE_UNKNOWN;
    $project_data['reason'] = t('No available releases found');
    return;
  }

  // If we're running a dev snapshot, compare the date of the dev snapshot
  // with the latest official version, and record the absolute latest in
  // 'latest_dev' so we can correctly decide if there's a newer release
  // than our current snapshot.
  if ($project_data['install_type'] == 'dev') {
    if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) {
      $project_data['latest_dev'] = $project_data['dev_version'];
457 458
    }
    else {
459
      $project_data['latest_dev'] = $project_data['latest_version'];
460 461
    }
  }
462

463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
  // Figure out the status, based on what we've seen and the install type.
  switch ($project_data['install_type']) {
    case 'official':
      if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) {
        $project_data['status'] = UPDATE_CURRENT;
      }
      else {
        $project_data['status'] = UPDATE_NOT_CURRENT;
      }
      break;

    case 'dev':
      $latest = $available['releases'][$project_data['latest_dev']];
      if (empty($project_data['datestamp'])) {
        $project_data['status'] = UPDATE_NOT_CHECKED;
        $project_data['reason'] = t('Unknown release date');
      }
      elseif (($project_data['datestamp'] + 100 > $latest['date'])) {
        $project_data['status'] = UPDATE_CURRENT;
      }
      else {
        $project_data['status'] = UPDATE_NOT_CURRENT;
      }
      break;

    default:
      $project_data['status'] = UPDATE_UNKNOWN;
      $project_data['reason'] = t('Invalid info');
  }
492
}