diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7798de36b9..e1503e4f47 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -139,7 +139,7 @@ stages: - composer require --dev drupal/coder:^8.2@stable micheh/phpcs-gitlab phpcompatibility/php-compatibility dealerdirect/phpcodesniffer-composer-installer - export TARGET_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}${CI_COMMIT_BRANCH} script: - - git fetch -vn --depth=$GIT_DEPTH "${CI_MERGE_REQUEST_PROJECT_URL:-origin}" "+refs/heads/$TARGET_BRANCH:refs/heads/$TARGET_BRANCH" + - git fetch -vn --depth=$GIT_DEPTH origin "+refs/heads/$TARGET_BRANCH:refs/heads/$TARGET_BRANCH" - export MODIFIED=`git diff --name-only refs/heads/$TARGET_BRANCH|while read r;do echo "$CI_PROJECT_DIR/$r";done|tr "\n" " "` - echo -e "$MODIFIED" | tr " " "\n" - echo "If this list contains more files than what you changed, then you need to rebase your branch." diff --git a/.gitlab-ci/pipeline.yml b/.gitlab-ci/pipeline.yml index c7c6485928..3272b26fd8 100644 --- a/.gitlab-ci/pipeline.yml +++ b/.gitlab-ci/pipeline.yml @@ -104,7 +104,7 @@ stages: - *prepare-dirs - *install-drupal - export TARGET_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}${CI_COMMIT_BRANCH} - - git fetch -vn --depth=50 "$CI_MERGE_REQUEST_PROJECT_URL" "+refs/heads/$TARGET_BRANCH:refs/heads/$TARGET_BRANCH" + - git fetch -vn --depth=50 origin "+refs/heads/$TARGET_BRANCH:refs/heads/$TARGET_BRANCH" - | echo "ℹ️ Changes from ${TARGET_BRANCH}" git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --name-only diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c7f7639369..73c7f8ff16 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Drupal 7.100, 2024-03-06 +------------------------ +- Security improvements +- Announcements module added + Drupal 7.99, 2023-12-06 ----------------------- - Various security improvements diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 3d8f3c84bc..42449f8eb1 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.99'); +define('VERSION', '7.100'); /** * Core API compatibility. diff --git a/modules/announcements_feed/announcements-feed.tpl.php b/modules/announcements_feed/announcements-feed.tpl.php new file mode 100644 index 0000000000..ba777f1604 --- /dev/null +++ b/modules/announcements_feed/announcements-feed.tpl.php @@ -0,0 +1,71 @@ + + +
+ + +
+ +
+ +
+ +
+ diff --git a/modules/announcements_feed/announcements_feed.css b/modules/announcements_feed/announcements_feed.css new file mode 100644 index 0000000000..01449b8ef2 --- /dev/null +++ b/modules/announcements_feed/announcements_feed.css @@ -0,0 +1,16 @@ +/** + * @file + * Styles for the announcements feed. + */ + +.announcements { + padding-bottom: 1rem; +} +.featured-announcements-wrapper .leaf { + padding-top: 0; +} +.announcements .announcements--view-all { + padding-left: 30px; + border-top: 1px solid #ccc; + padding-top: 10px; +} diff --git a/modules/announcements_feed/announcements_feed.inc b/modules/announcements_feed/announcements_feed.inc new file mode 100644 index 0000000000..7c5481f9d6 --- /dev/null +++ b/modules/announcements_feed/announcements_feed.inc @@ -0,0 +1,231 @@ + CSS_DEFAULT, + 'every_page' => TRUE, + )); + try { + $announcements = announcements_feed_get_all_announcements(); + } + catch (Exception $e) { + drupal_set_message(t('An error occurred while parsing the announcements feed, check the logs for more information.'), 'error'); + return array(); + } + $build = array(); + foreach ($announcements as $announcement) { + $key = $announcement['featured'] ? '#featured' : '#standard'; + $build[$key][] = $announcement; + } + $build = array_merge($build, array( + '#theme' => 'announcements_feed', + '#count' => count($announcements), + '#feed_link' => variable_get('announcements_feed_link', ANNOUNCEMENTS_FEED_DEFAULT_LINK), + )); + return $build; +} + +/** + * Generate an array of announcements with keys. + * + * @return array + * An array of announcements. + */ +function announcements_feed_get_all_announcements() { + $announcements = announcements_feed_fetch(); + $announcements_feed = array(); + foreach ($announcements as $announcement) { + $announcements_feed[] = array( + 'id' => $announcement['id'], + 'title' => $announcement['title'], + 'link' => $announcement['url'], + 'date_modified' => $announcement['date_modified'], + 'date_published' => $announcement['date_published'], + 'teaser' => $announcement['content_html'], + 'version' => $announcement['_drupalorg']['version'], + 'featured' => (bool) $announcement['_drupalorg']['featured'], + ); + } + + return $announcements_feed; +} + +/** + * Fetches the feed either from a local cache or fresh remotely. + * + * The feed follows the "JSON Feed" format: + * - https://www.jsonfeed.org/version/1.1/ + * + * The structure of an announcement item in the feed is: + * - id: Id. + * - title: Title of the announcement. + * - content_html: Announcement teaser. + * - url: URL + * - date_modified: Last updated timestamp. + * - date_published: Created timestamp. + * - _drupalorg.featured: 1 if featured, 0 if not featured. + * - _drupalorg.version: Target version of Drupal, as a Composer version. + * + * @param bool $force + * (optional) Whether to always fetch new items or not. Defaults to FALSE. + * + * @return array + * An array of announcements from the feed relevant to the Drupal version. + * The array is empty if there were no matching announcements. If an error + * occurred while fetching/decoding the feed, it is thrown as an exception. + * + * @throws Exception + */ +function announcements_feed_fetch($force = FALSE) { + $announcements = cache_get('announcements_feed'); + if ($force || empty($announcements)) { + $announcements_feed_json_url = variable_get('announcements_feed_json_url', ANNOUNCEMENTS_FEED_DEFAULT_JSON_URL); + $response = drupal_http_request($announcements_feed_json_url); + if ($response->code == 200) { + $feeds = json_decode($response->data, TRUE); + if (!isset($feeds['items'])) { + watchdog('announcements_feed', 'The feed format is not valid.', NULL, WATCHDOG_ERROR); + throw new Exception('Announcements feed JSON format is invalid'); + } + $announcements = array(); + if ($feeds['items']) { + $announcements = $feeds['items']; + } + $announcements = array_filter($announcements, 'announcements_feed_filter_announcements'); + cache_set('announcements_feed', $announcements, 'cache', REQUEST_TIME + variable_get('announcements_feed_max_age', ANNOUNCEMENTS_FEED_DEFAULT_MAX_AGE)); + } + else { + watchdog( + 'announcements_feed', + 'The feed failed to fetch with an error code: @code, error message: @message.', + array('@code' => $response->code, '@message' => $response->error), + WATCHDOG_ERROR + ); + throw new Exception($response->error, $response->code); + } + } + else { + $announcements = $announcements->data; + } + // The drupal.org endpoint is sorted by created date in descending order. + // We will limit the announcements based on the configuration limit. + $announcements_feed_limit = variable_get('announcements_feed_limit', ANNOUNCEMENTS_FEED_DEFAULT_LIMIT); + $announcements = array_slice($announcements, 0, $announcements_feed_limit); + // For the remaining announcements, put all the featured announcements + // before the rest. + uasort($announcements, 'announcements_feed_sort_featured'); + + return $announcements; +} + +/** + * Sort the elements of announcements_feed by values in comparison function. + */ +function announcements_feed_sort_featured($a, $b) { + $a_value = (int) $a['_drupalorg']['featured']; + $b_value = (int) $b['_drupalorg']['featured']; + if ($a_value == $b_value) { + return 0; + } + + return ($a_value < $b_value) ? -1 : 1; +} + +/** + * Filter the announcements relevant to the Drupal version used with valid URL controlled by drupal.org. + * + * @param array $announcement + * Announcement feed array item to check. + * + * @return bool + * Return TRUE if $announcement is relevant and the URL is valid. + */ +function announcements_feed_filter_announcements($announcement) { + $announcement_url = ''; + $announcement_version = ''; + if (!empty($announcement['url'])) { + $announcement_url = $announcement['url']; + } + if (!empty($announcement['_drupalorg']['version'])) { + $announcement_version = $announcement['_drupalorg']['version']; + } + + return announcements_feed_validate_url($announcement_url) && announcements_feed_is_relevant_item($announcement_version); +} + +/** + * Check whether the version given is relevant to the Drupal version used. + * + * @param string $version + * Version to check. + * + * @return bool + * Return TRUE if the version matches Drupal version. + */ +function announcements_feed_is_relevant_item($version) { + if ($version == '*') { + return TRUE; + } + // Split the version if received in || formats. + $version_patterns = '/\|\|/'; + $all_versions = preg_split($version_patterns, $version); + // The operation is optional and defaults to equals. + $p_op = '(?P!=|\^|==|=|<|<=|>|>=|<>)?'; + $operations = '='; + // Extracts major version from version string like 7, 8, 9. + $p_major = '(?P\d+)'; + // Extracts minor version from version string. + $p_minor = '(?P(?:\d+|x)(?:-[A-Za-z]+\d+)?)'; + foreach ($all_versions as $version) { + if (preg_match("/^\s*$p_op\s*$p_major(\.$p_minor)?/", $version, $matches)) { + $feed_version = $matches['major']; + if (!empty($matches['minor'])) { + $feed_version = $matches['major'] . '.' . $matches['minor']; + } + if (!empty($matches['operation'])) { + $operations = $matches['operation']; + if ($operations == '^') { + $operations = '>='; + } + } + if (isset($operations) && version_compare(VERSION, $feed_version, $operations)) { + return TRUE; + } + } + } + + return FALSE; +} + +/** + * Check whether a link is controlled by drupal.org. + * + * @param string $url + * URL to check. + * + * @return bool + * Return TRUE if the URL is controlled by drupal.org. + */ +function announcements_feed_validate_url($url) { + if (empty($url)) { + return FALSE; + } + $host = parse_url($url, PHP_URL_HOST); + + // First character can only be a letter or a digit. + // @see https://www.rfc-editor.org/rfc/rfc1123#page-13 + return $host && preg_match('/^([a-zA-Z0-9][a-zA-Z0-9\-_]*\.)?drupal\.org$/', $host); +} diff --git a/modules/announcements_feed/announcements_feed.info b/modules/announcements_feed/announcements_feed.info new file mode 100644 index 0000000000..4a64f80b4d --- /dev/null +++ b/modules/announcements_feed/announcements_feed.info @@ -0,0 +1,6 @@ +name = Announcements +description = Displays announcements from the Drupal community. +package = Core +version = VERSION +core = 7.x +files[] = tests/announce_feed_test.test diff --git a/modules/announcements_feed/announcements_feed.install b/modules/announcements_feed/announcements_feed.install new file mode 100644 index 0000000000..76308ad90e --- /dev/null +++ b/modules/announcements_feed/announcements_feed.install @@ -0,0 +1,19 @@ +' . t('About') . ''; + $output .= '

' . t('The Announcements module displays announcements from the Drupal community. For more information, see the online documentation for the Announcements module.', array('@documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed')) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
' . t('Accessing announcements') . '
'; + $output .= '
' . t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration menu, or access Announcements, to see all announcements relevant to the Drupal version of your site.', array('@link' => url('admin/announcements_feed'))) . '
'; + $output .= '
'; + + return $output; + } + +} + +/** + * Implements hook_menu(). + */ +function announcements_feed_menu() { + $items['admin/announcements_feed'] = array( + 'title' => 'Announcements', + 'description' => 'Announcements and updates posted by the drupal.org community.', + 'page callback' => 'announcements_feed_get_announcements', + 'access arguments' => array('access announcements'), + 'position' => 'left', + 'weight' => 2, + 'file' => 'announcements_feed.inc', + 'options' => array( + 'attributes' => array( + 'class' => array('announcement-menu-class'), + ), + ), + ); + + return $items; +} + +/** + * Implements hook_permission(). + */ +function announcements_feed_permission() { + return array( + 'access announcements' => array( + 'title' => t('View official announcements related to Drupal'), + ), + ); +} + +/** + * Implements hook_theme(). + */ +function announcements_feed_theme() { + return array( + 'announcements_feed' => array( + 'variables' => array( + 'featured' => NULL, + 'standard' => NULL, + 'feed_link' => NULL, + 'count' => 0, + ), + 'template' => 'announcements-feed', + 'path' => drupal_get_path('module', 'announcements_feed'), + ), + ); +} + +/** + * Processes variables for announcements-feed.tpl.php. + * + * @see announcements-feed.tpl.php + */ +function template_preprocess_announcements_feed(&$variables) { + if (!empty($variables['featured'])) { + foreach($variables['featured'] as &$announcement) { + _announcements_feed_preprocess_item($announcement); + } + } + if (!empty($variables['standard'])) { + foreach($variables['standard'] as &$announcement) { + _announcements_feed_preprocess_item($announcement); + } + } + if (isset($variables['feed_link'])) { + $variables['feed_link'] = check_url($variables['feed_link']); + } +} + +/** + * Helper to preprocess individual announcement items. + */ +function _announcements_feed_preprocess_item(&$item) { + $item['title'] = filter_xss($item['title']); + $item['teaser'] = filter_xss($item['teaser']); + $item['link'] = check_url($item['link']); + $item['date_modified'] = check_plain($item['date_modified']); + $item['date_published'] = check_plain($item['date_published']); + $item['version'] = check_plain($item['version']); +} + +/** + * Implements hook_page_alter(). + */ +function announcements_feed_page_alter(&$page) { + if (isset($page['page_top']['toolbar'])) { + // If the toolbar is available, add a pre-render function to add the class. + $page['page_top']['toolbar']['#pre_render'][] = 'announcements_feed_toolbar_pre_render_alter'; + } +} + +/** + * Pre-render function for adding default class to announcement link. + */ +function announcements_feed_toolbar_pre_render_alter($toolbar) { + $path = drupal_get_path('module', 'announcements_feed'); + $toolbar['#attached']['css'][] = $path . '/announcements_feed-toolbar.css'; + module_load_include('inc', 'announcements_feed'); + foreach ($toolbar['toolbar_menu']['#links'] as &$link) { + if ($link['href'] === 'admin/announcements_feed') { + $link['attributes']['class'][] = 'announcement-default'; + } + } + + return $toolbar; +} + +/** + * Implements hook_cron(). + */ +function announcements_feed_cron() { + module_load_include('inc', 'announcements_feed', 'announcements_feed'); + $cron_interval = variable_get('announcements_feed_cron_interval', ANNOUNCEMENTS_FEED_DEFAULT_CRON_INTERVAL); + $last_check = variable_get('announcements_feed_last_fetch', 0); + $time = time(); + if (($time - $last_check) > $cron_interval) { + try { + announcements_feed_fetch(TRUE); + // Update the last_fetch variable to the current time. + variable_set('announcements_feed_last_fetch', $time); + } + catch (Exception $e) { + watchdog('announcements_feed', 'Exception occurred: @message', array('@message' => $e->getMessage()), WATCHDOG_ERROR); + } + } +} diff --git a/modules/announcements_feed/tests/announce_feed/community-feeds.json b/modules/announcements_feed/tests/announce_feed/community-feeds.json new file mode 100644 index 0000000000..ee6672fb38 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed/community-feeds.json @@ -0,0 +1,57 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Drupal Announcements Feed", + "home_page_url": "https://www.drupal.org", + "feed_url": "https://www.drupal.org/announcements.json", + "favicon": "https://www.drupal.org/favicon.ico", + "items": [ + { + "id": "201", + "title": "new 9 - 10 Drupal 9.1.3 is available", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + }, + { + "id": "2021", + "title": "updated 10 - DrupalCon is here", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": true, + "version": ">=7.0" + } + }, + { + "id": "2031", + "title": "new 9 only - Download latest drupal here", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + }, + { + "id": "2043", + "title": "Only 10 - Drupal 106 is available", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:39+00:00", + "date_published": "2021-01-18T07:29:39+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + } + ] +} diff --git a/modules/announcements_feed/tests/announce_feed/empty.json b/modules/announcements_feed/tests/announce_feed/empty.json new file mode 100644 index 0000000000..c49ae3aaed --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed/empty.json @@ -0,0 +1,8 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Drupal Announcements Feed", + "home_page_url": "https://www.drupal.org", + "feed_url": "https://www.drupal.org/announcements.json", + "favicon": "https://www.drupal.org/favicon.ico", + "items": [] +} diff --git a/modules/announcements_feed/tests/announce_feed/hacked.json b/modules/announcements_feed/tests/announce_feed/hacked.json new file mode 100644 index 0000000000..b97a054408 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed/hacked.json @@ -0,0 +1,33 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Drupal Announcements Feed", + "home_page_url": "https://www.drupal.org", + "feed_url": "https://www.drupal.org/announcements.json", + "favicon": "https://www.drupal.org/favicon.ico", + "items": [ + { + "id": "2031", + "title": "new 9 only - ", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "javascript://alert(document.cookie)", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + }, + { + "id": "2021", + "title": "updated 10 - look at this: ", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "123456", + "_drupalorg": { + "featured": true, + "version": ">=7.0" + } + } + ] +} diff --git a/modules/announcements_feed/tests/announce_feed/invalid-feeds.json b/modules/announcements_feed/tests/announce_feed/invalid-feeds.json new file mode 100644 index 0000000000..46c23d5de9 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed/invalid-feeds.json @@ -0,0 +1,33 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title" "Drupal Announcements Feed", + "home_page_url": "https://www.drupal.org", + "feed_url": "https://www.drupal.org/announcements.json", + "favicon": "https://www.drupal.org/favicon.ico", + "items": [ + { + "id" "201", + "title": "new 9 - 10 Drupal 9.1.3 is available", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + } + { + "id": "2021", + "title": "updated 10 - DrupalCon is here" + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00" + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": true + "version": ">=7.0" + } + } + ] +} diff --git a/modules/announcements_feed/tests/announce_feed/removed.json b/modules/announcements_feed/tests/announce_feed/removed.json new file mode 100644 index 0000000000..7a27be7b9d --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed/removed.json @@ -0,0 +1,45 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Drupal Announcements Feed", + "home_page_url": "https://www.drupal.org", + "feed_url": "https://www.drupal.org/announcements.json", + "favicon": "https://www.drupal.org/favicon.ico", + "items": [ + { + "id": "201", + "title": "new 9 - 10 Drupal 9.1.3 is available", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": true, + "version": "^9 | ^10" + } + }, + { + "id": "2021", + "title": "updated 10 - DrupalCon is here", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": false, + "version": "^10" + } + }, + { + "id": "2031", + "title": "new 9 only - Download latest drupal here", + "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information", + "url": "https://www.drupal.org/project/announce", + "date_modified": "2021-01-19T07:29:38+00:00", + "date_published": "2021-01-18T07:29:38+00:00", + "_drupalorg": { + "featured": false, + "version": "^9" + } + } + ] +} diff --git a/modules/announcements_feed/tests/announce_feed/updated.json b/modules/announcements_feed/tests/announce_feed/updated.json new file mode 100644 index 0000000000..94dec3c7b1 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed/updated.json @@ -0,0 +1,79 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Drupal Announcements Feed", + "home_page_url": "https://www.drupal.org", + "feed_url": "https://www.drupal.org/announcements.json", + "favicon": "https://www.drupal.org/favicon.ico", + "items": [{ + "id": "3347976", + "title": "Contribute to Drupal and the Open Web", + "content_html": "

