update.module 34.6 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
6
7
8
9
 * Handles updates of Drupal core and contributed projects.
 *
 * The module checks for available updates of Drupal core and any installed
 * contributed modules and themes. It warns site administrators if newer
 * releases are available via the system status report (admin/reports/status),
10
 * the module and theme pages, and optionally via email. It also provides the
11
 * ability to install contributed modules and themes via an user interface.
12
13
 */

14
use Drupal\Core\File\Exception\FileException;
15
use Drupal\Core\Link;
16
use Drupal\Core\Url;
17
use Drupal\Core\Form\FormStateInterface;
18
use Drupal\Core\Routing\RouteMatchInterface;
19
use Drupal\Core\Site\Settings;
20

21
// These are internally used constants for this code, do not modify.
22
23

/**
24
 * Project is missing security update(s).
25
26
27
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateManagerInterface::NOT_SECURE instead.
28
29
 *
 * @see https://www.drupal.org/node/2831620
30
 */
31
const UPDATE_NOT_SECURE = 1;
32
33

/**
34
 * Current release has been unpublished and is no longer available.
35
36
37
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateManagerInterface::REVOKED instead.
38
39
 *
 * @see https://www.drupal.org/node/2831620
40
 */
41
const UPDATE_REVOKED = 2;
42
43
44

/**
 * Current release is no longer supported by the project maintainer.
45
46
47
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateManagerInterface::NOT_SUPPORTED instead.
48
49
 *
 * @see https://www.drupal.org/node/2831620
50
 */
51
const UPDATE_NOT_SUPPORTED = 3;
52
53
54

/**
 * Project has a new release available, but it is not a security release.
55
56
57
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateManagerInterface::NOT_CURRENT instead.
58
59
 *
 * @see https://www.drupal.org/node/2831620
60
 */
61
const UPDATE_NOT_CURRENT = 4;
62
63
64

/**
 * Project is up to date.
65
66
67
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateManagerInterface::CURRENT instead.
68
69
 *
 * @see https://www.drupal.org/node/2831620
70
 */
71
const UPDATE_CURRENT = 5;
72
73
74

/**
 * Project's status cannot be checked.
75
76
77
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateFetcherInterface::NOT_CHECKED instead.
78
79
 *
 * @see https://www.drupal.org/node/2831620
80
 */
81
const UPDATE_NOT_CHECKED = -1;
82
83
84

/**
 * No available update data was found for project.
85
86
87
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateFetcherInterface::UNKNOWN instead.
88
89
 *
 * @see https://www.drupal.org/node/2831620
90
 */
91
const UPDATE_UNKNOWN = -2;
92

93
94
/**
 * There was a failure fetching available update data for this project.
95
96
97
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateFetcherInterface::NOT_FETCHED instead.
98
99
 *
 * @see https://www.drupal.org/node/2831620
100
 */
101
const UPDATE_NOT_FETCHED = -3;
102

103
104
/**
 * We need to (re)fetch available update data for this project.
105
106
107
 *
 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
 *   Use \Drupal\update\UpdateFetcherInterface::FETCH_PENDING instead.
108
109
 *
 * @see https://www.drupal.org/node/2831620
110
 */
111
const UPDATE_FETCH_PENDING = -4;
112

113
/**
114
 * Implements hook_help().
115
 */
