StatusInfoDrush.php

  1. 8.0.x lib/Drush/UpdateService/StatusInfoDrush.php
  2. 7.x lib/Drush/UpdateService/StatusInfoDrush.php
  3. master lib/Drush/UpdateService/StatusInfoDrush.php

Implementation of 'drush' update_status engine for any Drupal version.

Namespace

Drush\UpdateService

Classes

Namesort descending Description
StatusInfoDrush

File

lib/Drush/UpdateService/StatusInfoDrush.php
View source
  1. <?php
  2. /**
  3. * @file
  4. * Implementation of 'drush' update_status engine for any Drupal version.
  5. */
  6. namespace Drush\UpdateService;
  7. use Drush\Log\LogLevel;
  8. class StatusInfoDrush implements StatusInfoInterface {
  9. /**
  10. * {@inheritdoc}
  11. */
  12. public function __construct($type, $engine, $config) {
  13. $this->engine_type = $type;
  14. $this->engine = $engine;
  15. $this->engine_config = $config;
  16. }
  17. /**
  18. * {@inheritdoc}
  19. */
  20. function lastCheck() {
  21. $older = 0;
  22. // Iterate all projects and get the time of the older release info.
  23. $projects = drush_get_projects();
  24. foreach ($projects as $project_name => $project) {
  25. $request = pm_parse_request($project_name, NULL, $projects);
  26. $url = Project::buildFetchUrl($request);
  27. $cache_file = drush_download_file_name($url);
  28. if (file_exists($cache_file)) {
  29. $ctime = filectime($cache_file);
  30. $older = (!$older) ? $ctime : min($ctime, $older);
  31. }
  32. }
  33. return $older;
  34. }
  35. /**
  36. * {@inheritdoc}
  37. */
  38. function refresh() {
  39. $release_info = drush_include_engine('release_info', 'updatexml');
  40. // Clear all caches for the available projects.
  41. $projects = drush_get_projects();
  42. foreach ($projects as $project_name => $project) {
  43. $request = pm_parse_request($project_name, NULL, $projects);
  44. $release_info->clearCached($request);
  45. }
  46. }
  47. /**
  48. * Get update information for all installed projects.
  49. *
  50. * @return
  51. * Array of update status information.
  52. */
  53. function getStatus($projects, $check_disabled) {
  54. // Exclude disabled projects.
  55. if (!$check_disabled) {
  56. foreach ($projects as $project_name => $project) {
  57. if (!$project['status']) {
  58. unset($projects[$project_name]);
  59. }
  60. }
  61. }
  62. $available = $this->getAvailableReleases($projects);
  63. $update_info = $this->calculateUpdateStatus($available, $projects);
  64. return $update_info;
  65. }
  66. /**
  67. * Obtains release info for projects.
  68. */
  69. private function getAvailableReleases($projects) {
  70. drush_log(dt('Checking available update data ...'), LogLevel::OK);
  71. $release_info = drush_include_engine('release_info', 'updatexml');
  72. $available = array();
  73. foreach ($projects as $project_name => $project) {
  74. // Discard projects with unknown installation path.
  75. if ($project_name != 'drupal' && !isset($project['path'])) {
  76. continue;
  77. }
  78. drush_log(dt('Checking available update data for !project.', array('!project' => $project['label'])), LogLevel::OK);
  79. $request = pm_parse_request($project_name, NULL, $project_name);
  80. $project_release_info = $release_info->get($request);
  81. if ($project_release_info) {
  82. $available[$project_name] = $project_release_info;
  83. }
  84. }
  85. // Clear any error set by a failed project. This avoid rollbacks.
  86. drush_clear_error();
  87. return $available;
  88. }
  89. /**
  90. * Calculates update status for given projects.
  91. */
  92. private function calculateUpdateStatus($available, $projects) {
  93. $update_info = array();
  94. foreach ($available as $project_name => $project_release_info) {
  95. // Obtain project 'global' status. NULL status is ok (project published),
  96. // otherwise it signals something is bad with the project (revoked, etc).
  97. $project_status = $this->calculateProjectStatus($project_release_info);
  98. // Discard custom projects.
  99. if ($project_status == DRUSH_UPDATESTATUS_UNKNOWN) {
  100. continue;
  101. }
  102. // Prepare update info.
  103. $project = $projects[$project_name];
  104. $is_core = ($project['type'] == 'core');
  105. $version = pm_parse_version($project['version'], $is_core);
  106. // If project version ends with 'dev', this is a dev snapshot.
  107. $install_type = (substr($project['version'], -3, 3) == 'dev') ? 'dev' : 'official';
  108. $project_update_info = array(
  109. 'name' => $project_name,
  110. 'label' => $project['label'],
  111. 'path' => isset($project['path']) ? $project['path'] : '',
  112. 'install_type' => $install_type,
  113. 'existing_version' => $project['version'],
  114. 'existing_major' => $version['version_major'],
  115. 'status' => $project_status,
  116. 'datestamp' => empty($project['datestamp']) ? NULL : $project['datestamp'],
  117. );
  118. // If we don't have a project status yet, it means this is
  119. // a published project and we need to obtain its update status
  120. // and recommended release.
  121. if (is_null($project_status)) {
  122. $this->calculateProjectUpdateStatus($project_release_info, $project_update_info);
  123. }
  124. // We want to ship all release info data including all releases,
  125. // not just the ones selected by calculateProjectUpdateStatus().
  126. // We use it to allow the user to update to a specific version.
  127. unset($project_update_info['releases']);
  128. $update_info[$project_name] = $project_update_info + $project_release_info->getInfo();
  129. }
  130. return $update_info;
  131. }
  132. /**
  133. * Obtain the project status in the update service.
  134. *
  135. * This is not the update status of the installed version
  136. * but the project 'global' status (unpublished, revoked, etc).
  137. *
  138. * @see update_calculate_project_status().
  139. */
  140. private function calculateProjectStatus($project_release_info) {
  141. $project_status = NULL;
  142. // If connection to the update service went wrong, or the received xml
  143. // is malformed, we don't have a UpdateService::Project object.
  144. if (!$project_release_info) {
  145. $project_status = DRUSH_UPDATESTATUS_NOT_FETCHED;
  146. }
  147. else {
  148. switch ($project_release_info->getStatus()) {
  149. case 'insecure':
  150. $project_status = DRUSH_UPDATESTATUS_NOT_SECURE;
  151. break;
  152. case 'unpublished':
  153. case 'revoked':
  154. $project_status = DRUSH_UPDATESTATUS_REVOKED;
  155. break;
  156. case 'unsupported':
  157. $project_status = DRUSH_UPDATESTATUS_NOT_SUPPORTED;
  158. break;
  159. case 'unknown':
  160. $project_status = DRUSH_UPDATESTATUS_UNKNOWN;
  161. break;
  162. }
  163. }
  164. return $project_status;
  165. }
  166. /**
  167. * Obtain the update status of a project and the recommended release.
  168. *
  169. * This is a stripped down version of update_calculate_project_status().
  170. * That function has the same logic in Drupal 6,7,8.
  171. * Note: in Drupal 6 this is part of update_calculate_project_data().
  172. *
  173. * @see update_calculate_project_status().
  174. */
  175. private function calculateProjectUpdateStatus($project_release_info, &$project_data) {
  176. $available = $project_release_info->getInfo();
  177. /**
  178. * Here starts the code adapted from update_calculate_project_status().
  179. * Line 492 in Drupal 7.
  180. *
  181. * Changes are:
  182. * - Use DRUSH_UPDATESTATUS_* constants instead of DRUSH_UPDATESTATUS_*
  183. * - Remove error conditions we already handle
  184. * - Remove presentation code ('extra' and 'reason' keys in $project_data)
  185. * - Remove "also available" information.
  186. */
  187. // Figure out the target major version.
  188. $existing_major = $project_data['existing_major'];
  189. $supported_majors = array();
  190. if (isset($available['supported_majors'])) {
  191. $supported_majors = explode(',', $available['supported_majors']);
  192. }
  193. elseif (isset($available['default_major'])) {
  194. // Older release history XML file without supported or recommended.
  195. $supported_majors[] = $available['default_major'];
  196. }
  197. if (in_array($existing_major, $supported_majors)) {
  198. // Still supported, stay at the current major version.
  199. $target_major = $existing_major;
  200. }
  201. elseif (isset($available['recommended_major'])) {
  202. // Since 'recommended_major' is defined, we know this is the new XML
  203. // format. Therefore, we know the current release is unsupported since
  204. // its major version was not in the 'supported_majors' list. We should
  205. // find the best release from the recommended major version.
  206. $target_major = $available['recommended_major'];
  207. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SUPPORTED;
  208. }
  209. elseif (isset($available['default_major'])) {
  210. // Older release history XML file without recommended, so recommend
  211. // the currently defined "default_major" version.
  212. $target_major = $available['default_major'];
  213. }
  214. else {
  215. // Malformed XML file? Stick with the current version.
  216. $target_major = $existing_major;
  217. }
  218. // Make sure we never tell the admin to downgrade. If we recommended an
  219. // earlier version than the one they're running, they'd face an
  220. // impossible data migration problem, since Drupal never supports a DB
  221. // downgrade path. In the unfortunate case that what they're running is
  222. // unsupported, and there's nothing newer for them to upgrade to, we
  223. // can't print out a "Recommended version", but just have to tell them
  224. // what they have is unsupported and let them figure it out.
  225. $target_major = max($existing_major, $target_major);
  226. $release_patch_changed = '';
  227. $patch = '';
  228. foreach ($available['releases'] as $version => $release) {
  229. // First, if this is the existing release, check a few conditions.
  230. if ($project_data['existing_version'] === $version) {
  231. if (isset($release['terms']['Release type']) &&
  232. in_array('Insecure', $release['terms']['Release type'])) {
  233. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SECURE;
  234. }
  235. elseif ($release['status'] == 'unpublished') {
  236. $project_data['status'] = DRUSH_UPDATESTATUS_REVOKED;
  237. }
  238. elseif (isset($release['terms']['Release type']) &&
  239. in_array('Unsupported', $release['terms']['Release type'])) {
  240. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SUPPORTED;
  241. }
  242. }
  243. // Otherwise, ignore unpublished, insecure, or unsupported releases.
  244. if ($release['status'] == 'unpublished' ||
  245. (isset($release['terms']['Release type']) &&
  246. (in_array('Insecure', $release['terms']['Release type']) ||
  247. in_array('Unsupported', $release['terms']['Release type'])))) {
  248. continue;
  249. }
  250. // See if this is a higher major version than our target and discard it.
  251. // Note: at this point Drupal record it as an "Also available" release.
  252. if (isset($release['version_major']) && $release['version_major'] > $target_major) {
  253. continue;
  254. }
  255. // Look for the 'latest version' if we haven't found it yet. Latest is
  256. // defined as the most recent version for the target major version.
  257. if (!isset($project_data['latest_version'])
  258. && $release['version_major'] == $target_major) {
  259. $project_data['latest_version'] = $version;
  260. $project_data['releases'][$version] = $release;
  261. }
  262. // Look for the development snapshot release for this branch.
  263. if (!isset($project_data['dev_version'])
  264. && $release['version_major'] == $target_major
  265. && isset($release['version_extra'])
  266. && $release['version_extra'] == 'dev') {
  267. $project_data['dev_version'] = $version;
  268. $project_data['releases'][$version] = $release;
  269. }
  270. // Look for the 'recommended' version if we haven't found it yet (see
  271. // phpdoc at the top of this function for the definition).
  272. if (!isset($project_data['recommended'])
  273. && $release['version_major'] == $target_major
  274. && isset($release['version_patch'])) {
  275. if ($patch != $release['version_patch']) {
  276. $patch = $release['version_patch'];
  277. $release_patch_changed = $release;
  278. }
  279. if (empty($release['version_extra']) && $patch == $release['version_patch']) {
  280. $project_data['recommended'] = $release_patch_changed['version'];
  281. $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed;
  282. }
  283. }
  284. // Stop searching once we hit the currently installed version.
  285. if ($project_data['existing_version'] === $version) {
  286. break;
  287. }
  288. // If we're running a dev snapshot and have a timestamp, stop
  289. // searching for security updates once we hit an official release
  290. // older than what we've got. Allow 100 seconds of leeway to handle
  291. // differences between the datestamp in the .info file and the
  292. // timestamp of the tarball itself (which are usually off by 1 or 2
  293. // seconds) so that we don't flag that as a new release.
  294. if ($project_data['install_type'] == 'dev') {
  295. if (empty($project_data['datestamp'])) {
  296. // We don't have current timestamp info, so we can't know.
  297. continue;
  298. }
  299. elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) {
  300. // We're newer than this, so we can skip it.
  301. continue;
  302. }
  303. }
  304. // See if this release is a security update.
  305. if (isset($release['terms']['Release type'])
  306. && in_array('Security update', $release['terms']['Release type'])) {
  307. $project_data['security updates'][] = $release;
  308. }
  309. }
  310. // If we were unable to find a recommended version, then make the latest
  311. // version the recommended version if possible.
  312. if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) {
  313. $project_data['recommended'] = $project_data['latest_version'];
  314. }
  315. //
  316. // Check to see if we need an update or not.
  317. //
  318. if (!empty($project_data['security updates'])) {
  319. // If we found security updates, that always trumps any other status.
  320. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SECURE;
  321. }
  322. if (isset($project_data['status'])) {
  323. // If we already know the status, we're done.
  324. return;
  325. }
  326. // If we don't know what to recommend, there's nothing we can report.
  327. // Bail out early.
  328. if (!isset($project_data['recommended'])) {
  329. $project_data['status'] = DRUSH_UPDATESTATUS_UNKNOWN;
  330. $project_data['reason'] = t('No available releases found');
  331. return;
  332. }
  333. // If we're running a dev snapshot, compare the date of the dev snapshot
  334. // with the latest official version, and record the absolute latest in
  335. // 'latest_dev' so we can correctly decide if there's a newer release
  336. // than our current snapshot.
  337. if ($project_data['install_type'] == 'dev') {
  338. if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) {
  339. $project_data['latest_dev'] = $project_data['dev_version'];
  340. }
  341. else {
  342. $project_data['latest_dev'] = $project_data['latest_version'];
  343. }
  344. }
  345. // Figure out the status, based on what we've seen and the install type.
  346. switch ($project_data['install_type']) {
  347. case 'official':
  348. if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) {
  349. $project_data['status'] = DRUSH_UPDATESTATUS_CURRENT;
  350. }
  351. else {
  352. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CURRENT;
  353. }
  354. break;
  355. case 'dev':
  356. $latest = $available['releases'][$project_data['latest_dev']];
  357. if (empty($project_data['datestamp'])) {
  358. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CHECKED;
  359. }
  360. elseif (($project_data['datestamp'] + 100 > $latest['date'])) {
  361. $project_data['status'] = DRUSH_UPDATESTATUS_CURRENT;
  362. }
  363. else {
  364. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CURRENT;
  365. }
  366. break;
  367. default:
  368. $project_data['status'] = DRUSH_UPDATESTATUS_UNKNOWN;
  369. }
  370. }
  371. }