Drupal is an open source project. But what does that mean? How can you as a site-owner participate in the project of building a better web?

", + "url": "https://www.drupal.org/about/announcements/blog/contribute-to-drupal-and-the-open-web", + "date_modified": "2023-03-14T19:43:12+00:00", + "date_published": "2023-03-14T19:39:44+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + }, + { + "id": "3343486", + "title": "What to expect from the Announcements feed?", + "content_html": "

Drupal is introducing a new project Announcements feature so that you can see news and updates about the Drupal project and community directly in your Drupal dashboard. Learn what to expect.

", + "url": "https://www.drupal.org/about/announcements/blog/what-to-expect-from-the-announcements-feed", + "date_modified": "2023-02-21T19:45:53+00:00", + "date_published": "2023-02-21T19:45:53+00:00", + "_drupalorg": { + "featured": true, + "version": ">=7.0" + } + }, + { + "id": "3327047", + "title": "Drupal 10.0.0 is available", + "content_html": "

Thanks to 2129 contributors from 616 organizations resolving 4083 issues in the past two and a half years, Drupal 10.0.0 is available today! This new version sets Drupal up for continued stability and security for the longer term. All new features will be added to Drupal 10 going forward.

", + "url": "https://www.drupal.org/blog/drupal-10-0-0", + "date_modified": "2023-03-14T19:47:33+00:00", + "date_published": "2022-12-15T17:43:23+00:00", + "_drupalorg": { + "featured": false, + "version": ">=7.0" + } + }, + { + "id": "3343429", + "title": "New community feed available.", + "content_html": "