116
function update_help($route_name, RouteMatchInterface $route_match) {
117
118
  switch ($route_name) {
    case 'help.page.update':
119
120
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
121
      $output .= '<p>' . t('The Update Manager module periodically checks for new versions of your site\'s software (including contributed modules and themes), and alerts administrators to available updates. The Update Manager system is also used by some other modules to manage updates and downloads; for example, the Interface Translation module uses the Update Manager to download translations from the localization server. Note that whenever the Update Manager system is used, anonymous usage statistics are sent to Drupal.org. If desired, you may disable the Update Manager module from the <a href=":modules">Extend page</a>; if you do so, functionality that depends on the Update Manager system will not work. For more information, see the <a href=":update">online documentation for the Update Manager module</a>.', [':update' => 'https://www.drupal.org/documentation/modules/update', ':modules' => Url::fromRoute('system.modules_list')->toString()]) . '</p>';
122
      // Only explain the Update manager if it has not been disabled.
123
      if (_update_manager_access()) {
124
        $output .= '<p>' . t('The Update Manager also allows administrators to update and install modules and themes through the administration interface.') . '</p>';
125
126
127
128
      }
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Checking for available updates') . '</dt>';
129
      $output .= '<dd>' . t('The <a href=":update-report">Available updates report</a> displays core, contributed modules, and themes for which there are new releases available for download. On the report page, you can also check manually for updates. You can configure the frequency of update checks, which are performed during cron runs, and whether notifications are sent on the <a href=":update-settings">Update Manager settings page</a>.', [':update-report' => Url::fromRoute('update.status')->toString(), ':update-settings' => Url::fromRoute('update.settings')->toString()]) . '</dd>';
130
      // Only explain the Update manager if it has not been disabled.
131
      if (_update_manager_access()) {
132
        $output .= '<dt>' . t('Performing updates through the Update page') . '</dt>';
133
        $output .= '<dd>' . t('The Update Manager module allows administrators to perform updates directly from the <a href=":update-page">Update page</a>. It lists all available updates, and you can confirm whether you want to download them. If you don\'t have sufficient access rights to your web server, you could be prompted for your FTP/SSH password. Afterwards the files are transferred into your site installation, overwriting your old files. Direct links to the Update page are also displayed on the <a href=":modules_page">Extend page</a> and the <a href=":themes_page">Appearance page</a>.', [':modules_page' => Url::fromRoute('system.modules_list')->toString(), ':themes_page' => Url::fromRoute('system.themes_page')->toString(), ':update-page' => Url::fromRoute('update.report_update')->toString()]) . '</dd>';
134
        $output .= '<dt>' . t('Installing new modules and themes through the Install page') . '</dt>';
135
        $output .= '<dd>' . t('You can also install new modules and themes in the same fashion, through the <a href=":install">Install page</a>, or by clicking the <em>Install new module/theme</em> links at the top of the <a href=":modules_page">Extend page</a> and the <a href=":themes_page">Appearance page</a>. In this case, you are prompted to provide either the URL to the download, or to upload a packaged release file from your local computer.', [':modules_page' => Url::fromRoute('system.modules_list')->toString(), ':themes_page' => Url::fromRoute('system.themes_page')->toString(), ':install' => Url::fromRoute('update.report_install')->toString()]) . '</dd>';
136
137
      }
      $output .= '</dl>';
138
      return $output;
139
140
141

    case 'update.status':
      return '<p>' . t('Here you can find information about available updates for your installed modules and themes. Note that each module or theme is part of a "project", which may or may not have the same name, and might include multiple modules or themes within it.') . '</p>';
142
143
144

    case 'system.modules_list':
      if (_update_manager_access()) {
145
        $output = '<p>' . t('Regularly review and install <a href=":updates">available updates</a> to maintain a secure and current site. Always run the <a href=":update-php">update script</a> each time a module is updated.', [':update-php' => Url::fromRoute('system.db_update')->toString(), ':updates' => Url::fromRoute('update.status')->toString()]) . '</p>';
146
147
      }
      else {
148
        $output = '<p>' . t('Regularly review <a href=":updates">available updates</a> to maintain a secure and current site. Always run the <a href=":update-php">update script</a> each time a module is updated.', [':update-php' => Url::fromRoute('system.db_update')->toString(), ':updates' => Url::fromRoute('update.status')->toString()]) . '</p>';
149
150
      }
      return $output;
151
152
  }
}
153

154
/**
155
 * Implements hook_page_top().
156
 */
157
function update_page_top() {
158
159
  /** @var \Drupal\Core\Routing\AdminContext $admin_context */
  $admin_context = \Drupal::service('router.admin_context');
160
161
  $route_match = \Drupal::routeMatch();
  if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('administer site configuration')) {
162
163
    $route_name = \Drupal::routeMatch()->getRouteName();
    switch ($route_name) {
164
      // These pages don't need additional nagging.
165
166
167
168
169
170
171
172
173
174
      case 'update.theme_update':
      case 'system.theme_install':
      case 'update.module_update':
      case 'update.module_install':
      case 'update.status':
      case 'update.report_update':
      case 'update.report_install':
      case 'update.settings':
      case 'system.status':
      case 'update.confirmation_page':
175
176
177
178
        return;

      // If we are on the appearance or modules list, display a detailed report
      // of the update status.
179
180
      case 'system.themes_page':
      case 'system.modules_list':
181
182
183
184
        $verbose = TRUE;
        break;

    }
185
186
    module_load_install('update');
    $status = update_requirements('runtime');
187
    foreach (['core', 'contrib'] as $report_type) {
188
      $type = 'update_' . $report_type;
189
      // hook_requirements() supports render arrays therefore we need to render
190
191
      // them before using
      // \Drupal\Core\Messenger\MessengerInterface::addStatus().
192
193
194
      if (isset($status[$type]['description']) && is_array($status[$type]['description'])) {
        $status[$type]['description'] = \Drupal::service('renderer')->renderPlain($status[$type]['description']);
      }
195
      if (!empty($verbose)) {
196
197
        if (isset($status[$type]['severity'])) {
          if ($status[$type]['severity'] == REQUIREMENT_ERROR) {
198
            \Drupal::messenger()->addError($status[$type]['description']);
199
          }
200
          elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) {
201
            \Drupal::messenger()->addWarning($status[$type]['description']);
202
203
204
205
206
          }
        }
      }
      // Otherwise, if we're on *any* admin page and there's a security
      // update missing, print an error message about it.
207
      else {
208
209
210
        if (isset($status[$type])
            && isset($status[$type]['reason'])
            && $status[$type]['reason'] === UPDATE_NOT_SECURE) {
211
          \Drupal::messenger()->addError($status[$type]['description']);
212
213
        }
      }
214
    }
