- <?php
- * @file
- * Handles update checking for Backdrop core and contributed projects.
- *
- * The module checks for available updates of Backdrop core and any installed
- * contributed modules, themes, and layouts. It warns site administrators if newer
- * releases are available via the system status report (admin/reports/status),
- * the module, theme and layout pages, and optionally via email.
- */
-
- * URL to check for updates, if a given project doesn't define its own.
- */
- define('UPDATE_DEFAULT_URL', 'https://updates.backdropcms.org/release-history');
-
-
- * Project is missing security update(s).
- */
- define('UPDATE_NOT_SECURE', 1);
-
- * Current release has been unpublished and is no longer available.
- */
- define('UPDATE_REVOKED', 2);
-
- * Current release is no longer supported by the project maintainer.
- */
- define('UPDATE_NOT_SUPPORTED', 3);
-
- * Project has a new release available, but it is not a security release.
- */
- define('UPDATE_NOT_CURRENT', 4);
-
- * Project is up to date.
- */
- define('UPDATE_CURRENT', 5);
-
- * Project's status cannot be checked.
- */
- define('UPDATE_NOT_CHECKED', -1);
-
- * No available update data was found for project.
- */
- define('UPDATE_UNKNOWN', -2);
-
- * There was a failure fetching available update data for this project.
- */
- define('UPDATE_NOT_FETCHED', -3);
-
- * We need to (re)fetch available update data for this project.
- */
- define('UPDATE_FETCH_PENDING', -4);
-
- * The update server is a placeholder server that has not yet been implemented.
- */
- define('UPDATE_NOT_IMPLEMENTED', -5);
-
- * Implements hook_menu().
- */
- function update_menu() {
- $items = array();
-
- $items['admin/reports/updates'] = array(
- 'title' => 'Available updates',
- 'description' => 'Get a status report about available updates for your installed modules, themes, and layouts.',
- 'page callback' => 'update_status',
- 'access arguments' => array('access site reports'),
- 'weight' => -50,
- 'file' => 'update.report.inc',
- );
- $items['admin/reports/updates/list'] = array(
- 'title' => 'List available updates',
- 'access arguments' => array('administer site configuration'),
- 'type' => MENU_DEFAULT_LOCAL_TASK,
- );
- $items['admin/reports/updates/settings'] = array(
- 'title' => 'Settings',
- 'page callback' => 'backdrop_get_form',
- 'page arguments' => array('update_settings'),
- 'access arguments' => array('administer site configuration'),
- 'file' => 'update.admin.inc',
- 'type' => MENU_LOCAL_TASK,
- 'weight' => 50,
- );
- $items['admin/reports/updates/check'] = array(
- 'title' => 'Manual update check',
- 'page callback' => 'update_manual_status',
- 'access arguments' => array('administer site configuration'),
- 'type' => MENU_CALLBACK,
- 'file' => 'update.fetch.inc',
- );
-
- return $items;
- }
-
- * Access callback: Resolves if the current user can access updater menu items.
- *
- * It both enforces the 'administer software updates' permission and the global
- * kill switch for the authorize.php script.
- *
- * @return
- * TRUE if the current user can access the updater menu items; FALSE
- * otherwise.
- *
- * @see update_menu()
- */
- function update_manager_access() {
- return settings_get('allow_authorize_operations', TRUE) && user_access('administer software updates');
- }
-
- * Implements hook_theme().
- */
- function update_theme($existing, $type, $theme, $path) {
- $base = array(
- 'file' => 'update.theme.inc',
- );
-
- return array(
- 'update_last_check' => array(
- 'variables' => array('last' => NULL),
- ) + $base,
- 'update_report' => array(
- 'variables' => array('data' => NULL),
- ) + $base,
- 'update_version' => array(
- 'variables' => array('version' => NULL, 'tag' => NULL, 'class' => array()),
- ) + $base,
- 'update_status_label' => array(
- 'variables' => array('status' => NULL),
- ) + $base,
- );
- }
-
- * Implements hook_cron().
- */
- function update_cron() {
-
- if (!_update_checking_enabled()) {
- return;
- }
-
-
- $frequency = config_get('update.settings', 'update_interval_days');
- if ($frequency == 0) {
- return;
- }
-
- $interval = 60 * 60 * 24 * $frequency;
- if ((REQUEST_TIME - state_get('update_last_check', 0)) > $interval) {
-
-
-
- update_refresh();
- update_fetch_data();
- }
- else {
-
-
- update_get_available(TRUE);
- }
- if ((REQUEST_TIME - state_get('update_last_email_notification', 0)) > $interval) {
-
-
- module_load_include('inc', 'update', 'update.fetch');
- _update_cron_notify();
- }
- }
-
- * Implements hook_themes_enabled().
- *
- * If themes are enabled, we invalidate the cache of available updates.
- */
- function update_themes_enabled($themes) {
-
- _update_cache_clear();
- }
-
- * Implements hook_themes_disabled().
- *
- * If themes are disabled, we invalidate the cache of available updates.
- */
- function update_themes_disabled($themes) {
-
- _update_cache_clear();
- }
-
- * Implements hook_form_FORM_ID_alter() for system_modules().
- *
- * Adds a form submission handler to the system modules form, so that if a site
- * admin saves the form, we invalidate the cache of available updates.
- *
- * @see _update_cache_clear()
- */
- function update_form_system_modules_alter(&$form, $form_state) {
- $form['#submit'][] = 'update_cache_clear_submit';
- }
-
- * Form submission handler for system_modules().
- *
- * @see update_form_system_modules_alter()
- */
- function update_cache_clear_submit($form, &$form_state) {
-
- _update_cache_clear();
- }
-
- * Returns a warning message when there is no data about available updates.
- */
- function _update_no_data() {
- $destination = backdrop_get_destination();
- return t('No update data available.') . ' ' . l(t('Check manually for updates'), 'admin/reports/updates/check', array('query' => $destination)) . '.';
- }
-
- * Check if update checking should make HTTP requests to the update server.
- *
- * This function primarily is to ensure that Backdrop does not do update
- * checking against backdropcms.org or other remote servers during tests.
- */
- function _update_checking_enabled() {
- return empty($GLOBALS['backdrop_test_info']['test_run_id']) || strstr(config_get('update.settings', 'update_url'), $GLOBALS['base_url']);
- }
-
- * Tries to get update information from cache and refreshes it when necessary.
- *
- * In addition to checking the cache lifetime, this function also ensures that
- * there are no .info files for enabled modules or themes that have a newer
- * modification timestamp than the last time we checked for available update
- * data. If any .info 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.
- *
- * @param $refresh
- * (optional) Boolean to indicate if this method should refresh the cache
- * automatically if there's no data. Defaults to FALSE.
- *
- * @return
- * Array of data about available releases, keyed by project machine name.
- *
- * @see update_refresh()
- * @see update_get_projects()
- */
- function update_get_available($refresh = FALSE) {
- module_load_include('inc', 'update', 'update.compare');
- $needs_refresh = FALSE;
-
-
- $available = _update_get_cached_available_releases();
- $projects = update_get_projects();
- foreach ($projects as $key => $project) {
-
- if (empty($available[$key])) {
- update_create_fetch_task($project);
- $needs_refresh = TRUE;
- continue;
- }
-
-
-
-
-
-
- if ($project['info']['_info_file_ctime'] > $available[$key]['last_fetch']) {
- $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING;
- }
-
-
-
- if (empty($available[$key]['releases'])) {
- $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING;
- }
-
-
-
- if (!empty($available[$key]['fetch_status']) && $available[$key]['fetch_status'] == UPDATE_FETCH_PENDING) {
- update_create_fetch_task($project);
- $needs_refresh = TRUE;
- }
- }
-
- if ($needs_refresh && $refresh) {
-
- update_fetch_data();
-
-
- $available = _update_get_cached_available_releases();
- }
-
- return $available;
- }
-
- * Creates a new fetch task after loading the necessary include file.
- *
- * @param $project
- * Associative array of information about a project. See update_get_projects()
- * for the keys used.
- *
- * @see _update_create_fetch_task()
- */
- function update_create_fetch_task($project) {
- module_load_include('inc', 'update', 'update.fetch');
- return _update_create_fetch_task($project);
- }
-
- * Refreshes the release data after loading the necessary include file.
- *
- * @see _update_refresh()
- */
- function update_refresh() {
- module_load_include('inc', 'update', 'update.fetch');
- return _update_refresh();
- }
-
- * Attempts to fetch update data after loading the necessary include file.
- *
- * @see _update_fetch_data()
- */
- function update_fetch_data() {
- module_load_include('inc', 'update', 'update.fetch');
- return _update_fetch_data();
- }
-
- * Returns all currently cached data about available releases for all projects.
- *
- * @return
- * Array of data about available releases, keyed by project machine name.
- */
- function _update_get_cached_available_releases() {
- $data = array();
- $cache_items = _update_get_cache_multiple('available_releases');
- foreach ($cache_items as $cid => $cache) {
- $cache->data['last_fetch'] = $cache->created;
- if ($cache->expire < REQUEST_TIME) {
- $cache->data['fetch_status'] = UPDATE_FETCH_PENDING;
- }
-
-
-
- $parts = explode('::', $cid, 2);
- $data[$parts[1]] = $cache->data;
- }
- return $data;
- }
-
- * Implements hook_mail().
- *
- * Constructs the email notification message when the site is out of date.
- *
- * @param $key
- * Unique key to indicate what message to build, always 'status_notify'.
- * @param $message
- * Reference to the message array being built.
- * @param $params
- * 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.
- *
- * @see backdrop_mail()
- * @see _update_cron_notify()
- * @see _update_message_text()
- */
- function update_mail($key, &$message, $params) {
- $language = $message['language'];
- $langcode = $language->langcode;
- $site_name_localized = config_get_translated('system.core', 'site_name', array(), array('langcode' => $langcode));
- $message['subject'] .= t('New release(s) available for !site_name', array('!site_name' => $site_name_localized), array('langcode' => $langcode));
- foreach ($params as $msg_type => $msg_reason) {
- $message['body'][] = _update_message_text($msg_type, $msg_reason, FALSE, $language);
- }
- $message['body'][] = t('See the available updates page for more information:', array(), array('langcode' => $langcode)) . "\n" . url('admin/reports/updates', array('absolute' => TRUE, 'language' => $language));
- if (update_manager_access()) {
- $message['body'][] = t('You can automatically install your missing updates using the Update Manager:', array(), array('langcode' => $langcode)) . "\n" . url('admin/reports/updates/update', array('absolute' => TRUE, 'language' => $language));
- }
- $settings_url = url('admin/reports/updates/settings', array('absolute' => TRUE));
- if (config_get('update.settings', 'update_threshold') == 'all') {
- $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.', array('!url' => $settings_url));
- }
- else {
- $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.', array('!url' => $settings_url));
- }
- }
-
- * Returns the appropriate message text when site is out of date or not secure.
- *
- * These error messages are shared by both update_requirements() for the
- * site-wide status report at admin/reports/status and in the body of the
- * notification email messages generated by update_cron().
- *
- * @param $msg_type
- * String to indicate what kind of message to generate. Can be either 'core'
- * or 'contrib'.
- * @param $msg_reason
- * Integer constant specifying why message is generated.
- * @param $report_link
- * (optional) Boolean that controls if a link to the updates report should be
- * added. Defaults to FALSE.
- * @param $language
- * (optional) A language object to use. Defaults to NULL.
- *
- * @return
- * The properly translated error message for the given key.
- */
- function _update_message_text($msg_type, $msg_reason, $report_link = FALSE, $language = NULL) {
- $langcode = isset($language) ? $language->langcode : NULL;
- $text = '';
- switch ($msg_reason) {
- case UPDATE_NOT_SECURE:
- if ($msg_type == 'core') {
- $text = t('There is a security update available for your version of Backdrop. To ensure the security of your server, you should update immediately!', array(), array('langcode' => $langcode));
- }
- else {
- $text = t('There are security updates available for one or more of your modules, themes, or layouts. To ensure the security of your server, you should update immediately!', array(), array('langcode' => $langcode));
- }
- break;
-
- case UPDATE_REVOKED:
- if ($msg_type == 'core') {
- $text = t('Your version of Backdrop has been revoked and is no longer available for download. Upgrading is strongly recommended!', array(), array('langcode' => $langcode));
- }
- else {
- $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!', array(), array('langcode' => $langcode));
- }
- break;
-
- case UPDATE_NOT_SUPPORTED:
- if ($msg_type == 'core') {
- $text = t('Your version of Backdrop CMS is no longer supported. Upgrading is strongly recommended!', array(), array('langcode' => $langcode));
- }
- else {
- $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.', array(), array('langcode' => $langcode));
- }
- break;
-
- case UPDATE_NOT_CURRENT:
- if ($msg_type == 'core') {
- $text = t('There are updates available for your version of Backdrop. To ensure the proper functioning of your site, you should update as soon as possible.', array(), array('langcode' => $langcode));
- }
- else {
- $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.', array(), array('langcode' => $langcode));
- }
- break;
-
- case UPDATE_UNKNOWN:
- case UPDATE_NOT_CHECKED:
- if ($msg_type == 'core') {
- $text = t('Update checking is not available for your version of Backdrop.');
- }
- else {
- $text = t('Update checking is not available for some of your projects.');
- }
- break;
- case UPDATE_FETCH_PENDING:
- if ($msg_type == 'core') {
- $text = t('Update may be needed for your version of Backdrop.');
- }
- else {
- $text = t('Updates may be needed for one or more of your projects.');
- }
- break;
- case UPDATE_NOT_FETCHED:
- if ($msg_type == 'core') {
- $text = t('There was a problem checking <a href="@update-report">available updates</a> for Backdrop.', array('@update-report' => url('admin/reports/updates')), array('langcode' => $langcode));
- }
- else {
- $text = t('There was a problem checking <a href="@update-report">available updates</a> for your modules or themes.', array('@update-report' => url('admin/reports/updates')), array('langcode' => $langcode));
- }
- break;
- case UPDATE_NOT_IMPLEMENTED:
- $report_link = FALSE;
- $text = t('The update server has not yet been implemented, so no update statuses could be retrieved. Until the update server becomes active, please check for updates manually.');
- break;
- }
-
- if ($report_link && current_path() != 'admin/config/system/updates') {
- $text .= ' ' . t('See the <a href="@available_updates">available updates</a> page for more information.', array('@available_updates' => url('admin/reports/updates', array('language' => $language))), array('langcode' => $langcode));
- }
-
- return $text;
- }
-
- * Orders projects based on their status.
- *
- * Callback for uasort() within update_requirements().
- */
- function _update_project_status_sort($a, $b) {
-
-
-
-
- $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;
- }
-
- * @defgroup update_status_cache Private update status cache system
- * @{
- * Functions to manage the update status cache.
- *
- * We specifically do NOT use the core cache API for saving the fetched data
- * about available updates. It is vitally important that this cache is only
- * cleared when we're populating it after successfully fetching new available
- * update data. Usage of the core cache API results in all sorts of potential
- * problems that would result in attempting to fetch available update data all
- * the time, including if a site has a "minimum cache lifetime" (which is both a
- * minimum and a maximum) defined, or if a site uses memcache or another
- * pluggable cache system that assumes volatile caches.
- *
- * The Update Manager module still uses the {cache_update} table, but instead of
- * using cache_set(), cache_get(), and cache_clear_all(), there are private
- * helper functions that implement these same basic tasks but ensure that the
- * cache is not prematurely cleared, and that the data is always stored in the
- * database, even if memcache or another cache backend is in use.
- */
-
- * Stores data in the private update status cache table.
- *
- * @param $cid
- * The cache ID to save the data with.
- * @param $data
- * The data to store.
- * @param $expire
- * One of the following values:
- * - CACHE_PERMANENT: Indicates that the item should never be removed except
- * by explicitly using _update_cache_clear().
- * - A Unix timestamp: Indicates that the item should be kept at least until
- * the given time, after which it will be invalidated.
- *
- * @see _update_cache_get()
- */
- function _update_cache_set($cid, $data, $expire) {
- $fields = array(
- 'created' => REQUEST_TIME,
- 'expire' => $expire,
- );
- if (!is_string($data)) {
- $fields['data'] = serialize($data);
- $fields['serialized'] = 1;
- }
- else {
- $fields['data'] = $data;
- $fields['serialized'] = 0;
- }
- db_merge('cache_update')
- ->key(array('cid' => $cid))
- ->fields($fields)
- ->execute();
- }
-
- * Retrieves data from the private update status cache table.
- *
- * @param $cid
- * The cache ID to retrieve.
- *
- * @return
- * An array of data for the given cache ID, or NULL if the ID was not found.
- *
- * @see _update_cache_set()
- */
- function _update_cache_get($cid) {
- $cache = db_query("SELECT data, created, expire, serialized FROM {cache_update} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
- if (isset($cache->data)) {
- if ($cache->serialized) {
- $cache->data = unserialize($cache->data);
- }
- }
- return $cache;
- }
-
- * Returns an array of cache items with a given cache ID prefix.
- *
- * @param string $cid_prefix
- * The cache ID prefix.
- *
- * @return
- * Associative array of cache items, keyed by cache ID.
- */
- function _update_get_cache_multiple($cid_prefix) {
- $data = array();
- $result = db_select('cache_update')
- ->fields('cache_update', array('cid', 'data', 'created', 'expire', 'serialized'))
- ->condition('cache_update.cid', $cid_prefix . '::%', 'LIKE')
- ->execute();
- foreach ($result as $cache) {
- if ($cache) {
- if ($cache->serialized) {
- $cache->data = unserialize($cache->data);
- }
- $data[$cache->cid] = $cache;
- }
- }
- return $data;
- }
-
- * Invalidates cached data relating to update status.
- *
- * @param $cid
- * (optional) Cache ID of the record to clear from the private update module
- * cache. If empty, all records will be cleared from the table except fetch
- * tasks. Defaults to NULL.
- * @param $wildcard
- * (optional) If TRUE, cache IDs starting with $cid are deleted in addition to
- * the exact cache ID specified by $cid. Defaults to FALSE.
- */
- function _update_cache_clear($cid = NULL, $wildcard = FALSE) {
-
-
- if (empty($cid) || $cid == 'update') {
- db_delete('cache_update')
-
-
- ->condition('cid', 'fetch_task::%', 'NOT LIKE')
- ->execute();
- }
- else {
- $query = db_delete('cache_update');
- if ($wildcard) {
- $query->condition('cid', $cid . '%', 'LIKE');
- }
- else {
- $query->condition('cid', $cid);
- }
- $query->execute();
- }
- }
-
- * Implements hook_flush_caches().
- *
- * Called from update.php (among others) to flush the caches. Since we're
- * running update.php, we are likely to install a new version of something, in
- * which case, we want to check for available update data again. However,
- * because we have our own caching system, we need to directly clear the
- * database table ourselves at this point and return nothing, for example, on
- * sites that use memcache where cache_clear_all() won't know how to purge this
- * data.
- *
- * However, we only want to do this from update.php, since otherwise, we'd lose
- * all the available update data on every cron run. So, we specifically check if
- * the site is in MAINTENANCE_MODE == 'update' (which indicates update.php is
- * running, not update module... alas for overloaded names).
- */
- function update_flush_caches() {
- if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
- _update_cache_clear();
- }
- return array();
- }
-
- * Implements hook_admin_bar_cache_info().
- */
- function update_admin_bar_cache_info() {
- $caches['update'] = array(
- 'title' => t('Update data'),
- 'callback' => '_update_cache_clear',
- );
- return $caches;
- }
-
- * Implements hook_config_info().
- */
- function update_config_info() {
- $prefixes['update.settings'] = array(
- 'label' => t('Update settings'),
- 'group' => t('Configuration'),
- );
- return $prefixes;
- }