Drupal is introducing a new project Announcements feature so that you can see news and updates about the Drupal project and community directly in your Drupal dashboard. Learn what to expect.

", + "url": "https://www.drupal.org/about/announcements/blog/what-to-expect-from-the-announcements-feed", + "date_modified": "2023-03-21T19:45:53+00:00", + "date_published": "2023-03-21T19:45:53+00:00", + "_drupalorg": { + "featured": true, + "version": ">=7.0" + } + }, + { + "id": "3343431", + "title": "Any plan to migrate to Drupal 9/10?", + "content_html": "

Drupal is introducing a new project Announcements feature so that you can see news and updates about the Drupal project and community directly in your Drupal dashboard. Learn what to expect.

", + "url": "https://www.drupal.org/about/announcements/blog/what-to-expect-from-the-announcements-feed", + "date_modified": "2023-02-25T19:45:53+00:00", + "date_published": "2023-02-25T19:45:53+00:00", + "_drupalorg": { + "featured": true, + "version": ">=7.0" + } + }, + { + "id": "3343440", + "title": "Only 9 - Drupal 106 is available and this feed is Updated", + "content_html": "

Drupal is introducing a new project Announcements feature so that you can see news and updates about the Drupal project and community directly in your Drupal dashboard. Learn what to expect.

