update.fetch.inc

  1. drupal
    1. 6 modules/update/update.fetch.inc
    2. 7 modules/update/update.fetch.inc
    3. 8 core/modules/update/update.fetch.inc

Code required only when fetching information about available updates.

Functions & methods

NameDescription
update_fetch_data_batchProcess a step in the batch for fetching available update data.
update_fetch_data_finishedBatch API callback when all fetch tasks have been completed.
update_manual_statusCallback to manually check the update status without cron.
update_parse_xmlParse the XML of the Drupal release history info files.
_update_build_fetch_urlGenerates the URL to fetch information about project updates.
_update_create_fetch_taskAdd a task to the queue for fetching release history data for a project.
_update_cron_notifyPerform any notifications that should be done once cron fetches new data.
_update_fetch_dataAttempt to drain the queue of tasks for release history data to fetch.
_update_get_fetch_url_baseReturn the base of the URL to fetch available update data for a project.
_update_process_fetch_taskProcess a task to fetch available update data for a single project.
_update_refreshClear out all the cached available update data and initiate re-fetching.

File

modules/update/update.fetch.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * Code required only when fetching information about available updates.
  5. */
  6. /**
  7. * Callback to manually check the update status without cron.
  8. */
  9. function update_manual_status() {
  10. _update_refresh();
  11. $batch = array(
  12. 'operations' => array(
  13. array('update_fetch_data_batch', array()),
  14. ),
  15. 'finished' => 'update_fetch_data_finished',
  16. 'title' => t('Checking available update data'),
  17. 'progress_message' => t('Trying to check available update data ...'),
  18. 'error_message' => t('Error checking available update data.'),
  19. 'file' => drupal_get_path('module', 'update') . '/update.fetch.inc',
  20. );
  21. batch_set($batch);
  22. batch_process('admin/reports/updates');
  23. }
  24. /**
  25. * Process a step in the batch for fetching available update data.
  26. */
  27. function update_fetch_data_batch(&$context) {
  28. $queue = DrupalQueue::get('update_fetch_tasks');
  29. if (empty($context['sandbox']['max'])) {
  30. $context['finished'] = 0;
  31. $context['sandbox']['max'] = $queue->numberOfItems();
  32. $context['sandbox']['progress'] = 0;
  33. $context['message'] = t('Checking available update data ...');
  34. $context['results']['updated'] = 0;
  35. $context['results']['failures'] = 0;
  36. $context['results']['processed'] = 0;
  37. }
  38. // Grab another item from the fetch queue.
  39. for ($i = 0; $i < 5; $i++) {
  40. if ($item = $queue->claimItem()) {
  41. if (_update_process_fetch_task($item->data)) {
  42. $context['results']['updated']++;
  43. $context['message'] = t('Checked available update data for %title.', array('%title' => $item->data['info']['name']));
  44. }
  45. else {
  46. $context['message'] = t('Failed to check available update data for %title.', array('%title' => $item->data['info']['name']));
  47. $context['results']['failures']++;
  48. }
  49. $context['sandbox']['progress']++;
  50. $context['results']['processed']++;
  51. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  52. $queue->deleteItem($item);
  53. }
  54. else {
  55. // If the queue is currently empty, we're done. It's possible that
  56. // another thread might have added new fetch tasks while we were
  57. // processing this batch. In that case, the usual 'finished' math could
  58. // get confused, since we'd end up processing more tasks that we thought
  59. // we had when we started and initialized 'max' with numberOfItems(). By
  60. // forcing 'finished' to be exactly 1 here, we ensure that batch
  61. // processing is terminated.
  62. $context['finished'] = 1;
  63. return;
  64. }
  65. }
  66. }
  67. /**
  68. * Batch API callback when all fetch tasks have been completed.
  69. *
  70. * @param $success
  71. * Boolean indicating the success of the batch.
  72. * @param $results
  73. * Associative array holding the results of the batch, including the key
  74. * 'updated' which holds the total number of projects we fetched available
  75. * update data for.
  76. */
  77. function update_fetch_data_finished($success, $results) {
  78. if ($success) {
  79. if (!empty($results)) {
  80. if (!empty($results['updated'])) {
  81. drupal_set_message(format_plural($results['updated'], 'Checked available update data for one project.', 'Checked available update data for @count projects.'));
  82. }
  83. if (!empty($results['failures'])) {
  84. drupal_set_message(format_plural($results['failures'], 'Failed to get available update data for one project.', 'Failed to get available update data for @count projects.'), 'error');
  85. }
  86. }
  87. }
  88. else {
  89. drupal_set_message(t('An error occurred trying to get available update data.'), 'error');
  90. }
  91. }
  92. /**
  93. * Attempt to drain the queue of tasks for release history data to fetch.
  94. */
  95. function _update_fetch_data() {
  96. $queue = DrupalQueue::get('update_fetch_tasks');
  97. $end = time() + variable_get('update_max_fetch_time', UPDATE_MAX_FETCH_TIME);
  98. while (time() < $end && ($item = $queue->claimItem())) {
  99. _update_process_fetch_task($item->data);
  100. $queue->deleteItem($item);
  101. }
  102. }
  103. /**
  104. * Process a task to fetch available update data for a single project.
  105. *
  106. * Once the release history XML data is downloaded, it is parsed and saved
  107. * into the {cache_update} table in an entry just for that project.
  108. *
  109. * @param $project
  110. * Associative array of information about the project to fetch data for.
  111. * @return
  112. * TRUE if we fetched parsable XML, otherwise FALSE.
  113. */
  114. function _update_process_fetch_task($project) {
  115. global $base_url;
  116. $fail = &drupal_static(__FUNCTION__, array());
  117. // This can be in the middle of a long-running batch, so REQUEST_TIME won't
  118. // necessarily be valid.
  119. $now = time();
  120. if (empty($fail)) {
  121. // If we have valid data about release history XML servers that we have
  122. // failed to fetch from on previous attempts, load that from the cache.
  123. if (($cache = _update_cache_get('fetch_failures')) && ($cache->expire > $now)) {
  124. $fail = $cache->data;
  125. }
  126. }
  127. $max_fetch_attempts = variable_get('update_max_fetch_attempts', UPDATE_MAX_FETCH_ATTEMPTS);
  128. $success = FALSE;
  129. $available = array();
  130. $site_key = drupal_hmac_base64($base_url, drupal_get_private_key());
  131. $url = _update_build_fetch_url($project, $site_key);
  132. $fetch_url_base = _update_get_fetch_url_base($project);
  133. $project_name = $project['name'];
  134. if (empty($fail[$fetch_url_base]) || $fail[$fetch_url_base] < $max_fetch_attempts) {
  135. $xml = drupal_http_request($url);
  136. if (!isset($xml->error) && isset($xml->data)) {
  137. $data = $xml->data;
  138. }
  139. }
  140. if (!empty($data)) {
  141. $available = update_parse_xml($data);
  142. // @todo: Purge release data we don't need (http://drupal.org/node/238950).
  143. if (!empty($available)) {
  144. // Only if we fetched and parsed something sane do we return success.
  145. $success = TRUE;
  146. }
  147. }
  148. else {
  149. $available['project_status'] = 'not-fetched';
  150. if (empty($fail[$fetch_url_base])) {
  151. $fail[$fetch_url_base] = 1;
  152. }
  153. else {
  154. $fail[$fetch_url_base]++;
  155. }
  156. }
  157. $frequency = variable_get('update_check_frequency', 1);
  158. $cid = 'available_releases::' . $project_name;
  159. _update_cache_set($cid, $available, $now + (60 * 60 * 24 * $frequency));
  160. // Stash the $fail data back in the DB for the next 5 minutes.
  161. _update_cache_set('fetch_failures', $fail, $now + (60 * 5));
  162. // Whether this worked or not, we did just (try to) check for updates.
  163. variable_set('update_last_check', $now);
  164. // Now that we processed the fetch task for this project, clear out the
  165. // record in {cache_update} for this task so we're willing to fetch again.
  166. _update_cache_clear('fetch_task::' . $project_name);
  167. return $success;
  168. }
  169. /**
  170. * Clear out all the cached available update data and initiate re-fetching.
  171. */
  172. function _update_refresh() {
  173. module_load_include('inc', 'update', 'update.compare');
  174. // Since we're fetching new available update data, we want to clear
  175. // our cache of both the projects we care about, and the current update
  176. // status of the site. We do *not* want to clear the cache of available
  177. // releases just yet, since that data (even if it's stale) can be useful
  178. // during update_get_projects(); for example, to modules that implement
  179. // hook_system_info_alter() such as cvs_deploy.
  180. _update_cache_clear('update_project_projects');
  181. _update_cache_clear('update_project_data');
  182. $projects = update_get_projects();
  183. // Now that we have the list of projects, we should also clear our cache of
  184. // available release data, since even if we fail to fetch new data, we need
  185. // to clear out the stale data at this point.
  186. _update_cache_clear('available_releases::', TRUE);
  187. foreach ($projects as $key => $project) {
  188. update_create_fetch_task($project);
  189. }
  190. }
  191. /**
  192. * Add a task to the queue for fetching release history data for a project.
  193. *
  194. * We only create a new fetch task if there's no task already in the queue for
  195. * this particular project (based on 'fetch_task::' entries in the
  196. * {cache_update} table).
  197. *
  198. * @param $project
  199. * Associative array of information about a project as created by
  200. * update_get_projects(), including keys such as 'name' (short name),
  201. * and the 'info' array with data from a .info file for the project.
  202. *
  203. * @see update_get_projects()
  204. * @see update_get_available()
  205. * @see update_refresh()
  206. * @see update_fetch_data()
  207. * @see _update_process_fetch_task()
  208. */
  209. function _update_create_fetch_task($project) {
  210. $fetch_tasks = &drupal_static(__FUNCTION__, array());
  211. if (empty($fetch_tasks)) {
  212. $fetch_tasks = _update_get_cache_multiple('fetch_task');
  213. }
  214. $cid = 'fetch_task::' . $project['name'];
  215. if (empty($fetch_tasks[$cid])) {
  216. $queue = DrupalQueue::get('update_fetch_tasks');
  217. $queue->createItem($project);
  218. // Due to race conditions, it is possible that another process already
  219. // inserted a row into the {cache_update} table and the following query will
  220. // throw an exception.
  221. // @todo: Remove the need for the manual check by relying on a queue that
  222. // enforces unique items.
  223. try {
  224. db_insert('cache_update')
  225. ->fields(array(
  226. 'cid' => $cid,
  227. 'created' => REQUEST_TIME,
  228. ))
  229. ->execute();
  230. }
  231. catch (Exception $e) {
  232. // The exception can be ignored safely.
  233. }
  234. $fetch_tasks[$cid] = REQUEST_TIME;
  235. }
  236. }
  237. /**
  238. * Generates the URL to fetch information about project updates.
  239. *
  240. * This figures out the right URL to use, based on the project's .info file
  241. * and the global defaults. Appends optional query arguments when the site is
  242. * configured to report usage stats.
  243. *
  244. * @param $project
  245. * The array of project information from update_get_projects().
  246. * @param $site_key
  247. * The anonymous site key hash (optional).
  248. *
  249. * @see update_fetch_data()
  250. * @see _update_process_fetch_task()
  251. * @see update_get_projects()
  252. */
  253. function _update_build_fetch_url($project, $site_key = '') {
  254. $name = $project['name'];
  255. $url = _update_get_fetch_url_base($project);
  256. $url .= '/' . $name . '/' . DRUPAL_CORE_COMPATIBILITY;
  257. // Only append a site_key and the version information if we have a site_key
  258. // in the first place, and if this is not a disabled module or theme. We do
  259. // not want to record usage statistics for disabled code.
  260. if (!empty($site_key) && (strpos($project['project_type'], 'disabled') === FALSE)) {
  261. $url .= (strpos($url, '?') !== FALSE) ? '&' : '?';
  262. $url .= 'site_key=';
  263. $url .= rawurlencode($site_key);
  264. if (!empty($project['info']['version'])) {
  265. $url .= '&version=';
  266. $url .= rawurlencode($project['info']['version']);
  267. }
  268. }
  269. return $url;
  270. }
  271. /**
  272. * Return the base of the URL to fetch available update data for a project.
  273. *
  274. * @param $project
  275. * The array of project information from update_get_projects().
  276. * @return
  277. * The base of the URL used for fetching available update data. This does
  278. * not include the path elements to specify a particular project, version,
  279. * site_key, etc.
  280. *
  281. * @see _update_build_fetch_url()
  282. */
  283. function _update_get_fetch_url_base($project) {
  284. return isset($project['info']['project status url']) ? $project['info']['project status url'] : variable_get('update_fetch_url', UPDATE_DEFAULT_URL);
  285. }
  286. /**
  287. * Perform any notifications that should be done once cron fetches new data.
  288. *
  289. * This method checks the status of the site using the new data and depending
  290. * on the configuration of the site, notifies administrators via email if there
  291. * are new releases or missing security updates.
  292. *
  293. * @see update_requirements()
  294. */
  295. function _update_cron_notify() {
  296. module_load_install('update');
  297. $status = update_requirements('runtime');
  298. $params = array();
  299. $notify_all = (variable_get('update_notification_threshold', 'all') == 'all');
  300. foreach (array('core', 'contrib') as $report_type) {
  301. $type = 'update_' . $report_type;
  302. if (isset($status[$type]['severity'])
  303. && ($status[$type]['severity'] == REQUIREMENT_ERROR || ($notify_all && $status[$type]['reason'] == UPDATE_NOT_CURRENT))) {
  304. $params[$report_type] = $status[$type]['reason'];
  305. }
  306. }
  307. if (!empty($params)) {
  308. $notify_list = variable_get('update_notify_emails', '');
  309. if (!empty($notify_list)) {
  310. $default_language = language_default();
  311. foreach ($notify_list as $target) {
  312. if ($target_user = user_load_by_mail($target)) {
  313. $target_language = user_preferred_language($target_user);
  314. }
  315. else {
  316. $target_language = $default_language;
  317. }
  318. drupal_mail('update', 'status_notify', $target, $target_language, $params);
  319. }
  320. }
  321. }
  322. }
  323. /**
  324. * Parse the XML of the Drupal release history info files.
  325. *
  326. * @param $raw_xml
  327. * A raw XML string of available release data for a given project.
  328. *
  329. * @return
  330. * Array of parsed data about releases for a given project, or NULL if there
  331. * was an error parsing the string.
  332. */
  333. function update_parse_xml($raw_xml) {
  334. try {
  335. $xml = new SimpleXMLElement($raw_xml);
  336. }
  337. catch (Exception $e) {
  338. // SimpleXMLElement::__construct produces an E_WARNING error message for
  339. // each error found in the XML data and throws an exception if errors
  340. // were detected. Catch any exception and return failure (NULL).
  341. return;
  342. }
  343. // If there is no valid project data, the XML is invalid, so return failure.
  344. if (!isset($xml->short_name)) {
  345. return;
  346. }
  347. $short_name = (string) $xml->short_name;
  348. $data = array();
  349. foreach ($xml as $k => $v) {
  350. $data[$k] = (string) $v;
  351. }
  352. $data['releases'] = array();
  353. if (isset($xml->releases)) {
  354. foreach ($xml->releases->children() as $release) {
  355. $version = (string) $release->version;
  356. $data['releases'][$version] = array();
  357. foreach ($release->children() as $k => $v) {
  358. $data['releases'][$version][$k] = (string) $v;
  359. }
  360. $data['releases'][$version]['terms'] = array();
  361. if ($release->terms) {
  362. foreach ($release->terms->children() as $term) {
  363. if (!isset($data['releases'][$version]['terms'][(string) $term->name])) {
  364. $data['releases'][$version]['terms'][(string) $term->name] = array();
  365. }
  366. $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value;
  367. }
  368. }
  369. }
  370. }
  371. return $data;
  372. }
Login or register to post comments