Chris@0: $project) { Chris@0: // Assume an official release until we see otherwise. Chris@0: $install_type = 'official'; Chris@0: Chris@0: $info = $project['info']; Chris@0: Chris@0: if (isset($info['version'])) { Chris@0: // Check for development snapshots Chris@0: if (preg_match('@(dev|HEAD)@', $info['version'])) { Chris@0: $install_type = 'dev'; Chris@0: } Chris@0: Chris@0: // Figure out what the currently installed major version is. We need Chris@0: // to handle both contribution (e.g. "5.x-1.3", major = 1) and core Chris@0: // (e.g. "5.1", major = 5) version strings. Chris@0: $matches = []; Chris@0: if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) { Chris@0: $info['major'] = $matches[2]; Chris@0: } Chris@0: elseif (!isset($info['major'])) { Chris@0: // This would only happen for version strings that don't follow the Chris@0: // drupal.org convention. We let contribs define "major" in their Chris@0: // .info.yml in this case, and only if that's missing would we hit this. Chris@0: $info['major'] = -1; Chris@0: } Chris@0: } Chris@0: else { Chris@0: // No version info available at all. Chris@0: $install_type = 'unknown'; Chris@0: $info['version'] = t('Unknown'); Chris@0: $info['major'] = -1; Chris@0: } Chris@0: Chris@0: // Finally, save the results we care about into the $projects array. Chris@0: $projects[$key]['existing_version'] = $info['version']; Chris@0: $projects[$key]['existing_major'] = $info['major']; Chris@0: $projects[$key]['install_type'] = $install_type; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Calculates the current update status of all projects on the site. Chris@0: * Chris@0: * The results of this function are expensive to compute, especially on sites Chris@0: * with lots of modules or themes, since it involves a lot of comparisons and Chris@0: * other operations. Therefore, we store the results. However, since this is not Chris@0: * the data about available updates fetched from the network, it is ok to Chris@0: * invalidate it somewhat quickly. If we keep this data for very long, site Chris@0: * administrators are more likely to see incorrect results if they upgrade to a Chris@0: * newer version of a module or theme but do not visit certain pages that Chris@0: * automatically clear this. Chris@0: * Chris@0: * @param array $available Chris@0: * Data about available project releases. Chris@0: * Chris@0: * @return Chris@0: * An array of installed projects with current update status information. Chris@0: * Chris@0: * @see update_get_available() Chris@0: * @see \Drupal\Update\UpdateManager::getProjects() Chris@0: * @see update_process_project_info() Chris@0: * @see \Drupal\update\UpdateManagerInterface::projectStorage() Chris@0: */ Chris@0: function update_calculate_project_data($available) { Chris@0: // Retrieve the projects from storage, if present. Chris@0: $projects = \Drupal::service('update.manager')->projectStorage('update_project_data'); Chris@0: // If $projects is empty, then the data must be rebuilt. Chris@0: // Otherwise, return the data and skip the rest of the function. Chris@0: if (!empty($projects)) { Chris@0: return $projects; Chris@0: } Chris@0: $projects = \Drupal::service('update.manager')->getProjects(); Chris@0: update_process_project_info($projects); Chris@0: foreach ($projects as $project => $project_info) { Chris@0: if (isset($available[$project])) { Chris@0: update_calculate_project_update_status($projects[$project], $available[$project]); Chris@0: } Chris@0: else { Chris@0: $projects[$project]['status'] = UPDATE_UNKNOWN; Chris@0: $projects[$project]['reason'] = t('No available releases found'); Chris@0: } Chris@0: } Chris@0: // Give other modules a chance to alter the status (for example, to allow a Chris@0: // contrib module to provide fine-grained settings to ignore specific Chris@0: // projects or releases). Chris@0: \Drupal::moduleHandler()->alter('update_status', $projects); Chris@0: Chris@0: // Store the site's update status for at most 1 hour. Chris@0: \Drupal::keyValueExpirable('update')->setWithExpire('update_project_data', $projects, 3600); Chris@0: return $projects; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Calculates the current update status of a specific project. Chris@0: * Chris@0: * This function is the heart of the update status feature. For each project it Chris@0: * is invoked with, it first checks if the project has been flagged with a Chris@0: * special status like "unsupported" or "insecure", or if the project node Chris@0: * itself has been unpublished. In any of those cases, the project is marked Chris@0: * with an error and the next project is considered. Chris@0: * Chris@0: * If the project itself is valid, the function decides what major release Chris@0: * series to consider. The project defines what the currently supported major Chris@0: * versions are for each version of core, so the first step is to make sure the Chris@0: * current version is still supported. If so, that's the target version. If the Chris@0: * current version is unsupported, the project maintainer's recommended major Chris@0: * version is used. There's also a check to make sure that this function never Chris@0: * recommends an earlier release than the currently installed major version. Chris@0: * Chris@0: * Given a target major version, the available releases are scanned looking for Chris@0: * the specific release to recommend (avoiding beta releases and development Chris@0: * snapshots if possible). For the target major version, the highest patch level Chris@0: * is found. If there is a release at that patch level with no extra ("beta", Chris@0: * etc.), then the release at that patch level with the most recent release date Chris@0: * is recommended. If every release at that patch level has extra (only betas), Chris@0: * then the latest release from the previous patch level is recommended. For Chris@0: * example: Chris@0: * Chris@0: * - 1.6-bugfix <-- recommended version because 1.6 already exists. Chris@0: * - 1.6 Chris@0: * Chris@0: * or Chris@0: * Chris@0: * - 1.6-beta Chris@0: * - 1.5 <-- recommended version because no 1.6 exists. Chris@0: * - 1.4 Chris@0: * Chris@0: * Also, the latest release from the same major version is looked for, even beta Chris@0: * releases, to display to the user as the "Latest version" option. Chris@0: * Additionally, the latest official release from any higher major versions that Chris@0: * have been released is searched for to provide a set of "Also available" Chris@0: * options. Chris@0: * Chris@0: * Finally, and most importantly, the release history continues to be scanned Chris@0: * until the currently installed release is reached, searching for anything Chris@0: * marked as a security update. If any security updates have been found between Chris@0: * the recommended release and the installed version, all of the releases that Chris@0: * included a security fix are recorded so that the site administrator can be Chris@0: * warned their site is insecure, and links pointing to the release notes for Chris@0: * each security update can be included (which, in turn, will link to the Chris@0: * official security announcements for each vulnerability). Chris@0: * Chris@0: * This function relies on the fact that the .xml release history data comes Chris@0: * sorted based on major version and patch level, then finally by release date Chris@0: * if there are multiple releases such as betas from the same major.patch Chris@0: * version (e.g., 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development Chris@0: * snapshots for a given major version are always listed last. Chris@0: * Chris@0: * @param $project_data Chris@0: * An array containing information about a specific project. Chris@0: * @param $available Chris@0: * Data about available project releases of a specific project. Chris@0: */ Chris@0: function update_calculate_project_update_status(&$project_data, $available) { Chris@0: foreach (['title', 'link'] as $attribute) { Chris@0: if (!isset($project_data[$attribute]) && isset($available[$attribute])) { Chris@0: $project_data[$attribute] = $available[$attribute]; Chris@0: } Chris@0: } Chris@0: Chris@0: // If the project status is marked as something bad, there's nothing else Chris@0: // to consider. Chris@0: if (isset($available['project_status'])) { Chris@0: switch ($available['project_status']) { Chris@0: case 'insecure': Chris@0: $project_data['status'] = UPDATE_NOT_SECURE; Chris@0: if (empty($project_data['extra'])) { Chris@0: $project_data['extra'] = []; Chris@0: } Chris@0: $project_data['extra'][] = [ Chris@0: 'label' => t('Project not secure'), Chris@0: '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!'), Chris@0: ]; Chris@0: break; Chris@0: case 'unpublished': Chris@0: case 'revoked': Chris@0: $project_data['status'] = UPDATE_REVOKED; Chris@0: if (empty($project_data['extra'])) { Chris@0: $project_data['extra'] = []; Chris@0: } Chris@0: $project_data['extra'][] = [ Chris@0: 'label' => t('Project revoked'), Chris@0: 'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'), Chris@0: ]; Chris@0: break; Chris@0: case 'unsupported': Chris@0: $project_data['status'] = UPDATE_NOT_SUPPORTED; Chris@0: if (empty($project_data['extra'])) { Chris@0: $project_data['extra'] = []; Chris@0: } Chris@0: $project_data['extra'][] = [ Chris@0: 'label' => t('Project not supported'), Chris@0: 'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'), Chris@0: ]; Chris@0: break; Chris@0: case 'not-fetched': Chris@0: $project_data['status'] = UPDATE_NOT_FETCHED; Chris@0: $project_data['reason'] = t('Failed to get available update data.'); Chris@0: break; Chris@0: Chris@0: default: Chris@0: // Assume anything else (e.g. 'published') is valid and we should Chris@0: // perform the rest of the logic in this function. Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if (!empty($project_data['status'])) { Chris@0: // We already know the status for this project, so there's nothing else to Chris@0: // compute. Record the project status into $project_data and we're done. Chris@0: $project_data['project_status'] = $available['project_status']; Chris@0: return; Chris@0: } Chris@0: Chris@0: // Figure out the target major version. Chris@0: $existing_major = $project_data['existing_major']; Chris@0: $supported_majors = []; Chris@0: if (isset($available['supported_majors'])) { Chris@0: $supported_majors = explode(',', $available['supported_majors']); Chris@0: } Chris@0: elseif (isset($available['default_major'])) { Chris@0: // Older release history XML file without supported or recommended. Chris@0: $supported_majors[] = $available['default_major']; Chris@0: } Chris@0: Chris@0: if (in_array($existing_major, $supported_majors)) { Chris@0: // Still supported, stay at the current major version. Chris@0: $target_major = $existing_major; Chris@0: } Chris@0: elseif (isset($available['recommended_major'])) { Chris@0: // Since 'recommended_major' is defined, we know this is the new XML Chris@0: // format. Therefore, we know the current release is unsupported since Chris@0: // its major version was not in the 'supported_majors' list. We should Chris@0: // find the best release from the recommended major version. Chris@0: $target_major = $available['recommended_major']; Chris@0: $project_data['status'] = UPDATE_NOT_SUPPORTED; Chris@0: } Chris@0: elseif (isset($available['default_major'])) { Chris@0: // Older release history XML file without recommended, so recommend Chris@0: // the currently defined "default_major" version. Chris@0: $target_major = $available['default_major']; Chris@0: } Chris@0: else { Chris@0: // Malformed XML file? Stick with the current version. Chris@0: $target_major = $existing_major; Chris@0: } Chris@0: Chris@0: // Make sure we never tell the admin to downgrade. If we recommended an Chris@0: // earlier version than the one they're running, they'd face an Chris@0: // impossible data migration problem, since Drupal never supports a DB Chris@0: // downgrade path. In the unfortunate case that what they're running is Chris@0: // unsupported, and there's nothing newer for them to upgrade to, we Chris@0: // can't print out a "Recommended version", but just have to tell them Chris@0: // what they have is unsupported and let them figure it out. Chris@0: $target_major = max($existing_major, $target_major); Chris@0: Chris@0: $release_patch_changed = ''; Chris@0: $patch = ''; Chris@0: Chris@0: // If the project is marked as UPDATE_FETCH_PENDING, it means that the Chris@0: // data we currently have (if any) is stale, and we've got a task queued Chris@0: // up to (re)fetch the data. In that case, we mark it as such, merge in Chris@0: // whatever data we have (e.g. project title and link), and move on. Chris@0: if (!empty($available['fetch_status']) && $available['fetch_status'] == UPDATE_FETCH_PENDING) { Chris@0: $project_data['status'] = UPDATE_FETCH_PENDING; Chris@0: $project_data['reason'] = t('No available update data'); Chris@0: $project_data['fetch_status'] = $available['fetch_status']; Chris@0: return; Chris@0: } Chris@0: Chris@0: // Defend ourselves from XML history files that contain no releases. Chris@0: if (empty($available['releases'])) { Chris@0: $project_data['status'] = UPDATE_UNKNOWN; Chris@0: $project_data['reason'] = t('No available releases found'); Chris@0: return; Chris@0: } Chris@0: foreach ($available['releases'] as $version => $release) { Chris@0: // First, if this is the existing release, check a few conditions. Chris@0: if ($project_data['existing_version'] === $version) { Chris@0: if (isset($release['terms']['Release type']) && Chris@0: in_array('Insecure', $release['terms']['Release type'])) { Chris@0: $project_data['status'] = UPDATE_NOT_SECURE; Chris@0: } Chris@0: elseif ($release['status'] == 'unpublished') { Chris@0: $project_data['status'] = UPDATE_REVOKED; Chris@0: if (empty($project_data['extra'])) { Chris@0: $project_data['extra'] = []; Chris@0: } Chris@0: $project_data['extra'][] = [ Chris@0: 'class' => ['release-revoked'], Chris@0: 'label' => t('Release revoked'), Chris@0: '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!'), Chris@0: ]; Chris@0: } Chris@0: elseif (isset($release['terms']['Release type']) && Chris@0: in_array('Unsupported', $release['terms']['Release type'])) { Chris@0: $project_data['status'] = UPDATE_NOT_SUPPORTED; Chris@0: if (empty($project_data['extra'])) { Chris@0: $project_data['extra'] = []; Chris@0: } Chris@0: $project_data['extra'][] = [ Chris@0: 'class' => ['release-not-supported'], Chris@0: 'label' => t('Release not supported'), Chris@0: '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!'), Chris@0: ]; Chris@0: } Chris@0: } Chris@0: Chris@0: // Otherwise, ignore unpublished, insecure, or unsupported releases. Chris@0: if ($release['status'] == 'unpublished' || Chris@0: (isset($release['terms']['Release type']) && Chris@0: (in_array('Insecure', $release['terms']['Release type']) || Chris@0: in_array('Unsupported', $release['terms']['Release type'])))) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: // See if this is a higher major version than our target and yet still Chris@0: // supported. If so, record it as an "Also available" release. Chris@0: // Note: Some projects have a HEAD release from CVS days, which could Chris@0: // be one of those being compared. They would not have version_major Chris@0: // set, so we must call isset first. Chris@0: if (isset($release['version_major']) && $release['version_major'] > $target_major) { Chris@0: if (in_array($release['version_major'], $supported_majors)) { Chris@0: if (!isset($project_data['also'])) { Chris@0: $project_data['also'] = []; Chris@0: } Chris@0: if (!isset($project_data['also'][$release['version_major']])) { Chris@0: $project_data['also'][$release['version_major']] = $version; Chris@0: $project_data['releases'][$version] = $release; Chris@0: } Chris@0: } Chris@0: // Otherwise, this release can't matter to us, since it's neither Chris@0: // from the release series we're currently using nor the recommended Chris@0: // release. We don't even care about security updates for this Chris@0: // branch, since if a project maintainer puts out a security release Chris@0: // at a higher major version and not at the lower major version, Chris@0: // they must remove the lower version from the supported major Chris@0: // versions at the same time, in which case we won't hit this code. Chris@0: continue; Chris@0: } Chris@0: Chris@0: // Look for the 'latest version' if we haven't found it yet. Latest is Chris@0: // defined as the most recent version for the target major version. Chris@0: if (!isset($project_data['latest_version']) Chris@0: && $release['version_major'] == $target_major) { Chris@0: $project_data['latest_version'] = $version; Chris@0: $project_data['releases'][$version] = $release; Chris@0: } Chris@0: Chris@0: // Look for the development snapshot release for this branch. Chris@0: if (!isset($project_data['dev_version']) Chris@0: && $release['version_major'] == $target_major Chris@0: && isset($release['version_extra']) Chris@0: && $release['version_extra'] == 'dev') { Chris@0: $project_data['dev_version'] = $version; Chris@0: $project_data['releases'][$version] = $release; Chris@0: } Chris@0: Chris@0: // Look for the 'recommended' version if we haven't found it yet (see Chris@0: // phpdoc at the top of this function for the definition). Chris@0: if (!isset($project_data['recommended']) Chris@0: && $release['version_major'] == $target_major Chris@0: && isset($release['version_patch'])) { Chris@0: if ($patch != $release['version_patch']) { Chris@0: $patch = $release['version_patch']; Chris@0: $release_patch_changed = $release; Chris@0: } Chris@0: if (empty($release['version_extra']) && $patch == $release['version_patch']) { Chris@0: $project_data['recommended'] = $release_patch_changed['version']; Chris@0: $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed; Chris@0: } Chris@0: } Chris@0: Chris@0: // Stop searching once we hit the currently installed version. Chris@0: if ($project_data['existing_version'] === $version) { Chris@0: break; Chris@0: } Chris@0: Chris@0: // If we're running a dev snapshot and have a timestamp, stop Chris@0: // searching for security updates once we hit an official release Chris@0: // older than what we've got. Allow 100 seconds of leeway to handle Chris@0: // differences between the datestamp in the .info.yml file and the Chris@0: // timestamp of the tarball itself (which are usually off by 1 or 2 Chris@0: // seconds) so that we don't flag that as a new release. Chris@0: if ($project_data['install_type'] == 'dev') { Chris@0: if (empty($project_data['datestamp'])) { Chris@0: // We don't have current timestamp info, so we can't know. Chris@0: continue; Chris@0: } Chris@0: elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) { Chris@0: // We're newer than this, so we can skip it. Chris@0: continue; Chris@0: } Chris@0: } Chris@0: Chris@0: // See if this release is a security update. Chris@0: if (isset($release['terms']['Release type']) Chris@0: && in_array('Security update', $release['terms']['Release type'])) { Chris@0: $project_data['security updates'][] = $release; Chris@0: } Chris@0: } Chris@0: Chris@0: // If we were unable to find a recommended version, then make the latest Chris@0: // version the recommended version if possible. Chris@0: if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) { Chris@0: $project_data['recommended'] = $project_data['latest_version']; Chris@0: } Chris@0: Chris@0: if (isset($project_data['status'])) { Chris@0: // If we already know the status, we're done. Chris@0: return; Chris@0: } Chris@0: Chris@0: // If we don't know what to recommend, there's nothing we can report. Chris@0: // Bail out early. Chris@0: if (!isset($project_data['recommended'])) { Chris@0: $project_data['status'] = UPDATE_UNKNOWN; Chris@0: $project_data['reason'] = t('No available releases found'); Chris@0: return; Chris@0: } Chris@0: Chris@0: // If we're running a dev snapshot, compare the date of the dev snapshot Chris@0: // with the latest official version, and record the absolute latest in Chris@0: // 'latest_dev' so we can correctly decide if there's a newer release Chris@0: // than our current snapshot. Chris@0: if ($project_data['install_type'] == 'dev') { Chris@0: if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) { Chris@0: $project_data['latest_dev'] = $project_data['dev_version']; Chris@0: } Chris@0: else { Chris@0: $project_data['latest_dev'] = $project_data['latest_version']; Chris@0: } Chris@0: } Chris@0: Chris@0: // Figure out the status, based on what we've seen and the install type. Chris@0: switch ($project_data['install_type']) { Chris@0: case 'official': Chris@0: if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) { Chris@0: $project_data['status'] = UPDATE_CURRENT; Chris@0: } Chris@0: else { Chris@0: $project_data['status'] = UPDATE_NOT_CURRENT; Chris@0: } Chris@0: break; Chris@0: Chris@0: case 'dev': Chris@0: $latest = $available['releases'][$project_data['latest_dev']]; Chris@0: if (empty($project_data['datestamp'])) { Chris@0: $project_data['status'] = UPDATE_NOT_CHECKED; Chris@0: $project_data['reason'] = t('Unknown release date'); Chris@0: } Chris@0: elseif (($project_data['datestamp'] + 100 > $latest['date'])) { Chris@0: $project_data['status'] = UPDATE_CURRENT; Chris@0: } Chris@0: else { Chris@0: $project_data['status'] = UPDATE_NOT_CURRENT; Chris@0: } Chris@0: break; Chris@0: Chris@0: default: Chris@0: $project_data['status'] = UPDATE_UNKNOWN; Chris@0: $project_data['reason'] = t('Invalid info'); Chris@0: } Chris@0: }