", + "url": "https://www.drupal.org/about/announcements/blog/what-to-expect-from-the-announcements-feed", + "date_modified": "2023-01-29T19:45:53+00:00", + "date_published": "2023-01-29T19:45:53+00:00", + "_drupalorg": { + "featured": true, + "version": ">=7.0" + } + }] +} diff --git a/modules/announcements_feed/tests/announce_feed_test.info b/modules/announcements_feed/tests/announce_feed_test.info new file mode 100644 index 0000000000..5c954bce28 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed_test.info @@ -0,0 +1,5 @@ +name = "Announcements feed test" +description = "Support module for announcements feed testing." +package = Testing +core = 7.x +hidden = TRUE diff --git a/modules/announcements_feed/tests/announce_feed_test.module b/modules/announcements_feed/tests/announce_feed_test.module new file mode 100644 index 0000000000..9f9c700074 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed_test.module @@ -0,0 +1,35 @@ + 'Announcements feed JSON', + 'page callback' => 'announce_feed_test_set_feed_config', + 'page arguments' => array(1), + // In unit tests, restrictions are not required. + 'access callback' => TRUE, + ); + + return $items; +} + +/** + * Helper function to set announcements feed URL. + */ +function announce_feed_test_set_feed_config($json_name) { + $file = __DIR__ . "/announce_feed/$json_name.json"; + if (!is_file($file)) { + // Return an empty response. + drupal_not_found(); + } + + $contents = file_get_contents($file); + drupal_json_output(drupal_json_decode($contents)); +} diff --git a/modules/announcements_feed/tests/announce_feed_test.test b/modules/announcements_feed/tests/announce_feed_test.test new file mode 100644 index 0000000000..80ae4c5035 --- /dev/null +++ b/modules/announcements_feed/tests/announce_feed_test.test @@ -0,0 +1,412 @@ + 'JSON feeds validation / processing', + 'description' => 'Testing how the code handles multiple types of JSON feeds.', + 'group' => 'Announcements', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + global $base_url; + module_load_include('inc', 'announce_feed_test', 'announce_feed_test'); + parent::setUp('user', 'toolbar', 'announcements_feed', 'announce_feed_test'); + $this->user = $this->drupalCreateUser(array( + 'access toolbar', + 'access announcements', + )); + $this->drupalLogin($this->user); + $this->responseJson = $base_url . '/announcements-feed-json/community-feeds'; + $this->updatedJson = $base_url . '/announcements-feed-json/updated'; + $this->emptyJson = $base_url . '/announcements-feed-json/empty'; + $this->removed = $base_url . '/announcements-feed-json/removed'; + variable_set('announcements_feed_json_url', $this->responseJson); + } + + /** + * Testing the feed with Updated and Removed JSON feeds. + */ + public function testAnnounceFeedUpdatedAndRemoved() { + $this->drupalLogin($this->user); + $this->drupalGet(''); + $this->clickLink('Announcements'); + variable_set('announcements_feed_json_url', $this->updatedJson); + cache_clear_all('announcements_feed', 'cache', TRUE); + $this->drupalGet('admin/announcements_feed'); + $this->assertText('Only 9 - Drupal 106 is available and this feed is Updated'); + $this->drupalLogout(); + + // Testing the removed JSON feed. + $this->drupalLogin($this->user); + $this->drupalGet(''); + $this->clickLink('Announcements'); + variable_set('announcements_feed_json_url', $this->removed); + cache_clear_all('announcements_feed', 'cache', TRUE); + $this->drupalGet('admin/announcements_feed'); + $this->assertNoText('Only 9 - Drupal 106 is available and this feed is Updated'); + + $this->drupalLogout(); + } + + /** + * Check the status of the feed with an empty JSON feed. + */ + public function testAnnounceFeedEmpty() { + // Change the feed URL to empty JSON file. + variable_set('announcements_feed_json_url', $this->emptyJson); + cache_clear_all('announcements_feed', 'cache', TRUE); + $this->drupalLogin($this->user); + // Only no announcements available message should show. + $this->clickLink('Announcements'); + $this->assertText('No announcements available'); + } + +} +/** + * Unit test for validate URL functions. + */ +class AnnounceFeedTestValidateUrl extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'JSON feed URLs validation', + 'description' => 'Unit test to check the validate URL functions.', + 'group' => 'Announcements', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + module_load_include('inc', 'announcements_feed', 'announcements_feed'); + } + + /** + * Test for validating the announcements_feed_validate_url function. + */ + public function testValidateUrl() { + $urls = array( + array('https://www.drupal.org', TRUE), + array('https://drupal.org', TRUE), + array('https://api.drupal.org', TRUE), + array('https://a.drupal.org', TRUE), + array('https://123.drupal.org', TRUE), + array('https://api-new.drupal.org', TRUE), + array('https://api_new.drupal.org', TRUE), + array('https://api-.drupal.org', TRUE), + array('https://www.example.org', FALSE), + array('https://example.org', FALSE), + array('https://api.example.org/project/announce', FALSE), + array('https://-api.drupal.org', FALSE), + array('https://a.example.org/project/announce', FALSE), + array('https://test.drupaal.com', FALSE), + array('https://api.drupal.org.example.com', FALSE), + array('https://example.org/drupal.org', FALSE), + ); + foreach ($urls as $url) { + $result = announcements_feed_validate_url($url[0]); + $this->assertEqual($url[1], $result, 'Returned ' . ($url[1] ? 'TRUE' : 'FALSE')); + } + } +} + +/** + * Unit test for version compatibility functions. + */ +class AnnounceFeedTestRelevantVersion extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Version-specific logic validation', + 'description' => 'Unit test to check the version-specific logic.', + 'group' => 'Announcements', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + module_load_include('inc', 'announcements_feed', 'announcements_feed'); + } + + /** + * Test for validating the announcements_feed_is_relevant_item function. + */ + public function testIsRelevantItem() { + $version_strings = array( + array('^7', TRUE), + // TRUE only if Drupal version is exactly 7.0. + array('=7.0', FALSE), + array('>=7', TRUE), + array('^7 || ^8 || ^9', TRUE), + array('>=7.52', TRUE), + array('^7.1 || ^8 || ^9', TRUE), + // TRUE only if Drupal version is exactly 7.9999. + array('=7.9999', FALSE), + array('^8 || ^9', FALSE), + array('>8', FALSE), + array('>=8.1', FALSE), + array('^8 || ^9 || ^10', FALSE), + ); + foreach ($version_strings as $strings) { + $result = announcements_feed_is_relevant_item($strings[0]); + $this->assertEqual($strings[1], $result, 'Returned ' . ($strings[1] ? 'TRUE' : 'FALSE')); + } + } +} + +/** + * Test the Announcements module permissions. + */ +class AnnounceFeedTestValidatePermissions extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Permissions validation', + 'description' => 'Tests the module permissions.', + 'group' => 'Announcements', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + global $base_url; + module_load_include('inc', 'announce_feed_test', 'announce_feed_test'); + parent::setUp('user', 'toolbar', 'announcements_feed', 'announce_feed_test'); + $response_json = $base_url . '/announcements-feed-json/community-feeds'; + variable_set('announcements_feed_json_url', $response_json); + } + + /** + * Testing the announcements page with access announcements permission. + */ + public function testAnnounceWithPermission() { + // Create a user with proper permission. + $account = $this->drupalCreateUser(array( + 'access toolbar', + 'access announcements', + )); + $this->drupalLogin($account); + $this->drupalGet(''); + $this->drupalGet('admin/announcements_feed'); + $this->assertText('Announcements'); + $this->drupalLogout(); + } + + /** + * Testing the announcements page without access announcements permission. + */ + public function testAnnounceWithoutPermission() { + $account = $this->drupalCreateUser(array('access toolbar')); + $this->drupalLogin($account); + $this->drupalGet('admin/announcements_feed'); + $this->assertResponse(403); + } +} + +/** + * Tests the announcements feed with invalid JSON URLs. + */ +class AnnounceFeedTestInvalidJsonTestCase extends DrupalWebTestCase { + + /** + * A user with permission to access toolbar and access announcements. + * + * @var object + */ + protected $user; + + /** + * A test endpoint which contains the community feeds. + * + * @var string + */ + protected $responseJson; + + /** + * A test endpoint which does not exist. + * + * @var string + */ + protected $unknownJson; + + /** + * A test endpoint which returns invalid JSON. + * + * @var string + */ + protected $invalidJson; + + /** + * A test endpoint that will have some feeds removed. + * + * @var string + */ + protected $removed; + + public static function getInfo() { + return array( + 'name' => 'Invalid / unknown JSON feed URL', + 'description' => 'Testing announcements feed with invalid JSON or non-existing JSON URL.', + 'group' => 'Announcements', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + global $base_url; + module_load_include('inc', 'announce_feed_test', 'announce_feed_test'); + parent::setUp('user', 'toolbar', 'announcements_feed', 'announce_feed_test'); + $this->user = $this->drupalCreateUser(array( + 'access toolbar', + 'access announcements', + )); + $this->drupalLogin($this->user); + $this->responseJson = $base_url . '/announcements-feed-json/community-feeds'; + $this->unknownJson = $base_url . '/announcements-feed-json/unknown'; + $this->invalidJson = $base_url . '/announcements-feed-json/invalid-feeds'; + variable_set('announcements_feed_json_url', $this->responseJson); + } + + /** + * Test the announcements feed with invalid JSON or non-existing JSON URL. + */ + public function testInvalidFeedResponse() { + // Test when the JSON URL is not found. + $this->drupalLogin($this->user); + $this->drupalGet(''); + $this->clickLink('Announcements'); + variable_set('announcements_feed_json_url', $this->unknownJson); + cache_clear_all('announcements_feed', 'cache', TRUE); + $this->drupalGet('admin/announcements_feed'); + $this->assertText('An error occurred while parsing the announcements feed, check the logs for more information.'); + + // Test when the JSON feed is invalid. + $this->drupalLogout(); + $this->drupalLogin($this->user); + $this->drupalGet(''); + $this->clickLink('Announcements'); + variable_set('announcements_feed_json_url', $this->invalidJson); + cache_clear_all('announcements_feed', 'cache', TRUE); + $this->drupalGet('admin/announcements_feed'); + $this->assertText('An error occurred while parsing the announcements feed, check the logs for more information.'); + + $this->drupalLogout(); + } +} + +/** + * Tests the announcements feed with malicious content. + */ +class AnnounceFeedTestSanitizationTestCase extends DrupalWebTestCase { + + /** + * A user with permission to access toolbar and access announcements. + * + * @var object + */ + protected $user; + + /** + * A test endpoint which contains the malicious content. + * + * @var string + */ + protected $hackedJson; + + public static function getInfo() { + return array( + 'name' => 'Hacked JSON feed URL', + 'description' => 'Testing announcements feed that contains malicious content.', + 'group' => 'Announcements', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + global $base_url; + module_load_include('inc', 'announce_feed_test', 'announce_feed_test'); + parent::setUp('user', 'toolbar', 'announcements_feed', 'announce_feed_test'); + $this->user = $this->drupalCreateUser(array( + 'access toolbar', + 'access announcements', + )); + $this->drupalLogin($this->user); + $this->hackedJson = $base_url . '/announcements-feed-json/hacked'; + variable_set('announcements_feed_json_url', $this->hackedJson); + } + + /** + * Test the announcements feed with malicious content. + */ + public function testSanitizedFeedResponse() { + $this->drupalLogin($this->user); + $this->drupalGet(''); + $this->clickLink('Announcements'); + $this->drupalGet('admin/announcements_feed'); + $this->assertNoRaw(""); + $this->assertNoRaw("onerror='alert(123)'"); + $this->assertNoRaw('alert(document.cookie)'); + $this->assertNoRaw(''); + $this->drupalLogout(); + } +} diff --git a/modules/simpletest/tests/module.test b/modules/simpletest/tests/module.test index 6a65457db7..4c89825f68 100644 --- a/modules/simpletest/tests/module.test +++ b/modules/simpletest/tests/module.test @@ -19,6 +19,14 @@ class ModuleUnitTest extends DrupalWebTestCase { ); } + /** + * {@inheritdoc} + */ + public function setUp() { + $GLOBALS['drupal_test_info']['test_class'] = __CLASS__; + parent::setUp(); + } + /** * The basic functionality of module_list(). */ diff --git a/modules/system/system.install b/modules/system/system.install index 2af5004f69..6221d30faa 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -3423,6 +3423,15 @@ function system_update_7086() { variable_set('hashed_session_ids_supported', TRUE); } +/** + * Enable the Announcements module; see 7.100 Release Notes for opt-out details. + */ +function system_update_7087() { + if (!variable_get('announcements_feed_enable_by_default_opt_out', FALSE)) { + module_enable(array('announcements_feed'), FALSE); + } +} + /** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. diff --git a/modules/user/user.module b/modules/user/user.module index 9385b35115..145fc480c5 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -2471,7 +2471,7 @@ function user_pass_rehash($password, $timestamp, $login, $uid, $mail = '') { $mail = $account->mail; } - return drupal_hmac_base64($timestamp . $login . $uid . $mail, drupal_get_hash_salt() . $password); + return drupal_hmac_base64($timestamp . ':' . $login . ':' . $uid . ':' . $mail, drupal_get_hash_salt() . $password); } /** diff --git a/modules/user/user.test b/modules/user/user.test index 83057870f8..f3e1002a4c 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -935,6 +935,31 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); } + /** + * Test uniqueness of output from user_pass_rehash() with no passwords. + */ + function testUniqueHashNoPasswordValue() { + $timestamp = REQUEST_TIME; + + // Minimal user objects are sufficient. + $user = drupal_anonymous_user(); + $user->login = $timestamp - 1000; + $user->pass = ''; + + $user_a = clone $user; + $user_a->uid = 12; + $user_a->mail = '3user@example.com'; + + $user_b = clone $user; + $user_b->uid = 123; + $user_b->mail = 'user@example.com'; + + $hash_a = user_pass_rehash($user_a->pass, $timestamp, $user_a->login, $user_a->uid, $user_a->mail); + $hash_b = user_pass_rehash($user_b->pass, $timestamp, $user_b->login, $user_b->uid, $user_b->mail); + + $this->assertNotEqual($hash_a, $hash_b, "No user_pass_rehash() hash collision for different users with no stored password."); + } + } /** diff --git a/profiles/standard/standard.install b/profiles/standard/standard.install index 158c4eaaa0..8895563bb3 100644 --- a/profiles/standard/standard.install +++ b/profiles/standard/standard.install @@ -429,4 +429,15 @@ function standard_install() { ->execute(); variable_set('admin_theme', 'seven'); variable_set('node_admin_theme', '1'); + + if (isset($GLOBALS['drupal_test_info']['test_class']) && $GLOBALS['drupal_test_info']['test_class'] == 'ModuleUnitTest') { + // If we're installing the standard profile for ModuleUnitTest, opt-out of + // enabling Announcements by default. This allows testModuleList() to pass, + // and also tests the opt-out. + variable_set('announcements_feed_enable_by_default_opt_out', TRUE); + } + + if (!variable_get('announcements_feed_enable_by_default_opt_out', FALSE)) { + module_enable(array('announcements_feed'), FALSE); + } }