Projet

Général

Profil

Paste
Télécharger (14,6 ko) Statistiques
| Branche: | Révision:

root / drupal7 / modules / update / update.fetch.inc @ 08f5d39b

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
 * Batch callback: Processes a step in batch for fetching available update data.
33
 *
34
 * @param $context
35
 *   Reference to an array used for Batch API storage.
36
 */
37
function update_fetch_data_batch(&$context) {
38
  $queue = DrupalQueue::get('update_fetch_tasks');
39
  if (empty($context['sandbox']['max'])) {
40
    $context['finished'] = 0;
41
    $context['sandbox']['max'] = $queue->numberOfItems();
42
    $context['sandbox']['progress'] = 0;
43
    $context['message'] = t('Checking available update data ...');
44
    $context['results']['updated'] = 0;
45
    $context['results']['failures'] = 0;
46
    $context['results']['processed'] = 0;
47
  }
48

    
49
  // Grab another item from the fetch queue.
50
  for ($i = 0; $i < 5; $i++) {
51
    if ($item = $queue->claimItem()) {
52
      if (_update_process_fetch_task($item->data)) {
53
        $context['results']['updated']++;
54
        $context['message'] = t('Checked available update data for %title.', array('%title' => $item->data['info']['name']));
55
      }
56
      else {
57
        $context['message'] = t('Failed to check available update data for %title.', array('%title' => $item->data['info']['name']));
58
        $context['results']['failures']++;
59
      }
60
      $context['sandbox']['progress']++;
61
      $context['results']['processed']++;
62
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
63
      $queue->deleteItem($item);
64
    }
65
    else {
66
      // If the queue is currently empty, we're done. It's possible that
67
      // another thread might have added new fetch tasks while we were
68
      // processing this batch. In that case, the usual 'finished' math could
69
      // get confused, since we'd end up processing more tasks that we thought
70
      // we had when we started and initialized 'max' with numberOfItems(). By
71
      // forcing 'finished' to be exactly 1 here, we ensure that batch
72
      // processing is terminated.
73
      $context['finished'] = 1;
74
      return;
75
    }
76
  }
77
}
78

    
79
/**
80
 * Batch callback: Performs actions when all fetch tasks have been completed.
81
 *
82
 * @param $success
83
 *   TRUE if the batch operation was successful; FALSE if there were errors.
84
 * @param $results
85
 *   An associative array of results from the batch operation, including the key
86
 *   'updated' which holds the total number of projects we fetched available
87
 *   update data for.
88
 */
89
function update_fetch_data_finished($success, $results) {
90
  if ($success) {
91
    if (!empty($results)) {
92
      if (!empty($results['updated'])) {
93
        drupal_set_message(format_plural($results['updated'], 'Checked available update data for one project.', 'Checked available update data for @count projects.'));
94
      }
95
      if (!empty($results['failures'])) {
96
        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');
97
      }
98
    }
99
  }
100
  else {
101
    drupal_set_message(t('An error occurred trying to get available update data.'), 'error');
102
  }
103
}
104

    
105
/**
106
 * Attempts to drain the queue of tasks for release history data to fetch.
107
 */
108
function _update_fetch_data() {
109
  $queue = DrupalQueue::get('update_fetch_tasks');
110
  $end = time() + variable_get('update_max_fetch_time', UPDATE_MAX_FETCH_TIME);
111
  while (time() < $end && ($item = $queue->claimItem())) {
112
    _update_process_fetch_task($item->data);
113
    $queue->deleteItem($item);
114
  }
115
}
116

    
117
/**
118
 * Processes a task to fetch available update data for a single project.
119
 *
120
 * Once the release history XML data is downloaded, it is parsed and saved into
121
 * the {cache_update} table in an entry just for that project.
122
 *
123
 * @param $project
124
 *   Associative array of information about the project to fetch data for.
125
 *
126
 * @return
127
 *   TRUE if we fetched parsable XML, otherwise FALSE.
128
 */
