updatexml.inc

  1. 6.x commands/pm/release_info/updatexml.inc
  2. 5.x commands/pm/release_info/updatexml.inc

Drush release info engine for update.drupal.org and compatible services.

This engine does connect directly to the update service. It doesn't depend on a bootstraped site.

Functions

Namesort descending Description
release_info_check_project Check if a project is available in a update service.
release_info_fetch Obtain the most appropiate release for the requested project.
release_info_filter_releases Filter a list of releases.
release_info_get_releases Obtain releases info for given requests and fill in status information.
release_info_print_releasenotes Prints release notes for given projects.
updatexml_best_release_found Given a list of candidate releases, return the best one. This will be the first stable release if there are stable releases; otherwise, it will be any available release.
updatexml_determine_project_type Determine a project type from its update service xml.
updatexml_dev_release Pick the first dev release from XML list.
updatexml_get_releases_from_xml Obtain releases for a project's xml as returned by the update service.
updatexml_get_release_history_xml Download the release history xml for the specified request.
updatexml_get_url
updatexml_most_appropriate_release Pick most appropriate release from XML list.
updatexml_parse_release No longer used by Drush core. Called by tests in releaseInfoTest.php. See equivalent logic in release_info_fetch.
updatexml_specific_release_version Pick a specific version from XML list.
_release_info_compare_date Helper function for release_info_filter_releases().

Constants

File