215
216
217
218
  }
}

/**
219
 * Resolves if the current user can access updater menu items.
220
221
222
 *
 * It both enforces the 'administer software updates' permission and the global
 * kill switch for the authorize.php script.
223
 *
224
225
226
 * @return
 *   TRUE if the current user can access the updater menu items; FALSE
 *   otherwise.
227
 */
228
function _update_manager_access() {
229
  return Settings::get('allow_authorize_operations', TRUE) && \Drupal::currentUser()->hasPermission('administer software updates');
230
231
232
}

/**
233
 * Implements hook_theme().
234
235
 */
function update_theme() {
236
237
238
239
240
241
  return [
    'update_last_check' => [
      'variables' => ['last' => 0],
    ],
    'update_report' => [
      'variables' => ['data' => NULL],
242
      'file' => 'update.report.inc',
243
244
245
    ],
    'update_project_status' => [
      'variables' => ['project' => []],
246
      'file' => 'update.report.inc',
247
    ],
248
249
    // We are using template instead of '#type' => 'table' here to keep markup
    // out of preprocess and allow for easier changes to markup.
250
251
    'update_version' => [
      'variables' => ['version' => NULL, 'title' => NULL, 'attributes' => []],
252
      'file' => 'update.report.inc',
253
254
    ],
  ];
255
256
257
}

/**
258
 * Implements hook_cron().
259
260
 */
function update_cron() {
261
  $update_config = \Drupal::config('update.settings');
262
  $frequency = $update_config->get('check.interval_days');
263
  $interval = 60 * 60 * 24 * $frequency;
264
  $last_check = \Drupal::state()->get('update.last_check') ?: 0;
265
  if ((REQUEST_TIME - $last_check) > $interval) {
266
    // If the configured update interval has elapsed, we want to invalidate
267
    // the data for all projects, attempt to re-fetch, and trigger any
268
    // configured notifications about the new status.
269
    update_refresh();
270
    update_fetch_data();
271
  }
272
273
274
275
276
  else {
    // Otherwise, see if any individual projects are now stale or still
    // missing data, and if so, try to fetch the data.
    update_get_available(TRUE);
  }
277
  $last_email_notice = \Drupal::state()->get('update.last_email_notification') ?: 0;
278
  if ((REQUEST_TIME - $last_email_notice) > $interval) {
279
280
281
282
283
    // If configured time between notifications elapsed, send email about
    // updates possibly available.
    module_load_include('inc', 'update', 'update.fetch');
    _update_cron_notify();
  }
284
285
286

  // Clear garbage from disk.
  update_clear_update_disk_cache();
287
288
289
}

/**
290
 * Implements hook_themes_installed().
291
 *
292
 * If themes are installed, we invalidate the information of available updates.
293
 */
294
function update_themes_installed($themes) {
295
296
  // Clear all update module data.
  update_storage_clear();
297
298
299
}

/**
300
 * Implements hook_themes_uninstalled().
301
 *
302
 * If themes are uninstalled, we invalidate the information of available updates.
303
 */
304
function update_themes_uninstalled($themes) {
305
306
  // Clear all update module data.
  update_storage_clear();
307
308
309
}

/**
310
 * Implements hook_form_FORM_ID_alter() for system_modules().
311
 *
312
 * Adds a form submission handler to the system modules form, so that if a site
313
 * admin saves the form, we invalidate the information of available updates.
314
 *
315
 * @see _update_cache_clear()
316
 */
