Index: tests/phpunit/tests/template.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php
--- a/tests/phpunit/tests/template.php	(revision f98e99c8b6d47963c925760d5c980a438b718709)
+++ b/tests/phpunit/tests/template.php	(date 1659444353445)
@@ -456,6 +456,92 @@
 		);
 	}
 
+	/**
+	 * @ticket 17851
+	 * @covers ::add_settings_section
+	 */
+	public function test_add_settings_section() {
+		add_settings_section( 'test-section', 'Section title', '__return_false', 'test-page' );
+
+		global $wp_settings_sections;
+		$this->assertIsArray( $wp_settings_sections );
+		$this->assertArrayHasKey( 'test-page', $wp_settings_sections );
+		$this->assertIsArray( $wp_settings_sections['test-page'] );
+		$this->assertArrayHasKey( 'test-section', $wp_settings_sections['test-page'] );
+
+		$this->assertEqualSetsWithIndex(
+			array(
+				'id'             => 'test-section',
+				'title'          => 'Section title',
+				'callback'       => '__return_false',
+				'before_section' => '',
+				'after_section'  => '',
+				'section_class'  => '',
+			),
+			$wp_settings_sections['test-page']['test-section']
+		);
+	}
+
+	/**
+	 * @ticket 17851
+	 * @covers ::add_settings_section
+	 * @covers ::do_settings_sections
+	 */
+	public function test_add_settings_section_with_extra_args() {
+		$args = array(
+			'before_section' => '<div class="%s">',
+			'after_section'  => '</div><!-- end of the test section -->',
+			'section_class'  => 'test-section-wrap',
+		);
+
+		add_settings_section( 'test-section', 'Section title', '__return_false', 'test-page', $args );
+		add_settings_field( 'test-field', 'Field title', '__return_false', 'test-page', 'test-section' );
+
+		global $wp_settings_sections;
+		$this->assertIsArray( $wp_settings_sections );
+		$this->assertArrayHasKey( 'test-page', $wp_settings_sections );
+		$this->assertIsArray( $wp_settings_sections['test-page'] );
+		$this->assertArrayHasKey( 'test-section', $wp_settings_sections['test-page'] );
+
+		$this->assertEqualSetsWithIndex(
+			array(
+				'id'             => 'test-section',
+				'title'          => 'Section title',
+				'callback'       => '__return_false',
+				'before_section' => '<div class="%s">',
+				'after_section'  => '</div><!-- end of the test section -->',
+				'section_class'  => 'test-section-wrap',
+			),
+			$wp_settings_sections['test-page']['test-section']
+		);
+
+		ob_start();
+		do_settings_sections( 'test-page' );
+		$output = ob_get_clean();
+
+		$this->assertStringContainsString( '<div class="test-section-wrap">', $output );
+		$this->assertStringContainsString( '</div><!-- end of the test section -->', $output );
+
+	}
+
+	/**
+	 * @ticket 17851
+	 * @covers ::add_settings_section
+	 *
+	 * @expectedIncorrectUsage add_settings_section
+	 */
+	public function test_add_settings_section_missing_section_class_placeholder() {
+		$args = array(
+			'before_section' => '<div class="test-section-wrapper">',
+			'after_section'  => '</div><!-- end of the test section -->',
+			'section_class'  => 'test-section-wrap',
+		);
+
+		add_settings_section( 'test-section', 'Section title', '__return_false', 'test-page', $args );
+
+		//https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/
+
+	}
 
 	public function assertTemplateHierarchy( $url, array $expected, $message = '' ) {
 		$this->go_to( $url );
Index: src/wp-admin/includes/template.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php
--- a/src/wp-admin/includes/template.php	(revision f98e99c8b6d47963c925760d5c980a438b718709)
+++ b/src/wp-admin/includes/template.php	(date 1659442773871)
@@ -1561,6 +1561,7 @@
  * fields. It can output nothing if you want.
  *
  * @since 2.7.0
+ * @since 6.1.0 Added an `$args` parameter for the section's wrapper HTML and class name.
  *
  * @global array $wp_settings_sections Storage array of all settings sections added to admin pages.
  *
@@ -1570,10 +1571,46 @@
  * @param string   $page     The slug-name of the settings page on which to show the section. Built-in pages include
  *                           'general', 'reading', 'writing', 'discussion', 'media', etc. Create your own using
  *                           add_options_page();
+ * @param array    $args     {
+ *     Arguments used to create the settings section.
+ *
+ *     @type string $before_section HTML content to prepend to the section's HTML output.
+ *                                  Receives the section's class name as `%s`. Default empty.
+ *     @type string $after_section  HTML content to append to the section's HTML output. Default empty.
+ *     @type string $section_class  The class name to use for the section. Default empty.
+ * }
  */
-function add_settings_section( $id, $title, $callback, $page ) {
+function add_settings_section( $id, $title, $callback, $page, $args = array() ) {
 	global $wp_settings_sections;
 
+	$defaults = array(
+		'id'             => $id,
+		'title'          => $title,
+		'callback'       => $callback,
+		'before_section' => '',
+		'after_section'  => '',
+		'section_class'  => '',
+	);
+
+	$section = wp_parse_args( $args, $defaults );
+
+	if ( array_key_exists( 'before_section', $section )
+		&& is_string( $section['before_section'] )
+		&& strlen( $section['before_section'] ) > 0
+		&& 1 !== preg_match( '/%s/', $section['before_section'] )
+	) {
+		_doing_it_wrong(
+			'add_settings_section',
+			sprintf(
+				/* translators: before_section */
+				__( '%s argument must contain a placeholder for the section CSS class.' ),
+				'<code>before_section</code>'
+			),
+			'6.1.0'
+		);
+		return;
+	}
+
 	if ( 'misc' === $page ) {
 		_deprecated_argument(
 			__FUNCTION__,
@@ -1600,11 +1637,7 @@
 		$page = 'reading';
 	}
 
-	$wp_settings_sections[ $page ][ $id ] = array(
-		'id'       => $id,
-		'title'    => $title,
-		'callback' => $callback,
-	);
+	$wp_settings_sections[ $page ][ $id ] = $section;
 }
 
 /**
@@ -1700,6 +1733,10 @@
 	}
 
 	foreach ( (array) $wp_settings_sections[ $page ] as $section ) {
+		if ( '' !== $section['before_section'] ) {
+			echo sprintf( $section['before_section'], $section['section_class'] );
+		}
+
 		if ( $section['title'] ) {
 			echo "<h2>{$section['title']}</h2>\n";
 		}
@@ -1714,6 +1751,10 @@
 		echo '<table class="form-table" role="presentation">';
 		do_settings_fields( $page, $section['id'] );
 		echo '</table>';
+
+		if ( '' !== $section['after_section'] ) {
+			echo $section['after_section'];
+		}
 	}
 }
 
