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