129
function _update_process_fetch_task($project) {
130
  global $base_url;
131
  $fail = &drupal_static(__FUNCTION__, array());
132
  // This can be in the middle of a long-running batch, so REQUEST_TIME won't
133
  // necessarily be valid.
134
  $now = time();
135
  if (empty($fail)) {
136
    // If we have valid data about release history XML servers that we have
137
    // failed to fetch from on previous attempts, load that from the cache.
138
    if (($cache = _update_cache_get('fetch_failures')) && ($cache->expire > $now)) {
139
      $fail = $cache->data;
140
    }
141
  }
142

    
143
  $max_fetch_attempts = variable_get('update_max_fetch_attempts', UPDATE_MAX_FETCH_ATTEMPTS);
144

    
145
  $success = FALSE;
146
  $available = array();
147
  $site_key = drupal_hmac_base64($base_url, drupal_get_private_key());
148
  $url = _update_build_fetch_url($project, $site_key);
149
  $fetch_url_base = _update_get_fetch_url_base($project);
150
  $project_name = $project['name'];
151

    
152
  if (empty($fail[$fetch_url_base]) || $fail[$fetch_url_base] < $max_fetch_attempts) {
153
    $xml = drupal_http_request($url);
154
    if (!isset($xml->error) && isset($xml->data)) {
155
      $data = $xml->data;
156
    }
157
  }
158

    
159
  if (!empty($data)) {
160
    $available = update_parse_xml($data);
161
    // @todo: Purge release data we don't need (http://drupal.org/node/238950).
162
    if (!empty($available)) {
163
      // Only if we fetched and parsed something sane do we return success.
164
      $success = TRUE;
165
    }
166
  }
167
  else {
168
    $available['project_status'] = 'not-fetched';
169
    if (empty($fail[$fetch_url_base])) {
170
      $fail[$fetch_url_base] = 1;
171
    }
172
    else {
173
      $fail[$fetch_url_base]++;
174
    }
175
  }
176

    
177
  $frequency = variable_get('update_check_frequency', 1);
178
  $cid = 'available_releases::' . $project_name;
179
  _update_cache_set($cid, $available, $now + (60 * 60 * 24 * $frequency));
180

    
181
  // Stash the $fail data back in the DB for the next 5 minutes.
182
  _update_cache_set('fetch_failures', $fail, $now + (60 * 5));
183

    
184
  // Whether this worked or not, we did just (try to) check for updates.
185
  variable_set('update_last_check', $now);
186

    
187
  // Now that we processed the fetch task for this project, clear out the
188
  // record in {cache_update} for this task so we're willing to fetch again.
189
  _update_cache_clear('fetch_task::' . $project_name);
190

    
191
  return $success;
192
}
193

    
194
/**
195
 * Clears out all the cached available update data and initiates re-fetching.
196
 */
197
function _update_refresh() {
198
  module_load_include('inc', 'update', 'update.compare');
199

    
200
  // Since we're fetching new available update data, we want to clear
201
  // our cache of both the projects we care about, and the current update
202
  // status of the site. We do *not* want to clear the cache of available
203
  // releases just yet, since that data (even if it's stale) can be useful
204
  // during update_get_projects(); for example, to modules that implement
205
  // hook_system_info_alter() such as cvs_deploy.
206
  _update_cache_clear('update_project_projects');
207
  _update_cache_clear('update_project_data');
208

    
209
  $projects = update_get_projects();
210

    
211
  // Now that we have the list of projects, we should also clear our cache of
212
  // available release data, since even if we fail to fetch new data, we need
213
  // to clear out the stale data at this point.
214
  _update_cache_clear('available_releases::', TRUE);
215

    
216
  foreach ($projects as $key => $project) {
217
    update_create_fetch_task($project);
218
  }
219
}
220

    
221
/**
222
 * Adds a task to the queue for fetching release history data for a project.
223
 *
224
 * We only create a new fetch task if there's no task already in the queue for
225
 * this particular project (based on 'fetch_task::' entries in the
226
 * {cache_update} table).
227
 *
228
 * @param $project
229
 *   Associative array of information about a project as created by
230
 *   update_get_projects(), including keys such as 'name' (short name), and the
231
 *   'info' array with data from a .info file for the project.
232
 *
233
 * @see update_get_projects()
234
 * @see update_get_available()
235
 * @see update_refresh()
236
 * @see update_fetch_data()
237
 * @see _update_process_fetch_task()
238
 */
239
function _update_create_fetch_task($project) {
240
  $fetch_tasks = &drupal_static(__FUNCTION__, array());
241
  if (empty($fetch_tasks)) {
242
    $fetch_tasks = _update_get_cache_multiple('fetch_task');
243
  }
244
  $cid = 'fetch_task::' . $project['name'];
245
  if (empty($fetch_tasks[$cid])) {
246
    $queue = DrupalQueue::get('update_fetch_tasks');
247
    $queue->createItem($project);
248
    // Due to race conditions, it is possible that another process already
249
    // inserted a row into the {cache_update} table and the following query will
250
    // throw an exception.
251
    // @todo: Remove the need for the manual check by relying on a queue that
252
    // enforces unique items.
253
    try {
254
      db_insert('cache_update')
255
        ->fields(array(
256
          'cid' => $cid,
257
          'created' => REQUEST_TIME,
258
        ))
259
        ->execute();
260
    }
261
    catch (Exception $e) {
262
      // The exception can be ignored safely.
263
    }
264
    $fetch_tasks[$cid] = REQUEST_TIME;
265
  }
266
}
267

    
268
/**
269
 * Generates the URL to fetch information about project updates.
270
 *
271
 * This figures out the right URL to use, based on the project's .info file and
272
 * the global defaults. Appends optional query arguments when the site is
273
 * configured to report usage stats.
274
 *
275
 * @param $project
276
 *   The array of project information from update_get_projects().
277
 * @param $site_key
278
 *   (optional) The anonymous site key hash. Defaults to an empty string.
279
 *
280
 * @return
281
 *   The URL for fetching information about updates to the specified project.
282
 *
283
 * @see update_fetch_data()
284
 * @see _update_process_fetch_task()
285
 * @see update_get_projects()
286
 */
