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