diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 6f6262f2c2811..4245a0a57feb8 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2789,6 +2789,118 @@ public function get_test_search_engine_visibility() { return $result; } + /** + * Tests whether the network allows uploading every file type a new network would. + * + * The list of file types a network allows uploading (the `upload_filetypes` + * network option) is populated when the network is created, and is not updated + * when WordPress adds support for new file types, such as WebP or AVIF. This + * recommends adding the types a network created today would allow. Types the + * network allows beyond those are not reported. + * + * @since 7.2.0 + * + * @return array> The test result. + */ + public function get_test_network_upload_filetypes(): array { + $result = array( + 'label' => __( 'Your network allows uploading all default file types' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Performance' ), + 'color' => 'blue', + ), + 'description' => sprintf( + '

%s

', + __( 'The file types your network allows uploading include every type a new network allows by default.' ) + ), + 'actions' => '', + 'test' => 'network_upload_filetypes', + ); + + $missing = $this->get_missing_network_upload_filetypes(); + + if ( empty( $missing ) ) { + return $result; + } + + $result['status'] = 'recommended'; + $result['label'] = __( 'Your network does not allow uploading some default file types' ); + $result['description'] = sprintf( + '

%s

%s

%s

', + __( 'The file types your network allows uploading are set when the network is created, and do not change when WordPress adds support for new file types, such as the WebP and AVIF image formats.' ), + sprintf( + /* translators: %s: List of file extensions. */ + __( 'A network created today would allow these file types, but yours does not: %s' ), + wp_sprintf( + '%l', + array_map( + static function ( $extension ) { + return '' . esc_html( $extension ) . ''; + }, + $missing + ) + ) + ), + __( 'Adding them is optional. Leave out any your network should not accept.' ) + ); + + if ( current_user_can( 'manage_network_options' ) ) { + $result['actions'] = sprintf( + '

%2$s

', + esc_url( network_admin_url( 'settings.php#upload_filetypes' ) ), + __( 'Update the allowed upload file types' ) + ); + } + + return $result; + } + + /** + * Gets the file types a new network would allow uploading, but this network does not. + * + * populate_network_meta() fills the `upload_filetypes` network option with the + * extensions from get_allowed_mime_types(). This compares those against what the + * network's list lets through, the same way check_upload_mimes() does, so an + * extension is not reported when another extension of the same file type, such + * as `jpe` next to `jpg`, already allows it. + * + * The defaults are built as get_allowed_mime_types() builds them for a user + * without the `unfiltered_html` capability, so that HTML and JavaScript files + * are never recommended, and so that the result does not depend on who is + * viewing it. + * + * @since 7.2.0 + * + * @return string[] The missing file extensions. + */ + private function get_missing_network_upload_filetypes(): array { + $default_types = wp_get_mime_types(); + unset( $default_types['swf'], $default_types['exe'], $default_types['htm|html'], $default_types['js'] ); + + // check_upload_mimes() narrows the list to the network's own on multisite. + $priority = has_filter( 'upload_mimes', 'check_upload_mimes' ); + if ( false !== $priority ) { + remove_filter( 'upload_mimes', 'check_upload_mimes', $priority ); + } + + /** This filter is documented in wp-includes/functions.php */ + $default_types = apply_filters( 'upload_mimes', $default_types, null ); + + if ( false !== $priority ) { + add_filter( 'upload_mimes', 'check_upload_mimes', $priority ); + } + + $missing_types = array_diff_key( $default_types, check_upload_mimes( $default_types ) ); + + $missing = array(); + foreach ( array_keys( $missing_types ) as $extensions ) { + array_push( $missing, ...explode( '|', $extensions ) ); + } + + return array_values( array_unique( $missing ) ); + } + /** * Tests if opcode cache is enabled and available. * @@ -3404,6 +3516,14 @@ public static function get_tests() { ); } + // The file types that can be uploaded are a network setting on multisite. + if ( is_multisite() ) { + $tests['direct']['network_upload_filetypes'] = array( + 'label' => __( 'Network upload file types' ), + 'test' => 'network_upload_filetypes', + ); + } + /** * Filters which site status tests are run on a site. * diff --git a/tests/phpunit/tests/admin/wpSiteHealthGetTestNetworkUploadFiletypes.php b/tests/phpunit/tests/admin/wpSiteHealthGetTestNetworkUploadFiletypes.php new file mode 100644 index 0000000000000..8e380ddcbffbb --- /dev/null +++ b/tests/phpunit/tests/admin/wpSiteHealthGetTestNetworkUploadFiletypes.php @@ -0,0 +1,185 @@ +assertArrayNotHasKey( 'network_upload_filetypes', WP_Site_Health::get_tests()['direct'] ); + } + + /** + * Tests that the test is registered on multisite. + * + * @group ms-required + * + * @covers WP_Site_Health::get_tests + */ + public function test_is_registered_on_multisite() { + $this->assertArrayHasKey( 'network_upload_filetypes', WP_Site_Health::get_tests()['direct'] ); + } + + /** + * Tests that a network created before WordPress 5.8 is told about the image + * formats added since. + * + * @group ms-required + */ + public function test_recommends_missing_default_types() { + update_site_option( 'upload_filetypes', 'jpg jpeg png gif' ); + + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertSame( 'recommended', $result['status'] ); + $this->assertSame( 'network_upload_filetypes', $result['test'] ); + $this->assertStringContainsString( 'webp', $result['description'] ); + $this->assertStringContainsString( 'avif', $result['description'] ); + } + + /** + * Tests that the network's default list is used when the option is missing. + * + * @group ms-required + */ + public function test_recommends_missing_default_types_without_option() { + delete_site_option( 'upload_filetypes' ); + + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertSame( 'recommended', $result['status'] ); + $this->assertStringContainsString( 'webp', $result['description'] ); + } + + /** + * Tests that a network allowing every default type passes. + * + * @group ms-required + */ + public function test_passes_with_default_types() { + update_site_option( 'upload_filetypes', $this->get_default_upload_filetypes() ); + + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertSame( 'good', $result['status'] ); + } + + /** + * Tests that types the network allows beyond the defaults are not reported. + * + * @group ms-required + */ + public function test_ignores_additional_types() { + update_site_option( 'upload_filetypes', $this->get_default_upload_filetypes() . ' svg json' ); + + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertSame( 'good', $result['status'] ); + } + + /** + * Tests that an extension is not reported when another extension of the same + * file type already allows it. + * + * check_upload_mimes() keeps `jpg|jpeg|jpe` when the network allows `jpg`. + * + * @group ms-required + */ + public function test_does_not_report_extensions_of_allowed_types() { + update_site_option( 'upload_filetypes', 'jpg png gif' ); + + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertSame( 'recommended', $result['status'] ); + $this->assertStringNotContainsString( 'jpe', $result['description'] ); + $this->assertStringNotContainsString( 'jpeg', $result['description'] ); + } + + /** + * Tests that HTML and JavaScript are never recommended, even to a user who + * can upload them. + * + * @group ms-required + */ + public function test_does_not_recommend_html_or_javascript() { + wp_set_current_user( self::factory()->user->create() ); + grant_super_admin( get_current_user_id() ); + $this->assertTrue( current_user_can( 'unfiltered_html' ), 'A super admin should be able to upload HTML.' ); + + update_site_option( 'upload_filetypes', 'jpg jpeg png gif' ); + + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertStringNotContainsString( 'html', $result['description'] ); + $this->assertStringNotContainsString( 'htm', $result['description'] ); + $this->assertStringNotContainsString( 'js', $result['description'] ); + } + + /** + * Tests that the link to the network settings is only shown to users who can + * change them. + * + * @group ms-required + */ + public function test_links_to_network_settings_for_super_admins_only() { + update_site_option( 'upload_filetypes', 'jpg jpeg png gif' ); + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + $this->assertSame( '', $result['actions'], 'A site administrator should not get the link.' ); + + grant_super_admin( get_current_user_id() ); + $result = ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + $this->assertStringContainsString( network_admin_url( 'settings.php#upload_filetypes' ), $result['actions'], 'A super admin should get the link.' ); + } + + /** + * Tests that the test leaves check_upload_mimes() hooked to `upload_mimes`. + * + * @group ms-required + */ + public function test_restores_check_upload_mimes() { + $priority = has_filter( 'upload_mimes', 'check_upload_mimes' ); + + ( new WP_Site_Health() )->get_test_network_upload_filetypes(); + + $this->assertSame( $priority, has_filter( 'upload_mimes', 'check_upload_mimes' ) ); + } + + /** + * Gets the file types a network created today allows, as populate_network_meta() + * stores them. + * + * @return string The file extensions, separated by spaces. + */ + private function get_default_upload_filetypes() { + $extensions = array(); + foreach ( array_keys( wp_get_mime_types() ) as $extension ) { + array_push( $extensions, ...explode( '|', $extension ) ); + } + + return implode( ' ', array_unique( $extensions ) ); + } +}