diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
index 234d71a2a175a..0573d3d0a32be 100644
--- a/src/wp-includes/formatting.php
+++ b/src/wp-includes/formatting.php
@@ -5852,6 +5852,121 @@ function wp_spaces_regexp() {
return $spaces;
}
+/**
+ * Recursively sanitizes data by detecting the data type and applying appropriate sanitization.
+ *
+ * This function iterates through each value in arrays and objects, detects the data type,
+ * and executes the sanitization function that best fits the data. It provides a comprehensive
+ * solution for plugin developers to sanitize complex data structures without having to
+ * manually handle each data type.
+ *
+ * The function supports custom sanitization contexts through the $context parameter:
+ * - 'text': Uses sanitize_text_field() for strings
+ * - 'textarea': Uses sanitize_textarea_field() for strings (preserves newlines)
+ * - 'email': Uses sanitize_email() for strings
+ * - 'url': Uses sanitize_url() for strings
+ * - 'key': Uses sanitize_key() for strings
+ * - 'title': Uses sanitize_title() for strings
+ * - 'html_class': Uses sanitize_html_class() for strings
+ * - 'mime_type': Uses sanitize_mime_type() for strings
+ * - 'auto' (default): Auto-detects the best sanitization method
+ *
+ * @since 6.5.0
+ *
+ * @param mixed $data The data to sanitize. Can be string, array, object, or other types.
+ * @param string $context Optional. The sanitization context. Default 'auto'.
+ * @param int $depth Optional. Internal recursion depth counter. Default 0.
+ * @return mixed Sanitized data in the same structure as the input.
+ */
+function recursively_sanitize( $data, $context = 'auto', $depth = 0 ) {
+ // Prevent infinite recursion - limit to reasonable depth.
+ if ( $depth > 50 ) {
+ return null;
+ }
+
+ // Handle arrays recursively.
+ if ( is_array( $data ) ) {
+ $sanitized = array();
+ foreach ( $data as $key => $value ) {
+ $sanitized_key = sanitize_key( $key );
+ $sanitized[ $sanitized_key ] = recursively_sanitize( $value, $context, $depth + 1 );
+ }
+ return $sanitized;
+ }
+
+ // Handle objects recursively.
+ if ( is_object( $data ) ) {
+ $sanitized = new stdClass();
+ foreach ( $data as $key => $value ) {
+ $sanitized_key = sanitize_key( $key );
+ $sanitized->$sanitized_key = recursively_sanitize( $value, $context, $depth + 1 );
+ }
+ return $sanitized;
+ }
+
+ // Handle null, boolean, and numeric values.
+ if ( is_null( $data ) || is_bool( $data ) || is_numeric( $data ) ) {
+ return $data;
+ }
+
+ // Handle string sanitization based on context.
+ if ( is_string( $data ) ) {
+ switch ( $context ) {
+ case 'text':
+ $sanitized = sanitize_text_field( $data );
+ break;
+ case 'textarea':
+ $sanitized = sanitize_textarea_field( $data );
+ break;
+ case 'email':
+ $sanitized = sanitize_email( $data );
+ break;
+ case 'url':
+ $sanitized = sanitize_url( $data );
+ break;
+ case 'key':
+ $sanitized = sanitize_key( $data );
+ break;
+ case 'title':
+ $sanitized = sanitize_title( $data );
+ break;
+ case 'html_class':
+ $sanitized = sanitize_html_class( $data );
+ break;
+ case 'mime_type':
+ $sanitized = sanitize_mime_type( $data );
+ break;
+ case 'auto':
+ default:
+ // Auto-detect best sanitization method.
+ if ( is_email( $data ) ) {
+ $sanitized = sanitize_email( $data );
+ } elseif ( wp_http_validate_url( $data ) ) {
+ $sanitized = sanitize_url( $data );
+ } else {
+ // Default to text field sanitization.
+ $sanitized = sanitize_text_field( $data );
+ }
+ break;
+ }
+ } else {
+ $sanitized = sanitize_text_field( (string) $data );
+ }
+
+ /**
+ * Filters the recursively sanitized data.
+ *
+ * @since 6.5.0
+ *
+ * @param mixed $sanitized The sanitized data.
+ * @param mixed $data The original data before sanitization.
+ * @param string $context The sanitization context used.
+ * @param int $depth The current recursion depth.
+ */
+ return apply_filters( 'recursively_sanitize', $sanitized, $data, $context, $depth );
+}
+
+
/**
* Enqueues the important emoji-related styles.
*
diff --git a/tests/phpunit/tests/formatting/recursivelySanitize.php b/tests/phpunit/tests/formatting/recursivelySanitize.php
new file mode 100644
index 0000000000000..d0936815b3a3e
--- /dev/null
+++ b/tests/phpunit/tests/formatting/recursivelySanitize.php
@@ -0,0 +1,129 @@
+assertEquals( $expected, recursively_sanitize( $input, $context ) );
+ }
+
+ public function data_recursively_sanitize() {
+ return array(
+ // Empty array.
+ array( array(), 'auto', array() ),
+
+ // Simple string.
+ array( 'Text with ', 'auto', 'Text with' ),
+
+ // Array with sanitized keys and values.
+ array(
+ array(
+ 'text' => 'Simple text',
+ 'key with spaces' => 'value',
+ ),
+ 'auto',
+ array(
+ 'text' => 'Simple text',
+ 'keywithspaces' => 'value',
+ ),
+ ),
+
+ // Nested array.
+ array(
+ array(
+ 'level1' => array(
+ 'text' => 'Text with ',
+ ),
+ ),
+ 'auto',
+ array(
+ 'level1' => array(
+ 'text' => 'Text with',
+ ),
+ ),
+ ),
+
+ // Object.
+ array(
+ (object) array(
+ 'text' => 'Text with ',
+ 'key with spaces' => 'value',
+ ),
+ 'auto',
+ (object) array(
+ 'text' => 'Text with',
+ 'keywithspaces' => 'value',
+ ),
+ ),
+
+ // Mixed array and object.
+ array(
+ array(
+ 'user' => (object) array(
+ 'profile' => array(
+ 'bio' => 'Bio with ',
+ ),
+ ),
+ ),
+ 'auto',
+ array(
+ 'user' => (object) array(
+ 'profile' => array(
+ 'bio' => 'Bio with',
+ ),
+ ),
+ ),
+ ),
+
+ // Primitive types.
+ array( null, 'auto', null ),
+ array( true, 'auto', true ),
+ array( false, 'auto', false ),
+ array( 42, 'auto', 42 ),
+
+ // Context-specific sanitization.
+ array(
+ array( 'text' => "Multi-line\ntext" ),
+ 'text',
+ array( 'text' => 'Multi-line text' ),
+ ),
+ array(
+ array( 'text' => "Multi-line\ntext" ),
+ 'textarea',
+ array( 'text' => "Multi-line\ntext" ),
+ ),
+ );
+ }
+
+ public function test_recursively_sanitize_should_prevent_infinite_recursion() {
+ // Create a structure with many levels of nesting.
+ $input = 'test';
+ for ( $i = 0; $i < 60; $i++ ) {
+ $input = array( 'level' => $input );
+ }
+
+ // This should not cause infinite recursion or fatal errors
+ $result = recursively_sanitize( $input, 'auto' );
+
+ // The function should handle deep nesting gracefully
+ $this->assertTrue( is_array( $result ) || is_null( $result ) );
+ }
+
+ public function test_recursively_sanitize_filter() {
+ $filter = new MockAction();
+ add_filter( 'recursively_sanitize', array( $filter, 'filter' ) );
+
+ recursively_sanitize( 'test string', 'auto' );
+
+ $this->assertSame( 1, $filter->get_call_count() );
+
+ remove_filter( 'recursively_sanitize', array( $filter, 'filter' ) );
+ }
+}