5 * Implementation of 'drush' update_status engine for any Drupal version.
8 namespace Drush\UpdateService;
10 use Drush\Log\LogLevel;
12 class StatusInfoDrush implements StatusInfoInterface {
17 public function __construct($type, $engine, $config) {
18 $this->engine_type = $type;
19 $this->engine = $engine;
20 $this->engine_config = $config;
26 function lastCheck() {
29 // Iterate all projects and get the time of the older release info.
30 $projects = drush_get_projects();
31 foreach ($projects as $project_name => $project) {
32 $request = pm_parse_request($project_name, NULL, $projects);
33 $url = Project::buildFetchUrl($request);
34 $cache_file = drush_download_file_name($url);
35 if (file_exists($cache_file)) {
36 $ctime = filectime($cache_file);
37 $older = (!$older) ? $ctime : min($ctime, $older);
48 $release_info = drush_include_engine('release_info', 'updatexml');
50 // Clear all caches for the available projects.
51 $projects = drush_get_projects();
52 foreach ($projects as $project_name => $project) {
53 $request = pm_parse_request($project_name, NULL, $projects);
54 $release_info->clearCached($request);
59 * Get update information for all installed projects.
62 * Array of update status information.
64 function getStatus($projects, $check_disabled) {
65 // Exclude disabled projects.
66 if (!$check_disabled) {
67 foreach ($projects as $project_name => $project) {
68 if (!$project['status']) {
69 unset($projects[$project_name]);
73 $available = $this->getAvailableReleases($projects);
74 $update_info = $this->calculateUpdateStatus($available, $projects);
79 * Obtains release info for projects.
81 private function getAvailableReleases($projects) {
82 drush_log(dt('Checking available update data ...'), LogLevel::OK);
84 $release_info = drush_include_engine('release_info', 'updatexml');
87 foreach ($projects as $project_name => $project) {
88 // Discard projects with unknown installation path.
89 if ($project_name != 'drupal' && !isset($project['path'])) {
92 drush_log(dt('Checking available update data for !project.', array('!project' => $project['label'])), LogLevel::OK);
93 $request = $project_name . (isset($project['core']) ? '-' . $project['core'] : '');
94 $request = pm_parse_request($request, NULL, $projects);
95 $project_release_info = $release_info->get($request);
96 if ($project_release_info) {
97 $available[$project_name] = $project_release_info;
101 // Clear any error set by a failed project. This avoid rollbacks.
108 * Calculates update status for given projects.
110 private function calculateUpdateStatus($available, $projects) {
111 $update_info = array();
112 foreach ($available as $project_name => $project_release_info) {
113 // Obtain project 'global' status. NULL status is ok (project published),
114 // otherwise it signals something is bad with the project (revoked, etc).
115 $project_status = $this->calculateProjectStatus($project_release_info);
116 // Discard custom projects.
117 if ($project_status == DRUSH_UPDATESTATUS_UNKNOWN) {
121 // Prepare update info.
122 $project = $projects[$project_name];
123 $is_core = ($project['type'] == 'core');
124 $version = pm_parse_version($project['version'], $is_core);
125 // If project version ends with 'dev', this is a dev snapshot.
126 $install_type = (substr($project['version'], -3, 3) == 'dev') ? 'dev' : 'official';
127 $project_update_info = array(
128 'name' => $project_name,
129 'label' => $project['label'],
130 'path' => isset($project['path']) ? $project['path'] : '',
131 'install_type' => $install_type,
132 'existing_version' => $project['version'],
133 'existing_major' => $version['version_major'],
134 'status' => $project_status,
135 'datestamp' => empty($project['datestamp']) ? NULL : $project['datestamp'],
138 // If we don't have a project status yet, it means this is
139 // a published project and we need to obtain its update status
140 // and recommended release.
141 if (is_null($project_status)) {
142 $this->calculateProjectUpdateStatus($project_release_info, $project_update_info);
145 // We want to ship all release info data including all releases,
146 // not just the ones selected by calculateProjectUpdateStatus().
147 // We use it to allow the user to update to a specific version.
148 unset($project_update_info['releases']);
149 $update_info[$project_name] = $project_update_info + $project_release_info->getInfo();
156 * Obtain the project status in the update service.
158 * This is not the update status of the installed version
159 * but the project 'global' status (unpublished, revoked, etc).
161 * @see update_calculate_project_status().
163 private function calculateProjectStatus($project_release_info) {
164 $project_status = NULL;
166 // If connection to the update service went wrong, or the received xml
167 // is malformed, we don't have a UpdateService::Project object.
168 if (!$project_release_info) {
169 $project_status = DRUSH_UPDATESTATUS_NOT_FETCHED;
172 switch ($project_release_info->getStatus()) {
174 $project_status = DRUSH_UPDATESTATUS_NOT_SECURE;
178 $project_status = DRUSH_UPDATESTATUS_REVOKED;
181 $project_status = DRUSH_UPDATESTATUS_NOT_SUPPORTED;
184 $project_status = DRUSH_UPDATESTATUS_UNKNOWN;
188 return $project_status;
192 * Obtain the update status of a project and the recommended release.
194 * This is a stripped down version of update_calculate_project_status().
195 * That function has the same logic in Drupal 6,7,8.
196 * Note: in Drupal 6 this is part of update_calculate_project_data().
198 * @see update_calculate_project_status().
200 private function calculateProjectUpdateStatus($project_release_info, &$project_data) {
201 $available = $project_release_info->getInfo();
204 * Here starts the code adapted from update_calculate_project_status().
205 * Line 492 in Drupal 7.
208 * - Use DRUSH_UPDATESTATUS_* constants instead of DRUSH_UPDATESTATUS_*
209 * - Remove error conditions we already handle
210 * - Remove presentation code ('extra' and 'reason' keys in $project_data)
211 * - Remove "also available" information.
214 // Figure out the target major version.
215 $existing_major = $project_data['existing_major'];
216 $supported_majors = array();
217 if (isset($available['supported_majors'])) {
218 $supported_majors = explode(',', $available['supported_majors']);
220 elseif (isset($available['default_major'])) {
221 // Older release history XML file without supported or recommended.
222 $supported_majors[] = $available['default_major'];
225 if (in_array($existing_major, $supported_majors)) {
226 // Still supported, stay at the current major version.
227 $target_major = $existing_major;
229 elseif (isset($available['recommended_major'])) {
230 // Since 'recommended_major' is defined, we know this is the new XML
231 // format. Therefore, we know the current release is unsupported since
232 // its major version was not in the 'supported_majors' list. We should
233 // find the best release from the recommended major version.
234 $target_major = $available['recommended_major'];
235 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SUPPORTED;
237 elseif (isset($available['default_major'])) {
238 // Older release history XML file without recommended, so recommend
239 // the currently defined "default_major" version.
240 $target_major = $available['default_major'];
243 // Malformed XML file? Stick with the current version.
244 $target_major = $existing_major;
247 // Make sure we never tell the admin to downgrade. If we recommended an
248 // earlier version than the one they're running, they'd face an
249 // impossible data migration problem, since Drupal never supports a DB
250 // downgrade path. In the unfortunate case that what they're running is
251 // unsupported, and there's nothing newer for them to upgrade to, we
252 // can't print out a "Recommended version", but just have to tell them
253 // what they have is unsupported and let them figure it out.
254 $target_major = max($existing_major, $target_major);
256 $release_patch_changed = '';
259 foreach ($available['releases'] as $version => $release) {
260 // First, if this is the existing release, check a few conditions.
261 if ($project_data['existing_version'] === $version) {
262 if (isset($release['terms']['Release type']) &&
263 in_array('Insecure', $release['terms']['Release type'])) {
264 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SECURE;
266 elseif ($release['status'] == 'unpublished') {
267 $project_data['status'] = DRUSH_UPDATESTATUS_REVOKED;
269 elseif (isset($release['terms']['Release type']) &&
270 in_array('Unsupported', $release['terms']['Release type'])) {
271 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SUPPORTED;
275 // Otherwise, ignore unpublished, insecure, or unsupported releases.
276 if ($release['status'] == 'unpublished' ||
277 (isset($release['terms']['Release type']) &&
278 (in_array('Insecure', $release['terms']['Release type']) ||
279 in_array('Unsupported', $release['terms']['Release type'])))) {
283 // See if this is a higher major version than our target and discard it.
284 // Note: at this point Drupal record it as an "Also available" release.
285 if (isset($release['version_major']) && $release['version_major'] > $target_major) {
289 // Look for the 'latest version' if we haven't found it yet. Latest is
290 // defined as the most recent version for the target major version.
291 if (!isset($project_data['latest_version'])
292 && $release['version_major'] == $target_major) {
293 $project_data['latest_version'] = $version;
294 $project_data['releases'][$version] = $release;
297 // Look for the development snapshot release for this branch.
298 if (!isset($project_data['dev_version'])
299 && $release['version_major'] == $target_major
300 && isset($release['version_extra'])
301 && $release['version_extra'] == 'dev') {
302 $project_data['dev_version'] = $version;
303 $project_data['releases'][$version] = $release;
306 // Look for the 'recommended' version if we haven't found it yet (see
307 // phpdoc at the top of this function for the definition).
308 if (!isset($project_data['recommended'])
309 && $release['version_major'] == $target_major
310 && isset($release['version_patch'])) {
311 if ($patch != $release['version_patch']) {
312 $patch = $release['version_patch'];
313 $release_patch_changed = $release;
315 if (empty($release['version_extra']) && $patch == $release['version_patch']) {
316 $project_data['recommended'] = $release_patch_changed['version'];
317 $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed;
321 // Stop searching once we hit the currently installed version.
322 if ($project_data['existing_version'] === $version) {
326 // If we're running a dev snapshot and have a timestamp, stop
327 // searching for security updates once we hit an official release
328 // older than what we've got. Allow 100 seconds of leeway to handle
329 // differences between the datestamp in the .info file and the
330 // timestamp of the tarball itself (which are usually off by 1 or 2
331 // seconds) so that we don't flag that as a new release.
332 if ($project_data['install_type'] == 'dev') {
333 if (empty($project_data['datestamp'])) {
334 // We don't have current timestamp info, so we can't know.
337 elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) {
338 // We're newer than this, so we can skip it.
343 // See if this release is a security update.
344 if (isset($release['terms']['Release type'])
345 && in_array('Security update', $release['terms']['Release type'])) {
346 $project_data['security updates'][] = $release;
350 // If we were unable to find a recommended version, then make the latest
351 // version the recommended version if possible.
352 if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) {
353 $project_data['recommended'] = $project_data['latest_version'];
357 // Check to see if we need an update or not.
360 if (!empty($project_data['security updates'])) {
361 // If we found security updates, that always trumps any other status.
362 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SECURE;
365 if (isset($project_data['status'])) {
366 // If we already know the status, we're done.
370 // If we don't know what to recommend, there's nothing we can report.
372 if (!isset($project_data['recommended'])) {
373 $project_data['status'] = DRUSH_UPDATESTATUS_UNKNOWN;
374 $project_data['reason'] = t('No available releases found');
378 // If we're running a dev snapshot, compare the date of the dev snapshot
379 // with the latest official version, and record the absolute latest in
380 // 'latest_dev' so we can correctly decide if there's a newer release
381 // than our current snapshot.
382 if ($project_data['install_type'] == 'dev') {
383 if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) {
384 $project_data['latest_dev'] = $project_data['dev_version'];
387 $project_data['latest_dev'] = $project_data['latest_version'];
391 // Figure out the status, based on what we've seen and the install type.
392 switch ($project_data['install_type']) {
394 if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) {
395 $project_data['status'] = DRUSH_UPDATESTATUS_CURRENT;
398 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CURRENT;
403 $latest = $available['releases'][$project_data['latest_dev']];
404 if (empty($project_data['datestamp'])) {
405 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CHECKED;
407 elseif (($project_data['datestamp'] + 100 > $latest['date'])) {
408 $project_data['status'] = DRUSH_UPDATESTATUS_CURRENT;
411 $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CURRENT;
416 $project_data['status'] = DRUSH_UPDATESTATUS_UNKNOWN;