287
function _update_build_fetch_url($project, $site_key = '') {
288
  $name = $project['name'];
289
  $url = _update_get_fetch_url_base($project);
290
  $url .= '/' . $name . '/' . DRUPAL_CORE_COMPATIBILITY;
291

    
292
  // Only append usage information if we have a site key and the project is
293
  // enabled. We do not want to record usage statistics for disabled projects.
294
  if (!empty($site_key) && (strpos($project['project_type'], 'disabled') === FALSE)) {
295
    // Append the site key.
296
    $url .= (strpos($url, '?') !== FALSE) ? '&' : '?';
297
    $url .= 'site_key=';
298
    $url .= rawurlencode($site_key);
299

    
300
    // Append the version.
301
    if (!empty($project['info']['version'])) {
302
      $url .= '&version=';
303
      $url .= rawurlencode($project['info']['version']);
304
    }
305

    
306
    // Append the list of modules or themes enabled.
307
    $list = array_keys($project['includes']);
308
    $url .= '&list=';
309
    $url .= rawurlencode(implode(',', $list));
310
  }
311
  return $url;
312
}
313

    
314
/**
315
 * Returns the base of the URL to fetch available update data for a project.
316
 *
317
 * @param $project
318
 *   The array of project information from update_get_projects().
319
 *
320
 * @return
321
 *   The base of the URL used for fetching available update data. This does
322
 *   not include the path elements to specify a particular project, version,
323
 *   site_key, etc.
324
 *
325
 * @see _update_build_fetch_url()
326
 */
327
function _update_get_fetch_url_base($project) {
328
  return isset($project['info']['project status url']) ? $project['info']['project status url'] : variable_get('update_fetch_url', UPDATE_DEFAULT_URL);
329
}
330

    
331
/**
332
 * Performs any notifications that should be done once cron fetches new data.
333
 *
334
 * This method checks the status of the site using the new data and, depending
335
 * on the configuration of the site, notifies administrators via e-mail if there
336
 * are new releases or missing security updates.
337
 *
338
 * @see update_requirements()
339
 */
340
function _update_cron_notify() {
341
  module_load_install('update');
342
  $status = update_requirements('runtime');
343
  $params = array();
344
  $notify_all = (variable_get('update_notification_threshold', 'all') == 'all');
345
  foreach (array('core', 'contrib') as $report_type) {
346
    $type = 'update_' . $report_type;
347
    if (isset($status[$type]['severity'])
348
        && ($status[$type]['severity'] == REQUIREMENT_ERROR || ($notify_all && $status[$type]['reason'] == UPDATE_NOT_CURRENT))) {
349
      $params[$report_type] = $status[$type]['reason'];
350
    }
351
  }
352
  if (!empty($params)) {
353
    $notify_list = variable_get('update_notify_emails', '');
354
    if (!empty($notify_list)) {
355
      $default_language = language_default();
356
      foreach ($notify_list as $target) {
357
        if ($target_user = user_load_by_mail($target)) {
358
          $target_language = user_preferred_language($target_user);
359
        }
360
        else {
361
          $target_language = $default_language;
362
        }
363
        $message = drupal_mail('update', 'status_notify', $target, $target_language, $params);
364
        // Track when the last mail was successfully sent to avoid sending
365
        // too many e-mails.
366
        if ($message['result']) {
367
          variable_set('update_last_email_notification', REQUEST_TIME);
368
        }
369
      }
370
    }
371
  }
372
}
373

    
374
/**
375
 * Parses the XML of the Drupal release history info files.
376
 *
377
 * @param $raw_xml
378
 *   A raw XML string of available release data for a given project.
379
 *
380
 * @return
381
 *   Array of parsed data about releases for a given project, or NULL if there
382
 *   was an error parsing the string.
383
 */
384
function update_parse_xml($raw_xml) {
385
  try {
386
    $xml = new SimpleXMLElement($raw_xml);
387
  }
388
  catch (Exception $e) {
389
    // SimpleXMLElement::__construct produces an E_WARNING error message for
390
    // each error found in the XML data and throws an exception if errors
391
    // were detected. Catch any exception and return failure (NULL).
392
    return;
393
  }
394
  // If there is no valid project data, the XML is invalid, so return failure.
395
  if (!isset($xml->short_name)) {
396
    return;
397
  }
398
  $short_name = (string) $xml->short_name;
399
  $data = array();
400
  foreach ($xml as $k => $v) {
401
    $data[$k] = (string) $v;
402
  }
403
  $data['releases'] = array();
404
  if (isset($xml->releases)) {
405
    foreach ($xml->releases->children() as $release) {
406
      $version = (string) $release->version;
407
      $data['releases'][$version] = array();
408
      foreach ($release->children() as $k => $v) {
409
        $data['releases'][$version][$k] = (string) $v;
410
      }
411
      $data['releases'][$version]['terms'] = array();
412
      if ($release->terms) {
413
        foreach ($release->terms->children() as $term) {
414
          if (!isset($data['releases'][$version]['terms'][(string) $term->name])) {
415
            $data['releases'][$version]['terms'][(string) $term->name] = array();
416
          }
417
          $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value;
418
        }
419
      }
420
    }
421
  }
422
  return $data;
423
}