diff --git a/classes/PodsAdmin.php b/classes/PodsAdmin.php index eaf7b0cef0..eafd9e78f2 100644 --- a/classes/PodsAdmin.php +++ b/classes/PodsAdmin.php @@ -4676,7 +4676,11 @@ public function add_debug_information( $info ) { $settings_values = $settings->get_settings(); - $auto_start = pods_v( $auto_start, $settings_fields['session_auto_start']['data'], __( 'Unknown', 'pods' ) ); + $session_auto_start_data = ( isset( $settings_fields['session_auto_start'] ) && isset( $settings_fields['session_auto_start']['data'] ) ) + ? $settings_fields['session_auto_start']['data'] + : []; + + $auto_start = pods_v( $auto_start, $session_auto_start_data, __( 'Unknown', 'pods' ) ); require_once ABSPATH . '/wp-admin/includes/file.php'; @@ -4687,6 +4691,13 @@ public function add_debug_information( $info ) { global $wpdb; + // Cache session_save_path() once and guard the FS checks below. Hosts that + // restrict /var/lib/php/sessions through open_basedir (or run memcached/redis + // handlers with tcp:// handlers) would otherwise trigger file_exists() + // warnings on every Site Health request -- see issue #7263. + $save_path = session_save_path(); + $can_check_session_fs = $filesystem_ok && $wp_filesystem && ! empty( $save_path ) && 0 !== strpos( $save_path, 'tcp://' ); + $info['pods'] = [ 'label' => 'Pods', 'description' => __( 'Debug information for Pods installations.', 'pods' ), @@ -4717,11 +4728,11 @@ public function add_debug_information( $info ) { ], 'pods-session-save-path-exists' => [ 'label' => __( 'Session Save Path Exists', 'pods' ), - 'value' => ( $filesystem_ok && $wp_filesystem && $wp_filesystem->exists( session_save_path() ) ) ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ), + 'value' => ( $can_check_session_fs && $wp_filesystem->exists( $save_path ) ) ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ), ], 'pods-session-save-path-writable' => [ 'label' => __( 'Session Save Path Writeable', 'pods' ), - 'value' => ( $filesystem_ok && $wp_filesystem && $wp_filesystem->is_writable( session_save_path() ) ) ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ), + 'value' => ( $can_check_session_fs && $wp_filesystem->is_writable( $save_path ) ) ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ), ], 'pods-session-max-lifetime' => [ 'label' => __( 'Session Max Lifetime', 'pods' ), diff --git a/classes/PodsField.php b/classes/PodsField.php index 47238a1012..86ebc0dea1 100644 --- a/classes/PodsField.php +++ b/classes/PodsField.php @@ -975,15 +975,180 @@ public function maybe_sanitize_output( $value, $options = null ) { if ( $should_sanitize ) { if ( is_string( $value ) ) { - $value = wp_kses_post( $value ); + $value = $this->sanitize_output_string( $value ); } elseif ( is_array( $value ) || is_object( $value ) ) { - $value = wp_kses_post_deep( $value ); + $value = $this->sanitize_output_array( $value ); } } return $value; } + /** + * Sanitize a string field value before output. + * + * Code (Syntax Highlighting) fields commonly embed third-party scripts and + * iframes (SimpleShop, Google Reviews, TikTok widgets, etc.). WordPress's + * default {@see wp_kses_post()} allow-list drops external `', + $this->options + ); + + $this->assertStringNotContainsString( 'assertStringContainsString( '

hi

', $output ); + } + + /** + * The same applies to external script embeds. + */ + public function test_external_script_is_stripped_without_unfiltered_html() { + $subscriber = $this->factory()->user->create( [ 'role' => 'subscriber' ] ); + wp_set_current_user( $subscriber ); + + $output = $this->field->maybe_sanitize_output( + '', + $this->options + ); + + $this->assertStringNotContainsString( 'assertStringNotContainsString( 'evil.example.com', $output ); + } + + /** + * Logged-out visitors -- the common front-end case -- must never receive script. + */ + public function test_script_is_stripped_for_anonymous_visitors() { + wp_set_current_user( 0 ); + + $output = $this->field->maybe_sanitize_output( + '', + $this->options + ); + + $this->assertStringNotContainsString( 'assertStringNotContainsString( 'document.cookie', $output ); + } + + /** + * Iframe embeds are the common widget case and must keep working for everyone. + */ + public function test_iframe_embeds_survive_for_anonymous_visitors() { + wp_set_current_user( 0 ); + + $output = $this->field->maybe_sanitize_output( + '', + $this->options + ); + + $this->assertStringContainsString( 'assertStringContainsString( 'youtube.com/embed/abc', $output ); + } + + /** + * Users who hold unfiltered_html keep the embed capability the feature was added for. + */ + public function test_script_is_allowed_with_unfiltered_html() { + if ( is_multisite() ) { + $this->markTestSkipped( 'unfiltered_html is reserved for super admins on multisite.' ); + } + + $admin = $this->factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin ); + + if ( ! current_user_can( 'unfiltered_html' ) ) { + $this->markTestSkipped( 'unfiltered_html is not available in this environment.' ); + } + + $output = $this->field->maybe_sanitize_output( + '', + $this->options + ); + + $this->assertStringContainsString( 'assertStringContainsString( 'example.com/widget.js', $output ); + } + + /** + * The allow-list filter remains the documented escape hatch for sites that + * deliberately want public script embeds. + */ + public function test_allowed_html_filter_can_restore_script() { + wp_set_current_user( 0 ); + + $callback = static function ( $allowed ) { + $allowed['script'] = [ 'src' => true ]; + + return $allowed; + }; + + add_filter( 'pods_code_field_sanitize_allowed_html', $callback ); + + $output = $this->field->maybe_sanitize_output( + '', + $this->options + ); + + remove_filter( 'pods_code_field_sanitize_allowed_html', $callback ); + + $this->assertStringContainsString( 'example.com/widget.js', $output ); + } +} diff --git a/tests/codeception/wpunit/Pods/Field/PodsField_CodeTest.php b/tests/codeception/wpunit/Pods/Field/PodsField_CodeTest.php new file mode 100644 index 0000000000..0cd0283c96 --- /dev/null +++ b/tests/codeception/wpunit/Pods/Field/PodsField_CodeTest.php @@ -0,0 +1,135 @@ +field = new PodsField_Code(); + + // Make sure no leftover shortcode from prior runs. + if ( shortcode_exists( 'pods_code_test' ) ) { + remove_shortcode( 'pods_code_test' ); + } + } + + public function tearDown(): void { + if ( shortcode_exists( 'pods_code_test' ) ) { + remove_shortcode( 'pods_code_test' ); + } + + unset( $this->field ); + } + + /** + * @covers PodsField_Code::display + */ + public function test_display_preserves_external_script_src() { + $value = ''; + + $output = $this->field->display( + $value, + 'code_field', + [ 'code_sanitize_html' => 1 ], + null, + 0 + ); + + $this->assertIsString( $output ); + $this->assertStringContainsString( 'assertStringContainsString( 'src="https://example.com/widget.js"', $output ); + } + + /** + * @covers PodsField_Code::display + */ + public function test_display_preserves_iframe_src_with_dimensions() { + $value = ''; + + $output = $this->field->display( + $value, + 'code_field', + [ 'code_sanitize_html' => 1 ], + null, + 0 + ); + + $this->assertStringContainsString( 'assertStringContainsString( 'src="https://www.youtube.com/embed/abc123"', $output ); + $this->assertStringContainsString( 'allowfullscreen', $output ); + } + + /** + * @covers PodsField_Code::display + */ + public function test_display_runs_shortcodes_when_allowed() { + add_shortcode( 'pods_code_test', static function () { + return 'SHORTCODE_RAN'; + } ); + + $output = $this->field->display( + '[pods_code_test]', + 'code_field', + [ + 'code_sanitize_html' => 1, + 'code_allow_shortcode' => 1, + ], + null, + 0 + ); + + $this->assertStringContainsString( 'SHORTCODE_RAN', $output ); + } + + /** + * @covers PodsField_Code::display + */ + public function test_display_does_not_run_shortcodes_when_disabled() { + add_shortcode( 'pods_code_test', static function () { + return 'SHORTCODE_RAN'; + } ); + + $output = $this->field->display( + '[pods_code_test]', + 'code_field', + [ + 'code_sanitize_html' => 1, + 'code_allow_shortcode' => 0, + ], + null, + 0 + ); + + $this->assertStringNotContainsString( 'SHORTCODE_RAN', $output ); + $this->assertStringContainsString( '[pods_code_test]', $output ); + } + + /** + * @covers PodsField_Code::display + */ + public function test_display_with_sanitize_disabled_returns_raw() { + $value = ''; + + $output = $this->field->display( + $value, + 'code_field', + [ 'code_sanitize_html' => 0 ], + null, + 0 + ); + + $this->assertSame( $value, $output ); + } +}