From e969a82bd8b0461120544b76941741f3b0551620 Mon Sep 17 00:00:00 2001 From: missusk Date: Fri, 28 Aug 2026 21:38:23 +0530 Subject: [PATCH] Preserve whitespace in array settings --- tests/php/smoke/WpscCacheSettingTest.php | 60 ++++++++++++++++++++++++ wp-cache-phase2.php | 50 +++++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/tests/php/smoke/WpscCacheSettingTest.php b/tests/php/smoke/WpscCacheSettingTest.php index ba37b65d..717d566a 100644 --- a/tests/php/smoke/WpscCacheSettingTest.php +++ b/tests/php/smoke/WpscCacheSettingTest.php @@ -151,6 +151,66 @@ public function test_value_round_trips( string $value ): void { $this->assertSame( $value, $this->read_back() ); } + /** Array values must preserve whitespace inside every string element. */ + public function test_array_value_round_trips_internal_whitespace(): void { + $value = array( + 'double space' => 'Mozilla Foo', + 'tab' => "a\tb", + 'newlines' => "one\ntwo\r\nthree", + 'trailing backslash' => 'C:\\cache\\', + 'escape sequence' => '\' . "\n" . \'', + 'nested' => array( 'mixed' => "left\t right" ), + ); + + wp_cache_setting( self::FIELD, $value ); + + $this->assertSame( $value, $this->read_back() ); + } + + /** Array values containing newlines must still occupy one physical line. */ + public function test_array_value_with_newlines_is_written_on_one_line(): void { + $before = count( file( $GLOBALS['wp_cache_config_file'] ) ); + $value = array( 'line endings' => "one\ntwo\r\nthree" ); + + wp_cache_setting( self::FIELD, $value ); + + $this->assertCount( + $before, + file( $GLOBALS['wp_cache_config_file'] ), + 'The array value was written across more than one physical line.' + ); + $this->assertSame( $value, $this->read_back() ); + } + + /** Rewriting an array containing newlines must leave the file parseable. */ + public function test_overwriting_array_with_newlines_leaves_the_file_parseable(): void { + wp_cache_setting( self::FIELD, array( 'multiline' => "one\ntwo" ) ); + wp_cache_setting( self::FIELD, array( 'plain' => 'value' ) ); + + $this->assertSame( array( 'plain' => 'value' ), $this->read_back() ); + } + + /** Array elements cannot end their string literal and inject a statement. */ + public function test_array_value_cannot_smuggle_a_statement(): void { + $value = array( "/var/www/cache/';\$GLOBALS['wpsc_config_side_effect'] = true;" ); + + wp_cache_setting( self::FIELD, $value ); + $written = $this->read_back(); + + $this->assertArrayNotHasKey( 'wpsc_config_side_effect', $GLOBALS ); + $this->assertSame( $value, $written ); + } + + /** Ordinary arrays keep their existing compact source representation. */ + public function test_ordinary_array_value_is_written_unchanged(): void { + wp_cache_setting( self::FIELD, array( 'one', 'two' ) ); + + $this->assertStringContainsString( + '$' . self::FIELD . " = array ( 0 => 'one', 1 => 'two', );", + file_get_contents( $GLOBALS['wp_cache_config_file'] ) + ); + } + /** * The entry must occupy exactly one physical line whatever the value holds. * diff --git a/wp-cache-phase2.php b/wp-cache-phase2.php index 9d01d798..b969b7b7 100644 --- a/wp-cache-phase2.php +++ b/wp-cache-phase2.php @@ -1673,6 +1673,53 @@ function is_writeable_ACLSafe( $path ) { return true; } +/** + * Export a value as PHP source that occupies one physical line. + * + * @param mixed $value Value to export. + * @return string Exported PHP source. + */ +function wpsc_var_export_one_line( $value ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- Generating PHP source for the config file, which is what var_export() is for. + $exported = var_export( $value, true ); + $one_line = ''; + $in_string = false; + $escaped = false; + $length = strlen( $exported ); + + for ( $i = 0; $i < $length; ++$i ) { + $character = $exported[ $i ]; + + if ( ! $in_string && ( ' ' === $character || "\t" === $character || "\r" === $character || "\n" === $character ) ) { + if ( '' !== $one_line && ' ' !== $one_line[ strlen( $one_line ) - 1 ] ) { + $one_line .= ' '; + } + continue; + } + + if ( "\r" === $character || "\n" === $character ) { + $one_line .= "\r" === $character ? '\' . "\r" . \'' : '\' . "\n" . \''; + $escaped = false; + continue; + } + + $one_line .= $character; + + if ( ! $in_string ) { + $in_string = "'" === $character; + continue; + } + + if ( "'" === $character && ! $escaped ) { + $in_string = false; + } + + $escaped = '\\' === $character ? ! $escaped : false; + } + + return $one_line; +} + function wp_cache_setting( $field, $value ) { global $wp_cache_config_file; @@ -1683,8 +1730,7 @@ function wp_cache_setting( $field, $value ) { $output_value = $value === true ? 'true' : 'false'; return wp_cache_replace_line( '^ *\$' . $field, "\$$field = $output_value;", $wp_cache_config_file ); } elseif ( is_object( $value ) || is_array( $value ) ) { - $text = var_export( $value, true ); - $text = preg_replace( '/[\s]+/', ' ', $text ); + $text = wpsc_var_export_one_line( $value ); return wp_cache_replace_line( '^ *\$' . $field, "\$$field = $text;", $wp_cache_config_file ); } else { /*