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 @@ + + +
' . 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 .= '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); + } }