/[drupal]/drupal/modules/update/update.fetch.inc
ViewVC logotype

Contents of /drupal/modules/update/update.fetch.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


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

  ViewVC Help
Powered by ViewVC 1.1.2