317
function update_form_system_modules_alter(&$form, FormStateInterface $form_state) {
318
  $form['#submit'][] = 'update_storage_clear_submit';
319
320
321
}

/**
322
323
324
 * Form submission handler for system_modules().
 *
 * @see update_form_system_modules_alter()
325
 */
326
function update_storage_clear_submit($form, FormStateInterface $form_state) {
327
328
  // Clear all update module data.
  update_storage_clear();
329
330
331
}

/**
332
 * Returns a warning message when there is no data about available updates.
333
334
 */
function _update_no_data() {
335
  $destination = \Drupal::destination()->getAsArray();
336
  return t('No update information available. <a href=":run_cron">Run cron</a> or <a href=":check_manually">check manually</a>.', [
337
338
    ':run_cron' => Url::fromRoute('system.run_cron', [], ['query' => $destination])->toString(),
    ':check_manually' => Url::fromRoute('update.manual_status', [], ['query' => $destination])->toString(),
339
  ]);
340
341
342
}

/**
343
 * Tries to get update information and refreshes it when necessary.
344
 *
345
 * In addition to checking the lifetime, this function also ensures that
346
 * there are no .info.yml files for enabled modules or themes that have a newer
347
 * modification timestamp than the last time we checked for available update
348
349
350
351
 * data. If any .info.yml file was modified, it almost certainly means a new
 * version of something was installed. Without fresh available update data, the
 * logic in update_calculate_project_data() will be wrong and produce confusing,
 * bogus results.
352
 *
353
 * @param $refresh
354
355
 *   (optional) Boolean to indicate if this method should refresh automatically
 *   if there's no data. Defaults to FALSE.
356
357
358
 *
 * @return
 *   Array of data about available releases, keyed by project shortname.
359
360
 *
 * @see update_refresh()
361
 * @see \Drupal\Update\UpdateManager::getProjects()
362
363
 */
function update_get_available($refresh = FALSE) {
364
  module_load_include('inc', 'update', 'update.compare');
365
  $needs_refresh = FALSE;
366

367
  // Grab whatever data we currently have.
368
  $available = \Drupal::keyValueExpirable('update_available_releases')->getAll();
369
  $projects = \Drupal::service('update.manager')->getProjects();
370
  foreach ($projects as $key => $project) {
371
372
    // If there's no data at all, we clearly need to fetch some.
    if (empty($available[$key])) {
373
      // update_create_fetch_task($project);
374
      \Drupal::service('update.processor')->createFetchTask($project);
375
376
377
378
      $needs_refresh = TRUE;
      continue;
    }

379
380
381
382
    // See if the .info.yml file is newer than the last time we checked for
    // data, and if so, mark this project's data as needing to be re-fetched.
    // Any time an admin upgrades their local installation, the .info.yml file
    // will be changed, so this is the only way we can be sure we're not showing
383
384
385
386
387
388
389
    // bogus information right after they upgrade.
    if ($project['info']['_info_file_ctime'] > $available[$key]['last_fetch']) {
      $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING;
    }

    // If we have project data but no release data, we need to fetch. This
    // can be triggered when we fail to contact a release history server.
390
    if (empty($available[$key]['releases']) && !$available[$key]['last_fetch']) {
391
392
393
394
395
396
      $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING;
    }

    // If we think this project needs to fetch, actually create the task now
    // and remember that we think we're missing some data.
    if (!empty($available[$key]['fetch_status']) && $available[$key]['fetch_status'] == UPDATE_FETCH_PENDING) {
397
      \Drupal::service('update.processor')->createFetchTask($project);
398
399
400
      $needs_refresh = TRUE;
    }
  }
401
402
403
404
405

  if ($needs_refresh && $refresh) {
    // Attempt to drain the queue of fetch tasks.
    update_fetch_data();
    // After processing the queue, we've (hopefully) got better data, so pull
406
    // the latest data again and use that directly.
407
    $available = \Drupal::keyValueExpirable('update_available_releases')->getAll();
408
  }
409

410
411
412
  return $available;
}

413
414
415
416
417
418
419
420
421
422
423
424
425
/**
 * Identifies equivalent security releases with a hardcoded list.
 *
 * Generally, only the latest minor version of Drupal 8 is supported. However,
 * when security fixes are backported to an old branch, and the site owner
 * updates to the release containing the backported fix, they should not
 * see "Security update required!" again if the only other security releases
 * are releases for the same advisories.
 *
 * @return string[]
 *   A list of security release numbers that are equivalent to this release
 *   (i.e. covered by the same advisory), for backported security fixes only.
 *
426
427
428
429
430
 * @internal
 *
 * @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use the
 *   'Insecure' release type tag in update XML provided by Drupal.org to
 *   determine if releases are insecure.
431
432
 */
