diff --git a/src/js/_enqueues/admin/post.js b/src/js/_enqueues/admin/post.js index d50fe6007d33b..9a5bbd7309acd 100644 --- a/src/js/_enqueues/admin/post.js +++ b/src/js/_enqueues/admin/post.js @@ -316,7 +316,46 @@ jQuery( function($) { copyAttachmentURLSuccessTimeout, __ = wp.i18n.__, _x = wp.i18n._x; + /** + * Show a warning in the classic editor when the post content includes + * serialized block comments, which may not be visible in the Visual tab. + */ + function maybeShowBlockMarkupWarning() { + var hasBlockMarkup, $warning, $editorWrapper; + + if ( ! $textarea.length ) { + return; + } + + hasBlockMarkup = //.test( $textarea.val() ); + if ( ! hasBlockMarkup ) { + return; + } + + $warning = $( '#classic-editor-block-markup-notice' ); + if ( $warning.length ) { + return; + } + + $warning = $( '
', { + id: 'classic-editor-block-markup-notice', + 'class': 'notice notice-warning inline' + } ).append( + $( '

' ).text( + __( + 'This content includes Gutenberg block markup that may be visible in the Code editor but not in the Visual editor. The page may not be empty. Review the content carefully before overwriting or deleting it.' + ) + ) + ); + + $editorWrapper = $( '#wp-content-wrap' ); + if ( $editorWrapper.length ) { + $editorWrapper.before( $warning ); + } + } + postboxes.add_postbox_toggles(pagenow); + maybeShowBlockMarkupWarning(); /* * Clear the window name. Otherwise if this is a former preview window where the user navigated to edit another post, diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 5337cc02c88c9..78bad24c1571f 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -45,6 +45,7 @@ // Misc hooks. add_action( 'admin_init', 'wp_admin_headers' ); add_action( 'admin_init', 'send_frame_options_header', 10, 0 ); +add_action( 'admin_init', 'wp_maybe_register_performance_optimization_settings' ); add_action( 'admin_head', 'wp_admin_canonical_url' ); add_action( 'admin_head', 'wp_site_icon' ); add_action( 'admin_head', 'wp_admin_viewport_meta' ); diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..c4d8bba992bac 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -2321,7 +2321,7 @@ function wp_ajax_widgets_order() { unset( $_POST['savewidgets'], $_POST['action'] ); // Save widgets order for all sidebars. - if ( is_array( $_POST['sidebars'] ) ) { + if ( isset( $_POST['sidebars'] ) && is_array( $_POST['sidebars'] ) ) { $sidebars = array(); foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) { diff --git a/src/wp-admin/menu.php b/src/wp-admin/menu.php index 57d94c75e26f2..a7745676bd8d1 100644 --- a/src/wp-admin/menu.php +++ b/src/wp-admin/menu.php @@ -409,6 +409,7 @@ function _add_plugin_file_editor_to_tools() { $submenu['options-general.php'][20] = array( __( 'Reading' ), 'manage_options', 'options-reading.php' ); $submenu['options-general.php'][25] = array( __( 'Discussion' ), 'manage_options', 'options-discussion.php' ); $submenu['options-general.php'][30] = array( __( 'Media' ), 'manage_options', 'options-media.php' ); + $submenu['options-general.php'][35] = array( __( 'Performance' ), 'manage_options', 'options-performance.php' ); $submenu['options-general.php'][40] = array( __( 'Permalinks' ), 'manage_options', 'options-permalink.php' ); $submenu['options-general.php'][45] = array( __( 'Privacy' ), 'manage_privacy_options', 'options-privacy.php' ); diff --git a/src/wp-admin/options-performance.php b/src/wp-admin/options-performance.php new file mode 100644 index 0000000000000..c47d18eaa1c30 --- /dev/null +++ b/src/wp-admin/options-performance.php @@ -0,0 +1,145 @@ +query( "DELETE FROM $wpdb->posts WHERE post_type = 'revision'" ); + $deleted += (int) $wpdb->query( "DELETE FROM $wpdb->comments WHERE comment_approved = 'spam'" ); + $deleted += (int) $wpdb->query( "DELETE FROM $wpdb->comments WHERE comment_approved = 'trash'" ); + $deleted += (int) $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key = '_edit_lock' OR meta_key = '_edit_last'" ); + wp_clean_performance_page_cache(); + + /* translators: %d: Number of database rows deleted. */ + add_settings_error( 'performance', 'performance_database_cleaned', sprintf( __( 'Database cleanup complete. %d rows were removed.' ), $deleted ), 'success' ); + } +} + +get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => '

' . __( 'Performance settings provide native controls for front-end caching, asset minification, critical CSS generation, lazy loading, image optimization, and database cleanup.' ) . '

', + ) +); + +require_once ABSPATH . 'wp-admin/admin-header.php'; + +$settings = wp_get_performance_optimization_settings(); + +?> + +
+

+ + + +
+ + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +

+
+ + + +
+ + +
+ + + +
+ +
+ + diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index b6295ddf19ed1..e38c4d59f74d6 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -363,6 +363,7 @@ add_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 ); add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); +add_action( 'template_redirect', 'wp_start_performance_page_cache', 0 ); add_action( 'init', '_register_core_block_patterns_and_categories' ); add_action( 'init', 'check_theme_switched', 99 ); add_action( 'init', array( 'WP_Block_Supports', 'init' ), 22 ); @@ -537,6 +538,7 @@ add_action( 'init', 'rest_api_init' ); add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 ); add_action( 'rest_api_init', 'register_initial_settings', 10 ); +add_action( 'rest_api_init', 'wp_register_performance_optimization_settings', 10 ); add_action( 'rest_api_init', 'create_initial_rest_routes', 99 ); add_action( 'parse_request', 'rest_api_loaded' ); @@ -795,6 +797,18 @@ add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 ); add_action( 'init', '_wp_register_default_font_collections' ); +// Collaboration. +add_action( 'admin_init', 'wp_collaboration_inject_setting' ); + +// Performance optimization. +add_action( 'save_post', 'wp_clean_performance_page_cache' ); +add_action( 'deleted_post', 'wp_clean_performance_page_cache' ); +add_action( 'clean_post_cache', 'wp_clean_performance_page_cache' ); +add_action( 'transition_comment_status', 'wp_clean_performance_page_cache' ); +add_filter( 'wp_get_loading_optimization_attributes', 'wp_performance_filter_loading_optimization_attributes', 10, 2 ); +add_filter( 'image_editor_output_format', 'wp_performance_filter_image_editor_output_format' ); + +// Add ignoredHookedBlocks metadata attribute to the template and template part post types. // Add ignoredHookedBlocks metadata attribute to the template and template part post types. add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' ); add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' ); diff --git a/src/wp-includes/performance.php b/src/wp-includes/performance.php new file mode 100644 index 0000000000000..05c819e2c7a75 --- /dev/null +++ b/src/wp-includes/performance.php @@ -0,0 +1,352 @@ + false, + 'minify_assets' => false, + 'critical_css' => false, + 'lazy_loading' => true, + 'image_optimization' => false, + 'database_cleanup' => false, + ); +} + +function wp_sanitize_performance_optimization_settings( $settings ) { + $defaults = wp_get_performance_optimization_defaults(); + + if ( ! is_array( $settings ) ) { + $settings = array(); + } + + $sanitized = array(); + foreach ( $defaults as $feature => $default ) { + $sanitized[ $feature ] = isset( $settings[ $feature ] ) ? wp_validate_boolean( $settings[ $feature ] ) : $default; + } + + return $sanitized; +} + +function wp_get_performance_optimization_settings() { + $settings = get_option( 'performance_optimization', array() ); + + return wp_parse_args( + wp_sanitize_performance_optimization_settings( $settings ), + wp_get_performance_optimization_defaults() + ); +} + +function wp_performance_optimization_enabled( $feature ) { + $settings = wp_get_performance_optimization_settings(); + + return ! empty( $settings[ $feature ] ); +} + +function wp_register_performance_optimization_settings() { + global $wp_registered_settings; + + if ( isset( $wp_registered_settings['performance_optimization'] ) ) { + return; + } + + register_setting( + 'performance', + 'performance_optimization', + array( + 'type' => 'object', + 'description' => __( 'Native performance optimization settings.' ), + 'sanitize_callback' => 'wp_sanitize_performance_optimization_settings', + 'default' => wp_get_performance_optimization_defaults(), + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'page_cache' => array( + 'type' => 'boolean', + 'description' => __( 'Enable built-in page caching for anonymous front-end requests.' ), + ), + 'minify_assets' => array( + 'type' => 'boolean', + 'description' => __( 'Minify front-end HTML and inline CSS and JavaScript.' ), + ), + 'critical_css' => array( + 'type' => 'boolean', + 'description' => __( 'Generate a small cached critical CSS block from early inline styles.' ), + ), + 'lazy_loading' => array( + 'type' => 'boolean', + 'description' => __( 'Add lazy-loading attributes to eligible images and iframes.' ), + ), + 'image_optimization' => array( + 'type' => 'boolean', + 'description' => __( 'Prefer modern image output formats when supported by the server.' ), + ), + 'database_cleanup' => array( + 'type' => 'boolean', + 'description' => __( 'Show database cleanup tools on the Performance settings screen.' ), + ), + ), + ), + ), + ) + ); +} + +function wp_maybe_register_performance_optimization_settings() { + global $pagenow; + + if ( wp_doing_ajax() ) { + return; + } + + $is_performance_screen = 'options-performance.php' === $pagenow; + $is_options_screen = 'options.php' === $pagenow; + + if ( $is_performance_screen || $is_options_screen ) { + wp_register_performance_optimization_settings(); + } +} + +function wp_performance_filter_loading_optimization_attributes( $loading_attrs, $tag_name ) { + if ( wp_performance_optimization_enabled( 'lazy_loading' ) || ( 'img' !== $tag_name && 'iframe' !== $tag_name ) ) { + return $loading_attrs; + } + + if ( isset( $loading_attrs['loading'] ) && 'lazy' === $loading_attrs['loading'] ) { + unset( $loading_attrs['loading'] ); + } + + return $loading_attrs; +} + +function wp_performance_filter_image_editor_output_format( $output_format ) { + if ( ! wp_performance_optimization_enabled( 'image_optimization' ) ) { + return $output_format; + } + + $modern_format = ''; + if ( wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) { + $modern_format = 'image/avif'; + } elseif ( wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) { + $modern_format = 'image/webp'; + } + + if ( ! $modern_format ) { + return $output_format; + } + + foreach ( array( 'image/jpeg', 'image/png' ) as $mime_type ) { + $output_format[ $mime_type ] = $modern_format; + } + + return $output_format; +} + +function wp_get_performance_cache_dir( $type = '' ) { + $dir = WP_CONTENT_DIR . '/cache/performance'; + + if ( $type ) { + $dir .= '/' . sanitize_key( $type ); + } + + return $dir; +} + +function wp_delete_performance_cache( $type = '' ) { + $dir = wp_get_performance_cache_dir( $type ); + + if ( ! is_dir( $dir ) ) { + return true; + } + + $items = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $items as $item ) { + if ( $item->isDir() ) { + rmdir( $item->getPathname() ); + } else { + unlink( $item->getPathname() ); + } + } + + return rmdir( $dir ); +} + +function wp_clean_performance_page_cache() { + wp_delete_performance_cache( 'page' ); +} + +function wp_can_optimize_performance_output() { + if ( + ( + ! wp_performance_optimization_enabled( 'page_cache' ) && + ! wp_performance_optimization_enabled( 'minify_assets' ) && + ! wp_performance_optimization_enabled( 'critical_css' ) + ) || + is_admin() || + is_user_logged_in() || + wp_doing_ajax() || + wp_is_json_request() || + is_feed() || + is_robots() || + is_trackback() || + ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) + ) { + return false; + } + + if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) { + return false; + } + + $has_logged_in_cookie = ! empty( $_COOKIE[ LOGGED_IN_COOKIE ] ); + $has_comment_author_cookie = ! empty( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ); + $has_preview = ! empty( $_GET['preview'] ); + + if ( $has_logged_in_cookie || $has_comment_author_cookie || $has_preview ) { + return false; + } + + return (bool) apply_filters( 'wp_can_optimize_performance_output', true ); +} + +function wp_can_use_performance_page_cache() { + if ( ! wp_performance_optimization_enabled( 'page_cache' ) || ! wp_can_optimize_performance_output() ) { + return false; + } + + return (bool) apply_filters( 'wp_can_use_performance_page_cache', true ); +} + +function wp_get_performance_page_cache_file() { + $url = home_url( add_query_arg( null, null ) ); + + return wp_get_performance_cache_dir( 'page' ) . '/' . md5( $url ) . '.html'; +} + +function wp_start_performance_page_cache() { + if ( ! wp_can_optimize_performance_output() ) { + return; + } + + $cache_file = wp_get_performance_page_cache_file(); + $ttl = (int) apply_filters( 'wp_performance_page_cache_ttl', HOUR_IN_SECONDS ); + + if ( wp_can_use_performance_page_cache() && is_readable( $cache_file ) && filemtime( $cache_file ) > time() - $ttl ) { + header( 'X-WP-Performance-Cache: HIT' ); + readfile( $cache_file ); + exit; + } + + if ( wp_performance_optimization_enabled( 'page_cache' ) ) { + header( 'X-WP-Performance-Cache: MISS' ); + } + + ob_start( 'wp_capture_performance_page_cache' ); +} + +function wp_capture_performance_page_cache( $output ) { + if ( ! wp_can_optimize_performance_output() || 200 !== http_response_code() || false === stripos( $output, ']*>(.*?)#is', $html, $matches ) ) { + return $html; + } + + $critical_css = ''; + foreach ( $matches[1] as $css ) { + $critical_css .= trim( wp_minify_performance_css( $css ) ); + if ( strlen( $critical_css ) >= 12000 ) { + break; + } + } + + if ( '' === trim( $critical_css ) ) { + return $html; + } + + $style = ''; + + return preg_replace( '##i', $style . '', $html, 1 ); +} + +function wp_minify_performance_output( $html ) { + $html = preg_replace_callback( + '#]*)>(.*?)#is', + static function ( $matches ) { + return '' . wp_minify_performance_css( $matches[2] ) . ''; + }, + $html + ); + + $html = preg_replace_callback( + '#]*)>(.*?)#is', + static function ( $matches ) { + if ( preg_match( '/\btype=(["\'])(?!text\/javascript|application\/javascript|module)/i', $matches[1] ) ) { + return $matches[0]; + } + + return '' . wp_minify_performance_js( $matches[2] ) . ''; + }, + $html + ); + + $html = preg_replace( '//s', '', $html ); + $html = preg_replace( '/>\s+<', $html ); + + return trim( $html ); +} + +function wp_minify_performance_css( $css ) { + $css = preg_replace( '#/\*.*?\*/#s', '', $css ); + $css = preg_replace( '/\s+/', ' ', $css ); + $css = preg_replace( '/\s*([{}:;,>])\s*/', '$1', $css ); + + return trim( $css ); +} + +function wp_minify_performance_js( $js ) { + $js = preg_replace( '#/\*.*?\*/#s', '', $js ); + $js = preg_replace( '/\s+/', ' ', $js ); + + return trim( $js ); +} diff --git a/src/wp-settings.php b/src/wp-settings.php index b2736bddadc3c..d8ab9c7d02421 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -261,6 +261,7 @@ require ABSPATH . WPINC . '/class-wp-oembed.php'; require ABSPATH . WPINC . '/class-wp-oembed-controller.php'; require ABSPATH . WPINC . '/media.php'; +require ABSPATH . WPINC . '/performance.php'; require ABSPATH . WPINC . '/http.php'; require ABSPATH . WPINC . '/html-api/html5-named-character-references.php'; require ABSPATH . WPINC . '/html-api/class-wp-html-attribute-token.php'; diff --git a/tests/phpunit/tests/ajax/wpAjaxWidgetsOrder.php b/tests/phpunit/tests/ajax/wpAjaxWidgetsOrder.php new file mode 100644 index 0000000000000..15dd6534e4e9d --- /dev/null +++ b/tests/phpunit/tests/ajax/wpAjaxWidgetsOrder.php @@ -0,0 +1,29 @@ +_setRole( 'administrator' ); + + $_POST['savewidgets'] = wp_create_nonce( 'save-sidebar-widgets' ); + + $this->expectException( 'WPAjaxDieStopException' ); + $this->expectExceptionMessage( '-1' ); + $this->_handleAjax( 'widgets-order' ); + } +} + diff --git a/tests/phpunit/tests/performance.php b/tests/phpunit/tests/performance.php new file mode 100644 index 0000000000000..0ff1fae10f850 --- /dev/null +++ b/tests/phpunit/tests/performance.php @@ -0,0 +1,98 @@ + '1', + 'lazy_loading' => '0', + 'unknown_value' => true, + ) + ); + + $this->assertTrue( $settings['page_cache'] ); + $this->assertFalse( $settings['lazy_loading'] ); + $this->assertFalse( $settings['minify_assets'] ); + $this->assertArrayNotHasKey( 'unknown_value', $settings ); + } + + /** + * Tests disabling lazy loading removes only lazy loading attributes. + */ + public function test_wp_performance_filter_loading_optimization_attributes_removes_lazy_loading_when_disabled() { + update_option( + 'performance_optimization', + array( + 'lazy_loading' => false, + ) + ); + + $attributes = wp_performance_filter_loading_optimization_attributes( + array( + 'loading' => 'lazy', + 'fetchpriority' => 'high', + ), + 'img' + ); + + $this->assertArrayNotHasKey( 'loading', $attributes ); + $this->assertSame( 'high', $attributes['fetchpriority'] ); + } + + /** + * Tests image output mappings are unchanged while image optimization is disabled. + */ + public function test_wp_performance_filter_image_editor_output_format_returns_existing_map_when_disabled() { + $output_format = array( + 'image/heic' => 'image/jpeg', + ); + + $this->assertSame( $output_format, wp_performance_filter_image_editor_output_format( $output_format ) ); + } + + /** + * Tests critical CSS generation from inline styles. + */ + public function test_wp_add_performance_critical_css_adds_inline_block() { + $html = ''; + + $this->assertStringContainsString( + '', + wp_add_performance_critical_css( $html ) + ); + } + + /** + * Tests output minification. + */ + public function test_wp_minify_performance_output_minifies_inline_css_and_html() { + $html = "\n\n

Test

\n"; + + $this->assertSame( + '

Test

', + wp_minify_performance_output( $html ) + ); + } +}