update.compare.inc 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. <?php
  2. /**
  3. * @file
  4. * Code required only when comparing available updates to existing data.
  5. */
  6. /**
  7. * Fetches an array of installed and enabled projects.
  8. *
  9. * This is only responsible for generating an array of projects (taking into
  10. * account projects that include more than one module or theme). Other
  11. * information like the specific version and install type (official release,
  12. * dev snapshot, etc) is handled later in update_process_project_info() since
  13. * that logic is only required when preparing the status report, not for
  14. * fetching the available release data.
  15. *
  16. * This array is fairly expensive to construct, since it involves a lot of disk
  17. * I/O, so we cache the results into the {cache_update} table using the
  18. * 'update_project_projects' cache ID. However, since this is not the data about
  19. * available updates fetched from the network, it is acceptable to invalidate it
  20. * somewhat quickly. If we keep this data for very long, site administrators are
  21. * more likely to see incorrect results if they upgrade to a newer version of a
  22. * module or theme but do not visit certain pages that automatically clear this
  23. * cache.
  24. *
  25. * @return
  26. * An associative array of currently enabled projects keyed by the
  27. * machine-readable project short name. Each project contains:
  28. * - name: The machine-readable project short name.
  29. * - info: An array with values from the main .info file for this project.
  30. * - name: The human-readable name of the project.
  31. * - package: The package that the project is grouped under.
  32. * - version: The version of the project.
  33. * - project: The Drupal.org project name.
  34. * - datestamp: The date stamp of the project's main .info file.
  35. * - _info_file_ctime: The maximum file change time for all of the .info
  36. * files included in this project.
  37. * - datestamp: The date stamp when the project was released, if known.
  38. * - includes: An associative array containing all projects included with this
  39. * project, keyed by the machine-readable short name with the human-readable
  40. * name as value.
  41. * - project_type: The type of project. Allowed values are 'module' and
  42. * 'theme'.
  43. * - project_status: This indicates if the project is enabled and will always
  44. * be TRUE, as the function only returns enabled projects.
  45. * - sub_themes: If the project is a theme it contains an associative array of
  46. * all sub-themes.
  47. * - base_themes: If the project is a theme it contains an associative array
  48. * of all base-themes.
  49. *
  50. * @see update_process_project_info()
  51. * @see update_calculate_project_data()
  52. * @see update_project_cache()
  53. */
  54. function update_get_projects() {
  55. $projects = &drupal_static(__FUNCTION__, array());
  56. if (empty($projects)) {
  57. // Retrieve the projects from cache, if present.
  58. $projects = update_project_cache('update_project_projects');
  59. if (empty($projects)) {
  60. // Still empty, so we have to rebuild the cache.
  61. $module_data = system_rebuild_module_data();
  62. $theme_data = system_rebuild_theme_data();
  63. _update_process_info_list($projects, $module_data, 'module', TRUE);
  64. _update_process_info_list($projects, $theme_data, 'theme', TRUE);
  65. if (variable_get('update_check_disabled', FALSE)) {
  66. _update_process_info_list($projects, $module_data, 'module', FALSE);
  67. _update_process_info_list($projects, $theme_data, 'theme', FALSE);
  68. }
  69. // Allow other modules to alter projects before fetching and comparing.
  70. drupal_alter('update_projects', $projects);
  71. // Cache the site's project data for at most 1 hour.
  72. _update_cache_set('update_project_projects', $projects, REQUEST_TIME + 3600);
  73. }
  74. }
  75. return $projects;
  76. }
  77. /**
  78. * Populates an array of project data.
  79. *
  80. * This iterates over a list of the installed modules or themes and groups them
  81. * by project and status. A few parts of this function assume that enabled
  82. * modules and themes are always processed first, and if disabled modules or
  83. * themes are being processed (there is a setting to control if disabled code
  84. * should be included or not in the 'Available updates' report), those are only
  85. * processed after $projects has been populated with information about the
  86. * enabled code. Modules and themes set as hidden are always ignored. This
  87. * function also records the latest change time on the .info files for each
  88. * module or theme, which is important data which is used when deciding if the
  89. * cached available update data should be invalidated.
  90. *
  91. * @param $projects
  92. * Reference to the array of project data of what's installed on this site.
  93. * @param $list
  94. * Array of data to process to add the relevant info to the $projects array.
  95. * @param $project_type
  96. * The kind of data in the list. Can be 'module' or 'theme'.
  97. * @param $status
  98. * Boolean that controls what status (enabled or disabled) to process out of
  99. * the $list and add to the $projects array.
  100. *
  101. * @see update_get_projects()
  102. */
  103. function _update_process_info_list(&$projects, $list, $project_type, $status) {
  104. $admin_theme = variable_get('admin_theme', 'seven');
  105. foreach ($list as $file) {
  106. // The admin theme is a special case. It should always be considered enabled
  107. // for the purposes of update checking.
  108. if ($file->name === $admin_theme) {
  109. $file->status = TRUE;
  110. }
  111. // A disabled base theme of an enabled sub-theme still has all of its code
  112. // run by the sub-theme, so we include it in our "enabled" projects list.
  113. if ($status && !$file->status && !empty($file->sub_themes)) {
  114. foreach ($file->sub_themes as $key => $name) {
  115. // Build a list of enabled sub-themes.
  116. if ($list[$key]->status) {
  117. $file->enabled_sub_themes[$key] = $name;
  118. }
  119. }
  120. // If there are no enabled subthemes, we should ignore this base theme
  121. // for the enabled case. If the site is trying to display disabled
  122. // themes, we'll catch it then.
  123. if (empty($file->enabled_sub_themes)) {
  124. continue;
  125. }
  126. }
  127. // Otherwise, just add projects of the proper status to our list.
  128. elseif ($file->status != $status) {
  129. continue;
  130. }
  131. // Skip if the .info file is broken.
  132. if (empty($file->info)) {
  133. continue;
  134. }
  135. // Skip if it's a hidden module or theme.
  136. if (!empty($file->info['hidden'])) {
  137. continue;
  138. }
  139. // If the .info doesn't define the 'project', try to figure it out.
  140. if (!isset($file->info['project'])) {
  141. $file->info['project'] = update_get_project_name($file);
  142. }
  143. // If we still don't know the 'project', give up.
  144. if (empty($file->info['project'])) {
  145. continue;
  146. }
  147. // If we don't already know it, grab the change time on the .info file
  148. // itself. Note: we need to use the ctime, not the mtime (modification
  149. // time) since many (all?) tar implementations will go out of their way to
  150. // set the mtime on the files it creates to the timestamps recorded in the
  151. // tarball. We want to see the last time the file was changed on disk,
  152. // which is left alone by tar and correctly set to the time the .info file
  153. // was unpacked.
  154. if (!isset($file->info['_info_file_ctime'])) {
  155. $info_filename = dirname($file->uri) . '/' . $file->name . '.info';
  156. $file->info['_info_file_ctime'] = filectime($info_filename);
  157. }
  158. if (!isset($file->info['datestamp'])) {
  159. $file->info['datestamp'] = 0;
  160. }
  161. $project_name = $file->info['project'];
  162. // Figure out what project type we're going to use to display this module
  163. // or theme. If the project name is 'drupal', we don't want it to show up
  164. // under the usual "Modules" section, we put it at a special "Drupal Core"
  165. // section at the top of the report.
  166. if ($project_name == 'drupal') {
  167. $project_display_type = 'core';
  168. }
  169. else {
  170. $project_display_type = $project_type;
  171. }
  172. if (empty($status) && empty($file->enabled_sub_themes)) {
  173. // If we're processing disabled modules or themes, append a suffix.
  174. // However, we don't do this to a a base theme with enabled
  175. // subthemes, since we treat that case as if it is enabled.
  176. $project_display_type .= '-disabled';
  177. }
  178. // Add a list of sub-themes that "depend on" the project and a list of base
  179. // themes that are "required by" the project.
  180. if ($project_name == 'drupal') {
  181. // Drupal core is always required, so this extra info would be noise.
  182. $sub_themes = array();
  183. $base_themes = array();
  184. }
  185. else {
  186. // Add list of enabled sub-themes.
  187. $sub_themes = !empty($file->enabled_sub_themes) ? $file->enabled_sub_themes : array();
  188. // Add list of base themes.
  189. $base_themes = !empty($file->base_themes) ? $file->base_themes : array();
  190. }
  191. if (!isset($projects[$project_name])) {
  192. // Only process this if we haven't done this project, since a single
  193. // project can have multiple modules or themes.
  194. $projects[$project_name] = array(
  195. 'name' => $project_name,
  196. // Only save attributes from the .info file we care about so we do not
  197. // bloat our RAM usage needlessly.
  198. 'info' => update_filter_project_info($file->info),
  199. 'datestamp' => $file->info['datestamp'],
  200. 'includes' => array($file->name => $file->info['name']),
  201. 'project_type' => $project_display_type,
  202. 'project_status' => $status,
  203. 'sub_themes' => $sub_themes,
  204. 'base_themes' => $base_themes,
  205. );
  206. }
  207. elseif ($projects[$project_name]['project_type'] == $project_display_type) {
  208. // Only add the file we're processing to the 'includes' array for this
  209. // project if it is of the same type and status (which is encoded in the
  210. // $project_display_type). This prevents listing all the disabled
  211. // modules included with an enabled project if we happen to be checking
  212. // for disabled modules, too.
  213. $projects[$project_name]['includes'][$file->name] = $file->info['name'];
  214. $projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
  215. $projects[$project_name]['datestamp'] = max($projects[$project_name]['datestamp'], $file->info['datestamp']);
  216. if (!empty($sub_themes)) {
  217. $projects[$project_name]['sub_themes'] += $sub_themes;
  218. }
  219. if (!empty($base_themes)) {
  220. $projects[$project_name]['base_themes'] += $base_themes;
  221. }
  222. }
  223. elseif (empty($status)) {
  224. // If we have a project_name that matches, but the project_display_type
  225. // does not, it means we're processing a disabled module or theme that
  226. // belongs to a project that has some enabled code. In this case, we add
  227. // the disabled thing into a separate array for separate display.
  228. $projects[$project_name]['disabled'][$file->name] = $file->info['name'];
  229. }
  230. }
  231. }
  232. /**
  233. * Determines what project a given file object belongs to.
  234. *
  235. * @param $file
  236. * A file object as returned by system_get_files_database().
  237. *
  238. * @return
  239. * The canonical project short name.
  240. *
  241. * @see system_get_files_database()
  242. */
  243. function update_get_project_name($file) {
  244. $project_name = '';
  245. if (isset($file->info['project'])) {
  246. $project_name = $file->info['project'];
  247. }
  248. elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
  249. $project_name = 'drupal';
  250. }
  251. return $project_name;
  252. }
  253. /**
  254. * Determines version and type information for currently installed projects.
  255. *
  256. * Processes the list of projects on the system to figure out the currently
  257. * installed versions, and other information that is required before we can
  258. * compare against the available releases to produce the status report.
  259. *
  260. * @param $projects
  261. * Array of project information from update_get_projects().
  262. */
  263. function update_process_project_info(&$projects) {
  264. foreach ($projects as $key => $project) {
  265. // Assume an official release until we see otherwise.
  266. $install_type = 'official';
  267. $info = $project['info'];
  268. if (isset($info['version'])) {
  269. // Check for development snapshots
  270. if (preg_match('@(dev|HEAD)@', $info['version'])) {
  271. $install_type = 'dev';
  272. }
  273. // Figure out what the currently installed major version is. We need
  274. // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
  275. // (e.g. "5.1", major = 5) version strings.
  276. $matches = array();
  277. if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
  278. $info['major'] = $matches[2];
  279. }
  280. elseif (!isset($info['major'])) {
  281. // This would only happen for version strings that don't follow the
  282. // drupal.org convention. We let contribs define "major" in their
  283. // .info in this case, and only if that's missing would we hit this.
  284. $info['major'] = -1;
  285. }
  286. }
  287. else {
  288. // No version info available at all.
  289. $install_type = 'unknown';
  290. $info['version'] = t('Unknown');
  291. $info['major'] = -1;
  292. }
  293. // Finally, save the results we care about into the $projects array.
  294. $projects[$key]['existing_version'] = $info['version'];
  295. $projects[$key]['existing_major'] = $info['major'];
  296. $projects[$key]['install_type'] = $install_type;
  297. }
  298. }
  299. /**
  300. * Calculates the current update status of all projects on the site.
  301. *
  302. * The results of this function are expensive to compute, especially on sites
  303. * with lots of modules or themes, since it involves a lot of comparisons and
  304. * other operations. Therefore, we cache the results into the {cache_update}
  305. * table using the 'update_project_data' cache ID. However, since this is not
  306. * the data about available updates fetched from the network, it is ok to
  307. * invalidate it somewhat quickly. If we keep this data for very long, site
  308. * administrators are more likely to see incorrect results if they upgrade to a
  309. * newer version of a module or theme but do not visit certain pages that
  310. * automatically clear this cache.
  311. *
  312. * @param array $available
  313. * Data about available project releases.
  314. *
  315. * @return
  316. * An array of installed projects with current update status information.
  317. *
  318. * @see update_get_available()
  319. * @see update_get_projects()
  320. * @see update_process_project_info()
  321. * @see update_project_cache()
  322. */
  323. function update_calculate_project_data($available) {
  324. // Retrieve the projects from cache, if present.
  325. $projects = update_project_cache('update_project_data');
  326. // If $projects is empty, then the cache must be rebuilt.
  327. // Otherwise, return the cached data and skip the rest of the function.
  328. if (!empty($projects)) {
  329. return $projects;
  330. }
  331. $projects = update_get_projects();
  332. update_process_project_info($projects);
  333. foreach ($projects as $project => $project_info) {
  334. if (isset($available[$project])) {
  335. update_calculate_project_update_status($project, $projects[$project], $available[$project]);
  336. }
  337. else {
  338. $projects[$project]['status'] = UPDATE_UNKNOWN;
  339. $projects[$project]['reason'] = t('No available releases found');
  340. }
  341. }
  342. // Give other modules a chance to alter the status (for example, to allow a
  343. // contrib module to provide fine-grained settings to ignore specific
  344. // projects or releases).
  345. drupal_alter('update_status', $projects);
  346. // Cache the site's update status for at most 1 hour.
  347. _update_cache_set('update_project_data', $projects, REQUEST_TIME + 3600);
  348. return $projects;
  349. }
  350. /**
  351. * Calculates the current update status of a specific project.
  352. *
  353. * This function is the heart of the update status feature. For each project it
  354. * is invoked with, it first checks if the project has been flagged with a
  355. * special status like "unsupported" or "insecure", or if the project node
  356. * itself has been unpublished. In any of those cases, the project is marked
  357. * with an error and the next project is considered.
  358. *
  359. * If the project itself is valid, the function decides what major release
  360. * series to consider. The project defines what the currently supported major
  361. * versions are for each version of core, so the first step is to make sure the
  362. * current version is still supported. If so, that's the target version. If the
  363. * current version is unsupported, the project maintainer's recommended major
  364. * version is used. There's also a check to make sure that this function never
  365. * recommends an earlier release than the currently installed major version.
  366. *
  367. * Given a target major version, the available releases are scanned looking for
  368. * the specific release to recommend (avoiding beta releases and development
  369. * snapshots if possible). For the target major version, the highest patch level
  370. * is found. If there is a release at that patch level with no extra ("beta",
  371. * etc.), then the release at that patch level with the most recent release date
  372. * is recommended. If every release at that patch level has extra (only betas),
  373. * then the latest release from the previous patch level is recommended. For
  374. * example:
  375. *
  376. * - 1.6-bugfix <-- recommended version because 1.6 already exists.
  377. * - 1.6
  378. *
  379. * or
  380. *
  381. * - 1.6-beta
  382. * - 1.5 <-- recommended version because no 1.6 exists.
  383. * - 1.4
  384. *
  385. * Also, the latest release from the same major version is looked for, even beta
  386. * releases, to display to the user as the "Latest version" option.
  387. * Additionally, the latest official release from any higher major versions that
  388. * have been released is searched for to provide a set of "Also available"
  389. * options.
  390. *
  391. * Finally, and most importantly, the release history continues to be scanned
  392. * until the currently installed release is reached, searching for anything
  393. * marked as a security update. If any security updates have been found between
  394. * the recommended release and the installed version, all of the releases that
  395. * included a security fix are recorded so that the site administrator can be
  396. * warned their site is insecure, and links pointing to the release notes for
  397. * each security update can be included (which, in turn, will link to the
  398. * official security announcements for each vulnerability).
  399. *
  400. * This function relies on the fact that the .xml release history data comes
  401. * sorted based on major version and patch level, then finally by release date
  402. * if there are multiple releases such as betas from the same major.patch
  403. * version (e.g., 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development
  404. * snapshots for a given major version are always listed last.
  405. *
  406. * @param $unused
  407. * Input is not being used, but remains in function for API compatibility
  408. * reasons.
  409. * @param $project_data
  410. * An array containing information about a specific project.
  411. * @param $available
  412. * Data about available project releases of a specific project.
  413. */
  414. function update_calculate_project_update_status($unused, &$project_data, $available) {
  415. foreach (array('title', 'link') as $attribute) {
  416. if (!isset($project_data[$attribute]) && isset($available[$attribute])) {
  417. $project_data[$attribute] = $available[$attribute];
  418. }
  419. }
  420. // If the project status is marked as something bad, there's nothing else
  421. // to consider.
  422. if (isset($available['project_status'])) {
  423. switch ($available['project_status']) {
  424. case 'insecure':
  425. $project_data['status'] = UPDATE_NOT_SECURE;
  426. if (empty($project_data['extra'])) {
  427. $project_data['extra'] = array();
  428. }
  429. $project_data['extra'][] = array(
  430. 'class' => array('project-not-secure'),
  431. 'label' => t('Project not secure'),
  432. '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!'),
  433. );
  434. break;
  435. case 'unpublished':
  436. case 'revoked':
  437. $project_data['status'] = UPDATE_REVOKED;
  438. if (empty($project_data['extra'])) {
  439. $project_data['extra'] = array();
  440. }
  441. $project_data['extra'][] = array(
  442. 'class' => array('project-revoked'),
  443. 'label' => t('Project revoked'),
  444. 'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
  445. );
  446. break;
  447. case 'unsupported':
  448. $project_data['status'] = UPDATE_NOT_SUPPORTED;
  449. if (empty($project_data['extra'])) {
  450. $project_data['extra'] = array();
  451. }
  452. $project_data['extra'][] = array(
  453. 'class' => array('project-not-supported'),
  454. 'label' => t('Project not supported'),
  455. 'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
  456. );
  457. break;
  458. case 'not-fetched':
  459. $project_data['status'] = UPDATE_NOT_FETCHED;
  460. $project_data['reason'] = t('Failed to get available update data.');
  461. break;
  462. default:
  463. // Assume anything else (e.g. 'published') is valid and we should
  464. // perform the rest of the logic in this function.
  465. break;
  466. }
  467. }
  468. if (!empty($project_data['status'])) {
  469. // We already know the status for this project, so there's nothing else to
  470. // compute. Record the project status into $project_data and we're done.
  471. $project_data['project_status'] = $available['project_status'];
  472. return;
  473. }
  474. // Figure out the target major version.
  475. $existing_major = $project_data['existing_major'];
  476. $supported_majors = array();
  477. if (isset($available['supported_majors'])) {
  478. $supported_majors = explode(',', $available['supported_majors']);
  479. }
  480. elseif (isset($available['default_major'])) {
  481. // Older release history XML file without supported or recommended.
  482. $supported_majors[] = $available['default_major'];
  483. }
  484. if (in_array($existing_major, $supported_majors)) {
  485. // Still supported, stay at the current major version.
  486. $target_major = $existing_major;
  487. }
  488. elseif (isset($available['recommended_major'])) {
  489. // Since 'recommended_major' is defined, we know this is the new XML
  490. // format. Therefore, we know the current release is unsupported since
  491. // its major version was not in the 'supported_majors' list. We should
  492. // find the best release from the recommended major version.
  493. $target_major = $available['recommended_major'];
  494. $project_data['status'] = UPDATE_NOT_SUPPORTED;
  495. }
  496. elseif (isset($available['default_major'])) {
  497. // Older release history XML file without recommended, so recommend
  498. // the currently defined "default_major" version.
  499. $target_major = $available['default_major'];
  500. }
  501. else {
  502. // Malformed XML file? Stick with the current version.
  503. $target_major = $existing_major;
  504. }
  505. // Make sure we never tell the admin to downgrade. If we recommended an
  506. // earlier version than the one they're running, they'd face an
  507. // impossible data migration problem, since Drupal never supports a DB
  508. // downgrade path. In the unfortunate case that what they're running is
  509. // unsupported, and there's nothing newer for them to upgrade to, we
  510. // can't print out a "Recommended version", but just have to tell them
  511. // what they have is unsupported and let them figure it out.
  512. $target_major = max($existing_major, $target_major);
  513. $release_patch_changed = '';
  514. $patch = '';
  515. // If the project is marked as UPDATE_FETCH_PENDING, it means that the
  516. // data we currently have (if any) is stale, and we've got a task queued
  517. // up to (re)fetch the data. In that case, we mark it as such, merge in
  518. // whatever data we have (e.g. project title and link), and move on.
  519. if (!empty($available['fetch_status']) && $available['fetch_status'] == UPDATE_FETCH_PENDING) {
  520. $project_data['status'] = UPDATE_FETCH_PENDING;
  521. $project_data['reason'] = t('No available update data');
  522. $project_data['fetch_status'] = $available['fetch_status'];
  523. return;
  524. }
  525. // Defend ourselves from XML history files that contain no releases.
  526. if (empty($available['releases'])) {
  527. $project_data['status'] = UPDATE_UNKNOWN;
  528. $project_data['reason'] = t('No available releases found');
  529. return;
  530. }
  531. foreach ($available['releases'] as $version => $release) {
  532. // First, if this is the existing release, check a few conditions.
  533. if ($project_data['existing_version'] === $version) {
  534. if (isset($release['terms']['Release type']) &&
  535. in_array('Insecure', $release['terms']['Release type'])) {
  536. $project_data['status'] = UPDATE_NOT_SECURE;
  537. }
  538. elseif ($release['status'] == 'unpublished') {
  539. $project_data['status'] = UPDATE_REVOKED;
  540. if (empty($project_data['extra'])) {
  541. $project_data['extra'] = array();
  542. }
  543. $project_data['extra'][] = array(
  544. 'class' => array('release-revoked'),
  545. 'label' => t('Release revoked'),
  546. '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!'),
  547. );
  548. }
  549. elseif (isset($release['terms']['Release type']) &&
  550. in_array('Unsupported', $release['terms']['Release type'])) {
  551. $project_data['status'] = UPDATE_NOT_SUPPORTED;
  552. if (empty($project_data['extra'])) {
  553. $project_data['extra'] = array();
  554. }
  555. $project_data['extra'][] = array(
  556. 'class' => array('release-not-supported'),
  557. 'label' => t('Release not supported'),
  558. '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!'),
  559. );
  560. }
  561. }
  562. // Otherwise, ignore unpublished, insecure, or unsupported releases.
  563. if ($release['status'] == 'unpublished' ||
  564. (isset($release['terms']['Release type']) &&
  565. (in_array('Insecure', $release['terms']['Release type']) ||
  566. in_array('Unsupported', $release['terms']['Release type'])))) {
  567. continue;
  568. }
  569. // See if this is a higher major version than our target and yet still
  570. // supported. If so, record it as an "Also available" release.
  571. // Note: some projects have a HEAD release from CVS days, which could
  572. // be one of those being compared. They would not have version_major
  573. // set, so we must call isset first.
  574. if (isset($release['version_major']) && $release['version_major'] > $target_major) {
  575. if (in_array($release['version_major'], $supported_majors)) {
  576. if (!isset($project_data['also'])) {
  577. $project_data['also'] = array();
  578. }
  579. if (!isset($project_data['also'][$release['version_major']])) {
  580. $project_data['also'][$release['version_major']] = $version;
  581. $project_data['releases'][$version] = $release;
  582. }
  583. }
  584. // Otherwise, this release can't matter to us, since it's neither
  585. // from the release series we're currently using nor the recommended
  586. // release. We don't even care about security updates for this
  587. // branch, since if a project maintainer puts out a security release
  588. // at a higher major version and not at the lower major version,
  589. // they must remove the lower version from the supported major
  590. // versions at the same time, in which case we won't hit this code.
  591. continue;
  592. }
  593. // Look for the 'latest version' if we haven't found it yet. Latest is
  594. // defined as the most recent version for the target major version.
  595. if (!isset($project_data['latest_version'])
  596. && $release['version_major'] == $target_major) {
  597. $project_data['latest_version'] = $version;
  598. $project_data['releases'][$version] = $release;
  599. }
  600. // Look for the development snapshot release for this branch.
  601. if (!isset($project_data['dev_version'])
  602. && $release['version_major'] == $target_major
  603. && isset($release['version_extra'])
  604. && $release['version_extra'] == 'dev') {
  605. $project_data['dev_version'] = $version;
  606. $project_data['releases'][$version] = $release;
  607. }
  608. // Look for the 'recommended' version if we haven't found it yet (see
  609. // phpdoc at the top of this function for the definition).
  610. if (!isset($project_data['recommended'])
  611. && $release['version_major'] == $target_major
  612. && isset($release['version_patch'])) {
  613. if ($patch != $release['version_patch']) {
  614. $patch = $release['version_patch'];
  615. $release_patch_changed = $release;
  616. }
  617. if (empty($release['version_extra']) && $patch == $release['version_patch']) {
  618. $project_data['recommended'] = $release_patch_changed['version'];
  619. $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed;
  620. }
  621. }
  622. // Stop searching once we hit the currently installed version.
  623. if ($project_data['existing_version'] === $version) {
  624. break;
  625. }
  626. // If we're running a dev snapshot and have a timestamp, stop
  627. // searching for security updates once we hit an official release
  628. // older than what we've got. Allow 100 seconds of leeway to handle
  629. // differences between the datestamp in the .info file and the
  630. // timestamp of the tarball itself (which are usually off by 1 or 2
  631. // seconds) so that we don't flag that as a new release.
  632. if ($project_data['install_type'] == 'dev') {
  633. if (empty($project_data['datestamp'])) {
  634. // We don't have current timestamp info, so we can't know.
  635. continue;
  636. }
  637. elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) {
  638. // We're newer than this, so we can skip it.
  639. continue;
  640. }
  641. }
  642. // See if this release is a security update.
  643. if (isset($release['terms']['Release type'])
  644. && in_array('Security update', $release['terms']['Release type'])) {
  645. $project_data['security updates'][] = $release;
  646. }
  647. }
  648. // If we were unable to find a recommended version, then make the latest
  649. // version the recommended version if possible.
  650. if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) {
  651. $project_data['recommended'] = $project_data['latest_version'];
  652. }
  653. //
  654. // Check to see if we need an update or not.
  655. //
  656. if (!empty($project_data['security updates'])) {
  657. // If we found security updates, that always trumps any other status.
  658. $project_data['status'] = UPDATE_NOT_SECURE;
  659. }
  660. if (isset($project_data['status'])) {
  661. // If we already know the status, we're done.
  662. return;
  663. }
  664. // If we don't know what to recommend, there's nothing we can report.
  665. // Bail out early.
  666. if (!isset($project_data['recommended'])) {
  667. $project_data['status'] = UPDATE_UNKNOWN;
  668. $project_data['reason'] = t('No available releases found');
  669. return;
  670. }
  671. // If we're running a dev snapshot, compare the date of the dev snapshot
  672. // with the latest official version, and record the absolute latest in
  673. // 'latest_dev' so we can correctly decide if there's a newer release
  674. // than our current snapshot.
  675. if ($project_data['install_type'] == 'dev') {
  676. if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) {
  677. $project_data['latest_dev'] = $project_data['dev_version'];
  678. }
  679. else {
  680. $project_data['latest_dev'] = $project_data['latest_version'];
  681. }
  682. }
  683. // Figure out the status, based on what we've seen and the install type.
  684. switch ($project_data['install_type']) {
  685. case 'official':
  686. if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) {
  687. $project_data['status'] = UPDATE_CURRENT;
  688. }
  689. else {
  690. $project_data['status'] = UPDATE_NOT_CURRENT;
  691. }
  692. break;
  693. case 'dev':
  694. $latest = $available['releases'][$project_data['latest_dev']];
  695. if (empty($project_data['datestamp'])) {
  696. $project_data['status'] = UPDATE_NOT_CHECKED;
  697. $project_data['reason'] = t('Unknown release date');
  698. }
  699. elseif (($project_data['datestamp'] + 100 > $latest['date'])) {
  700. $project_data['status'] = UPDATE_CURRENT;
  701. }
  702. else {
  703. $project_data['status'] = UPDATE_NOT_CURRENT;
  704. }
  705. break;
  706. default:
  707. $project_data['status'] = UPDATE_UNKNOWN;
  708. $project_data['reason'] = t('Invalid info');
  709. }
  710. }
  711. /**
  712. * Retrieves data from {cache_update} or empties the cache when necessary.
  713. *
  714. * Two very expensive arrays computed by this module are the list of all
  715. * installed modules and themes (and .info data, project associations, etc), and
  716. * the current status of the site relative to the currently available releases.
  717. * These two arrays are cached in the {cache_update} table and used whenever
  718. * possible. The cache is cleared whenever the administrator visits the status
  719. * report, available updates report, or the module or theme administration
  720. * pages, since we should always recompute the most current values on any of
  721. * those pages.
  722. *
  723. * Note: while both of these arrays are expensive to compute (in terms of disk
  724. * I/O and some fairly heavy CPU processing), neither of these is the actual
  725. * data about available updates that we have to fetch over the network from
  726. * updates.drupal.org. That information is stored with the
  727. * 'update_available_releases' cache ID -- it needs to persist longer than 1
  728. * hour and never get invalidated just by visiting a page on the site.
  729. *
  730. * @param $cid
  731. * The cache ID of data to return from the cache. Valid options are
  732. * 'update_project_data' and 'update_project_projects'.
  733. *
  734. * @return
  735. * The cached value of the $projects array generated by
  736. * update_calculate_project_data() or update_get_projects(), or an empty array
  737. * when the cache is cleared.
  738. */
  739. function update_project_cache($cid) {
  740. $projects = array();
  741. // On certain paths, we should clear the cache and recompute the projects for
  742. // update status of the site to avoid presenting stale information.
  743. $q = $_GET['q'];
  744. $paths = array(
  745. 'admin/modules',
  746. 'admin/modules/update',
  747. 'admin/appearance',
  748. 'admin/appearance/update',
  749. 'admin/reports',
  750. 'admin/reports/updates',
  751. 'admin/reports/updates/update',
  752. 'admin/reports/status',
  753. 'admin/reports/updates/check',
  754. );
  755. if (in_array($q, $paths)) {
  756. _update_cache_clear($cid);
  757. }
  758. else {
  759. $cache = _update_cache_get($cid);
  760. if (!empty($cache->data) && $cache->expire > REQUEST_TIME) {
  761. $projects = $cache->data;
  762. }
  763. }
  764. return $projects;
  765. }
  766. /**
  767. * Filters the project .info data to only save attributes we need.
  768. *
  769. * @param array $info
  770. * Array of .info file data as returned by drupal_parse_info_file().
  771. *
  772. * @return
  773. * Array of .info file data we need for the update manager.
  774. *
  775. * @see _update_process_info_list()
  776. */
  777. function update_filter_project_info($info) {
  778. $whitelist = array(
  779. '_info_file_ctime',
  780. 'datestamp',
  781. 'major',
  782. 'name',
  783. 'package',
  784. 'project',
  785. 'project status url',
  786. 'version',
  787. );
  788. return array_intersect_key($info, drupal_map_assoc($whitelist));
  789. }