function _update_equivalent_security_releases() {
433
  trigger_error("_update_equivalent_security_releases() was a temporary fix and will be removed before 9.0.0. Use the 'Insecure' release type tag in update XML provided by Drupal.org to determine if releases are insecure.", E_USER_DEPRECATED);
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
  switch (\Drupal::VERSION) {
    case '8.3.8':
      return ['8.4.5', '8.5.0-rc1'];
    case '8.3.9':
      return ['8.4.6', '8.5.1'];
    case '8.4.5':
      return ['8.5.0-rc1'];
    case '8.4.6':
      return ['8.5.1'];
    case '8.4.7':
      return ['8.5.2'];
    case '8.4.8':
      return ['8.5.3'];
  }

  return [];
}

452
/**
453
454
455
456
 * Adds a task to the queue for fetching release history data for a project.
 *
 * We only create a new fetch task if there's no task already in the queue for
 * this particular project (based on 'update_fetch_task' key-value collection).
457
458
 *
 * @param $project
459
 *   Associative array of information about a project as created by
460
461
462
 *   \Drupal\Update\UpdateManager::getProjects(), including keys such as 'name'
 *   (short name), and the 'info' array with data from a .info.yml file for the
 *   project.
463
464
 *
 * @see \Drupal\update\UpdateFetcher::createFetchTask()
465
466
 */
function update_create_fetch_task($project) {
467
  \Drupal::service('update.processor')->createFetchTask($project);
468
469
}

470
/**
471
 * Refreshes the release data after loading the necessary include file.
472
473
 */
function update_refresh() {
474
  \Drupal::service('update.manager')->refreshUpdateData();
475
476
}

477
/**
478
479
 * Attempts to fetch update data after loading the necessary include file.
 *
480
 * @see \Drupal\update\UpdateProcessor::fetchData()
481
482
 */
function update_fetch_data() {
483
  \Drupal::service('update.processor')->fetchData();
484
485
}

486
487
488
489
490
491
492
493
494
495
496
497
498
499
/**
 * Batch callback: Performs actions when all fetch tasks have been completed.
 *
 * @param $success
 *   TRUE if the batch operation was successful; FALSE if there were errors.
 * @param $results
 *   An associative array of results from the batch operation, including the key
 *   'updated' which holds the total number of projects we fetched available
 *   update data for.
 */
function update_fetch_data_finished($success, $results) {
  if ($success) {
    if (!empty($results)) {
      if (!empty($results['updated'])) {
500
        \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural($results['updated'], 'Checked available update data for one project.', 'Checked available update data for @count projects.'));
501
502
      }
      if (!empty($results['failures'])) {
503
        \Drupal::messenger()->addError(\Drupal::translation()->formatPlural($results['failures'], 'Failed to get available update data for one project.', 'Failed to get available update data for @count projects.'));
504
505
506
507
      }
    }
  }
  else {
508
    \Drupal::messenger()->addError(t('An error occurred trying to get available update data.'), 'error');
509
510
511
  }
}

512
/**
513
 * Implements hook_mail().
514
 *
515
 * Constructs the email notification message when the site is out of date.
516
517
518
519
520
521
 *
 * @param $key
 *   Unique key to indicate what message to build, always 'status_notify'.
 * @param $message
 *   Reference to the message array being built.
 * @param $params
522
523
524
525
 *   Array of parameters to indicate what kind of text to include in the message
 *   body. This is a keyed array of message type ('core' or 'contrib') as the
 *   keys, and the status reason constant (UPDATE_NOT_SECURE, etc) for the
 *   values.
526
 *
527
 * @see \Drupal\Core\Mail\MailManagerInterface::mail()
528
529
 * @see _update_cron_notify()
 * @see _update_message_text()
530
531
 */