commands/pm/release_info/updatexml.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * Drush release info engine for update.drupal.org and compatible services.
  5. *
  6. * This engine does connect directly to the update service. It doesn't depend
  7. * on a bootstraped site.
  8. */
  9. define('RELEASE_INFO_DEFAULT_URL', 'http://updates.drupal.org/release-history');
  10. /**
  11. * Obtain the most appropiate release for the requested project.
  12. *
  13. * @param Array &$request
  14. * A project request as returned by pm_parse_project_version(). The array will
  15. * be expanded with the project type.
  16. * @param String $restrict_to
  17. * One of:
  18. * 'dev': Forces choosing a -dev release.
  19. * 'version': Forces choosing a point release.
  20. * '': No restriction.
  21. * Default is ''.
  22. * @param String $select
  23. * Strategy for selecting a release, should be one of:
  24. * - auto: Try to select the latest release, if none found allow the user
  25. * to choose.
  26. * - always: Force the user to choose a release.
  27. * - never: Try to select the latest release, if none found then fail.
  28. * - ignore: Ignore and return NULL.
  29. * If no supported release is found, allow to ask the user to choose one.
  30. * @param Boolean $all
  31. * In case $select = TRUE this indicates that all available releases will be
  32. * offered the user to choose.
  33. *
  34. * @return
  35. * The selected release xml object.
  36. */
  37. function release_info_fetch(&$request, $restrict_to = '', $select = 'never', $all = FALSE) {
  38. if (!in_array($select, array('auto', 'never', 'always', 'ignore'))) {
  39. return drush_set_error('DRUSH_PM_UNKNOWN_SELECT_STRATEGY', dt("Error: select strategy must be one of: auto, never, always, ignore", array()));
  40. }
  41. $xml = updatexml_get_release_history_xml($request);
  42. if (!$xml) {
  43. return FALSE;
  44. }
  45. $request['project_type'] = updatexml_determine_project_type($xml);
  46. if ($select != 'always') {
  47. if ($restrict_to == 'dev') {
  48. $release = updatexml_dev_release($request, $xml);
  49. if ($release === FALSE) {
  50. return drush_set_error('DRUSN_PM_NO_DEV_RELEASE', dt('There is no development release for project !project.', array('!project' => $request['name'])));
  51. }
  52. }
  53. if (empty($release)) {
  54. $release = updatexml_specific_release_version($request, $xml);
  55. if ($release === FALSE) {
  56. return drush_set_error('DRUSH_PM_COULD_NOT_FIND_VERSION', dt("Could not locate !project version !version.", array('!project' => $request['name'], '!version' => $request['version'])));
  57. }
  58. }
  59. // If there was no specific release requested, try to identify the most appropriate release.
  60. if (empty($release)) {
  61. $release = updatexml_most_appropriate_release($request, $xml);
  62. }
  63. if ($release) {
  64. return $release;
  65. }
  66. else {
  67. $message = dt('There are no stable releases for project !project.', array('!project' => $request['name']));
  68. if ($select == 'never') {
  69. return drush_set_error('DRUSH_PM_NO_STABLE_RELEASE', $message);
  70. }
  71. drush_log($message, 'warning');
  72. if ($select == 'ignore') {
  73. return;
  74. }
  75. }
  76. }
  77. $project_info = updatexml_get_releases_from_xml($xml, $request['name']);
  78. $releases = release_info_filter_releases($project_info['releases'], $all, $restrict_to);
  79. $options = array();
  80. foreach($releases as $version => $release) {
  81. $options[$version] = array($version, '-', gmdate('Y-M-d', $release['date']), '-', implode(', ', $release['release_status']));
  82. }
  83. $choice = drush_choice($options, dt('Choose one of the available releases for !project:', array('!project' => $request['name'])));
  84. if (!$choice) {
  85. return drush_user_abort();
  86. }
  87. return $project_info['releases'][$choice];
  88. }
  89. /**
  90. * Obtain releases info for given requests and fill in status information.
  91. *
  92. * @param $requests
  93. * An array of project names optionally with a version.
  94. */
  95. function release_info_get_releases($requests) {
  96. $info = array();
  97. foreach ($requests as $name => $request) {
  98. $xml = updatexml_get_release_history_xml($request);
  99. if (!$xml) {
  100. continue;
  101. }
  102. $project_info = updatexml_get_releases_from_xml($xml, $name);
  103. $info[$name] = $project_info;
  104. }
  105. return $info;
  106. }
  107. /**
  108. * Check if a project is available in a update service.
  109. *
  110. * Optionally check for consistency by comparing given project type and
  111. * the type obtained from the update service.
  112. */
  113. function release_info_check_project($request, $type = NULL) {
  114. $xml = updatexml_get_release_history_xml($request);
  115. if (!$xml) {
  116. return FALSE;
  117. }
  118. if ($type) {
  119. $project_type = updatexml_determine_project_type($xml);
  120. if ($project_type != $type) {
  121. return FALSE;
  122. }
  123. }
  124. return TRUE;
  125. }
  126. /**
  127. * Prints release notes for given projects.
  128. *
  129. * @param $requests
  130. * An array of drupal.org project names optionally with a version.
  131. * @param $print_status
  132. * Boolean. Used by pm-download to not print a informative note.
  133. * @param $tmpfile
  134. * If provided, a file that contains contents to show before the
  135. * release notes.
  136. */
  137. function release_info_print_releasenotes($requests, $print_status = TRUE, $tmpfile = NULL) {
  138. $info = release_info_get_releases($requests);
  139. if (!$info) {
  140. return drush_log(dt('No valid projects given.'), 'ok');
  141. }
  142. if (!isset($tmpfile)) {
  143. $tmpfile = drush_tempnam('rln-' . implode('-', array_keys($requests)) . '.');
  144. }
  145. // Select versions to show.
  146. foreach ($info as $name => $project) {
  147. $selected_versions = array();
  148. // If the request includes version, show the release notes for this version.
  149. if (isset($requests[$name]['version'])) {
  150. $selected_versions[] = $requests[$name]['version'];
  151. }
  152. else {
  153. // If requested project is installed,
  154. // show release notes for the installed version and all newer versions.
  155. if (isset($project['recommended'], $project['installed'])) {
  156. $releases = array_reverse($project['releases']);
  157. foreach($releases as $version => $release) {
  158. if ($release['date'] >= $project['releases'][$project['installed']]['date']) {
  159. $release += array('version_extra' => '');
  160. $project['releases'][$project['installed']] += array('version_extra' => '');
  161. if ($release['version_extra'] == 'dev' && $project['releases'][$project['installed']]['version_extra'] != 'dev') {
  162. continue;
  163. }
  164. $selected_versions[] = $version;
  165. }
  166. }
  167. }
  168. else {
  169. // Project is not installed and user did not specify a version,
  170. // so show the release notes for the recommended version.
  171. $selected_versions[] = $project['recommended'];
  172. }
  173. }
  174. foreach ($selected_versions as $version) {
  175. if (!isset($project['releases'][$version]['release_link'])) {
  176. drush_log(dt("Project !project does not have release notes for version !version.", array('!project' => $name, '!version' => $version)), 'warning');
  177. continue;
  178. }
  179. // Download the release node page and get the html as xml to explore it.
  180. $release_link = $project['releases'][$version]['release_link'];
  181. $filename = drush_download_file($release_link, drush_tempnam($name));
  182. @$dom = DOMDocument::loadHTMLFile($filename);
  183. if ($dom) {
  184. drush_log(dt("Successfully parsed and loaded the HTML contained in the release notes' page for !project (!version) project.", array('!project' => $name, '!version' => $version)), 'notice');
  185. }
  186. else {
  187. drush_log(dt("Error while requesting the release notes page for !project project.", array('!project' => $name)), 'error');
  188. continue;
  189. }
  190. $xml = simplexml_import_dom($dom);
  191. // Extract last update time and the notes.
  192. $last_updated = $xml->xpath('//div[contains(@class,"views-field-changed")]');
  193. $last_updated = $last_updated[0]->asXML();
  194. $notes = $xml->xpath('//div[contains(@class,"field-name-body")]');
  195. $notes = $notes[0]->asXML();
  196. // Build the notes header.
  197. $header = array();
  198. $header[] = '<hr>';
  199. $header[] = dt("> RELEASE NOTES FOR '!name' PROJECT, VERSION !version:", array('!name' => strtoupper($name), '!version' => $version));
  200. $header[] = dt("> !last_updated.", array('!last_updated' => trim(drush_html_to_text($last_updated))));
  201. if ($print_status) {
  202. $header[] = '> ' . implode(', ', $project['releases'][$version]['release_status']);
  203. }
  204. $header[] = '<hr>';
  205. // Finally add the release notes for the requested project to the tmpfile.
  206. $content = implode("\n", $header) . "\n" . $notes . "\n";
  207. #TODO# accept $html as a method argument
  208. if (!drush_get_option('html', FALSE)) {
  209. $content = drush_html_to_text($content, array('br', 'p', 'ul', 'ol', 'li', 'hr'));
  210. }
  211. file_put_contents($tmpfile, $content, FILE_APPEND);
  212. }
  213. }
  214. drush_print_file($tmpfile);
  215. }
  216. /**
  217. * Helper function for release_info_filter_releases().
  218. */
  219. function _release_info_compare_date($a, $b) {
  220. if ($a['date'] == $b['date']) {
  221. return 0;
  222. }
  223. if ($a['version_major'] == $b['version_major']) {
  224. return ($a['date'] > $b['date']) ? -1 : 1;
  225. }
  226. return ($a['version_major'] > $b['version_major']) ? -1 : 1;
  227. }
  228. /**
  229. * Filter a list of releases.
  230. *
  231. * @param $releases
  232. * Array of release information
  233. * @param $all
  234. * Show all releases. If FALSE, shows only the first release that is
  235. * Recommended or Supported or Security or Installed.
  236. * @param String $restrict_to
  237. * If set to 'dev', show only development release.
  238. * @param $show_all_until_installed
  239. * If TRUE, then all releases will be shown until the INSTALLED release
  240. * is found, at which point the algorithm will stop.
  241. */
  242. function release_info_filter_releases($releases, $all = FALSE, $restrict_to = '', $show_all_until_installed = TRUE) {
  243. // Start off by sorting releases by release date.
  244. uasort($releases, '_release_info_compare_date');
  245. // Find version_major for the installed release.
  246. $installed_version_major = FALSE;
  247. foreach ($releases as $version => $release_info) {
  248. if (in_array("Installed", $release_info['release_status'])) {
  249. $installed_version_major = $release_info['version_major'];
  250. }
  251. }
  252. // Now iterate through and filter out the releases we're interested in.
  253. $options = array();
  254. $limits_list = array();
  255. $dev = $restrict_to == 'dev';
  256. foreach ($releases as $version => $release_info) {
  257. if (!$dev || ((array_key_exists('version_extra', $release_info)) && ($release_info['version_extra'] == 'dev'))) {
  258. $saw_unique_status = FALSE;
  259. foreach ($release_info['release_status'] as $one_status) {
  260. // We will show the first release of a given kind;
  261. // after we show the first security release, we show
  262. // no other. We do this on a per-major-version basis,
  263. // though, so if a project has three major versions, then
  264. // we will show the first security release from each.
  265. // This rule is overridden by $all and $show_all_until_installed.
  266. $test_key = $release_info['version_major'] . $one_status;
  267. if (!array_key_exists($test_key, $limits_list)) {
  268. $limits_list[$test_key] = TRUE;
  269. $saw_unique_status = TRUE;
  270. // Once we see the "Installed" release we will stop
  271. // showing all releases
  272. if ($one_status == "Installed") {
  273. $show_all_until_installed = FALSE;
  274. $installed_release_date = $release_info['date'];
  275. }
  276. }
  277. }
  278. if ($all || ($show_all_until_installed && ($installed_version_major == $release_info['version_major'])) || $saw_unique_status) {
  279. $options[$release_info['version']] = $release_info;
  280. }
  281. }
  282. }
  283. // If "show all until installed" is still true, that means that
  284. // we never encountered the installed release anywhere in releases,
  285. // and therefore we did not filter out any releases at all. If this
  286. // is the case, then call ourselves again, this time with
  287. // $show_all_until_installed set to FALSE from the beginning.
  288. // The other situation we might encounter is when we do not encounter
  289. // the installed release, and $options is still empty. This means
  290. // that there were no supported or recommented or security or development
  291. // releases found. If this is the case, then we will force ALL to TRUE
  292. // and show everything on the second iteration.
  293. if (($all === FALSE) && ($show_all_until_installed === TRUE)) {
  294. $options = release_info_filter_releases($releases, empty($options), $restrict_to, FALSE);
  295. }
  296. return $options;
  297. }
  298. /**
  299. * No longer used by Drush core. Called by tests in releaseInfoTest.php.
  300. * See equivalent logic in release_info_fetch.
  301. */
  302. function updatexml_parse_release($request, $xml, $restrict_to = '') {
  303. if ($restrict_to == 'dev') {
  304. return updatexml_dev_release($request, $xml);
  305. }
  306. $release = updatexml_specific_release_version($request, $xml);
  307. if (is_array($release) && !empty($release)) {
  308. return $release;
  309. }
  310. return updatexml_most_appropriate_release($request, $xml);
  311. }
  312. /**
  313. * Pick a specific version from XML list.
  314. *
  315. * @param array $request
  316. * An array with project and version strings as returned by
  317. * pm_parse_project_version().
  318. * @param resource $xml
  319. * A handle to the XML document.
  320. * @param String $restrict_to
  321. * One of:
  322. * 'dev': Forces a -dev release.
  323. * 'version': Forces a point release.
  324. * '': No restriction (auto-selects latest recommended or supported release
  325. * if requested release is not found).
  326. * Default is ''.
  327. * @return
  328. * array - The selected release xml object. Empty if user did not specify a
  329. * specific release.
  330. * FALSE - The specified version could not be found.
  331. */
  332. function updatexml_specific_release_version($request, $xml) {
  333. if (!empty($request['version'])) {
  334. $matches = array();
  335. // See if we only have a branch version.
  336. if (preg_match('/^\d+\.x-(\d+)$/', $request['version'], $matches)) {
  337. $xpath_releases = "/project/releases/release[status='published'][version_major=" . (string)$matches[1] . "]";
  338. $releases = @$xml->xpath($xpath_releases);
  339. }
  340. else {
  341. // In some cases, the request only says something like '7.x-3.x' but the
  342. // version strings include '-dev' on the end, so we need to append that
  343. // here for the xpath to match below.
  344. if (substr($request['version'], -2) == '.x') {
  345. $request['version'] .= '-dev';
  346. }
  347. $releases = $xml->xpath("/project/releases/release[status='published'][version='" . $request['version'] . "']");
  348. }
  349. if (empty($releases)) {
  350. return FALSE;
  351. }
  352. return updatexml_best_release_found($releases);
  353. }
  354. return array();
  355. }
  356. /**
  357. * Pick the first dev release from XML list.
  358. *
  359. * @param array $request
  360. * An array with project and version strings as returned by
  361. * pm_parse_project_version().
  362. * @param resource $xml
  363. * A handle to the XML document.
  364. * @return
  365. * array - The selected release xml object.
  366. * FALSE - No dev releases were found.
  367. */
  368. function updatexml_dev_release($request, $xml) {
  369. $releases = @$xml->xpath("/project/releases/release[status='published'][version_extra='dev']");
  370. if (empty($releases)) {
  371. return FALSE;
  372. }
  373. return updatexml_best_release_found($releases);
  374. }
  375. /**
  376. * Pick most appropriate release from XML list.
  377. *
  378. * @param array $request
  379. * An array with project and version strings as returned by
  380. * pm_parse_project_version().
  381. * @param resource $xml
  382. * A handle to the XML document.
  383. * @return
  384. * array - The selected release xml object.
  385. * FALSE - No releases were found.
  386. */
  387. function updatexml_most_appropriate_release($request, $xml) {
  388. $releases = array();
  389. foreach(array('recommended_major', 'supported_majors') as $release_type) {
  390. if ($versions = $xml->xpath("/project/$release_type")) {
  391. $xpath = "/project/releases/release[status='published'][version_major=" . (string)$versions[0] . "]";
  392. $releases = @$xml->xpath($xpath);
  393. if (!empty($releases)) {
  394. break;
  395. }
  396. }
  397. }
  398. return updatexml_best_release_found($releases);
  399. }
  400. /**
  401. * Given a list of candidate releases, return the best one.
  402. * This will be the first stable release if there are stable
  403. * releases; otherwise, it will be any available release.
  404. */
  405. function updatexml_best_release_found($releases) {
  406. // If there are releases found, let's try first to fetch one with no
  407. // 'version_extra'. Otherwise, use all.
  408. if (!empty($releases)) {
  409. $stable_releases = array();
  410. foreach ($releases as $one_release) {
  411. if (!array_key_exists('version_extra', $one_release)) {
  412. $stable_releases[] = $one_release;
  413. }
  414. }
  415. if (!empty($stable_releases)) {
  416. $releases = $stable_releases;
  417. }
  418. }
  419. if (empty($releases)) {
  420. return FALSE;
  421. }
  422. // First published release is just the first value in $releases.
  423. return (array)$releases[0];
  424. }
  425. function updatexml_get_url($request) {
  426. $status_url = isset($request['status url']) ? $request['status url'] : RELEASE_INFO_DEFAULT_URL;
  427. return $status_url . '/' . $request['name'] . '/' . $request['drupal_version'];
  428. }
  429. /**
  430. * Download the release history xml for the specified request.
  431. */
  432. function updatexml_get_release_history_xml($request) {
  433. $url = updatexml_get_url($request);
  434. drush_log('Downloading release history from ' . $url);
  435. // Some hosts have allow_url_fopen disabled.
  436. if ($path = drush_download_file($url, drush_tempnam($request['name']), drush_get_option('cache-duration-releasexml', 24*3600))) {
  437. $xml = simplexml_load_file($path);
  438. }
  439. if (!$xml) {
  440. // We are not getting here since drupal.org always serves an XML response.
  441. return drush_set_error('DRUSH_PM_DOWNLOAD_FAILED', dt('Could not download project status information from !url', array('!url' => $url)));
  442. }
  443. if ($error = $xml->xpath('/error')) {
  444. // Don't set an error here since it stops processing during site-upgrade.
  445. drush_log($error[0], 'warning'); // 'DRUSH_PM_COULD_NOT_LOAD_UPDATE_FILE',
  446. return FALSE;
  447. }
  448. // Unpublished project?
  449. $project_status = $xml->xpath('/project/project_status');
  450. if ($project_status[0][0] == 'unpublished') {
  451. return drush_set_error('DRUSH_PM_PROJECT_UNPUBLISHED', dt("Project !project is unpublished and has no releases available.", array('!project' => $request['name'])), 'warning');
  452. }
  453. return $xml;
  454. }
  455. /**
  456. * Obtain releases for a project's xml as returned by the update service.
  457. */
  458. function updatexml_get_releases_from_xml($xml, $project) {
  459. // If bootstraped, we can obtain which is the installed release of a project.
  460. static $installed_projects = FALSE;
  461. if (!$installed_projects) {
  462. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) {
  463. $installed_projects = drush_get_projects();
  464. }
  465. else {
  466. $installed_projects = array();
  467. }
  468. }
  469. foreach (array('title', 'short_name', 'dc:creator', 'api_version', 'recommended_major', 'supported_majors', 'default_major', 'project_status', 'link') as $item) {
  470. if (array_key_exists($item, $xml)) {
  471. $value = $xml->xpath($item);
  472. $project_info[$item] = (string)$value[0];
  473. }
  474. }
  475. $recommended_major = @$xml->xpath("/project/recommended_major");
  476. $recommended_major = empty($recommended_major)?"":(string)$recommended_major[0];
  477. $supported_majors = @$xml->xpath("/project/supported_majors");
  478. $supported_majors = empty($supported_majors)?array():array_flip(explode(',', (string)$supported_majors[0]));
  479. $releases_xml = @$xml->xpath("/project/releases/release[status='published']");
  480. $recommended_version = NULL;
  481. $latest_version = NULL;
  482. $releases = array();
  483. foreach ($releases_xml as $release) {
  484. $release_info = array();
  485. foreach (array('name', 'version', 'tag', 'version_major', 'version_extra', 'status', 'release_link', 'download_link', 'date', 'mdhash', 'filesize') as $item) {
  486. if (array_key_exists($item, $release)) {
  487. $value = $release->xpath($item);
  488. $release_info[$item] = (string)$value[0];
  489. }
  490. }
  491. $statuses = array();
  492. if (array_key_exists($release_info['version_major'], $supported_majors)) {
  493. $statuses[] = "Supported";
  494. unset($supported_majors[$release_info['version_major']]);
  495. }
  496. if ($release_info['version_major'] == $recommended_major) {
  497. if (!isset($latest_version)) {
  498. $latest_version = $release_info['version'];
  499. }
  500. // The first stable version (no 'version extra') in the recommended major
  501. // is the recommended release
  502. if (empty($release_info['version_extra']) && (!isset($recommended_version))) {
  503. $statuses[] = "Recommended";
  504. $recommended_version = $release_info['version'];
  505. }
  506. }
  507. if (!empty($release_info['version_extra']) && ($release_info['version_extra'] == "dev")) {
  508. $statuses[] = "Development";
  509. }
  510. foreach ($release->xpath('terms/term/value') as $release_type) {
  511. // There are three kinds of release types that we recognize:
  512. // "Bug fixes", "New features" and "Security update".
  513. // We will add "Security" for security updates, and nothing
  514. // for the other kinds.
  515. if (strpos($release_type, "Security") !== FALSE) {
  516. $statuses[] = "Security";
  517. }
  518. }
  519. // Add to status whether the project is installed.
  520. if (isset($installed_projects[$project])) {
  521. if ($installed_projects[$project]['version'] == $release_info['version']) {
  522. $statuses[] = dt('Installed');
  523. $project_info['installed'] = $release_info['version'];
  524. }
  525. }
  526. $release_info['release_status'] = $statuses;
  527. $releases[$release_info['version']] = $release_info;
  528. }
  529. // If there is no -stable- release in the recommended major,
  530. // then take the latest verion in the recommended major to be
  531. // the recommended release.
  532. if (!isset($recommended_version) && isset($latest_version)) {
  533. $recommended_version = $latest_version;
  534. $releases[$recommended_version]['release_status'][] = "Recommended";
  535. }
  536. $project_info['releases'] = $releases;
  537. $project_info['recommended'] = $recommended_version;
  538. return $project_info;
  539. }
  540. /**
  541. * Determine a project type from its update service xml.
  542. */
  543. function updatexml_determine_project_type($xml) {
  544. $project_types = array(
  545. 'core' => 'project_core',
  546. 'profile' => 'project_distribution',
  547. 'module' => 'project_module',
  548. 'theme' => 'project_theme',
  549. 'theme engine' => 'project_theme_engine',
  550. 'translation' => 'project_translation'
  551. );
  552. $type = (string)$xml->type;
  553. // Probably unused but kept for possible legacy compat.
  554. $type = ($type == 'profile-legacy') ? 'profile' : $type;
  555. $type = array_search($type, $project_types);
  556. return $type;
  557. }