function update_mail($key, &$message, $params) {
532
  $langcode = $message['langcode'];
533
  $language = \Drupal::languageManager()->getLanguage($langcode);
534
  $message['subject'] .= t('New release(s) available for @site_name', ['@site_name' => \Drupal::config('system.site')->get('name')], ['langcode' => $langcode]);
535
  foreach ($params as $msg_type => $msg_reason) {
536
    $message['body'][] = _update_message_text($msg_type, $msg_reason, $langcode);
537
  }
538
  $message['body'][] = t('See the available updates page for more information:', [], ['langcode' => $langcode]) . "\n" . Url::fromRoute('update.status', [], ['absolute' => TRUE, 'language' => $language])->toString();
539
  if (_update_manager_access()) {
540
    $message['body'][] = t('You can automatically install your missing updates using the Update manager:', [], ['langcode' => $langcode]) . "\n" . Url::fromRoute('update.report_update', [], ['absolute' => TRUE, 'language' => $language])->toString();
541
  }
542
  $settings_url = Url::fromRoute('update.settings', [], ['absolute' => TRUE])->toString();
543
  if (\Drupal::config('update.settings')->get('notification.threshold') == 'all') {
544
    $message['body'][] = t('Your site is currently configured to send these emails when any updates are available. To get notified only for security updates, @url.', ['@url' => $settings_url]);
545
546
  }
  else {
547
    $message['body'][] = t('Your site is currently configured to send these emails only when security updates are available. To get notified for any available updates, @url.', ['@url' => $settings_url]);
548
  }
549
550
551
}

/**
552
 * Returns the appropriate message text when site is out of date or not secure.
553
554
 *
 * These error messages are shared by both update_requirements() for the
555
 * site-wide status report at admin/reports/status and in the body of the
556
 * notification email messages generated by update_cron().
557
558
 *
 * @param $msg_type
559
560
 *   String to indicate what kind of message to generate. Can be either 'core'
 *   or 'contrib'.
561
 * @param $msg_reason
562
 *   Integer constant specifying why message is generated.
563
564
 * @param $langcode
 *   (optional) A language code to use. Defaults to NULL.
565
 *
566
567
568
 * @return
 *   The properly translated error message for the given key.
 */
569
function _update_message_text($msg_type, $msg_reason, $langcode = NULL) {
570
571
  $text = '';
  switch ($msg_reason) {
572
573
    case UPDATE_NOT_SECURE:
      if ($msg_type == 'core') {
574
        $text = t('There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately!', [], ['langcode' => $langcode]);
575
576
      }
      else {
577
        $text = t('There are security updates available for one or more of your modules or themes. To ensure the security of your server, you should update immediately!', [], ['langcode' => $langcode]);
578
579
580
581
582
      }
      break;

    case UPDATE_REVOKED:
      if ($msg_type == 'core') {
583
        $text = t('Your version of Drupal has been revoked and is no longer available for download. Upgrading is strongly recommended!', [], ['langcode' => $langcode]);
584
585
      }
      else {
586
        $text = t('The installed version of at least one of your modules or themes has been revoked and is no longer available for download. Upgrading or disabling is strongly recommended!', [], ['langcode' => $langcode]);
587
588
589
590
591
      }
      break;

    case UPDATE_NOT_SUPPORTED:
      if ($msg_type == 'core') {
592
        $text = t('Your version of Drupal is no longer supported. Upgrading is strongly recommended!', [], ['langcode' => $langcode]);
593
594
      }
      else {
595
        $text = t('The installed version of at least one of your modules or themes is no longer supported. Upgrading or disabling is strongly recommended. See the project homepage for more details.', [], ['langcode' => $langcode]);
596
597
598
      }
      break;

599
600
    case UPDATE_NOT_CURRENT:
      if ($msg_type == 'core') {
601
        $text = t('There are updates available for your version of Drupal. To ensure the proper functioning of your site, you should update as soon as possible.', [], ['langcode' => $langcode]);
602
603
      }
      else {
604
        $text = t('There are updates available for one or more of your modules or themes. To ensure the proper functioning of your site, you should update as soon as possible.', [], ['langcode' => $langcode]);
605
606
607
      }
      break;

608
609
    case UPDATE_UNKNOWN:
    case UPDATE_NOT_CHECKED:
610
    case UPDATE_NOT_FETCHED:
611
    case UPDATE_FETCH_PENDING:
612
      if ($msg_type == 'core') {
613
        $text = t('There was a problem checking <a href=":update-report">available updates</a> for Drupal.', [':update-report' => Url::fromRoute('update.status')->toString()], ['langcode' => $langcode]);
614
615
      }
      else {
616
        $text = t('There was a problem checking <a href=":update-report">available updates</a> for your modules or themes.', [':update-report' => Url::fromRoute('update.status')->toString()], ['langcode' => $langcode]);
617
618
619
620
      }
      break;
  }

621
  return $text;
622
}
623
624

/**
625
 * Orders projects based on their status.
626
 *
627
 * Callback for uasort() within update_requirements().
628
629
630
631
632
633
634
635
636
637
 */
function _update_project_status_sort($a, $b) {
  // The status constants are numerically in the right order, so we can
  // usually subtract the two to compare in the order we want. However,
  // negative status values should be treated as if they are huge, since we
  // always want them at the bottom of the list.
  $a_status = $a['status'] > 0 ? $a['status'] : (-10 * $a['status']);
  $b_status = $b['status'] > 0 ? $b['status'] : (-10 * $b['status']);
  return $a_status - $b_status;
}
638

639
/**
640
641
642
 * Prepares variables for last time update data was checked templates.
 *
 * Default template: update-last-check.html.twig.
643
 *
644
 * In addition to properly formatting the given timestamp, this function also
645
646
647
648
 * provides a "Check manually" link that refreshes the available update and
 * redirects back to the same page.
 *
 * @param $variables
649
 *   An associative array containing:
650
 *   - last: The timestamp when the site last checked for available updates.
651
652
653
 *
 * @see theme_update_report()
 */
654
function template_preprocess_update_last_check(&$variables) {
655
  $variables['time'] = \Drupal::service('date.formatter')->formatTimeDiffSince($variables['last']);
656
  $variables['link'] = Link::fromTextAndUrl(t('Check manually'), Url::fromRoute('update.manual_status', [], ['query' => \Drupal::destination()->getAsArray()]))->toString();
657
658
}

659
660
661
662
/**
 * Implements hook_verify_update_archive().
 *
 * First, we ensure that the archive isn't a copy of Drupal core, which the
663
 * update manager does not yet support. See https://www.drupal.org/node/606592.
664
 *
665
 * Then, we make sure that at least one module included in the archive file has
666
 * an .info.yml file which claims that the code is compatible with the current
667
 * version of Drupal core.
668
 *
669
 * @see \Drupal\Core\Extension\ExtensionDiscovery
670
671
 */
function update_verify_update_archive($project, $archive_file, $directory) {
672
  $errors = [];
673
674
675
676

  // Make sure this isn't a tarball of Drupal core.
  if (
    file_exists("$directory/$project/index.php")
677
    && file_exists("$directory/$project/core/install.php")
678
679
680
    && file_exists("$directory/$project/core/includes/bootstrap.inc")
    && file_exists("$directory/$project/core/modules/node/node.module")
    && file_exists("$directory/$project/core/modules/system/system.module")
681
  ) {
682
683
684
    return [
      'no-core' => t('Automatic updating of Drupal core is not supported. See the <a href=":upgrade-guide">upgrade guide</a> for information on how to update Drupal core manually.', [':upgrade-guide' => 'https://www.drupal.org/upgrade']),
    ];
685
686
  }

687
  // Parse all the .info.yml files and make sure at least one is compatible with
688
689
690
691
692
  // this version of Drupal core. If one is compatible, then the project as a
  // whole is considered compatible (since, for example, the project may ship
  // with some out-of-date modules that are not necessary for its overall
  // functionality).
  $compatible_project = FALSE;
693
  $incompatible = [];
694
695
696
  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  $file_system = \Drupal::service('file_system');
  $files = $file_system->scanDirectory("$directory/$project", '/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', ['key' => 'name', 'min_depth' => 0]);
697
  foreach ($files as $file) {
698
    // Get the .info.yml file for the module or theme this file belongs to.
699
    $info = \Drupal::service('info_parser')->parse($file->uri);
700
701

    // If the module or theme is incompatible with Drupal core, set an error.
702
    if ($info['core_incompatible']) {
703
      $incompatible[] = !empty($info['name']) ? $info['name'] : t('Unknown');
704
    }
705
706
707
708
709
710
711
    else {
      $compatible_project = TRUE;
      break;
    }
  }

  if (empty($files)) {
712
    $errors[] = t('%archive_file does not contain any .info.yml files.', ['%archive_file' => $file_system->basename($archive_file)]);
713
  }
714
  elseif (!$compatible_project) {
715
    $errors[] = \Drupal::translation()->formatPlural(
716
      count($incompatible),
717
718
      '%archive_file contains a version of %names that is not compatible with Drupal @version.',
      '%archive_file contains versions of modules or themes that are not compatible with Drupal @version: %names',
719
      [
720
        '@version' => \Drupal::VERSION,
721
722
723
        '%archive_file' => $file_system->basename($archive_file),
        '%names' => implode(', ', $incompatible),
      ]
724
725
726
727
728
729
    );
  }

  return $errors;
}

730
/**
731
 * Invalidates stored data relating to update status.
732
 */
733
function update_storage_clear() {
734
735
  \Drupal::keyValueExpirable('update')->deleteAll();
  \Drupal::keyValueExpirable('update_available_release')->deleteAll();
736
737
}

738
/**
739
 * Returns a short unique identifier for this Drupal installation.
740
741
742
743
744
745
746
 *
 * @return
 *   An eight character string uniquely identifying this Drupal installation.
 */
function _update_manager_unique_identifier() {
  $id = &drupal_static(__FUNCTION__, '');
  if (empty($id)) {
747
    $id = substr(hash('sha256', Settings::getHashSalt()), 0, 8);
748
749
750
751
752
  }
  return $id;
}

/**
753
 * Returns the directory where update archive files should be extracted.
754
755
 *
 * @param $create
756
757
 *   (optional) Whether to attempt to create the directory if it does not
 *   already exist. Defaults to TRUE.
758
759
 *
 * @return
760
761
 *   The full path to the temporary directory where update file archives should
 *   be extracted.
762
763
764
765
766
767
768
769
770
771
772
773
774
 */
function _update_manager_extract_directory($create = TRUE) {
  $directory = &drupal_static(__FUNCTION__, '');
  if (empty($directory)) {
    $directory = 'temporary://update-extraction-' . _update_manager_unique_identifier();
    if ($create && !file_exists($directory)) {
      mkdir($directory);
    }
  }
  return $directory;
}

/**
775
 * Returns the directory where update archive files should be cached.
776
777
 *
 * @param $create
778
779
 *   (optional) Whether to attempt to create the directory if it does not
 *   already exist. Defaults to TRUE.
780
781
 *
 * @return
782
783
 *   The full path to the temporary directory where update file archives should
 *   be cached.
784
785
786
787
788
789
790
791
792
793
794
795
 */
function _update_manager_cache_directory($create = TRUE) {
  $directory = &drupal_static(__FUNCTION__, '');
  if (empty($directory)) {
    $directory = 'temporary://update-cache-' . _update_manager_unique_identifier();
    if ($create && !file_exists($directory)) {
      mkdir($directory);
    }
  }
  return $directory;
}

796
/**
797
 * Clears the temporary files and directories based on file age from disk.
798
799
 */
function update_clear_update_disk_cache() {
800
801
  // List of update module cache directories. Do not create the directories if
  // they do not exist.
802
  $directories = [
803
804
    _update_manager_cache_directory(FALSE),
    _update_manager_extract_directory(FALSE),
805
  ];
806
807
808

  // Search for files and directories in base folder only without recursion.
  foreach ($directories as $directory) {
809
810
811
    if (is_dir($directory)) {
      \Drupal::service('file_system')->scanDirectory($directory, '/.*/', ['callback' => 'update_delete_file_if_stale', 'recurse' => FALSE]);
    }
812
813
814
815
  }
}

/**
816
 * Deletes stale files and directories from the update manager disk cache.
817
 *
818
819
820
 * Files and directories older than 6 hours and development snapshots older than
 * 5 minutes are considered stale. We only cache development snapshots for 5
 * minutes since otherwise updated snapshots might not be downloaded as
821
822
823
 * expected.
 *
 * When checking file ages, we need to use the ctime, not the mtime
824
825
826
827
828
 * (modification time) since many (all?) tar implementations go out of their way
 * to set the mtime on the files they create to the timestamps recorded in the
 * tarball. We want to see the last time the file was changed on disk, which is
 * left alone by tar and correctly set to the time the archive file was
 * unpacked.
829
830
831
832
833
834
835
 *
 * @param $path
 *   A string containing a file path or (streamwrapper) URI.
 */
function update_delete_file_if_stale($path) {
  if (file_exists($path)) {
    $filectime = filectime($path);
836
837
838
    $max_age = \Drupal::config('system.file')->get('temporary_maximum_age');

    if (REQUEST_TIME - $filectime > $max_age || (preg_match('/.*-dev\.(tar\.gz|zip)/i', $path) && REQUEST_TIME - $filectime > 300)) {
839
840
841
842
843
844
      try {
        \Drupal::service('file_system')->deleteRecursive($path);
      }
      catch (FileException $e) {
        // Ignore failed deletes.
      }
845
846
847
    }
  }
}