Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish 3.1.0 release #1225

Merged
merged 5 commits into from
May 20, 2024
Merged

Publish 3.1.0 release #1225

merged 5 commits into from
May 20, 2024

Conversation

westonruter
Copy link
Member

@westonruter westonruter commented May 17, 2024

Previously: #1125

  • Bump stable tag in readme.txt
  • Bump versions in load.php
  • Run npm run since
  • Run npm run readme
  • Review changelogs for accuracy.
  • Review diff with previous plugin versions.
  • Test the builds.

Fixes #1212

@westonruter westonruter added the skip changelog PRs that should not be mentioned in changelogs label May 17, 2024
@westonruter westonruter added this to the performance-lab 3.1.0 milestone May 17, 2024
@westonruter westonruter changed the title Publish/3.1.0 Publish 3.1.0 release May 17, 2024
@westonruter westonruter added the [Type] Documentation Documentation to be added or enhanced label May 17, 2024
@westonruter
Copy link
Member Author

westonruter commented May 17, 2024

Diff overview via #1227:

auto-sizes

svn status:

M       auto-sizes.php
M       hooks.php
M       readme.txt
svn diff
Index: auto-sizes.php
===================================================================
--- auto-sizes.php	(revision 3088546)
+++ auto-sizes.php	(working copy)
@@ -4,8 +4,8 @@
  * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/auto-sizes
  * Description: Instructs browsers to automatically choose the right image size for lazy-loaded images.
  * Requires at least: 6.4
- * Requires PHP: 7.0
- * Version: 1.0.1
+ * Requires PHP: 7.2
+ * Version: 1.0.2
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -25,6 +25,6 @@
 	return;
 }
 
-define( 'IMAGE_AUTO_SIZES_VERSION', '1.0.1' );
+define( 'IMAGE_AUTO_SIZES_VERSION', '1.0.2' );
 
 require_once __DIR__ . '/hooks.php';
Index: hooks.php
===================================================================
--- hooks.php	(revision 3088546)
+++ hooks.php	(working copy)
@@ -15,10 +15,14 @@
  *
  * @since 1.0.0
  *
- * @param array $attr Attributes for the image markup.
- * @return array The filtered attributes for the image markup.
+ * @param array<string, string>|mixed $attr Attributes for the image markup.
+ * @return array<string, string> The filtered attributes for the image markup.
  */
-function auto_sizes_update_image_attributes( $attr ) {
+function auto_sizes_update_image_attributes( $attr ): array {
+	if ( ! is_array( $attr ) ) {
+		$attr = array();
+	}
+
 	// Bail early if the image is not lazy-loaded.
 	if ( ! isset( $attr['loading'] ) || 'lazy' !== $attr['loading'] ) {
 		return $attr;
@@ -30,7 +34,7 @@
 	}
 
 	// Don't add 'auto' to the sizes attribute if it already exists.
-	if ( false !== strpos( $attr['sizes'], 'auto,' ) ) {
+	if ( str_contains( $attr['sizes'], 'auto,' ) ) {
 		return $attr;
 	}
 
@@ -45,10 +49,14 @@
  *
  * @since 1.0.0
  *
- * @param string $html The HTML image tag markup being filtered.
+ * @param string|mixed $html The HTML image tag markup being filtered.
  * @return string The filtered HTML image tag markup.
  */
-function auto_sizes_update_content_img_tag( $html ) {
+function auto_sizes_update_content_img_tag( $html ): string {
+	if ( ! is_string( $html ) ) {
+		$html = '';
+	}
+
 	// Bail early if the image is not lazy-loaded.
 	if ( false === strpos( $html, 'loading="lazy"' ) ) {
 		return $html;
@@ -77,7 +85,7 @@
  *
  * @since 1.0.1
  */
-function auto_sizes_render_generator() {
+function auto_sizes_render_generator(): void {
 	// Use the plugin slug as it is immutable.
 	echo '<meta name="generator" content="auto-sizes ' . esc_attr( IMAGE_AUTO_SIZES_VERSION ) . '">' . "\n";
 }
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -3,8 +3,8 @@
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
-Requires PHP:      7.0
-Stable tag:        1.0.1
+Requires PHP:      7.2
+Stable tag:        1.0.2
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, images, auto-sizes
@@ -48,6 +48,10 @@
 
 == Changelog ==
 
+= 1.0.2 =
+
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
 = 1.0.1 =
 
 * Add auto-sizes generator tag. ([1105](https://github.com/WordPress/performance/pull/1105))

dominant-color-images

svn status:

M       class-dominant-color-image-editor-gd.php
M       class-dominant-color-image-editor-imagick.php
M       helper.php
M       hooks.php
M       load.php
M       readme.txt
svn diff
Index: class-dominant-color-image-editor-gd.php
===================================================================
--- class-dominant-color-image-editor-gd.php	(revision 3088546)
+++ class-dominant-color-image-editor-gd.php	(working copy)
@@ -32,9 +32,17 @@
 		}
 		// The logic here is resize the image to 1x1 pixel, then get the color of that pixel.
 		$shorted_image = imagecreatetruecolor( 1, 1 );
-		imagecopyresampled( $shorted_image, $this->image, 0, 0, 0, 0, 1, 1, imagesx( $this->image ), imagesy( $this->image ) );
+		$image_width   = imagesx( $this->image );
+		$image_height  = imagesy( $this->image );
+		if ( false === $shorted_image || false === $image_width || false === $image_height ) {
+			return new WP_Error( 'image_editor_dominant_color_error', __( 'Dominant color detection failed.', 'dominant-color-images' ) );
+		}
+		imagecopyresampled( $shorted_image, $this->image, 0, 0, 0, 0, 1, 1, $image_width, $image_height );
 
 		$rgb = imagecolorat( $shorted_image, 0, 0 );
+		if ( false === $rgb ) {
+			return new WP_Error( 'image_editor_dominant_color_error', __( 'Dominant color detection failed.', 'dominant-color-images' ) );
+		}
 		$r   = ( $rgb >> 16 ) & 0xFF;
 		$g   = ( $rgb >> 8 ) & 0xFF;
 		$b   = $rgb & 0xFF;
@@ -46,7 +54,6 @@
 		return $hex;
 	}
 
-
 	/**
 	 * Looks for transparent pixels in the image.
 	 * If there are none, it returns false.
@@ -66,8 +73,14 @@
 		$h = imagesy( $this->image );
 		for ( $x = 0; $x < $w; $x++ ) {
 			for ( $y = 0; $y < $h; $y++ ) {
-				$rgb  = imagecolorat( $this->image, $x, $y );
+				$rgb = imagecolorat( $this->image, $x, $y );
+				if ( false === $rgb ) {
+					return new WP_Error( 'unable_to_obtain_rgb_via_imagecolorat' );
+				}
 				$rgba = imagecolorsforindex( $this->image, $rgb );
+				if ( ! is_array( $rgba ) ) {
+					return new WP_Error( 'unable_to_obtain_rgba_via_imagecolorsforindex' );
+				}
 				if ( $rgba['alpha'] > 0 ) {
 					return true;
 				}
Index: class-dominant-color-image-editor-imagick.php
===================================================================
--- class-dominant-color-image-editor-imagick.php	(revision 3088546)
+++ class-dominant-color-image-editor-imagick.php	(working copy)
@@ -82,7 +82,7 @@
 			for ( $x = 0; $x < $w; $x++ ) {
 				for ( $y = 0; $y < $h; $y++ ) {
 					$pixel = $this->image->getImagePixelColor( $x, $y );
-					$color = $pixel->getColor();
+					$color = $pixel->getColor( 2 );
 					if ( $color['a'] > 0 ) {
 						return true;
 					}
Index: helper.php
===================================================================
--- helper.php	(revision 3088546)
+++ helper.php	(working copy)
@@ -15,7 +15,7 @@
  * @param string[] $editors Array of available image editor class names. Defaults are 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD'.
  * @return string[] Registered image editors class names.
  */
-function dominant_color_set_image_editors( $editors ) {
+function dominant_color_set_image_editors( array $editors ): array {
 	if ( ! class_exists( 'Dominant_Color_Image_Editor_GD' ) ) {
 		require_once __DIR__ . '/class-dominant-color-image-editor-gd.php';
 	}
@@ -24,8 +24,8 @@
 	}
 
 	$replaces = array(
-		'WP_Image_Editor_GD'      => 'Dominant_Color_Image_Editor_GD',
-		'WP_Image_Editor_Imagick' => 'Dominant_Color_Image_Editor_Imagick',
+		WP_Image_Editor_GD::class      => Dominant_Color_Image_Editor_GD::class,
+		WP_Image_Editor_Imagick::class => Dominant_Color_Image_Editor_Imagick::class,
 	);
 
 	foreach ( $replaces as $old => $new ) {
@@ -46,9 +46,9 @@
  * @access private
  *
  * @param int $attachment_id The attachment ID.
- * @return array|WP_Error Array with the dominant color and has transparency values or WP_Error on error.
+ * @return array{ has_transparency?: bool, dominant_color?: string }|WP_Error Array with the dominant color and has transparency values or WP_Error on error.
  */
-function dominant_color_get_dominant_color_data( $attachment_id ) {
+function dominant_color_get_dominant_color_data( int $attachment_id ) {
 	$mime_type = get_post_mime_type( $attachment_id );
 	if ( 'application/pdf' === $mime_type ) {
 		return new WP_Error( 'no_image_found', __( 'Unable to load image.', 'dominant-color-images' ) );
@@ -57,7 +57,17 @@
 	if ( ! $file ) {
 		$file = get_attached_file( $attachment_id );
 	}
+	if ( ! $file ) {
+		return new WP_Error( 'no_image_found', __( 'Unable to load image.', 'dominant-color-images' ) );
+	}
 	add_filter( 'wp_image_editors', 'dominant_color_set_image_editors' );
+
+	/**
+	 * Editor.
+	 *
+	 * @see dominant_color_set_image_editors()
+	 * @var WP_Image_Editor|Dominant_Color_Image_Editor_GD|Dominant_Color_Image_Editor_Imagick|WP_Error $editor
+	 */
 	$editor = wp_get_image_editor(
 		$file,
 		array(
@@ -73,6 +83,10 @@
 		return $editor;
 	}
 
+	if ( ! ( $editor instanceof Dominant_Color_Image_Editor_GD || $editor instanceof Dominant_Color_Image_Editor_Imagick ) ) {
+		return new WP_Error( 'image_no_editor', __( 'No editor could be selected.', 'default' ) );
+	}
+
 	$has_transparency = $editor->has_transparency();
 	if ( is_wp_error( $has_transparency ) ) {
 		return $has_transparency;
@@ -97,7 +111,7 @@
  * @param string $size          Optional. Image size. Default 'medium'.
  * @return false|string Path to an image or false if not found.
  */
-function dominant_color_get_attachment_file_path( $attachment_id, $size = 'medium' ) {
+function dominant_color_get_attachment_file_path( int $attachment_id, string $size = 'medium' ) {
 	$imagedata = wp_get_attachment_metadata( $attachment_id );
 	if ( ! is_array( $imagedata ) ) {
 		return false;
@@ -108,6 +122,9 @@
 	}
 
 	$file = get_attached_file( $attachment_id );
+	if ( ! $file ) {
+		return false;
+	}
 
 	$filepath = str_replace( wp_basename( $file ), $imagedata['sizes'][ $size ]['file'], $file );
 
@@ -122,7 +139,7 @@
  * @param int $attachment_id Attachment ID for image.
  * @return string|null Hex value of dominant color or null if not set.
  */
-function dominant_color_get_dominant_color( $attachment_id ) {
+function dominant_color_get_dominant_color( int $attachment_id ): ?string {
 	if ( ! wp_attachment_is_image( $attachment_id ) ) {
 		return null;
 	}
@@ -146,7 +163,7 @@
  * @param int $attachment_id Attachment ID for image.
  * @return bool|null Whether the image has transparency, or null if not set.
  */
-function dominant_color_has_transparency( $attachment_id ) {
+function dominant_color_has_transparency( int $attachment_id ): ?bool {
 	$image_meta = wp_get_attachment_metadata( $attachment_id );
 	if ( ! is_array( $image_meta ) ) {
 		return null;
@@ -171,9 +188,12 @@
  *
  * @return string|null Hex color or null if error.
  */
-function dominant_color_rgb_to_hex( $red, $green, $blue ) {
-	$range = range( 0, 255 );
-	if ( ! in_array( $red, $range, true ) || ! in_array( $green, $range, true ) || ! in_array( $blue, $range, true ) ) {
+function dominant_color_rgb_to_hex( int $red, int $green, int $blue ): ?string {
+	if ( ! (
+		$red >= 0 && $red <= 255
+		&& $green >= 0 && $green <= 255
+		&& $blue >= 0 && $blue <= 255
+	) ) {
 		return null;
 	}
 
Index: hooks.php
===================================================================
--- hooks.php	(revision 3088546)
+++ hooks.php	(working copy)
@@ -16,11 +16,15 @@
  *
  * @since 1.0.0
  *
- * @param array $metadata      The attachment metadata.
- * @param int   $attachment_id The attachment ID.
- * @return array $metadata The attachment metadata.
+ * @param array|mixed $metadata      The attachment metadata.
+ * @param int         $attachment_id The attachment ID.
+ * @return array{ has_transparency?: bool, dominant_color?: string } $metadata The attachment metadata.
  */
-function dominant_color_metadata( $metadata, $attachment_id ) {
+function dominant_color_metadata( $metadata, int $attachment_id ): array {
+	if ( ! is_array( $metadata ) ) {
+		$metadata = array();
+	}
+
 	$dominant_color_data = dominant_color_get_dominant_color_data( $attachment_id );
 	if ( ! is_wp_error( $dominant_color_data ) ) {
 		if ( isset( $dominant_color_data['dominant_color'] ) ) {
@@ -41,11 +45,15 @@
  *
  * @since 1.0.0
  *
- * @param array  $attr       Attributes for the image markup.
- * @param object $attachment Image attachment post.
- * @return mixed $attr Attributes for the image markup.
+ * @param array|mixed $attr       Attributes for the image markup.
+ * @param WP_Post     $attachment Image attachment post.
+ * @return array{ 'data-has-transparency'?: string, class?: string, 'data-dominant-color'?: string, style?: string } Attributes for the image markup.
  */
-function dominant_color_update_attachment_image_attributes( $attr, $attachment ) {
+function dominant_color_update_attachment_image_attributes( $attr, WP_Post $attachment ): array {
+	if ( ! is_array( $attr ) ) {
+		$attr = array();
+	}
+
 	$image_meta = wp_get_attachment_metadata( $attachment->ID );
 	if ( ! is_array( $image_meta ) ) {
 		return $attr;
@@ -77,12 +85,15 @@
  *
  * @since 1.0.0
  *
- * @param string $filtered_image The filtered image.
- * @param string $context        The context of the image.
- * @param int    $attachment_id  The attachment ID.
+ * @param string|mixed $filtered_image The filtered image.
+ * @param string       $context        The context of the image.
+ * @param int          $attachment_id  The attachment ID.
  * @return string image tag
  */
-function dominant_color_img_tag_add_dominant_color( $filtered_image, $context, $attachment_id ) {
+function dominant_color_img_tag_add_dominant_color( $filtered_image, string $context, int $attachment_id ): string {
+	if ( ! is_string( $filtered_image ) ) {
+		$filtered_image = '';
+	}
 
 	// Only apply this in `the_content` for now, since otherwise it can result in duplicate runs due to a problem with full site editing logic.
 	if ( 'the_content' !== $context ) {
@@ -142,11 +153,11 @@
 		$extra_class  = $image_meta['has_transparency'] ? 'has-transparency' : 'not-transparent';
 	}
 
-	if ( ! empty( $data ) ) {
+	if ( $data ) {
 		$filtered_image = str_replace( '<img ', '<img ' . $data, $filtered_image );
 	}
 
-	if ( ! empty( $extra_class ) ) {
+	if ( $extra_class ) {
 		$filtered_image = str_replace( ' class="', ' class="' . $extra_class . ' ', $filtered_image );
 	}
 
@@ -159,7 +170,7 @@
  *
  * @since 1.0.0
  */
-function dominant_color_add_inline_style() {
+function dominant_color_add_inline_style(): void {
 	$handle = 'dominant-color-styles';
 	// PHPCS ignore reason: Version not used since this handle is only registered for adding an inline style.
 	// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
@@ -168,7 +179,7 @@
 	$custom_css = 'img[data-dominant-color]:not(.has-transparency) { background-color: var(--dominant-color); }';
 	wp_add_inline_style( $handle, $custom_css );
 }
-add_filter( 'wp_enqueue_scripts', 'dominant_color_add_inline_style' );
+add_action( 'wp_enqueue_scripts', 'dominant_color_add_inline_style' );
 
 /**
  * Displays the HTML generator tag for the Image Placeholders plugin.
@@ -177,7 +188,7 @@
  *
  * @since 1.0.0
  */
-function dominant_color_render_generator() {
+function dominant_color_render_generator(): void {
 	// Use the plugin slug as it is immutable.
 	echo '<meta name="generator" content="dominant-color-images ' . esc_attr( DOMINANT_COLOR_IMAGES_VERSION ) . '">' . "\n";
 }
Index: load.php
===================================================================
--- load.php	(revision 3088546)
+++ load.php	(working copy)
@@ -4,8 +4,8 @@
  * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/dominant-color-images
  * Description: Displays placeholders based on an image's dominant color while the image is loading.
  * Requires at least: 6.4
- * Requires PHP: 7.0
- * Version: 1.1.0
+ * Requires PHP: 7.2
+ * Version: 1.1.1
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -25,7 +25,7 @@
 	return;
 }
 
-define( 'DOMINANT_COLOR_IMAGES_VERSION', '1.1.0' );
+define( 'DOMINANT_COLOR_IMAGES_VERSION', '1.1.1' );
 
 require_once __DIR__ . '/helper.php';
 require_once __DIR__ . '/hooks.php';
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -3,8 +3,8 @@
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
-Requires PHP:      7.0
-Stable tag:        1.1.0
+Requires PHP:      7.2
+Stable tag:        1.1.1
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, images, dominant color
@@ -49,6 +49,17 @@
 
 == Changelog ==
 
+= 1.1.1 =
+
+**Enhancements**
+
+* Avoid needless array allocation in rgb to hex conversion. ([1104](https://github.com/WordPress/performance/pull/1104))
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
+**Bug Fixes**
+
+* Fix Imagick detecting partial transparency. ([1215](https://github.com/WordPress/performance/pull/1215))
+
 = 1.1.0 =
 
 * Rename plugin to "Image Placeholders". ([1101](https://github.com/WordPress/performance/pull/1101))

embed-optimizer

svn status:

M       hooks.php
M       load.php
M       readme.txt
svn diff
Index: hooks.php
===================================================================
--- hooks.php	(revision 3088546)
+++ hooks.php	(working copy)
@@ -19,7 +19,7 @@
  * @since 0.1.0
  *
  * @param string $html The oEmbed HTML.
- * @return string
+ * @return string Filtered oEmbed HTML.
  */
 function embed_optimizer_filter_oembed_html( string $html ): string {
 	$html_processor = new WP_HTML_Tag_Processor( $html );
@@ -75,6 +75,22 @@
 	if ( 1 === $iframe_count && $html_processor->has_bookmark( 'iframe' ) ) {
 		if ( $html_processor->seek( 'iframe' ) ) {
 			$html_processor->set_attribute( 'loading', 'lazy' );
+
+			// For post embeds, use visibility:hidden instead of clip since browsers will consistently load the
+			// lazy-loaded iframe (where Chromium is unreliably with clip) while at the same time improve accessibility
+			// by preventing links in the hidden iframe from receiving focus.
+			if ( $html_processor->has_class( 'wp-embedded-content' ) ) {
+				$style = $html_processor->get_attribute( 'style' );
+				if ( is_string( $style ) ) {
+					// WordPress core injects this clip CSS property:
+					// <https://github.com/WordPress/wordpress-develop/blob/6974b994de5/src/wp-includes/embed.php#L968>.
+					$style = str_replace( 'clip: rect(1px, 1px, 1px, 1px);', 'visibility: hidden;', $style );
+
+					// Note: wp-embed.js removes the style attribute entirely when the iframe is loaded:
+					// <https://github.com/WordPress/wordpress-develop/blob/6974b994d/src/js/_enqueues/wp/embed.js#L60>.
+					$html_processor->set_attribute( 'style', $style );
+				}
+			}
 		} else {
 			embed_optimizer_trigger_error( __FUNCTION__, esc_html__( 'Embed Optimizer unable to seek to iframe bookmark.', 'embed-optimizer' ) );
 		}
@@ -89,7 +105,7 @@
  *
  * @since 0.1.0
  */
-function embed_optimizer_lazy_load_scripts() {
+function embed_optimizer_lazy_load_scripts(): void {
 	$js = <<<JS
 		const lazyEmbedsScripts = document.querySelectorAll( 'script[type="application/vnd.embed-optimizer.javascript"]' );
 		const lazyEmbedScriptsByParents = new Map();
@@ -147,7 +163,7 @@
  * @param int    $error_level   Optional. The designated error type for this error.
  *                              Only works with E_USER family of constants. Default E_USER_NOTICE.
  */
-function embed_optimizer_trigger_error( string $function_name, string $message, int $error_level = E_USER_NOTICE ) {
+function embed_optimizer_trigger_error( string $function_name, string $message, int $error_level = E_USER_NOTICE ): void {
 	if ( ! function_exists( 'wp_trigger_error' ) ) {
 		return;
 	}
@@ -161,7 +177,7 @@
  *
  * @since 0.1.0
  */
-function embed_optimizer_render_generator() {
+function embed_optimizer_render_generator(): void {
 	// Use the plugin slug as it is immutable.
 	echo '<meta name="generator" content="embed-optimizer ' . esc_attr( EMBED_OPTIMIZER_VERSION ) . '">' . "\n";
 }
Index: load.php
===================================================================
--- load.php	(revision 3088546)
+++ load.php	(working copy)
@@ -4,8 +4,8 @@
  * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/embed-optimizer
  * Description: Optimizes the performance of embeds by lazy-loading iframes and scripts.
  * Requires at least: 6.4
- * Requires PHP: 7.0
- * Version: 0.1.1
+ * Requires PHP: 7.2
+ * Version: 0.1.2
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -20,7 +20,7 @@
 	exit;
 }
 
-define( 'EMBED_OPTIMIZER_VERSION', '0.1.1' );
+define( 'EMBED_OPTIMIZER_VERSION', '0.1.2' );
 
 // Load in the Embed Optimizer plugin hooks.
 require_once __DIR__ . '/hooks.php';
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -3,8 +3,8 @@
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
-Requires PHP:      7.0
-Stable tag:        0.1.1
+Requires PHP:      7.2
+Stable tag:        0.1.2
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, embeds
@@ -49,6 +49,16 @@
 
 == Changelog ==
 
+= 0.1.2 =
+
+**Enhancements**
+
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
+**Bug Fixes**
+
+* Hide post embed iframes with visibility:hidden instead of clipping. ([1192](https://github.com/WordPress/performance/pull/1192))
+
 = 0.1.1 =
 
 * Use plugin slug for generator tag. ([1103](https://github.com/WordPress/performance/pull/1103))

optimization-detective

svn status:

M       class-od-html-tag-processor.php
M       class-od-html-tag-walker.php
M       class-od-url-metric.php
M       class-od-url-metrics-group-collection.php
M       class-od-url-metrics-group.php
M       helper.php
M       load.php
M       optimization.php
M       readme.txt
M       storage/class-od-storage-lock.php
M       storage/class-od-url-metrics-post-type.php
M       storage/data.php
M       storage/rest-api.php
M       uninstall.php
svn diff
Index: class-od-html-tag-processor.php
===================================================================
--- class-od-html-tag-processor.php	(revision 3088546)
+++ class-od-html-tag-processor.php	(working copy)
@@ -33,7 +33,7 @@
 	 *
 	 * @param string $html HTML to process.
 	 */
-	public function __construct( $html ) {
+	public function __construct( string $html ) {
 		$this->old_text_replacement_signature_needed = version_compare( get_bloginfo( 'version' ), '6.5', '<' );
 		parent::__construct( $html );
 	}
Index: class-od-html-tag-walker.php
===================================================================
--- class-od-html-tag-walker.php	(revision 3088546)
+++ class-od-html-tag-walker.php	(working copy)
@@ -212,6 +212,9 @@
 		$this->open_stack_indices = array();
 		while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
 			$tag_name = $p->get_tag();
+			if ( ! is_string( $tag_name ) ) {
+				continue;
+			}
 			if ( ! $p->is_tag_closer() ) {
 
 				// Close an open P tag when a P-closing tag is encountered.
@@ -220,7 +223,7 @@
 				if ( in_array( $tag_name, self::P_CLOSING_TAGS, true ) ) {
 					$i = array_search( 'P', $this->open_stack_tags, true );
 					if ( false !== $i ) {
-						array_splice( $this->open_stack_tags, $i );
+						array_splice( $this->open_stack_tags, (int) $i );
 						array_splice( $this->open_stack_indices, count( $this->open_stack_tags ) );
 					}
 				}
@@ -287,7 +290,7 @@
 	 *
 	 * @param string $message Warning message.
 	 */
-	private function warn( string $message ) {
+	private function warn( string $message ): void {
 		wp_trigger_error(
 			__CLASS__ . '::open_tags',
 			esc_html( $message )
@@ -338,7 +341,7 @@
 	public function get_xpath(): string {
 		$xpath = '';
 		foreach ( $this->get_breadcrumbs() as list( $tag_name, $index ) ) {
-			$xpath .= sprintf( '/*[%d][self::%s]', $index, $tag_name );
+			$xpath .= sprintf( '/*[%d][self::%s]', $index + 1, $tag_name );
 		}
 		return $xpath;
 	}
Index: class-od-url-metric.php
===================================================================
--- class-od-url-metric.php	(revision 3088546)
+++ class-od-url-metric.php	(working copy)
@@ -25,7 +25,7 @@
  *                           }
  * @phpstan-type Data        array{
  *                               url: string,
- *                               timestamp: int,
+ *                               timestamp: float,
  *                               viewport: RectData,
  *                               elements: ElementData[]
  *                           }
@@ -45,22 +45,36 @@
 	/**
 	 * Constructor.
 	 *
-	 * @param array $data URL metric data.
+	 * @phpstan-param Data|array<string, mixed> $data Valid data or invalid data (in which case an exception is thrown).
 	 *
+	 * @param array<string, mixed> $data URL metric data.
+	 *
 	 * @throws OD_Data_Validation_Exception When the input is invalid.
 	 */
 	public function __construct( array $data ) {
+		$this->validate_data( $data );
+		$this->data = $data;
+	}
+
+	/**
+	 * Validate data.
+	 *
+	 * @phpstan-assert Data $data
+	 *
+	 * @param array<string, mixed> $data Data to validate.
+	 * @throws OD_Data_Validation_Exception When the input is invalid.
+	 */
+	private function validate_data( array $data ): void {
 		$valid = rest_validate_object_value_from_schema( $data, self::get_json_schema(), self::class );
 		if ( is_wp_error( $valid ) ) {
 			throw new OD_Data_Validation_Exception( esc_html( $valid->get_error_message() ) );
 		}
-		$this->data = $data;
 	}
 
 	/**
 	 * Gets JSON schema for URL Metric.
 	 *
-	 * @return array Schema.
+	 * @return array<string, mixed> Schema.
 	 */
 	public static function get_json_schema(): array {
 		$dom_rect_schema = array(
Index: class-od-url-metrics-group-collection.php
===================================================================
--- class-od-url-metrics-group-collection.php	(revision 3088546)
+++ class-od-url-metrics-group-collection.php	(working copy)
@@ -38,8 +38,13 @@
 	/**
 	 * Breakpoints in max widths.
 	 *
-	 * Valid values are from 1 to PHP_INT_MAX.
+	 * Valid values are from 1 to PHP_INT_MAX - 1. This is because:
 	 *
+	 * 1. It doesn't make sense for there to be a viewport width of zero, so the first breakpoint (max width) must be at least 1.
+	 * 2. After the last breakpoint, the final breakpoint group is set to be spanning one plus the last breakpoint max width up
+	 *    until PHP_INT_MAX. So a breakpoint cannot be PHP_INT_MAX because then the minimum viewport width for the final group
+	 *    would end up being larger than PHP_INT_MAX.
+	 *
 	 * @var int[]
 	 * @phpstan-var positive-int[]
 	 */
@@ -78,7 +83,7 @@
 		sort( $breakpoints );
 		$breakpoints = array_values( array_unique( $breakpoints, SORT_NUMERIC ) );
 		foreach ( $breakpoints as $breakpoint ) {
-			if ( $breakpoint <= 1 || PHP_INT_MAX === $breakpoint ) {
+			if ( ! is_int( $breakpoint ) || $breakpoint < 1 || PHP_INT_MAX === $breakpoint ) {
 				throw new InvalidArgumentException(
 					esc_html(
 						sprintf(
@@ -93,6 +98,11 @@
 				);
 			}
 		}
+		/**
+		 * Validated breakpoints.
+		 *
+		 * @var positive-int[] $breakpoints
+		 */
 		$this->breakpoints = $breakpoints;
 
 		// Set sample size.
@@ -133,6 +143,8 @@
 	/**
 	 * Create groups.
 	 *
+	 * @phpstan-return non-empty-array<OD_URL_Metrics_Group>
+	 *
 	 * @return OD_URL_Metrics_Group[] Groups.
 	 */
 	private function create_groups(): array {
@@ -155,7 +167,7 @@
 	 *
 	 * @param OD_URL_Metric $new_url_metric New URL metric.
 	 */
-	public function add_url_metric( OD_URL_Metric $new_url_metric ) {
+	public function add_url_metric( OD_URL_Metric $new_url_metric ): void {
 		foreach ( $this->groups as $group ) {
 			if ( $group->is_viewport_width_in_range( $new_url_metric->get_viewport_width() ) ) {
 				$group->add_url_metric( $new_url_metric );
Index: class-od-url-metrics-group.php
===================================================================
--- class-od-url-metrics-group.php	(revision 3088546)
+++ class-od-url-metrics-group.php	(working copy)
@@ -157,7 +157,7 @@
 	 *
 	 * @param OD_URL_Metric $url_metric URL metric.
 	 */
-	public function add_url_metric( OD_URL_Metric $url_metric ) {
+	public function add_url_metric( OD_URL_Metric $url_metric ): void {
 		if ( ! $this->is_viewport_width_in_range( $url_metric->get_viewport_width() ) ) {
 			throw new InvalidArgumentException(
 				esc_html__( 'URL metric is not in the viewport range for group.', 'optimization-detective' )
Index: helper.php
===================================================================
--- helper.php	(revision 3088546)
+++ helper.php	(working copy)
@@ -17,7 +17,7 @@
  *
  * @since 0.1.0
  */
-function od_render_generator_meta_tag() {
+function od_render_generator_meta_tag(): void {
 	// Use the plugin slug as it is immutable.
 	echo '<meta name="generator" content="optimization-detective ' . esc_attr( OPTIMIZATION_DETECTIVE_VERSION ) . '">' . "\n";
 }
Index: load.php
===================================================================
--- load.php	(revision 3088546)
+++ load.php	(working copy)
@@ -4,8 +4,8 @@
  * Plugin URI: https://github.com/WordPress/performance/issues/869
  * Description: Uses real user metrics to improve heuristics WordPress applies on the frontend to improve image loading priority.
  * Requires at least: 6.4
- * Requires PHP: 7.0
- * Version: 0.1.1
+ * Requires PHP: 7.2
+ * Version: 0.2.0
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -20,52 +20,102 @@
 	exit;
 }
 
-// Define the constant.
-if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) {
-	return;
-}
+(
+	/**
+	 * Register this copy of the plugin among other potential copies embedded in plugins or themes.
+	 *
+	 * @param string  $global_var_name Global variable name for storing the plugin pending loading.
+	 * @param string  $version         Version.
+	 * @param Closure $load            Callback that loads the plugin.
+	 */
+	static function ( string $global_var_name, string $version, Closure $load ): void {
+		if ( ! isset( $GLOBALS[ $global_var_name ] ) ) {
+			$bootstrap = static function () use ( $global_var_name ): void {
+				if (
+					isset( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] )
+					&&
+					$GLOBALS[ $global_var_name ]['load'] instanceof Closure
+					&&
+					is_string( $GLOBALS[ $global_var_name ]['version'] )
+				) {
+					call_user_func( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] );
+					unset( $GLOBALS[ $global_var_name ] );
+				}
+			};
 
-if (
-	( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) &&
-	! file_exists( __DIR__ . '/build/web-vitals.asset.php' )
-) {
-	// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
-	trigger_error(
-		esc_html(
-			sprintf(
-				/* translators: 1: File path. 2: CLI command. */
-				'[Optimization Detective] ' . __( 'Unable to load %1$s. Please make sure you have run %2$s.', 'optimization-detective' ),
-				'build/web-vitals.asset.php',
-				'`npm install && npm run build:optimization-detective`'
-			)
-		),
-		E_USER_ERROR
-	);
-}
+			// Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used
+			// because it is the first action that fires once the theme is loaded.
+			add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN );
+		}
 
-define( 'OPTIMIZATION_DETECTIVE_VERSION', '0.1.1' );
+		// Register this copy of the plugin.
+		if (
+			// Register this copy if none has been registered yet.
+			! isset( $GLOBALS[ $global_var_name ]['version'] )
+			||
+			// Or register this copy if the version greater than what is currently registered.
+			version_compare( $version, $GLOBALS[ $global_var_name ]['version'], '>' )
+			||
+			// Otherwise, register this copy if it is actually the one installed in the directory for plugins.
+			rtrim( WP_PLUGIN_DIR, '/' ) === dirname( __DIR__ )
+		) {
+			$GLOBALS[ $global_var_name ]['version'] = $version;
+			$GLOBALS[ $global_var_name ]['load']    = $load;
+		}
+	}
+)(
+	'optimization_detective_pending_plugin',
+	'0.2.0',
+	static function ( string $version ): void {
 
-require_once __DIR__ . '/helper.php';
+		// Define the constant.
+		if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) {
+			return;
+		}
 
-// Core infrastructure classes.
-require_once __DIR__ . '/class-od-data-validation-exception.php';
-require_once __DIR__ . '/class-od-html-tag-processor.php';
-require_once __DIR__ . '/class-od-url-metric.php';
-require_once __DIR__ . '/class-od-url-metrics-group.php';
-require_once __DIR__ . '/class-od-url-metrics-group-collection.php';
+		if (
+			( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) &&
+			! file_exists( __DIR__ . '/build/web-vitals.asset.php' )
+		) {
+			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
+			trigger_error(
+				esc_html(
+					sprintf(
+						/* translators: 1: File path. 2: CLI command. */
+						'[Optimization Detective] ' . __( 'Unable to load %1$s. Please make sure you have run %2$s.', 'optimization-detective' ),
+						'build/web-vitals.asset.php',
+						'`npm install && npm run build:optimization-detective`'
+					)
+				),
+				E_USER_ERROR
+			);
+		}
 
-// Storage logic.
-require_once __DIR__ . '/storage/class-od-url-metrics-post-type.php';
-require_once __DIR__ . '/storage/class-od-storage-lock.php';
-require_once __DIR__ . '/storage/data.php';
-require_once __DIR__ . '/storage/rest-api.php';
+		define( 'OPTIMIZATION_DETECTIVE_VERSION', $version );
 
-// Detection logic.
-require_once __DIR__ . '/detection.php';
+		require_once __DIR__ . '/helper.php';
 
-// Optimization logic.
-require_once __DIR__ . '/class-od-html-tag-walker.php';
-require_once __DIR__ . '/optimization.php';
+		// Core infrastructure classes.
+		require_once __DIR__ . '/class-od-data-validation-exception.php';
+		require_once __DIR__ . '/class-od-html-tag-processor.php';
+		require_once __DIR__ . '/class-od-url-metric.php';
+		require_once __DIR__ . '/class-od-url-metrics-group.php';
+		require_once __DIR__ . '/class-od-url-metrics-group-collection.php';
 
-// Add hooks for the above requires.
-require_once __DIR__ . '/hooks.php';
+		// Storage logic.
+		require_once __DIR__ . '/storage/class-od-url-metrics-post-type.php';
+		require_once __DIR__ . '/storage/class-od-storage-lock.php';
+		require_once __DIR__ . '/storage/data.php';
+		require_once __DIR__ . '/storage/rest-api.php';
+
+		// Detection logic.
+		require_once __DIR__ . '/detection.php';
+
+		// Optimization logic.
+		require_once __DIR__ . '/class-od-html-tag-walker.php';
+		require_once __DIR__ . '/optimization.php';
+
+		// Add hooks for the above requires.
+		require_once __DIR__ . '/hooks.php';
+	}
+);
Index: optimization.php
===================================================================
--- optimization.php	(revision 3088546)
+++ optimization.php	(working copy)
@@ -27,7 +27,7 @@
  * @access private
  * @link https://core.trac.wordpress.org/ticket/43258
  *
- * @param string $passthrough Optional. Filter value. Default null.
+ * @param string $passthrough Value for the template_include filter which is passed through.
  * @return string Unmodified value of $passthrough.
  */
 function od_buffer_output( string $passthrough ): string {
@@ -53,12 +53,18 @@
  * @since 0.1.0
  * @access private
  */
-function od_maybe_add_template_output_buffer_filter() {
-	if ( ! od_can_optimize_response() ) {
+function od_maybe_add_template_output_buffer_filter(): void {
+	if ( ! od_can_optimize_response() || isset( $_GET['optimization_detective_disabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 		return;
 	}
 	$callback = 'od_optimize_template_output_buffer';
-	if ( function_exists( 'perflab_wrap_server_timing' ) ) {
+	if (
+		function_exists( 'perflab_wrap_server_timing' )
+		&&
+		function_exists( 'perflab_server_timing_use_output_buffer' )
+		&&
+		perflab_server_timing_use_output_buffer()
+	) {
 		$callback = perflab_wrap_server_timing( $callback, 'optimization-detective', 'exist' );
 	}
 	add_filter( 'od_template_output_buffer', $callback );
@@ -80,7 +86,7 @@
 		// Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context.
 		is_customize_preview() ||
 		// Since the images detected in the response body of a POST request cannot, by definition, be cached.
-		'GET' !== $_SERVER['REQUEST_METHOD'] ||
+		( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) ||
 		// The aim is to optimize pages for the majority of site visitors, not those who administer the site. For admin
 		// users, additional elements will be present like the script from wp_customize_support_script() which will
 		// interfere with the XPath indices. Note that od_get_normalized_query_vars() is varied by is_user_logged_in()
@@ -173,6 +179,31 @@
 }
 
 /**
+ * Determines whether the response has an HTML Content-Type.
+ *
+ * @since 0.2.0
+ * @private
+ *
+ * @return bool Whether Content-Type is HTML.
+ */
+function od_is_response_html_content_type(): bool {
+	$is_html_content_type = false;
+
+	$headers_list = array_merge(
+		array( 'Content-Type: ' . ini_get( 'default_mimetype' ) ),
+		headers_list()
+	);
+	foreach ( $headers_list as $header ) {
+		$header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) );
+		if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && 'content-type' === $header_parts[0] ) {
+			$is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true );
+		}
+	}
+
+	return $is_html_content_type;
+}
+
+/**
  * Optimizes template output buffer.
  *
  * @since 0.1.0
@@ -182,6 +213,10 @@
  * @return string Filtered template output buffer.
  */
 function od_optimize_template_output_buffer( string $buffer ): string {
+	if ( ! od_is_response_html_content_type() ) {
+		return $buffer;
+	}
+
 	$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
 	$post = OD_URL_Metrics_Post_Type::get_post( $slug );
 
@@ -244,7 +279,7 @@
 			&&
 			$walker->get_attribute( 'src' )
 			&&
-			! str_starts_with( $walker->get_attribute( 'src' ), 'data:' )
+			! str_starts_with( (string) $walker->get_attribute( 'src' ), 'data:' )
 		);
 
 		/*
@@ -260,7 +295,7 @@
 		if (
 			$style
 			&&
-			preg_match( '/background(-image)?\s*:[^;]*?url\(\s*[\'"]?(?<background_image>.+?)[\'"]?\s*\)/', $style, $matches )
+			preg_match( '/background(-image)?\s*:[^;]*?url\(\s*[\'"]?(?<background_image>.+?)[\'"]?\s*\)/', (string) $style, $matches )
 			&&
 			! str_starts_with( $matches['background_image'], 'data:' )
 		) {
@@ -307,16 +342,20 @@
 				$img_attributes = array();
 				foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin' ) as $attr_name ) {
 					$value = $walker->get_attribute( $attr_name );
-					if ( null !== $value ) {
+					if ( is_string( $value ) ) {
 						$img_attributes[ $attr_name ] = $value;
 					}
 				}
 				foreach ( $lcp_element_minimum_viewport_widths_by_xpath[ $xpath ] as $minimum_viewport_width ) {
-					$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ]['img_attributes'] = $img_attributes;
+					if ( is_array( $lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ] ) ) {
+						$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ]['img_attributes'] = $img_attributes;
+					}
 				}
 			} elseif ( $background_image_url ) {
 				foreach ( $lcp_element_minimum_viewport_widths_by_xpath[ $xpath ] as $minimum_viewport_width ) {
-					$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ]['background_image'] = $background_image_url;
+					if ( is_array( $lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ] ) ) {
+						$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ]['background_image'] = $background_image_url;
+					}
 				}
 			}
 		}
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -3,8 +3,8 @@
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
-Requires PHP:      7.0
-Stable tag:        0.1.1
+Requires PHP:      7.2
+Stable tag:        0.2.0
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, images
@@ -137,6 +137,21 @@
 
 == Changelog ==
 
+= 0.2.0 =
+
+**Enhancements**
+
+* Add optimization_detective_disabled query var to disable behavior. ([1193](https://github.com/WordPress/performance/pull/1193))
+* Facilitate embedding Optimization Detective in other plugins/themes. ([1185](https://github.com/WordPress/performance/pull/1185))
+* Use PHP 7.2 features in Optimization Detective. ([1162](https://github.com/WordPress/performance/pull/1162))
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
+**Bug Fixes**
+
+* Avoid _doing_it_wrong() for Server-Timing in Optimization Detective when output buffering is not enabled. ([1194](https://github.com/WordPress/performance/pull/1194))
+* Ensure only HTML responses are optimized. ([1189](https://github.com/WordPress/performance/pull/1189))
+* Fix XPath indices to be 1-based instead of 0-based. ([1191](https://github.com/WordPress/performance/pull/1191))
+
 = 0.1.1 =
 
 * Use plugin slug for generator tag. ([1103](https://github.com/WordPress/performance/pull/1103))
Index: storage/class-od-storage-lock.php
===================================================================
--- storage/class-od-storage-lock.php	(revision 3088546)
+++ storage/class-od-storage-lock.php	(working copy)
@@ -67,7 +67,7 @@
 	 * @since 0.1.0
 	 * @access private
 	 */
-	public static function set_lock() {
+	public static function set_lock(): void {
 		$ttl = self::get_ttl();
 		$key = self::get_transient_key();
 		if ( 0 === $ttl ) {
Index: storage/class-od-url-metrics-post-type.php
===================================================================
--- storage/class-od-url-metrics-post-type.php	(revision 3088546)
+++ storage/class-od-url-metrics-post-type.php	(working copy)
@@ -44,7 +44,7 @@
 	 *
 	 * @since 0.1.0
 	 */
-	public static function add_hooks() {
+	public static function add_hooks(): void {
 		add_action( 'init', array( __CLASS__, 'register_post_type' ) );
 		add_action( 'admin_init', array( __CLASS__, 'schedule_garbage_collection' ) );
 		add_action( self::GC_CRON_EVENT_NAME, array( __CLASS__, 'delete_stale_posts' ) );
@@ -57,7 +57,7 @@
 	 *
 	 * @since 0.1.0
 	 */
-	public static function register_post_type() {
+	public static function register_post_type(): void {
 		register_post_type(
 			self::SLUG,
 			array(
@@ -85,7 +85,7 @@
 	 * @param string $slug URL metrics slug.
 	 * @return WP_Post|null Post object if exists.
 	 */
-	public static function get_post( string $slug ) {
+	public static function get_post( string $slug ): ?WP_Post {
 		$post_query = new WP_Query(
 			array(
 				'post_type'              => self::SLUG,
@@ -118,7 +118,7 @@
 	 */
 	public static function get_url_metrics_from_post( WP_Post $post ): array {
 		$this_function   = __FUNCTION__;
-		$trigger_warning = static function ( $message ) use ( $this_function ) {
+		$trigger_warning = static function ( $message ) use ( $this_function ): void {
 			wp_trigger_error( $this_function, esc_html( $message ), E_USER_WARNING );
 		};
 
@@ -225,6 +225,9 @@
 			),
 			JSON_UNESCAPED_SLASHES // No need for escaped slashes since not printed to frontend.
 		);
+		if ( ! is_string( $post_data['post_content'] ) ) {
+			return new WP_Error( 'json_encode_error', json_last_error_msg() );
+		}
 
 		$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
 		if ( $has_kses ) {
@@ -253,7 +256,7 @@
 	 *
 	 * @since 0.1.0
 	 */
-	public static function schedule_garbage_collection() {
+	public static function schedule_garbage_collection(): void {
 		if ( ! is_user_logged_in() ) {
 			return;
 		}
@@ -275,7 +278,7 @@
 	 *
 	 * @since 0.1.0
 	 */
-	public static function delete_stale_posts() {
+	public static function delete_stale_posts(): void {
 		$one_month_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-1 month' ) );
 
 		$query = new WP_Query(
@@ -290,7 +293,7 @@
 		);
 
 		foreach ( $query->posts as $post ) {
-			if ( self::SLUG === $post->post_type ) { // Sanity check.
+			if ( $post instanceof WP_Post && self::SLUG === $post->post_type ) { // Sanity check.
 				wp_delete_post( $post->ID, true );
 			}
 		}
@@ -303,7 +306,7 @@
 	 *
 	 * @since 0.1.0
 	 */
-	public static function delete_all_posts() {
+	public static function delete_all_posts(): void {
 		global $wpdb;
 
 		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
Index: storage/data.php
===================================================================
--- storage/data.php	(revision 3088546)
+++ storage/data.php	(working copy)
@@ -61,7 +61,7 @@
  * @since 0.1.0
  * @access private
  *
- * @return array Normalized query vars.
+ * @return array<string, mixed> Normalized query vars.
  */
 function od_get_normalized_query_vars(): array {
 	global $wp;
@@ -133,11 +133,11 @@
  *
  * @see od_get_normalized_query_vars()
  *
- * @param array $query_vars Normalized query vars.
+ * @param array<string, mixed> $query_vars Normalized query vars.
  * @return string Slug.
  */
 function od_get_url_metrics_slug( array $query_vars ): string {
-	return md5( wp_json_encode( $query_vars ) );
+	return md5( (string) wp_json_encode( $query_vars ) );
 }
 
 /**
@@ -312,7 +312,10 @@
  * @access private
  *
  * @param OD_URL_Metrics_Group_Collection $group_collection URL metrics group collection.
- * @return array LCP elements keyed by its minimum viewport width. If there is no supported LCP element at a breakpoint, then `false` is used.
+ * @return array<int, array{xpath: string}|false> LCP elements keyed by its minimum viewport width. If there is no
+ *                                                supported LCP element at a breakpoint, then `false` is used. Note that
+ *                                                the array shape is actually an ElementData from OD_URL_Metric but
+ *                                                PHPStan does not support importing a type onto a function.
  */
 function od_get_lcp_elements_by_minimum_viewport_widths( OD_URL_Metrics_Group_Collection $group_collection ): array {
 	$lcp_element_by_viewport_minimum_width = array();
Index: storage/rest-api.php
===================================================================
--- storage/rest-api.php	(revision 3088546)
+++ storage/rest-api.php	(working copy)
@@ -35,7 +35,7 @@
  * @since 0.1.0
  * @access private
  */
-function od_register_endpoint() {
+function od_register_endpoint(): void {
 
 	$args = array(
 		'slug'  => array(
@@ -94,6 +94,8 @@
  * @since 0.1.0
  * @access private
  *
+ * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+ *
  * @param WP_REST_Request $request Request.
  * @return WP_REST_Response|WP_Error Response.
  */
Index: uninstall.php
===================================================================
--- uninstall.php	(revision 3088546)
+++ uninstall.php	(working copy)
@@ -13,7 +13,7 @@
 
 require_once __DIR__ . '/storage/class-od-url-metrics-post-type.php';
 
-$od_delete_site_data = static function () {
+$od_delete_site_data = static function (): void {
 	// Delete all URL Metrics posts for the current site.
 	OD_URL_Metrics_Post_Type::delete_all_posts();
 	wp_unschedule_hook( OD_URL_Metrics_Post_Type::GC_CRON_EVENT_NAME );

speculation-rules

svn status:

M       class-plsr-url-pattern-prefixer.php
M       helper.php
M       hooks.php
M       load.php
M       readme.txt
M       settings.php
M       uninstall.php
svn diff
Index: class-plsr-url-pattern-prefixer.php
===================================================================
--- class-plsr-url-pattern-prefixer.php	(revision 3088546)
+++ class-plsr-url-pattern-prefixer.php	(working copy)
@@ -22,7 +22,7 @@
 	 * Map of `$context_string => $base_path` pairs.
 	 *
 	 * @since 1.0.0
-	 * @var array
+	 * @var array<string, string>
 	 */
 	private $contexts;
 
@@ -31,8 +31,8 @@
 	 *
 	 * @since 1.0.0
 	 *
-	 * @param array $contexts Optional. Map of `$context_string => $base_path` pairs. Default is the contexts returned
-	 *                        by the {@see PLSR_URL_Pattern_Prefixer::get_default_contexts()} method.
+	 * @param array<string, string> $contexts Optional. Map of `$context_string => $base_path` pairs. Default is the contexts returned
+	 *                                        by the {@see PLSR_URL_Pattern_Prefixer::get_default_contexts()} method.
 	 */
 	public function __construct( array $contexts = array() ) {
 		if ( $contexts ) {
@@ -103,12 +103,17 @@
 	 *
 	 * @since 1.0.0
 	 *
-	 * @return array Map of `$context_string => $base_path` pairs.
+	 * @return array<string, string> Map of `$context_string => $base_path` pairs.
 	 */
 	public static function get_default_contexts(): array {
 		return array(
-			'home' => self::escape_pattern_string( trailingslashit( wp_parse_url( home_url( '/' ), PHP_URL_PATH ) ) ),
-			'site' => self::escape_pattern_string( trailingslashit( wp_parse_url( site_url( '/' ), PHP_URL_PATH ) ) ),
+			'home'       => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ) ) ),
+			'site'       => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( site_url( '/' ), PHP_URL_PATH ) ) ),
+			'uploads'    => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( wp_upload_dir( null, false )['baseurl'], PHP_URL_PATH ) ) ),
+			'content'    => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( content_url(), PHP_URL_PATH ) ) ),
+			'plugins'    => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( plugins_url(), PHP_URL_PATH ) ) ),
+			'template'   => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( get_stylesheet_directory_uri(), PHP_URL_PATH ) ) ),
+			'stylesheet' => self::escape_pattern_string( trailingslashit( (string) wp_parse_url( get_template_directory_uri(), PHP_URL_PATH ) ) ),
 		);
 	}
 
Index: helper.php
===================================================================
--- helper.php	(revision 3088546)
+++ helper.php	(working copy)
@@ -19,9 +19,9 @@
  *
  * @since 1.0.0
  *
- * @return array Associative array of speculation rules by type.
+ * @return array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.
  */
-function plsr_get_speculation_rules() {
+function plsr_get_speculation_rules(): array {
 	$option = get_option( 'plsr_speculation_rules' );
 
 	/*
@@ -34,7 +34,7 @@
 		$option = array_merge( plsr_get_setting_default(), $option );
 	}
 
-	$mode      = $option['mode'];
+	$mode      = (string) $option['mode'];
 	$eagerness = $option['eagerness'];
 
 	$prefixer = new PLSR_URL_Pattern_Prefixer();
@@ -43,6 +43,11 @@
 		$prefixer->prefix_path_pattern( '/wp-login.php', 'site' ),
 		$prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ),
 		$prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ),
+		$prefixer->prefix_path_pattern( '/*', 'uploads' ),
+		$prefixer->prefix_path_pattern( '/*', 'content' ),
+		$prefixer->prefix_path_pattern( '/*', 'plugins' ),
+		$prefixer->prefix_path_pattern( '/*', 'template' ),
+		$prefixer->prefix_path_pattern( '/*', 'stylesheet' ),
 	);
 
 	/**
Index: hooks.php
===================================================================
--- hooks.php	(revision 3088546)
+++ hooks.php	(working copy)
@@ -18,7 +18,7 @@
  *
  * @since 1.0.0
  */
-function plsr_print_speculation_rules() {
+function plsr_print_speculation_rules(): void {
 	$rules = plsr_get_speculation_rules();
 	if ( empty( $rules ) ) {
 		return;
@@ -27,8 +27,8 @@
 	// This workaround is needed for WP 6.4. See <https://core.trac.wordpress.org/ticket/60320>.
 	$needs_html5_workaround = (
 		! current_theme_supports( 'html5', 'script' ) &&
-		version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.4', '>=' ) &&
-		version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.5', '<' )
+		version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.4', '>=' ) &&
+		version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.5', '<' )
 	);
 	if ( $needs_html5_workaround ) {
 		$backup_wp_theme_features = $GLOBALS['_wp_theme_features'];
@@ -36,7 +36,7 @@
 	}
 
 	wp_print_inline_script_tag(
-		wp_json_encode( $rules ),
+		(string) wp_json_encode( $rules ),
 		array( 'type' => 'speculationrules' )
 	);
 
@@ -53,7 +53,7 @@
  *
  * @since 1.1.0
  */
-function plsr_render_generator_meta_tag() {
+function plsr_render_generator_meta_tag(): void {
 	// Use the plugin slug as it is immutable.
 	echo '<meta name="generator" content="speculation-rules ' . esc_attr( SPECULATION_RULES_VERSION ) . '">' . "\n";
 }
Index: load.php
===================================================================
--- load.php	(revision 3088546)
+++ load.php	(working copy)
@@ -5,7 +5,7 @@
  * Description: Enables browsers to speculatively prerender or prefetch pages when hovering over links.
  * Requires at least: 6.4
  * Requires PHP: 7.2
- * Version: 1.2.2
+ * Version: 1.3.0
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -20,15 +20,65 @@
 	exit;
 }
 
-// Define the constant.
-if ( defined( 'SPECULATION_RULES_VERSION' ) ) {
-	return;
-}
+(
+	/**
+	 * Register this copy of the plugin among other potential copies embedded in plugins or themes.
+	 *
+	 * @param string  $global_var_name Global variable name for storing the plugin pending loading.
+	 * @param string  $version         Version.
+	 * @param Closure $load            Callback that loads the plugin.
+	 */
+	static function ( string $global_var_name, string $version, Closure $load ): void {
+		if ( ! isset( $GLOBALS[ $global_var_name ] ) ) {
+			$bootstrap = static function () use ( $global_var_name ): void {
+				if (
+					isset( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] )
+					&&
+					$GLOBALS[ $global_var_name ]['load'] instanceof Closure
+					&&
+					is_string( $GLOBALS[ $global_var_name ]['version'] )
+				) {
+					call_user_func( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] );
+					unset( $GLOBALS[ $global_var_name ] );
+				}
+			};
 
-define( 'SPECULATION_RULES_VERSION', '1.2.2' );
-define( 'SPECULATION_RULES_MAIN_FILE', plugin_basename( __FILE__ ) );
+			// Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used
+			// because it is the first action that fires once the theme is loaded.
+			add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN );
+		}
 
-require_once __DIR__ . '/class-plsr-url-pattern-prefixer.php';
-require_once __DIR__ . '/helper.php';
-require_once __DIR__ . '/hooks.php';
-require_once __DIR__ . '/settings.php';
+		// Register this copy of the plugin.
+		if (
+			// Register this copy if none has been registered yet.
+			! isset( $GLOBALS[ $global_var_name ]['version'] )
+			||
+			// Or register this copy if the version greater than what is currently registered.
+			version_compare( $version, $GLOBALS[ $global_var_name ]['version'], '>' )
+			||
+			// Otherwise, register this copy if it is actually the one installed in the directory for plugins.
+			rtrim( WP_PLUGIN_DIR, '/' ) === dirname( __DIR__ )
+		) {
+			$GLOBALS[ $global_var_name ]['version'] = $version;
+			$GLOBALS[ $global_var_name ]['load']    = $load;
+		}
+	}
+)(
+	'plsr_pending_plugin_info',
+	'1.3.0',
+	static function ( string $version ): void {
+
+		// Define the constant.
+		if ( defined( 'SPECULATION_RULES_VERSION' ) ) {
+			return;
+		}
+
+		define( 'SPECULATION_RULES_VERSION', $version );
+		define( 'SPECULATION_RULES_MAIN_FILE', plugin_basename( __FILE__ ) );
+
+		require_once __DIR__ . '/class-plsr-url-pattern-prefixer.php';
+		require_once __DIR__ . '/helper.php';
+		require_once __DIR__ . '/hooks.php';
+		require_once __DIR__ . '/settings.php';
+	}
+);
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -4,7 +4,7 @@
 Requires at least: 6.4
 Tested up to:      6.5
 Requires PHP:      7.2
-Stable tag:        1.2.2
+Stable tag:        1.3.0
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, javascript, speculation rules, prerender, prefetch
@@ -114,6 +114,14 @@
 
 == Changelog ==
 
+= 1.3.0 =
+
+**Enhancements**
+
+* Prevent speculatively loading links to the uploads, content, plugins, template, or stylesheet directories. ([1167](https://github.com/WordPress/performance/pull/1167))
+* Facilitate embedding Speculative Loading in other plugins/themes. ([1159](https://github.com/WordPress/performance/pull/1159))
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
 = 1.2.2 =
 
 **Bug Fixes**
Index: settings.php
===================================================================
--- settings.php	(revision 3088546)
+++ settings.php	(working copy)
@@ -18,7 +18,7 @@
  *
  * @return array<string, string> Associative array of `$mode => $label` pairs.
  */
-function plsr_get_mode_labels() {
+function plsr_get_mode_labels(): array {
 	return array(
 		'prefetch'  => _x( 'Prefetch', 'setting label', 'speculation-rules' ),
 		'prerender' => _x( 'Prerender', 'setting label', 'speculation-rules' ),
@@ -32,7 +32,7 @@
  *
  * @return array<string, string> Associative array of `$eagerness => $label` pairs.
  */
-function plsr_get_eagerness_labels() {
+function plsr_get_eagerness_labels(): array {
 	return array(
 		'conservative' => _x( 'Conservative (typically on click)', 'setting label', 'speculation-rules' ),
 		'moderate'     => _x( 'Moderate (typically on hover)', 'setting label', 'speculation-rules' ),
@@ -52,7 +52,7 @@
  *     @type string $eagerness Eagerness.
  * }
  */
-function plsr_get_setting_default() {
+function plsr_get_setting_default(): array {
 	return array(
 		'mode'      => 'prerender',
 		'eagerness' => 'moderate',
@@ -72,7 +72,7 @@
  *     @type string $eagerness Eagerness.
  * }
  */
-function plsr_sanitize_setting( $input ) {
+function plsr_sanitize_setting( $input ): array {
 	$default_value = plsr_get_setting_default();
 
 	if ( ! is_array( $input ) ) {
@@ -102,7 +102,7 @@
  * @since 1.0.0
  * @access private
  */
-function plsr_register_setting() {
+function plsr_register_setting(): void {
 	register_setting(
 		'reading',
 		'plsr_speculation_rules',
@@ -138,11 +138,11 @@
  * @since 1.0.0
  * @access private
  */
-function plsr_add_setting_ui() {
+function plsr_add_setting_ui(): void {
 	add_settings_section(
 		'plsr_speculation_rules',
 		__( 'Speculative Loading', 'speculation-rules' ),
-		static function () {
+		static function (): void {
 			?>
 			<p class="description">
 				<?php esc_html_e( 'This section allows you to control how URLs that your users navigate to are speculatively loaded to improve performance.', 'speculation-rules' ); ?>
@@ -196,7 +196,7 @@
  *     @type string $description Optional. A description to show for the field.
  * }
  */
-function plsr_render_settings_field( array $args ) {
+function plsr_render_settings_field( array $args ): void {
 	if ( empty( $args['field'] ) || empty( $args['title'] ) ) { // Invalid.
 		return;
 	}
@@ -206,8 +206,12 @@
 		return;
 	}
 
-	$value   = $option[ $args['field'] ];
-	$choices = call_user_func( "plsr_get_{$args['field']}_labels" );
+	$value    = $option[ $args['field'] ];
+	$callback = "plsr_get_{$args['field']}_labels";
+	if ( ! is_callable( $callback ) ) {
+		return;
+	}
+	$choices = call_user_func( $callback );
 
 	?>
 	<fieldset>
Index: uninstall.php
===================================================================
--- uninstall.php	(revision 3088546)
+++ uninstall.php	(working copy)
@@ -36,6 +36,6 @@
  *
  * @since 1.2.0
  */
-function plsr_delete_plugin_option() {
+function plsr_delete_plugin_option(): void {
 	delete_option( 'plsr_speculation_rules' );
 }

webp-uploads

svn status:

?       deprecated.php
M       helper.php
M       hooks.php
M       image-edit.php
M       load.php
M       readme.txt
M       rest-api.php
M       settings.php
M       uninstall.php
svn diff
Index: helper.php
===================================================================
--- helper.php	(revision 3088546)
+++ helper.php	(working copy)
@@ -19,7 +19,7 @@
  *
  * @return array<string, array<string>> An array of valid mime types, where the key is the mime type and the value is the extension type.
  */
-function webp_uploads_get_upload_image_mime_transforms() {
+function webp_uploads_get_upload_image_mime_transforms(): array {
 	$default_transforms = array(
 		'image/jpeg' => array( 'image/webp' ),
 		'image/webp' => array( 'image/webp' ),
@@ -71,15 +71,15 @@
  * @since 1.0.0
  * @access private
  *
- * @param int         $attachment_id         The ID of the attachment from where this image would be created.
- * @param string      $image_size            The size name that would be used to create the image source, out of the registered subsizes.
- * @param array       $size_data             An array with the dimensions of the image: height, width and crop.
- * @param string      $mime                  The target mime in which the image should be created.
- * @param string|null $destination_file_name The path where the file would be stored, including the extension. If null, `generate_filename` is used to create the destination file name.
+ * @param int                                          $attachment_id         The ID of the attachment from where this image would be created.
+ * @param string                                       $image_size            The size name that would be used to create the image source, out of the registered subsizes.
+ * @param array{ width: int, height: int, crop: bool } $size_data             An array with the dimensions of the image: height, width and crop.
+ * @param string                                       $mime                  The target mime in which the image should be created.
+ * @param string|null                                  $destination_file_name The path where the file would be stored, including the extension. If null, `generate_filename` is used to create the destination file name.
  *
- * @return array|WP_Error An array with the file and filesize if the image was created correctly, otherwise a WP_Error.
+ * @return array{ file: string, filesize: int }|WP_Error An array with the file and filesize if the image was created correctly, otherwise a WP_Error.
  */
-function webp_uploads_generate_additional_image_source( $attachment_id, $image_size, array $size_data, $mime, $destination_file_name = null ) {
+function webp_uploads_generate_additional_image_source( int $attachment_id, string $image_size, array $size_data, string $mime, ?string $destination_file_name = null ) {
 	/**
 	 * Filter to allow the generation of additional image sources, in which a defined mime type
 	 * can be transformed and create additional mime types for the file.
@@ -88,31 +88,40 @@
 	 *
 	 * @since 1.1.0
 	 *
-	 * @param array|null|WP_Error $image         Image data {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string} or null or WP_Error.
-	 * @param int                 $attachment_id The ID of the attachment from where this image would be created.
-	 * @param string              $image_size    The size name that would be used to create this image, out of the registered subsizes.
-	 * @param array               $size_data     An array with the dimensions of the image: height, width and crop {'height'=>int, 'width'=>int, 'crop'}.
-	 * @param string              $mime          The target mime in which the image should be created.
+	 * @param array{
+	 *            file: string,
+	 *            path?: string,
+	 *            filesize?: int
+	 *        }|null|WP_Error $image         Image data, null, or WP_Error.
+	 * @param int             $attachment_id The ID of the attachment from where this image would be created.
+	 * @param string          $image_size    The size name that would be used to create this image, out of the registered subsizes.
+	 * @param array{
+	 *            width: int,
+	 *            height: int,
+	 *            crop: bool
+	 *        }               $size_data     An array with the dimensions of the image.
+	 * @param string          $mime          The target mime in which the image should be created.
 	 */
 	$image = apply_filters( 'webp_uploads_pre_generate_additional_image_source', null, $attachment_id, $image_size, $size_data, $mime );
 	if ( is_wp_error( $image ) ) {
 		return $image;
 	}
+	if ( is_array( $image ) && array_key_exists( 'file', $image ) && is_string( $image['file'] ) ) {
+		// The filtered image provided all we need to short-circuit here.
+		if ( array_key_exists( 'filesize', $image ) && is_int( $image['filesize'] ) && $image['filesize'] > 0 ) {
+			return $image;
+		}
 
-	if (
-		is_array( $image ) &&
-		! empty( $image['file'] ) &&
-		(
-			! empty( $image['path'] ) ||
-			array_key_exists( 'filesize', $image )
-		)
-	) {
-		return array(
-			'file'     => $image['file'],
-			'filesize' => array_key_exists( 'filesize', $image )
-				? $image['filesize']
-				: wp_filesize( $image['path'] ),
-		);
+		// Supply the filesize based on the filter-provided path.
+		if ( array_key_exists( 'path', $image ) && is_int( $image['path'] ) ) {
+			$filesize = wp_filesize( $image['path'] );
+			if ( $filesize > 0 ) {
+				return array(
+					'file'     => $image['file'],
+					'filesize' => $filesize,
+				);
+			}
+		}
 	}
 
 	$allowed_mimes = array_flip( wp_get_mime_types() );
@@ -125,7 +134,7 @@
 	}
 
 	$image_path = wp_get_original_image_path( $attachment_id );
-	if ( ! file_exists( $image_path ) ) {
+	if ( ! $image_path || ! file_exists( $image_path ) ) {
 		return new WP_Error( 'original_image_file_not_found', __( 'The original image file does not exists, subsizes are created out of the original image.', 'webp-uploads' ) );
 	}
 
@@ -157,7 +166,7 @@
 		$destination_file_name = $editor->generate_filename( $suffix, null, $extension[0] );
 	}
 
-	remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
+	remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10 );
 	$image = $editor->save( $destination_file_name, $mime );
 	add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
 
@@ -188,9 +197,9 @@
  * @param string $size          The size name that would be used to create this image, out of the registered subsizes.
  * @param string $mime          A mime type we are looking to use to create this image.
  *
- * @return array|WP_Error
+ * @return array{ file: string, filesize: int }|WP_Error
  */
-function webp_uploads_generate_image_size( $attachment_id, $size, $mime ) {
+function webp_uploads_generate_image_size( int $attachment_id, string $size, string $mime ) {
 	$sizes    = wp_get_registered_image_subsizes();
 	$metadata = wp_get_attachment_metadata( $attachment_id );
 
@@ -228,33 +237,6 @@
 }
 
 /**
- * Returns the attachment sources array ordered by filesize.
- *
- * @since 1.0.0
- *
- * @param int    $attachment_id The attachment ID.
- * @param string $size          The attachment size.
- * @return array The attachment sources array.
- */
-function webp_uploads_get_attachment_sources( $attachment_id, $size = 'thumbnail' ) {
-	// Check for the sources attribute in attachment metadata.
-	$metadata = wp_get_attachment_metadata( $attachment_id );
-
-	// Return full image size sources.
-	if ( 'full' === $size && ! empty( $metadata['sources'] ) ) {
-		return $metadata['sources'];
-	}
-
-	// Return the resized image sources.
-	if ( ! empty( $metadata['sizes'][ $size ]['sources'] ) ) {
-		return $metadata['sizes'][ $size ]['sources'];
-	}
-
-	// Return an empty array if no sources found.
-	return array();
-}
-
-/**
  * Returns mime types that should be used for an image in the specific context.
  *
  * @since 1.0.0
@@ -261,9 +243,9 @@
  *
  * @param int    $attachment_id The attachment ID.
  * @param string $context       The current context.
- * @return array Mime types to use for the image.
+ * @return string[] Mime types to use for the image.
  */
-function webp_uploads_get_content_image_mimes( $attachment_id, $context ) {
+function webp_uploads_get_content_image_mimes( int $attachment_id, string $context ): array {
 	$target_mimes = array( 'image/webp', 'image/jpeg' );
 
 	/**
@@ -293,7 +275,7 @@
  *
  * @return bool True if in the <body> within a frontend request, false otherwise.
  */
-function webp_uploads_in_frontend_body() {
+function webp_uploads_in_frontend_body(): bool {
 	global $wp_query;
 
 	// Check if this request is generally outside (or before) any frontend context.
@@ -314,11 +296,11 @@
  *
  * @since 1.0.0
  *
- * @param array $original   An array with the metadata of the attachment.
- * @param array $additional An array containing the filename and file size for additional mime.
+ * @param array{ filesize?: int } $original   An array with the metadata of the attachment.
+ * @param array{ filesize?: int } $additional An array containing the filename and file size for additional mime.
  * @return bool True if the additional image is larger than the original image, otherwise false.
  */
-function webp_uploads_should_discard_additional_image_file( array $original, array $additional ) {
+function webp_uploads_should_discard_additional_image_file( array $original, array $additional ): bool {
 	$original_image_filesize   = isset( $original['filesize'] ) ? (int) $original['filesize'] : 0;
 	$additional_image_filesize = isset( $additional['filesize'] ) ? (int) $additional['filesize'] : 0;
 	if ( $original_image_filesize > 0 && $additional_image_filesize > 0 ) {
Index: hooks.php
===================================================================
--- hooks.php	(revision 3088546)
+++ hooks.php	(working copy)
@@ -26,23 +26,44 @@
  * @see   wp_generate_attachment_metadata()
  * @see   webp_uploads_get_upload_image_mime_transforms()
  *
- * @param array $metadata      An array with the metadata from this attachment.
- * @param int   $attachment_id The ID of the attachment where the hook was dispatched.
- * @return array An array with the updated structure for the metadata before is stored in the database.
+ * @phpstan-param array{
+ *      width: int,
+ *      height: int,
+ *      file: string,
+ *      sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
+ *      image_meta: array<string, mixed>,
+ *      filesize: int
+ *  } $metadata
+ *
+ * @param array<string, mixed> $metadata      An array with the metadata from this attachment.
+ * @param int                  $attachment_id The ID of the attachment where the hook was dispatched.
+ *
+ * @return array{
+ *     width: int,
+ *     height: int,
+ *     file: string,
+ *     sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
+ *     image_meta: array<string, mixed>,
+ *     filesize: int,
+ *     sources?: array<string, array{
+ *         file: string,
+ *         filesize: int
+ *     }>
+ * } An array with the updated structure for the metadata before is stored in the database.
  */
-function webp_uploads_create_sources_property( array $metadata, $attachment_id ) {
+function webp_uploads_create_sources_property( array $metadata, int $attachment_id ): array {
 	// This should take place only on the JPEG image.
 	$valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
 
 	// Not a supported mime type to create the sources property.
 	$mime_type = get_post_mime_type( $attachment_id );
-	if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
+	if ( ! is_string( $mime_type ) || ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
 		return $metadata;
 	}
 
 	$file = get_attached_file( $attachment_id, true );
 	// File does not exist.
-	if ( ! file_exists( $file ) ) {
+	if ( ! $file || ! file_exists( $file ) ) {
 		return $metadata;
 	}
 
@@ -105,7 +126,9 @@
 	if (
 		! in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true ) &&
 		isset( $valid_mime_transforms[ $mime_type ][0] ) &&
-		isset( $allowed_mimes[ $mime_type ] )
+		isset( $allowed_mimes[ $mime_type ] ) &&
+		array_key_exists( 'file', $metadata ) &&
+		is_string( $metadata['file'] )
 	) {
 		$valid_mime_type = $valid_mime_transforms[ $mime_type ][0];
 
@@ -123,12 +146,17 @@
 
 			// If WordPress already modified the original itself, keep the original and discard WordPress's generated version.
 			if ( ! empty( $metadata['original_image'] ) ) {
-				$uploadpath = wp_get_upload_dir();
-				wp_delete_file_from_directory( get_attached_file( $attachment_id ), $uploadpath['basedir'] );
+				$uploadpath    = wp_get_upload_dir();
+				$attached_file = get_attached_file( $attachment_id );
+				if ( $attached_file ) {
+					wp_delete_file_from_directory( $attached_file, $uploadpath['basedir'] );
+				}
 			}
 
 			// Replace the attached file with the custom MIME type version.
-			$metadata = _wp_image_meta_replace_original( $saved_data, $original_image, $metadata, $attachment_id );
+			if ( $original_image ) {
+				$metadata = _wp_image_meta_replace_original( $saved_data, $original_image, $metadata, $attachment_id );
+			}
 
 			// Unset sources entry for the original MIME type, then save (to avoid inconsistent data
 			// in case of an error after this logic).
@@ -222,12 +250,25 @@
  *
  * @see wp_get_missing_image_subsizes()
  *
- * @param array $missing_sizes Associative array of arrays of image sub-sizes.
- * @param array $image_meta The metadata from the image.
- * @param int   $attachment_id The ID of the attachment.
- * @return array Associative array of arrays of image sub-sizes.
+ * @phpstan-param array{
+ *     width: int,
+ *     height: int,
+ *     file: string,
+ *     sizes: array<string, array{file: string, width: int, height: int, mime-type: string}>,
+ *     image_meta: array<string, mixed>,
+ *     filesize: int
+ * } $image_meta
+ *
+ * @param array|mixed          $missing_sizes Associative array of arrays of image sub-sizes.
+ * @param array<string, mixed> $image_meta    The metadata from the image.
+ * @param int                  $attachment_id The ID of the attachment.
+ * @return array<string, array{ width: int, height: int, crop: bool }> Associative array of arrays of image sub-sizes.
  */
-function webp_uploads_wp_get_missing_image_subsizes( $missing_sizes, $image_meta, $attachment_id ) {
+function webp_uploads_wp_get_missing_image_subsizes( $missing_sizes, array $image_meta, int $attachment_id ): array {
+	if ( ! is_array( $missing_sizes ) ) {
+		$missing_sizes = array();
+	}
+
 	// Only setup the trace array if we no longer have more sizes.
 	if ( ! empty( $missing_sizes ) ) {
 		return $missing_sizes;
@@ -252,7 +293,7 @@
 	$trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 10 );
 
 	foreach ( $trace as $element ) {
-		if ( isset( $element['function'] ) && 'wp_update_image_subsizes' === $element['function'] ) {
+		if ( 'wp_update_image_subsizes' === $element['function'] ) {
 			webp_uploads_create_sources_property( $image_meta, $attachment_id );
 			break;
 		}
@@ -269,12 +310,16 @@
  *
  * @since 1.0.0
  *
- * @param string $output_format The image editor default output format mapping.
- * @param string $filename      Path to the image.
- * @param string $mime_type     The source image mime type.
- * @return string The new output format mapping.
+ * @param array<string, string>|mixed $output_format An array of mime type mappings. Maps a source mime type to a new destination mime type. Default empty array.
+ * @param string|null                 $filename      Path to the image.
+ * @param string|null                 $mime_type     The source image mime type.
+ * @return array<string, string> The new output format mapping.
  */
-function webp_uploads_filter_image_editor_output_format( $output_format, $filename, $mime_type ) {
+function webp_uploads_filter_image_editor_output_format( $output_format, ?string $filename, ?string $mime_type ): array {
+	if ( ! is_array( $output_format ) ) {
+		$output_format = array();
+	}
+
 	// Use the original mime type if this type is allowed.
 	$valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
 	if (
@@ -309,7 +354,7 @@
  *
  * @param int $attachment_id The ID of the attachment the sources are going to be deleted.
  */
-function webp_uploads_remove_sources_files( $attachment_id ) {
+function webp_uploads_remove_sources_files( int $attachment_id ): void {
 	$file = get_attached_file( $attachment_id );
 
 	if ( empty( $file ) ) {
@@ -352,7 +397,7 @@
 			}
 
 			$intermediate_file = str_replace( $basename, $properties['file'], $file );
-			if ( empty( $intermediate_file ) ) {
+			if ( ! $intermediate_file ) {
 				continue;
 			}
 
@@ -384,7 +429,7 @@
 		}
 
 		$full_size = str_replace( $basename, $properties['file'], $file );
-		if ( empty( $full_size ) ) {
+		if ( ! $full_size ) {
 			continue;
 		}
 
@@ -477,7 +522,7 @@
  * @param string $content The content of the current post.
  * @return string The content with the updated references to the images.
  */
-function webp_uploads_update_image_references( $content ) {
+function webp_uploads_update_image_references( string $content ): string {
 	// Bail early if request is not for the frontend.
 	if ( ! webp_uploads_in_frontend_body() ) {
 		return $content;
@@ -537,7 +582,7 @@
  * @param int    $attachment_id  The ID of the attachment being modified.
  * @return string The updated img tag.
  */
-function webp_uploads_img_tag_update_mime_type( $original_image, $context, $attachment_id ) {
+function webp_uploads_img_tag_update_mime_type( string $original_image, string $context, int $attachment_id ): string {
 	$image    = $original_image;
 	$metadata = wp_get_attachment_metadata( $attachment_id );
 
@@ -654,7 +699,7 @@
  * @param int    $attachment_id The ID of the attachment image.
  * @return string The updated HTML markup.
  */
-function webp_uploads_update_featured_image( $html, $post_id, $attachment_id ) {
+function webp_uploads_update_featured_image( string $html, int $post_id, int $attachment_id ): string {
 	return webp_uploads_img_tag_update_mime_type( $html, 'post_thumbnail_html', $attachment_id );
 }
 add_filter( 'post_thumbnail_html', 'webp_uploads_update_featured_image', 10, 3 );
@@ -664,7 +709,7 @@
  *
  * @since 1.0.0
  */
-function webp_uploads_wepb_fallback() {
+function webp_uploads_wepb_fallback(): void {
 	// Get mime type transforms for the site.
 	$transforms = webp_uploads_get_upload_image_mime_transforms();
 
@@ -697,7 +742,7 @@
 	$javascript = ob_get_clean();
 
 	wp_print_inline_script_tag(
-		preg_replace( '/\s+/', '', $javascript ),
+		(string) preg_replace( '/\s+/', '', (string) $javascript ),
 		array(
 			'id'            => 'webpUploadsFallbackWebpImages',
 			'data-rest-api' => esc_url_raw( trailingslashit( get_rest_url() ) ),
@@ -714,9 +759,9 @@
  *
  * @since 1.0.0
  *
- * @return array An array of image sizes that can have additional mime types.
+ * @return array<string, bool> An array of image sizes that can have additional mime types.
  */
-function webp_uploads_get_image_sizes_additional_mime_type_support() {
+function webp_uploads_get_image_sizes_additional_mime_type_support(): array {
 	$additional_sizes = wp_get_additional_image_sizes();
 	$allowed_sizes    = array(
 		'thumbnail'      => true,
@@ -735,9 +780,9 @@
 	 *
 	 * @since 1.0.0
 	 *
-	 * @param array $allowed_sizes A map of image size names and whether they are allowed to have additional mime types.
+	 * @param array<string, bool> $allowed_sizes A map of image size names and whether they are allowed to have additional mime types.
 	 */
-	$allowed_sizes = apply_filters( 'webp_uploads_image_sizes_with_additional_mime_type_support', $allowed_sizes );
+	$allowed_sizes = (array) apply_filters( 'webp_uploads_image_sizes_with_additional_mime_type_support', $allowed_sizes );
 
 	return $allowed_sizes;
 }
@@ -751,7 +796,7 @@
  * @param string $mime_type Image mime type.
  * @return int The updated quality for mime types.
  */
-function webp_uploads_modify_webp_quality( $quality, $mime_type ) {
+function webp_uploads_modify_webp_quality( int $quality, string $mime_type ): int {
 	// For WebP images, always return 82 (other MIME types were already using 82 by default anyway).
 	if ( 'image/webp' === $mime_type ) {
 		return 82;
@@ -769,29 +814,8 @@
  *
  * @since 1.0.0
  */
-function webp_uploads_render_generator() {
+function webp_uploads_render_generator(): void {
 	// Use the plugin slug as it is immutable.
 	echo '<meta name="generator" content="webp-uploads ' . esc_attr( WEBP_UPLOADS_VERSION ) . '">' . "\n";
 }
 add_action( 'wp_head', 'webp_uploads_render_generator' );
-
-/**
- * Adds a settings link to the plugin's action links.
- *
- * @since 1.1.0
- *
- * @param array $links An array of plugin action links.
- * @return array The modified list of actions.
- */
-function webp_uploads_settings_link( $links ) {
-	if ( ! is_array( $links ) ) {
-		return $links;
-	}
-	$links[] = sprintf(
-		'<a href="%1$s">%2$s</a>',
-		esc_url( admin_url( 'options-media.php#perflab_generate_webp_and_jpeg' ) ),
-		esc_html__( 'Settings', 'webp-uploads' )
-	);
-	return $links;
-}
-add_filter( 'plugin_action_links_' . WEBP_UPLOADS_MAIN_FILE, 'webp_uploads_settings_link' );
Index: image-edit.php
===================================================================
--- image-edit.php	(revision 3088546)
+++ image-edit.php	(working copy)
@@ -16,13 +16,36 @@
  *
  * @since 1.0.0
  *
- * @param array $metadata              Metadata of the attachment.
- * @param array $valid_mime_transforms List of valid mime transforms for current image mime type.
- * @param array $main_images           Path of all main image files of all mime types.
- * @param array $subsized_images       Path of all subsized image file of all mime types.
- * @return array Metadata with sources added.
+ * @phpstan-param array{
+ *      width: int,
+ *      height: int,
+ *      file: string,
+ *      sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
+ *      image_meta: array<string, mixed>,
+ *      filesize: int,
+ *      sources?: array<string, array{ file: string, filesize: int }>,
+ *      original_image?: string
+ * } $metadata
+ * @phpstan-param array<string, array{ file: string, path: string }> $main_images
+ * @phpstan-param array<string, array<string, array{ file: string }>> $subsized_images
+ *
+ * @param array    $metadata              Metadata of the attachment.
+ * @param string[] $valid_mime_transforms List of valid mime transforms for current image mime type.
+ * @param array    $main_images           Path of all main image files of all mime types.
+ * @param array    $subsized_images       Path of all subsized image file of all mime types.
+ *
+ * @return array{
+ *     width: int,
+ *     height: int,
+ *     file: string,
+ *     sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
+ *     image_meta: array<string, mixed>,
+ *     filesize: int,
+ *     original_image?: string,
+ *     sources?: array<string, array{ file: string, filesize: int }>
+ * } Metadata with sources added.
  */
-function webp_uploads_update_sources( $metadata, $valid_mime_transforms, $main_images, $subsized_images ) {
+function webp_uploads_update_sources( array $metadata, array $valid_mime_transforms, array $main_images, array $subsized_images ): array {
 	foreach ( $valid_mime_transforms as $targeted_mime ) {
 		// Make sure the path and file exists as those values are required.
 		$image_directory = null;
@@ -86,7 +109,7 @@
  *
  * @since 1.0.0
  *
- * @param bool|null       $override  Value to return instead of saving. Default null.
+ * @param bool|null|mixed $override  Value to return instead of saving. Default null.
  * @param string          $file_path Name of the file to be saved.
  * @param WP_Image_Editor $editor    The image editor instance.
  * @param string          $mime_type The mime type of the image.
@@ -93,14 +116,14 @@
  * @param int             $post_id   Attachment post ID.
  * @return bool|null Potentially modified $override value.
  */
-function webp_uploads_update_image_onchange( $override, $file_path, $editor, $mime_type, $post_id ) {
+function webp_uploads_update_image_onchange( $override, string $file_path, WP_Image_Editor $editor, string $mime_type, int $post_id ): ?bool {
 	if ( null !== $override ) {
-		return $override;
+		return (bool) $override;
 	}
 
 	$transforms = webp_uploads_get_upload_image_mime_transforms();
 	if ( empty( $transforms[ $mime_type ] ) ) {
-		return $override;
+		return null;
 	}
 
 	$mime_transforms = $transforms[ $mime_type ];
@@ -130,15 +153,19 @@
 			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
 			$target = isset( $_REQUEST['target'] ) ? sanitize_key( $_REQUEST['target'] ) : 'all';
 
-			foreach ( $old_metadata['sizes'] as $size_name => $size_details ) {
-				// If the target is 'nothumb', skip generating the 'thumbnail' size.
-				if ( webp_uploads_image_edit_thumbnails_separately() && 'nothumb' === $target && 'thumbnail' === $size_name ) {
-					continue;
-				}
+			if ( isset( $old_metadata['sizes'] ) ) {
+				foreach ( $old_metadata['sizes'] as $size_name => $size_details ) {
+					// If the target is 'nothumb', skip generating the 'thumbnail' size.
+					if ( webp_uploads_image_edit_thumbnails_separately() && 'nothumb' === $target && 'thumbnail' === $size_name ) {
+						continue;
+					}
 
-				if ( isset( $metadata['sizes'][ $size_name ] ) && ! empty( $metadata['sizes'][ $size_name ] ) &&
-					$metadata['sizes'][ $size_name ]['file'] !== $old_metadata['sizes'][ $size_name ]['file'] ) {
-					$resize_sizes[ $size_name ] = $metadata['sizes'][ $size_name ];
+					if (
+						isset( $metadata['sizes'][ $size_name ] ) && ! empty( $metadata['sizes'][ $size_name ] ) &&
+						$metadata['sizes'][ $size_name ]['file'] !== $old_metadata['sizes'][ $size_name ]['file']
+					) {
+						$resize_sizes[ $size_name ] = $metadata['sizes'][ $size_name ];
+					}
 				}
 			}
 
@@ -171,7 +198,7 @@
 					continue;
 				}
 
-				if ( ! $editor::supports_mime_type( $targeted_mime ) ) {
+				if ( $editor instanceof WP_Image_Editor && ! $editor::supports_mime_type( $targeted_mime ) ) {
 					continue;
 				}
 
@@ -196,7 +223,7 @@
 					$target_file_name     = preg_replace( "/\.$current_extension$/", ".$extension", $thumbnail_file );
 					$target_file_location = path_join( $original_directory, $target_file_name );
 
-					remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
+					remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10 );
 					$result = $editor->save( $target_file_location, $targeted_mime );
 					add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
 
@@ -205,10 +232,10 @@
 					}
 
 					$subsized_images[ $targeted_mime ] = array( 'thumbnail' => $result );
-				} else {
+				} elseif ( $editor instanceof WP_Image_Editor ) {
 					$destination = trailingslashit( $original_directory ) . "{$filename}.{$extension}";
 
-					remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
+					remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10 );
 					$result = $editor->save( $destination, $targeted_mime );
 					add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
 
@@ -227,7 +254,7 @@
 		2
 	);
 
-	return $override;
+	return null;
 }
 add_filter( 'wp_save_image_editor_file', 'webp_uploads_update_image_onchange', 10, 5 );
 
@@ -240,20 +267,35 @@
  *
  * @see wp_update_attachment_metadata()
  *
- * @param array $data          The current metadata of the attachment.
- * @param int   $attachment_id The ID of the current attachment.
- * @return array The updated metadata for the attachment to be stored in the meta table.
+ * @phpstan-param array{
+ *        width: int,
+ *        height: int,
+ *        file: string,
+ *        sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
+ *        image_meta: array<string, mixed>,
+ *        filesize: int,
+ *        original_image: string
+ *    } $data
+ *
+ * @param array<string, mixed> $data          The current metadata of the attachment.
+ * @param int                  $attachment_id The ID of the current attachment.
+ *
+ * @return array{
+ *     width: int,
+ *     height: int,
+ *     file: string,
+ *     sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
+ *     image_meta: array<string, mixed>,
+ *     filesize: int,
+ *     original_image: string
+ * } The updated metadata for the attachment to be stored in the meta table.
  */
-function webp_uploads_update_attachment_metadata( $data, $attachment_id ) {
+function webp_uploads_update_attachment_metadata( array $data, int $attachment_id ): array {
 	// PHPCS ignore reason: Update the attachment's metadata by either restoring or editing it.
 	// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
 	$trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 10 );
 
 	foreach ( $trace as $element ) {
-		if ( ! isset( $element['function'] ) ) {
-			continue;
-		}
-
 		switch ( $element['function'] ) {
 			case 'wp_save_image':
 				// Right after an image has been edited.
@@ -277,11 +319,31 @@
  *
  * @since 1.0.0
  *
- * @param int   $attachment_id The ID representing the attachment.
- * @param array $data          The current metadata of the attachment.
- * @return array The updated metadata for the attachment.
+ * @phpstan-param array{
+ *       width: int,
+ *       height: int,
+ *       file: string,
+ *       sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
+ *       image_meta: array<string, mixed>,
+ *       filesize: int,
+ *       original_image: string,
+ *       sources?: array<string, array{ file: string, filesize: int }>
+ *   } $data
+ *
+ * @param int                  $attachment_id The ID representing the attachment.
+ * @param array<string, mixed> $data          The current metadata of the attachment.
+ *
+ * @return array{
+ *     width: int,
+ *     height: int,
+ *     file: string,
+ *     sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
+ *     image_meta: array<string, mixed>,
+ *     filesize: int,
+ *     original_image: string
+ * } The updated metadata for the attachment.
  */
-function webp_uploads_backup_sources( $attachment_id, $data ) {
+function webp_uploads_backup_sources( int $attachment_id, array $data ): array {
 	// PHPCS ignore reason: A nonce check is not necessary here as this logic directly ties in with WordPress core
 	// function `wp_ajax_image_editor()` which already has one.
 	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -302,7 +364,7 @@
 	// Prevent execution of the callbacks more than once if the callback was already executed.
 	$has_been_processed = false;
 
-	$hook = static function ( $meta_id, $post_id, $meta_name ) use ( $attachment_id, $sources, &$has_been_processed ) {
+	$hook = static function ( $meta_id, $post_id, $meta_name ) use ( $attachment_id, $sources, &$has_been_processed ): void {
 		// Make sure this hook is only executed in the same context for the provided $attachment_id.
 		if ( $post_id !== $attachment_id ) {
 			return;
@@ -337,10 +399,10 @@
  *
  * @since 1.0.0
  *
- * @param int   $attachment_id The ID of the attachment.
- * @param array $sources       An array with the full sources to be stored on the next available key.
+ * @param int                                                 $attachment_id The ID of the attachment.
+ * @param array<string, array{ file: string, filesize: int }> $sources       An array with the full sources to be stored on the next available key.
  */
-function webp_uploads_backup_full_image_sources( $attachment_id, $sources ) {
+function webp_uploads_backup_full_image_sources( int $attachment_id, array $sources ): void {
 	if ( empty( $sources ) ) {
 		return;
 	}
@@ -367,7 +429,7 @@
  * @param int $attachment_id The ID of the attachment.
  * @return null|string The next available full size name.
  */
-function webp_uploads_get_next_full_size_key_from_backup( $attachment_id ) {
+function webp_uploads_get_next_full_size_key_from_backup( int $attachment_id ): ?string {
 	$backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
 	$backup_sizes = is_array( $backup_sizes ) ? $backup_sizes : array();
 
@@ -401,11 +463,30 @@
  *
  * @since 1.0.0
  *
- * @param int   $attachment_id The ID of the attachment.
- * @param array $data          The current metadata to be stored in the attachment.
- * @return array The updated metadata of the attachment.
+ * @phpstan-param array{
+ *        width: int,
+ *        height: int,
+ *        file: string,
+ *        sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
+ *        image_meta: array<string, mixed>,
+ *        filesize: int,
+ *        original_image: string
+ *    } $data
+ *
+ * @param int                  $attachment_id The ID of the attachment.
+ * @param array<string, mixed> $data          The current metadata to be stored in the attachment.
+ * @return array{
+ *     width: int,
+ *     height: int,
+ *     file: string,
+ *     sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
+ *     image_meta: array<string, mixed>,
+ *     filesize: int,
+ *     sources?: array<string, array{ file: string, filesize: int }>,
+ *     original_image: string
+ * } The updated metadata of the attachment.
  */
-function webp_uploads_restore_image( $attachment_id, $data ) {
+function webp_uploads_restore_image( int $attachment_id, array $data ): array {
 	$backup_sources = get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true );
 	if ( ! is_array( $backup_sources ) ) {
 		$backup_sources = array();
@@ -434,7 +515,7 @@
  *
  * @return bool True if editing image thumbnails is enabled, false otherwise.
  */
-function webp_uploads_image_edit_thumbnails_separately() {
+function webp_uploads_image_edit_thumbnails_separately(): bool {
 	/** This filter is documented in wp-admin/includes/image-edit.php */
 	return (bool) apply_filters( 'image_edit_thumbnails_separately', false );
 }
Index: load.php
===================================================================
--- load.php	(revision 3088546)
+++ load.php	(working copy)
@@ -4,8 +4,8 @@
  * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/webp-uploads
  * Description: Converts images to more modern formats such as WebP or AVIF during upload.
  * Requires at least: 6.4
- * Requires PHP: 7.0
- * Version: 1.1.0
+ * Requires PHP: 7.2
+ * Version: 1.1.1
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -25,7 +25,7 @@
 	return;
 }
 
-define( 'WEBP_UPLOADS_VERSION', '1.1.0' );
+define( 'WEBP_UPLOADS_VERSION', '1.1.1' );
 define( 'WEBP_UPLOADS_MAIN_FILE', plugin_basename( __FILE__ ) );
 
 require_once __DIR__ . '/helper.php';
@@ -33,3 +33,4 @@
 require_once __DIR__ . '/image-edit.php';
 require_once __DIR__ . '/settings.php';
 require_once __DIR__ . '/hooks.php';
+require_once __DIR__ . '/deprecated.php';
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -3,8 +3,8 @@
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
-Requires PHP:      7.0
-Stable tag:        1.1.0
+Requires PHP:      7.2
+Stable tag:        1.1.1
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, images, webp
@@ -60,6 +60,17 @@
 
 == Changelog ==
 
+= 1.1.1 =
+
+**Enhancements**
+
+* Prepend Settings link in webp-uploads. ([1146](https://github.com/WordPress/performance/pull/1146))
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
+**Documentation**
+
+* Updated inline documentation. ([1160](https://github.com/WordPress/performance/pull/1160))
+
 = 1.1.0 =
 
 * Add link to WebP settings to plugins table. ([1036](https://github.com/WordPress/performance/pull/1036))
Index: rest-api.php
===================================================================
--- rest-api.php	(revision 3088546)
+++ rest-api.php	(working copy)
@@ -20,7 +20,7 @@
  * @param WP_Post          $post     The post object.
  * @return WP_REST_Response A new response object for the attachment with additional sources.
  */
-function webp_uploads_update_rest_attachment( WP_REST_Response $response, WP_Post $post ) {
+function webp_uploads_update_rest_attachment( WP_REST_Response $response, WP_Post $post ): WP_REST_Response {
 	$data = $response->get_data();
 	if ( ! isset( $data['media_details'] ) || ! is_array( $data['media_details'] ) || ! isset( $data['media_details']['sizes'] ) || ! is_array( $data['media_details']['sizes'] ) ) {
 		return $response;
@@ -49,6 +49,6 @@
 		unset( $data['media_details']['sources'] );
 	}
 
-	return rest_ensure_response( $data );
+	return new WP_REST_Response( $data );
 }
 add_filter( 'rest_prepare_attachment', 'webp_uploads_update_rest_attachment', 10, 2 );
Index: settings.php
===================================================================
--- settings.php	(revision 3088546)
+++ settings.php	(working copy)
@@ -16,7 +16,7 @@
  *
  * @since 1.0.0
  */
-function webp_uploads_register_media_settings_field() {
+function webp_uploads_register_media_settings_field(): void {
 	register_setting(
 		'media',
 		'perflab_generate_webp_and_jpeg',
@@ -34,7 +34,7 @@
  *
  * @since 1.0.0
  */
-function webp_uploads_add_media_settings_field() {
+function webp_uploads_add_media_settings_field(): void {
 	// Add settings field.
 	add_settings_field(
 		'perflab_generate_webp_and_jpeg',
@@ -52,7 +52,7 @@
  *
  * @since 1.0.0
  */
-function webp_uploads_generate_webp_jpeg_setting_callback() {
+function webp_uploads_generate_webp_jpeg_setting_callback(): void {
 	if ( ! is_multisite() ) {
 		?>
 			</td>
@@ -69,11 +69,11 @@
 }
 
 /**
- * Adds custom style for media settings.
+ * Adds custom styles to hide specific elements in media settings.
  *
  * @since 1.0.0
  */
-function webp_uploads_media_setting_style() {
+function webp_uploads_media_setting_style(): void {
 	if ( is_multisite() ) {
 		return;
 	}
@@ -87,3 +87,30 @@
 	<?php
 }
 add_action( 'admin_head-options-media.php', 'webp_uploads_media_setting_style' );
+
+/**
+ * Adds a settings link to the plugin's action links.
+ *
+ * @since 1.1.0
+ * @since 1.1.1 Renamed from webp_uploads_settings_link() to webp_uploads_add_settings_action_link()
+ *
+ * @param string[]|mixed $links An array of plugin action links.
+ * @return string[]|mixed The modified list of actions.
+ */
+function webp_uploads_add_settings_action_link( $links ) {
+	if ( ! is_array( $links ) ) {
+		return $links;
+	}
+
+	$settings_link = sprintf(
+		'<a href="%1$s">%2$s</a>',
+		esc_url( admin_url( 'options-media.php#perflab_generate_webp_and_jpeg' ) ),
+		esc_html__( 'Settings', 'webp-uploads' )
+	);
+
+	return array_merge(
+		array( 'settings' => $settings_link ),
+		$links
+	);
+}
+add_filter( 'plugin_action_links_' . WEBP_UPLOADS_MAIN_FILE, 'webp_uploads_add_settings_action_link' );
Index: uninstall.php
===================================================================
--- uninstall.php	(revision 3088546)
+++ uninstall.php	(working copy)
@@ -36,6 +36,6 @@
  *
  * @since 1.1.0
  */
-function webp_uploads_delete_plugin_option() {
+function webp_uploads_delete_plugin_option(): void {
 	delete_option( 'perflab_generate_webp_and_jpeg' );
 }

performance-lab

svn status:

M       includes/admin/load.php
M       includes/admin/plugins.php
M       includes/admin/server-timing.php
M       includes/server-timing/class-perflab-server-timing-metric.php
M       includes/server-timing/class-perflab-server-timing.php
M       includes/server-timing/defaults.php
?       includes/server-timing/hooks.php
M       includes/server-timing/load.php
M       includes/server-timing/object-cache.copy.php
M       includes/site-health/audit-autoloaded-options/helper.php
M       includes/site-health/audit-autoloaded-options/hooks.php
M       includes/site-health/audit-enqueued-assets/helper.php
M       includes/site-health/audit-enqueued-assets/hooks.php
?       includes/site-health/avif-support
M       includes/site-health/load.php
M       includes/site-health/webp-support/helper.php
M       includes/site-health/webp-support/hooks.php
M       load.php
!       plugins.json
M       readme.txt
svn diff
Index: includes/admin/load.php
===================================================================
--- includes/admin/load.php	(revision 3088546)
+++ includes/admin/load.php	(working copy)
@@ -15,7 +15,7 @@
  * @since 1.0.0
  * @since 3.0.0 Renamed to perflab_add_features_page().
  */
-function perflab_add_features_page() {
+function perflab_add_features_page(): void {
 	$hook_suffix = add_options_page(
 		__( 'Performance Features', 'performance-lab' ),
 		__( 'Performance', 'performance-lab' ),
@@ -29,9 +29,8 @@
 		add_action( "load-{$hook_suffix}", 'perflab_load_features_page', 10, 0 );
 		add_filter( 'plugin_action_links_' . plugin_basename( PERFLAB_MAIN_FILE ), 'perflab_plugin_action_links_add_settings' );
 	}
+}
 
-	return $hook_suffix;
-}
 add_action( 'admin_menu', 'perflab_add_features_page' );
 
 /**
@@ -41,7 +40,7 @@
  * @since 3.0.0 Renamed to perflab_load_features_page(), and the
  *              $module and $hook_suffix parameters were removed.
  */
-function perflab_load_features_page() {
+function perflab_load_features_page(): void {
 	// Handle script enqueuing for settings page.
 	add_action( 'admin_enqueue_scripts', 'perflab_enqueue_features_page_scripts' );
 
@@ -50,6 +49,9 @@
 
 	// Handle style for settings page.
 	add_action( 'admin_head', 'perflab_print_features_page_style' );
+
+	// Handle script for settings page.
+	add_action( 'admin_footer', 'perflab_print_plugin_progress_indicator_script' );
 }
 
 /**
@@ -58,7 +60,7 @@
  * @since 1.0.0
  * @since 3.0.0 Renamed to perflab_render_settings_page().
  */
-function perflab_render_settings_page() {
+function perflab_render_settings_page(): void {
 	?>
 	<div class="wrap">
 		<?php perflab_render_plugins_ui(); ?>
@@ -76,23 +78,29 @@
  *
  * @param string $hook_suffix The current admin page.
  */
-function perflab_admin_pointer( $hook_suffix ) {
-	if ( ! in_array( $hook_suffix, array( 'index.php', 'plugins.php' ), true ) ) {
-		return;
-	}
-
+function perflab_admin_pointer( string $hook_suffix ): void {
 	// Do not show admin pointer in multisite Network admin or User admin UI.
 	if ( is_network_admin() || is_user_admin() ) {
 		return;
 	}
-
 	$current_user = get_current_user_id();
-	$dismissed    = explode( ',', (string) get_user_meta( $current_user, 'dismissed_wp_pointers', true ) );
+	$dismissed    = array_filter( explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) );
 
 	if ( in_array( 'perflab-admin-pointer', $dismissed, true ) ) {
 		return;
 	}
 
+	if ( ! in_array( $hook_suffix, array( 'index.php', 'plugins.php' ), true ) ) {
+
+		// Do not show on the settings page and dismiss the pointer.
+		if ( isset( $_GET['page'] ) && PERFLAB_SCREEN === $_GET['page'] && ( ! in_array( 'perflab-admin-pointer', $dismissed, true ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			$dismissed[] = 'perflab-admin-pointer';
+			update_user_meta( $current_user, 'dismissed_wp_pointers', implode( ',', $dismissed ) );
+		}
+
+		return;
+	}
+
 	// Enqueue pointer CSS and JS.
 	wp_enqueue_style( 'wp-pointer' );
 	wp_enqueue_script( 'wp-pointer' );
@@ -108,11 +116,11 @@
  * @since 1.0.0
  * @since 2.4.0 Optional arguments were added to make the function reusable for different pointers.
  *
- * @param string $pointer_id Optional. ID of the pointer. Default 'perflab-admin-pointer'.
- * @param array  $args       Optional. Pointer arguments. Supports 'heading' and 'content' entries.
- *                           Defaults are the heading and content for the 'perflab-admin-pointer'.
+ * @param string                                    $pointer_id Optional. ID of the pointer. Default 'perflab-admin-pointer'.
+ * @param array{heading?: string, content?: string} $args       Optional. Pointer arguments. Supports 'heading' and 'content' entries.
+ *                                                              Defaults are the heading and content for the 'perflab-admin-pointer'.
  */
-function perflab_render_pointer( $pointer_id = 'perflab-admin-pointer', $args = array() ) {
+function perflab_render_pointer( string $pointer_id = 'perflab-admin-pointer', array $args = array() ): void {
 	if ( ! isset( $args['heading'] ) ) {
 		$args['heading'] = __( 'Performance Lab', 'performance-lab' );
 	}
@@ -169,10 +177,14 @@
  *
  * @see perflab_add_features_page()
  *
- * @param array $links List of plugin action links HTML.
- * @return array Modified list of plugin action links HTML.
+ * @param string[]|mixed $links List of plugin action links HTML.
+ * @return string[]|mixed Modified list of plugin action links HTML.
  */
 function perflab_plugin_action_links_add_settings( $links ) {
+	if ( ! is_array( $links ) ) {
+		return $links;
+	}
+
 	// Add link as the first plugin action link.
 	$settings_link = sprintf(
 		'<a href="%s">%s</a>',
@@ -179,9 +191,11 @@
 		esc_url( add_query_arg( 'page', PERFLAB_SCREEN, admin_url( 'options-general.php' ) ) ),
 		esc_html__( 'Settings', 'performance-lab' )
 	);
-	array_unshift( $links, $settings_link );
 
-	return $links;
+	return array_merge(
+		array( 'settings' => $settings_link ),
+		$links
+	);
 }
 
 /**
@@ -192,7 +206,7 @@
  *
  * @since 2.3.0
  */
-function perflab_dismiss_wp_pointer_wrapper() {
+function perflab_dismiss_wp_pointer_wrapper(): void {
 	if ( isset( $_POST['pointer'] ) && 'perflab-admin-pointer' !== $_POST['pointer'] ) {
 		// Another plugin's pointer, do nothing.
 		return;
@@ -207,21 +221,41 @@
  * @since 2.8.0
  * @since 3.0.0 Renamed to perflab_enqueue_features_page_scripts().
  */
-function perflab_enqueue_features_page_scripts() {
+function perflab_enqueue_features_page_scripts(): void {
 	// These assets are needed for the "Learn more" popover.
 	wp_enqueue_script( 'thickbox' );
 	wp_enqueue_style( 'thickbox' );
 	wp_enqueue_script( 'plugin-install' );
+
+	// Enqueue the a11y script.
+	wp_enqueue_script( 'wp-a11y' );
 }
 
 /**
+ * Sanitizes a plugin slug.
+ *
+ * @since 3.1.0
+ *
+ * @param mixed $unsanitized_plugin_slug Unsanitized plugin slug.
+ * @return string|null Validated and sanitized slug or else null.
+ */
+function perflab_sanitize_plugin_slug( $unsanitized_plugin_slug ): ?string {
+	if ( in_array( $unsanitized_plugin_slug, perflab_get_standalone_plugins(), true ) ) {
+		return $unsanitized_plugin_slug;
+	} else {
+		return null;
+	}
+}
+
+/**
  * Callback for handling installation/activation of plugin.
  *
  * @since 3.0.0
  */
-function perflab_install_activate_plugin_callback() {
+function perflab_install_activate_plugin_callback(): void {
 	check_admin_referer( 'perflab_install_activate_plugin' );
 
+	require_once ABSPATH . 'wp-admin/includes/plugin.php';
 	require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
 	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
 	require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
@@ -230,75 +264,21 @@
 		wp_die( esc_html__( 'Missing required parameter.', 'performance-lab' ) );
 	}
 
-	$plugin_slug = sanitize_text_field( wp_unslash( $_GET['slug'] ) );
-
+	$plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $_GET['slug'] ) );
 	if ( ! $plugin_slug ) {
 		wp_die( esc_html__( 'Invalid plugin.', 'performance-lab' ) );
 	}
 
-	$is_plugin_installed = isset( $_GET['file'] ) && $_GET['file'];
-
-	// Install the plugin if it is not installed yet.
-	if ( ! $is_plugin_installed ) {
-		// Check if the user have plugin installation capability.
-		if ( ! current_user_can( 'install_plugins' ) ) {
-			wp_die( esc_html__( 'Sorry, you are not allowed to install plugins on this site.', 'default' ) );
-		}
-
-		$api = perflab_query_plugin_info( $plugin_slug );
-
-		// Return early if plugin API returns an error.
-		if ( ! $api ) {
-			wp_die(
-				wp_kses(
-					sprintf(
-						/* translators: %s: Support forums URL. */
-						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.', 'default' ),
-						__( 'https://wordpress.org/support/forums/', 'default' )
-					),
-					array( 'a' => array( 'href' => true ) )
-				)
-			);
-		}
-
-		// Replace new Plugin_Installer_Skin with new Quiet_Upgrader_Skin when output needs to be suppressed.
-		$skin     = new WP_Ajax_Upgrader_Skin( array( 'api' => $api ) );
-		$upgrader = new Plugin_Upgrader( $skin );
-		$result   = $upgrader->install( $api['download_link'] );
-
-		if ( is_wp_error( $result ) ) {
-			wp_die( esc_html( $result->get_error_message() ) );
-		} elseif ( is_wp_error( $skin->result ) ) {
-			wp_die( esc_html( $skin->result->get_error_message() ) );
-		} elseif ( $skin->get_errors()->has_errors() ) {
-			wp_die( esc_html( $skin->get_error_messages() ) );
-		}
-
-		$plugins = get_plugins( '/' . $plugin_slug );
-
-		if ( empty( $plugins ) ) {
-			wp_die( esc_html__( 'Plugin not found.', 'default' ) );
-		}
-
-		$plugin_file_names = array_keys( $plugins );
-		$plugin_basename   = $plugin_slug . '/' . $plugin_file_names[0];
-	} else {
-		$plugin_basename = sanitize_text_field( wp_unslash( $_GET['file'] ) );
+	// Install and activate the plugin and its dependencies.
+	$result = perflab_install_and_activate_plugin( $plugin_slug );
+	if ( $result instanceof WP_Error ) {
+		wp_die( wp_kses_post( $result->get_error_message() ) );
 	}
 
-	if ( ! current_user_can( 'activate_plugin', $plugin_basename ) ) {
-		wp_die( esc_html__( 'Sorry, you are not allowed to activate this plugin.', 'default' ) );
-	}
-
-	$result = activate_plugin( $plugin_basename );
-	if ( is_wp_error( $result ) ) {
-		wp_die( esc_html( $result->get_error_message() ) );
-	}
-
 	$url = add_query_arg(
 		array(
 			'page'     => PERFLAB_SCREEN,
-			'activate' => 'true',
+			'activate' => $plugin_slug,
 		),
 		admin_url( 'options-general.php' )
 	);
@@ -314,9 +294,9 @@
  *
  * @since 3.0.0
  */
-function perflab_print_features_page_style() {
+function perflab_print_features_page_style(): void {
 	?>
-<style type="text/css">
+<style>
 	.plugin-card .name,
 	.plugin-card .desc, /* For WP <6.5 versions */
 	.plugin-card .desc > p {
@@ -323,12 +303,31 @@
 		margin-left: 0;
 	}
 	.plugin-card-top {
-		min-height: auto;
+		/* This is required to ensure the Settings link does not extend below the bottom of a plugin card on a wide screen. */
+		min-height: 90px;
 	}
+	@media screen and (max-width: 782px) {
+		.plugin-card-top {
+			/* Same reason as above, but now the button is taller to make it easier to tap on touch screens. */
+			min-height: 110px;
+		}
+	}
 	.plugin-card .perflab-plugin-experimental {
 		font-size: 80%;
 		font-weight: normal;
 	}
+
+	@media screen and (max-width: 1100px) and (min-width: 782px), (max-width: 480px) {
+		.plugin-card .action-links {
+			margin-left: auto;
+		}
+		/* Make sure the settings link gets spaced out from the Learn more link. */
+		.plugin-card .plugin-action-buttons > li:nth-child(3) {
+			margin-left: 2ex;
+			border-left: solid 1px;
+			padding-left: 2ex;
+		}
+	}
 </style>
 	<?php
 }
@@ -338,20 +337,56 @@
  *
  * @since 2.8.0
  */
-function perflab_plugin_admin_notices() {
+function perflab_plugin_admin_notices(): void {
 	if ( ! current_user_can( 'install_plugins' ) ) {
-		wp_admin_notice(
-			esc_html__( 'Due to your site\'s configuration, you may not be able to activate the performance features, unless the underlying plugin is already installed. Please install the relevant plugins manually.', 'performance-lab' ),
-			array(
-				'type' => 'warning',
-			)
+		$are_all_plugins_installed = true;
+		$installed_plugin_slugs    = array_map(
+			static function ( $name ) {
+				return strtok( $name, '/' );
+			},
+			array_keys( get_plugins() )
 		);
-		return;
+		foreach ( perflab_get_standalone_plugin_version_constants() as $plugin_slug => $constant_name ) {
+			if ( ! in_array( $plugin_slug, $installed_plugin_slugs, true ) ) {
+				$are_all_plugins_installed = false;
+				break;
+			}
+		}
+
+		if ( ! $are_all_plugins_installed ) {
+			wp_admin_notice(
+				esc_html__( 'Due to your site\'s configuration, you may not be able to activate the performance features, unless the underlying plugin is already installed. Please install the relevant plugins manually.', 'performance-lab' ),
+				array(
+					'type' => 'warning',
+				)
+			);
+			return;
+		}
 	}
 
+	$activated_plugin_slug = null;
 	if ( isset( $_GET['activate'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		$activated_plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $_GET['activate'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+	}
+
+	if ( $activated_plugin_slug ) {
+		$message = __( 'Feature activated.', 'performance-lab' );
+
+		$plugin_settings_url = perflab_get_plugin_settings_url( $activated_plugin_slug );
+		if ( $plugin_settings_url ) {
+			/* translators: %s is the settings URL */
+			$message .= ' ' . sprintf( __( 'Review <a href="%s">settings</a>.', 'performance-lab' ), esc_url( $plugin_settings_url ) );
+		}
+
 		wp_admin_notice(
-			esc_html__( 'Feature activated.', 'performance-lab' ),
+			wp_kses(
+				$message,
+				array(
+					'a' => array(
+						'href' => array(),
+					),
+				)
+			),
 			array(
 				'type'        => 'success',
 				'dismissible' => true,
@@ -359,3 +394,80 @@
 		);
 	}
 }
+
+/**
+ * Callback function that print plugin progress indicator script.
+ *
+ * @since 3.1.0
+ */
+function perflab_print_plugin_progress_indicator_script(): void {
+	$js_function = <<<JS
+		function addPluginProgressIndicator( message ) {
+			document.addEventListener( 'DOMContentLoaded', function () {
+				document.addEventListener( 'click', function ( event ) {
+					if (
+						event.target.classList.contains(
+							'perflab-install-active-plugin'
+						)
+					) {
+						const target = event.target;
+						target.classList.add( 'updating-message' );
+						target.textContent = message;
+
+						wp.a11y.speak( message );
+					}
+				} );
+			} );
+		}
+JS;
+
+	wp_print_inline_script_tag(
+		sprintf(
+			'( %s )( %s );',
+			$js_function,
+			wp_json_encode( __( 'Activating...', 'default' ) )
+		),
+		array( 'type' => 'module' )
+	);
+}
+
+/**
+ * Gets the URL to the plugin settings screen if one exists.
+ *
+ * @since 3.1.0
+ *
+ * @param string $plugin_slug Plugin slug passed to generate the settings link.
+ * @return string|null Either the plugin settings URL or null if not available.
+ */
+function perflab_get_plugin_settings_url( string $plugin_slug ): ?string {
+	$plugin_file = null;
+
+	foreach ( array_keys( get_plugins() ) as $file ) {
+		if ( strtok( $file, '/' ) === $plugin_slug ) {
+			$plugin_file = $file;
+			break;
+		}
+	}
+
+	if ( null === $plugin_file ) {
+		return null;
+	}
+
+	/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
+	$plugin_links = apply_filters( "plugin_action_links_{$plugin_file}", array() );
+
+	if ( ! is_array( $plugin_links ) || ! array_key_exists( 'settings', $plugin_links ) ) {
+		return null;
+	}
+
+	$p = new WP_HTML_Tag_Processor( $plugin_links['settings'] );
+	if ( ! $p->next_tag( array( 'tag_name' => 'A' ) ) ) {
+		return null;
+	}
+	$href = $p->get_attribute( 'href' );
+	if ( $href && is_string( $href ) ) {
+		return $href;
+	}
+
+	return null;
+}
Index: includes/admin/plugins.php
===================================================================
--- includes/admin/plugins.php	(revision 3088546)
+++ includes/admin/plugins.php	(working copy)
@@ -3,6 +3,7 @@
  * Admin settings helper functions.
  *
  * @package performance-lab
+ * @noinspection PhpRedundantOptionalArgumentInspection
  */
 
 if ( ! defined( 'ABSPATH' ) ) {
@@ -15,7 +16,7 @@
  * @since 2.8.0
  *
  * @param string $plugin_slug The string identifier for the plugin in questions slug.
- * @return array Array of plugin data, or empty if none/error.
+ * @return array{name: string, slug: string, short_description: string, requires: string|false, requires_php: string|false, requires_plugins: string[], download_link: string, version: string}|WP_Error Array of plugin data or WP_Error if failed.
  */
 function perflab_query_plugin_info( string $plugin_slug ) {
 	$plugin = get_transient( 'perflab_plugin_info_' . $plugin_slug );
@@ -24,19 +25,27 @@
 		return $plugin;
 	}
 
+	$fields = array(
+		'name',
+		'slug',
+		'short_description',
+		'requires',
+		'requires_php',
+		'requires_plugins',
+		'download_link',
+		'version', // Needed by install_plugin_install_status().
+	);
+
 	$plugin = plugins_api(
 		'plugin_information',
 		array(
 			'slug'   => $plugin_slug,
-			'fields' => array(
-				'short_description' => true,
-				'icons'             => true,
-			),
+			'fields' => array_fill_keys( $fields, true ),
 		)
 	);
 
 	if ( is_wp_error( $plugin ) ) {
-		return array();
+		return $plugin;
 	}
 
 	if ( is_object( $plugin ) ) {
@@ -43,8 +52,19 @@
 		$plugin = (array) $plugin;
 	}
 
+	// Only store what we need.
+	$plugin = wp_array_slice_assoc( $plugin, $fields );
+
+	// Make sure all fields default to false in case another plugin is modifying the response from WordPress.org via the plugins_api filter.
+	$plugin = array_merge( array_fill_keys( $fields, false ), $plugin );
+
 	set_transient( 'perflab_plugin_info_' . $plugin_slug, $plugin, HOUR_IN_SECONDS );
 
+	/**
+	 * Validated (mostly) plugin data.
+	 *
+	 * @var array{name: string, slug: string, short_description: string, requires: string|false, requires_php: string|false, requires_plugins: string[], download_link: string, version: string} $plugin
+	 */
 	return $plugin;
 }
 
@@ -53,9 +73,9 @@
  *
  * @since 2.8.0
  *
- * @return array List of WPP standalone plugins as slugs.
+ * @return string[] List of WPP standalone plugins as slugs.
  */
-function perflab_get_standalone_plugins() {
+function perflab_get_standalone_plugins(): array {
 	return array_keys(
 		perflab_get_standalone_plugin_data()
 	);
@@ -66,7 +86,7 @@
  *
  * @since 2.8.0
  */
-function perflab_render_plugins_ui() {
+function perflab_render_plugins_ui(): void {
 	require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
 	require_once ABSPATH . 'wp-admin/includes/plugin.php';
 
@@ -74,13 +94,34 @@
 	$experimental_plugins = array();
 
 	foreach ( perflab_get_standalone_plugin_data() as $plugin_slug => $plugin_data ) {
+		$api_data = perflab_query_plugin_info( $plugin_slug ); // Data from wordpress.org.
+
+		// Skip if the plugin is not on WordPress.org or there was a network error.
+		if ( $api_data instanceof WP_Error ) {
+			wp_admin_notice(
+				esc_html(
+					sprintf(
+						/* translators: 1: plugin slug. 2: error message. */
+						__( 'Failed to query WordPress.org Plugin Directory for plugin "%1$s". %2$s', 'performance-lab' ),
+						$plugin_slug,
+						$api_data->get_error_message()
+					)
+				),
+				array( 'type' => 'error' )
+			);
+			continue;
+		}
+
 		$plugin_data = array_merge(
+			array(
+				'experimental' => false,
+			),
 			$plugin_data, // Data defined within Performance Lab.
-			perflab_query_plugin_info( $plugin_slug ) // Data from wordpress.org.
+			$api_data
 		);
 
 		// Separate experimental plugins so that they're displayed after non-experimental plugins.
-		if ( isset( $plugin_data['experimental'] ) && $plugin_data['experimental'] ) {
+		if ( $plugin_data['experimental'] ) {
 			$experimental_plugins[ $plugin_slug ] = $plugin_data;
 		} else {
 			$plugins[ $plugin_slug ] = $plugin_data;
@@ -87,7 +128,7 @@
 		}
 	}
 
-	if ( empty( $plugins ) ) {
+	if ( ! $plugins && ! $experimental_plugins ) {
 		return;
 	}
 	?>
@@ -116,6 +157,148 @@
 }
 
 /**
+ * Checks if a given plugin is available.
+ *
+ * @since 3.1.0
+ * @see perflab_install_and_activate_plugin()
+ *
+ * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data                     Plugin data from the WordPress.org API.
+ * @param array<string, array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, activated: bool, installed: bool}>                      $processed_plugin_availabilities Plugin availabilities already processed. This param is only used by recursive calls.
+ * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, activated: bool, installed: bool} Availability.
+ */
+function perflab_get_plugin_availability( array $plugin_data, array &$processed_plugin_availabilities = array() ): array {
+	if ( array_key_exists( $plugin_data['slug'], $processed_plugin_availabilities ) ) {
+		// Prevent infinite recursion by returning the previously-computed value.
+		return $processed_plugin_availabilities[ $plugin_data['slug'] ];
+	}
+
+	$availability = array(
+		'compatible_php' => (
+			! $plugin_data['requires_php'] ||
+			is_php_version_compatible( $plugin_data['requires_php'] )
+		),
+		'compatible_wp'  => (
+			! $plugin_data['requires'] ||
+			is_wp_version_compatible( $plugin_data['requires'] )
+		),
+	);
+
+	$plugin_status = install_plugin_install_status( $plugin_data );
+
+	$availability['installed'] = ( 'install' !== $plugin_status['status'] );
+	$availability['activated'] = $plugin_status['file'] && is_plugin_active( $plugin_status['file'] );
+
+	// The plugin is already installed or the user can install plugins.
+	$availability['can_install'] = (
+		$availability['installed'] ||
+		current_user_can( 'install_plugins' )
+	);
+
+	// The plugin is activated or the user can activate plugins.
+	$availability['can_activate'] = (
+		$availability['activated'] ||
+		$plugin_status['file'] // When not false, the plugin is installed.
+			? current_user_can( 'activate_plugin', $plugin_status['file'] )
+			: current_user_can( 'activate_plugins' )
+	);
+
+	// Store pending availability before recursing.
+	$processed_plugin_availabilities[ $plugin_data['slug'] ] = $availability;
+
+	foreach ( $plugin_data['requires_plugins'] as $requires_plugin ) {
+		$dependency_plugin_data = perflab_query_plugin_info( $requires_plugin );
+		if ( $dependency_plugin_data instanceof WP_Error ) {
+			continue;
+		}
+
+		$dependency_availability = perflab_get_plugin_availability( $dependency_plugin_data );
+		foreach ( array( 'compatible_php', 'compatible_wp', 'can_install', 'can_activate', 'installed', 'activated' ) as $key ) {
+			$availability[ $key ] = $availability[ $key ] && $dependency_availability[ $key ];
+		}
+	}
+
+	$processed_plugin_availabilities[ $plugin_data['slug'] ] = $availability;
+	return $availability;
+}
+
+/**
+ * Installs and activates a plugin by its slug.
+ *
+ * Dependencies are recursively installed and activated as well.
+ *
+ * @since 3.1.0
+ * @see perflab_get_plugin_availability()
+ *
+ * @param string   $plugin_slug       Plugin slug.
+ * @param string[] $processed_plugins Slugs for plugins which have already been processed. This param is only used by recursive calls.
+ * @return WP_Error|null WP_Error on failure.
+ */
+function perflab_install_and_activate_plugin( string $plugin_slug, array &$processed_plugins = array() ): ?WP_Error {
+	if ( in_array( $plugin_slug, $processed_plugins, true ) ) {
+		// Prevent infinite recursion from possible circular dependency.
+		return null;
+	}
+	$processed_plugins[] = $plugin_slug;
+
+	$plugin_data = perflab_query_plugin_info( $plugin_slug );
+	if ( $plugin_data instanceof WP_Error ) {
+		return $plugin_data;
+	}
+
+	// Install and activate plugin dependencies first.
+	foreach ( $plugin_data['requires_plugins'] as $requires_plugin_slug ) {
+		$result = perflab_install_and_activate_plugin( $requires_plugin_slug );
+		if ( $result instanceof WP_Error ) {
+			return $result;
+		}
+	}
+
+	// Install the plugin.
+	$plugin_status = install_plugin_install_status( $plugin_data );
+	$plugin_file   = $plugin_status['file'];
+	if ( 'install' === $plugin_status['status'] ) {
+		if ( ! current_user_can( 'install_plugins' ) ) {
+			return new WP_Error( 'cannot_install_plugin', __( 'Sorry, you are not allowed to install plugins on this site.', 'default' ) );
+		}
+
+		// Replace new Plugin_Installer_Skin with new Quiet_Upgrader_Skin when output needs to be suppressed.
+		$skin     = new WP_Ajax_Upgrader_Skin( array( 'api' => $plugin_data ) );
+		$upgrader = new Plugin_Upgrader( $skin );
+		$result   = $upgrader->install( $plugin_data['download_link'] );
+
+		if ( is_wp_error( $result ) ) {
+			return $result;
+		} elseif ( is_wp_error( $skin->result ) ) {
+			return $skin->result;
+		} elseif ( $skin->get_errors()->has_errors() ) {
+			return $skin->get_errors();
+		}
+
+		$plugins = get_plugins( '/' . $plugin_slug );
+		if ( empty( $plugins ) ) {
+			return new WP_Error( 'plugin_not_found', __( 'Plugin not found.', 'default' ) );
+		}
+
+		$plugin_file_names = array_keys( $plugins );
+		$plugin_file       = $plugin_slug . '/' . $plugin_file_names[0];
+	}
+
+	// Activate the plugin.
+	if ( ! is_plugin_active( $plugin_file ) ) {
+		if ( ! current_user_can( 'activate_plugin', $plugin_file ) ) {
+			return new WP_Error( 'cannot_activate_plugin', __( 'Sorry, you are not allowed to activate this plugin.', 'default' ) );
+		}
+
+		$result = activate_plugin( $plugin_file );
+		if ( $result instanceof WP_Error ) {
+			return $result;
+		}
+	}
+
+	return null;
+}
+
+/**
  * Renders individual plugin cards.
  *
  * This is adapted from `WP_Plugin_Install_List_Table::display_rows()` in core.
@@ -125,48 +308,32 @@
  * @see WP_Plugin_Install_List_Table::display_rows()
  * @link https://github.com/WordPress/wordpress-develop/blob/0b8ca16ea3bd9722bd1a38f8ab68901506b1a0e7/src/wp-admin/includes/class-wp-plugin-install-list-table.php#L467-L830
  *
- * @param array $plugin_data Plugin data from the WordPress.org API.
+ * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string, experimental: bool} $plugin_data Plugin data augmenting data from the WordPress.org API.
  */
-function perflab_render_plugin_card( array $plugin_data ) {
-	// If no plugin data is returned, return.
-	if ( empty( $plugin_data ) ) {
-		return;
-	}
+function perflab_render_plugin_card( array $plugin_data ): void {
 
-	// Remove any HTML from the description.
+	$name        = wp_strip_all_tags( $plugin_data['name'] );
 	$description = wp_strip_all_tags( $plugin_data['short_description'] );
-	$title       = $plugin_data['name'];
 
 	/** This filter is documented in wp-admin/includes/class-wp-plugin-install-list-table.php */
 	$description = apply_filters( 'plugin_install_description', $description, $plugin_data );
-	$version     = $plugin_data['version'];
-	$name        = wp_strip_all_tags( $title . ' ' . $version );
 
-	$requires_php = isset( $plugin_data['requires_php'] ) ? $plugin_data['requires_php'] : null;
-	$requires_wp  = isset( $plugin_data['requires'] ) ? $plugin_data['requires'] : null;
+	$availability   = perflab_get_plugin_availability( $plugin_data );
+	$compatible_php = $availability['compatible_php'];
+	$compatible_wp  = $availability['compatible_wp'];
 
-	$compatible_php = is_php_version_compatible( $requires_php );
-	$compatible_wp  = is_wp_version_compatible( $requires_wp );
-	$action_links   = array();
+	$action_links = array();
 
-	$status = install_plugin_install_status( $plugin_data );
-
-	if ( is_plugin_active( $status['file'] ) ) {
+	if ( $availability['activated'] ) {
 		$action_links[] = sprintf(
 			'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
 			esc_html( _x( 'Active', 'plugin', 'default' ) )
 		);
 	} elseif (
-		$compatible_php &&
-		$compatible_wp &&
-		(
-			( $status['file'] && current_user_can( 'activate_plugin', $status['file'] ) ) ||
-			current_user_can( 'activate_plugins' )
-		) &&
-		(
-			'install' !== $status['status'] ||
-			current_user_can( 'install_plugins' )
-		)
+		$availability['compatible_php'] &&
+		$availability['compatible_wp'] &&
+		$availability['can_install'] &&
+		$availability['can_activate']
 	) {
 		$url = esc_url_raw(
 			add_query_arg(
@@ -174,7 +341,6 @@
 					'action'   => 'perflab_install_activate_plugin',
 					'_wpnonce' => wp_create_nonce( 'perflab_install_activate_plugin' ),
 					'slug'     => $plugin_data['slug'],
-					'file'     => $status['file'],
 				),
 				admin_url( 'options-general.php' )
 			)
@@ -186,7 +352,7 @@
 			esc_html__( 'Activate', 'default' )
 		);
 	} else {
-		$explanation    = 'install' !== $status['status'] || current_user_can( 'install_plugins' ) ? _x( 'Cannot Activate', 'plugin', 'default' ) : _x( 'Cannot Install', 'plugin', 'default' );
+		$explanation    = $availability['can_install'] ? _x( 'Cannot Activate', 'plugin', 'default' ) : _x( 'Cannot Install', 'plugin', 'default' );
 		$action_links[] = sprintf(
 			'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
 			esc_html( $explanation )
@@ -193,27 +359,52 @@
 		);
 	}
 
-	$details_link = esc_url_raw(
-		add_query_arg(
-			array(
-				'tab'       => 'plugin-information',
-				'plugin'    => $plugin_data['slug'],
-				'TB_iframe' => 'true',
-				'width'     => 600,
-				'height'    => 550,
-			),
-			admin_url( 'plugin-install.php' )
-		)
-	);
+	if ( current_user_can( 'install_plugins' ) ) {
+		$title_link_attr = ' class="thickbox open-plugin-details-modal"';
+		$details_link    = esc_url_raw(
+			add_query_arg(
+				array(
+					'tab'       => 'plugin-information',
+					'plugin'    => $plugin_data['slug'],
+					'TB_iframe' => 'true',
+					'width'     => 600,
+					'height'    => 550,
+				),
+				admin_url( 'plugin-install.php' )
+			)
+		);
 
-	$action_links[] = sprintf(
-		'<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
-		esc_url( $details_link ),
-		/* translators: %s: Plugin name and version. */
-		esc_attr( sprintf( __( 'More information about %s', 'default' ), $name ) ),
-		esc_attr( $name ),
-		esc_html__( 'Learn more', 'performance-lab' )
-	);
+		$action_links[] = sprintf(
+			'<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
+			esc_url( $details_link ),
+			/* translators: %s: Plugin name and version. */
+			esc_attr( sprintf( __( 'More information about %s', 'default' ), $name ) ),
+			esc_attr( $name ),
+			esc_html__( 'Learn more', 'performance-lab' )
+		);
+	} else {
+		$title_link_attr = ' target="_blank"';
+
+		/* translators: %s: Plugin name. */
+		$aria_label = sprintf( __( 'Visit plugin site for %s', 'default' ), $name );
+
+		$details_link = __( 'https://wordpress.org/plugins/', 'default' ) . $plugin_data['slug'] . '/';
+
+		$action_links[] = sprintf(
+			'<a href="%s" aria-label="%s" target="_blank">%s</a>',
+			esc_url( $details_link ),
+			esc_attr( $aria_label ),
+			esc_html__( 'Visit plugin site', 'default' )
+		);
+	}
+
+	if ( $availability['activated'] ) {
+		$settings_url = perflab_get_plugin_settings_url( $plugin_data['slug'] );
+		if ( $settings_url ) {
+			/* translators: %s is the settings URL */
+			$action_links[] = sprintf( '<a href="%s">%s</a>', esc_url( $settings_url ), esc_html__( 'Settings', 'performance-lab' ) );
+		}
+	}
 	?>
 	<div class="plugin-card plugin-card-<?php echo sanitize_html_class( $plugin_data['slug'] ); ?>">
 		<?php
@@ -279,26 +470,22 @@
 		<div class="plugin-card-top">
 			<div class="name column-name">
 				<h3>
-					<a href="<?php echo esc_url( $details_link ); ?>" class="thickbox open-plugin-details-modal">
-						<?php echo wp_kses_post( $title ); ?>
+					<a href="<?php echo esc_url( $details_link ); ?>"<?php echo $title_link_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
+						<?php echo wp_kses_post( $name ); ?>
 					</a>
-					<?php
-					if ( isset( $plugin_data['experimental'] ) && $plugin_data['experimental'] ) {
-						?>
+					<?php if ( $plugin_data['experimental'] ) : ?>
 						<em class="perflab-plugin-experimental">
 							<?php echo esc_html( _x( '(experimental)', 'plugin suffix', 'performance-lab' ) ); ?>
 						</em>
-						<?php
-					}
-					?>
+					<?php endif; ?>
 				</h3>
 			</div>
 			<div class="action-links">
-				<?php
-				if ( ! empty( $action_links ) ) {
-					echo wp_kses_post( '<ul class="plugin-action-buttons"><li>' . implode( '</li><li>', $action_links ) . '</li></ul>' );
-				}
-				?>
+				<ul class="plugin-action-buttons">
+					<?php foreach ( $action_links as $action_link ) : ?>
+						<li><?php echo wp_kses_post( $action_link ); ?></li>
+					<?php endforeach; ?>
+				</ul>
 			</div>
 			<div class="desc column-description">
 				<p><?php echo wp_kses_post( $description ); ?></p>
Index: includes/admin/server-timing.php
===================================================================
--- includes/admin/server-timing.php	(revision 3088546)
+++ includes/admin/server-timing.php	(working copy)
@@ -19,7 +19,7 @@
  *
  * @since 2.6.0
  */
-function perflab_add_server_timing_page() {
+function perflab_add_server_timing_page(): void {
 	$hook_suffix = add_management_page(
 		__( 'Server-Timing', 'performance-lab' ),
 		__( 'Server-Timing', 'performance-lab' ),
@@ -32,9 +32,8 @@
 	if ( false !== $hook_suffix ) {
 		add_action( "load-{$hook_suffix}", 'perflab_load_server_timing_page' );
 	}
+}
 
-	return $hook_suffix;
-}
 add_action( 'admin_menu', 'perflab_add_server_timing_page' );
 
 /**
@@ -42,7 +41,7 @@
  *
  * @since 2.6.0
  */
-function perflab_load_server_timing_page() {
+function perflab_load_server_timing_page(): void {
 	/*
 	 * This settings section technically includes a field, however it is directly rendered as part of the section
 	 * callback due to requiring custom markup.
@@ -57,7 +56,7 @@
 	// Minor style tweaks to improve appearance similar to other core settings screen instances.
 	add_action(
 		'admin_print_styles',
-		static function () {
+		static function (): void {
 			?>
 			<style>
 				.wrap p {
@@ -74,7 +73,7 @@
 	add_settings_section(
 		'benchmarking',
 		__( 'Benchmarking', 'performance-lab' ),
-		static function () {
+		static function (): void {
 			?>
 			<p>
 				<?php
@@ -125,7 +124,7 @@
 	add_settings_field(
 		'benchmarking_actions',
 		__( 'Actions', 'performance-lab' ),
-		static function () {
+		static function (): void {
 			perflab_render_server_timing_page_hooks_field( 'benchmarking_actions' );
 		},
 		PERFLAB_SERVER_TIMING_SCREEN,
@@ -135,7 +134,7 @@
 	add_settings_field(
 		'benchmarking_filters',
 		__( 'Filters', 'performance-lab' ),
-		static function () {
+		static function (): void {
 			perflab_render_server_timing_page_hooks_field( 'benchmarking_filters' );
 		},
 		PERFLAB_SERVER_TIMING_SCREEN,
@@ -149,7 +148,7 @@
  *
  * @since 2.6.0
  */
-function perflab_render_server_timing_page() {
+function perflab_render_server_timing_page(): void {
 	?>
 	<div class="wrap">
 		<?php settings_errors(); ?>
@@ -173,7 +172,7 @@
  *
  * @param string $slug Slug of the field and sub-key in the Server-Timing option.
  */
-function perflab_render_server_timing_page_hooks_field( $slug ) {
+function perflab_render_server_timing_page_hooks_field( string $slug ): void {
 	$options = (array) get_option( PERFLAB_SERVER_TIMING_SETTING, array() );
 
 	// Value for the sub-key is an array of hook names.
@@ -205,7 +204,7 @@
  *
  * @since 2.6.0
  */
-function perflab_render_server_timing_page_output_buffering_section() {
+function perflab_render_server_timing_page_output_buffering_section(): void {
 	$slug           = 'output_buffering';
 	$field_id       = "server_timing_{$slug}";
 	$field_name     = PERFLAB_SERVER_TIMING_SETTING . '[' . $slug . ']';
Index: includes/server-timing/class-perflab-server-timing-metric.php
===================================================================
--- includes/server-timing/class-perflab-server-timing-metric.php	(revision 3088546)
+++ includes/server-timing/class-perflab-server-timing-metric.php	(working copy)
@@ -44,7 +44,7 @@
 	 *
 	 * @param string $slug The metric slug.
 	 */
-	public function __construct( $slug ) {
+	public function __construct( string $slug ) {
 		$this->slug = $slug;
 	}
 
@@ -55,7 +55,7 @@
 	 *
 	 * @return string The metric slug.
 	 */
-	public function get_slug() {
+	public function get_slug(): string {
 		return $this->slug;
 	}
 
@@ -67,9 +67,9 @@
 	 *
 	 * @since 1.8.0
 	 *
-	 * @param int|float $value The metric value to set, in milliseconds.
+	 * @param int|float|mixed $value The metric value to set, in milliseconds.
 	 */
-	public function set_value( $value ) {
+	public function set_value( $value ): void {
 		if ( ! is_numeric( $value ) ) {
 			_doing_it_wrong(
 				__METHOD__,
@@ -110,7 +110,7 @@
 	}
 
 	/**
-	 * Captures the current time, as a reference point to calculate the duration of a task afterwards.
+	 * Captures the current time, as a reference point to calculate the duration of a task afterward.
 	 *
 	 * This should be used in combination with {@see Perflab_Server_Timing_Metric::measure_after()}. Alternatively,
 	 * {@see Perflab_Server_Timing_Metric::set_value()} can be used to set a calculated value manually.
@@ -117,7 +117,7 @@
 	 *
 	 * @since 1.8.0
 	 */
-	public function measure_before() {
+	public function measure_before(): void {
 		$this->before_value = microtime( true );
 	}
 
@@ -129,7 +129,7 @@
 	 *
 	 * @since 1.8.0
 	 */
-	public function measure_after() {
+	public function measure_after(): void {
 		if ( ! $this->before_value ) {
 			_doing_it_wrong(
 				__METHOD__,
Index: includes/server-timing/class-perflab-server-timing.php
===================================================================
--- includes/server-timing/class-perflab-server-timing.php	(revision 3088546)
+++ includes/server-timing/class-perflab-server-timing.php	(working copy)
@@ -9,6 +9,11 @@
 /**
  * Class controlling the Server-Timing header.
  *
+ * @phpstan-type MetricArguments array{
+ *                   measure_callback: callable( Perflab_Server_Timing_Metric ): void,
+ *                   access_cap: string
+ *               }
+ *
  * @since 1.8.0
  */
 class Perflab_Server_Timing {
@@ -17,7 +22,7 @@
 	 * Map of registered metric slugs and their metric instances.
 	 *
 	 * @since 1.8.0
-	 * @var array
+	 * @var array<string, Perflab_Server_Timing_Metric>
 	 */
 	private $registered_metrics = array();
 
@@ -25,7 +30,8 @@
 	 * Map of registered metric slugs and their registered data.
 	 *
 	 * @since 1.8.0
-	 * @var array
+	 * @phpstan-var array<string, MetricArguments>
+	 * @var array<string, array>
 	 */
 	private $registered_metrics_data = array();
 
@@ -36,8 +42,10 @@
 	 *
 	 * @since 1.8.0
 	 *
-	 * @param string $metric_slug The metric slug.
-	 * @param array  $args        {
+	 * @phpstan-param MetricArguments $args
+	 *
+	 * @param string                         $metric_slug The metric slug.
+	 * @param array<string, callable|string> $args        {
 	 *     Arguments for the metric.
 	 *
 	 *     @type callable $measure_callback The callback that initiates calculating the metric value. It will receive
@@ -48,7 +56,7 @@
 	 *                                      needs to be set to "exist".
 	 * }
 	 */
-	public function register_metric( $metric_slug, array $args ) {
+	public function register_metric( string $metric_slug, array $args ): void {
 		if ( isset( $this->registered_metrics[ $metric_slug ] ) ) {
 			_doing_it_wrong(
 				__METHOD__,
@@ -94,11 +102,16 @@
 			);
 			return;
 		}
+		/**
+		 * Validated args.
+		 *
+		 * @var MetricArguments $args
+		 */
 
 		$this->registered_metrics[ $metric_slug ]      = new Perflab_Server_Timing_Metric( $metric_slug );
 		$this->registered_metrics_data[ $metric_slug ] = $args;
 
-		// If the current user has already been determined and they lack the necessary access,
+		// If the current user has already been determined, and they lack the necessary access,
 		// do not even attempt to calculate the metric.
 		if ( did_action( 'set_current_user' ) && ! current_user_can( $args['access_cap'] ) ) {
 			return;
@@ -116,7 +129,7 @@
 	 * @param string $metric_slug The metric slug.
 	 * @return bool True if registered, false otherwise.
 	 */
-	public function has_registered_metric( $metric_slug ) {
+	public function has_registered_metric( string $metric_slug ): bool {
 		return isset( $this->registered_metrics[ $metric_slug ] ) && isset( $this->registered_metrics_data[ $metric_slug ] );
 	}
 
@@ -127,7 +140,7 @@
 	 *
 	 * @since 1.8.0
 	 */
-	public function send_header() {
+	public function send_header(): void {
 		if ( headers_sent() ) {
 			_doing_it_wrong(
 				__METHOD__,
@@ -161,7 +174,7 @@
 	 *
 	 * @return string The Server-Timing header value.
 	 */
-	public function get_header() {
+	public function get_header(): string {
 		// Get all metric header values, as long as the current user has access to the metric.
 		$metric_header_values = array_filter(
 			array_map(
@@ -187,7 +200,7 @@
 	 * Returns whether an output buffer should be used to gather Server-Timing metrics during template rendering.
 	 *
 	 * Without an output buffer, it is only possible to cover metrics from before serving the template, i.e. before
-	 * the HTML output starts. Therefore sites that would like to gather metrics while serving the template should
+	 * the HTML output starts. Therefore, sites that would like to gather metrics while serving the template should
 	 * enable this via the {@see 'perflab_server_timing_use_output_buffer'} filter.
 	 *
 	 * @since 1.8.0
@@ -194,7 +207,7 @@
 	 *
 	 * @return bool True if an output buffer should be used, false otherwise.
 	 */
-	public function use_output_buffer() {
+	public function use_output_buffer(): bool {
 		$options = (array) get_option( PERFLAB_SERVER_TIMING_SETTING, array() );
 		$enabled = ! empty( $options['output_buffering'] );
 
@@ -202,7 +215,7 @@
 		 * Filters whether an output buffer should be used to be able to gather additional Server-Timing metrics.
 		 *
 		 * Without an output buffer, it is only possible to cover metrics from before serving the template, i.e. before
-		 * the HTML output starts. Therefore sites that would like to gather metrics while serving the template should
+		 * the HTML output starts. Therefore, sites that would like to gather metrics while serving the template should
 		 * enable this.
 		 *
 		 * @since 1.8.0
@@ -247,7 +260,7 @@
 	 * @param Perflab_Server_Timing_Metric $metric The metric to format.
 	 * @return string|null Segment for the Server-Timing header, or null if no value set.
 	 */
-	private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ) {
+	private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ): ?string {
 		$value = $metric->get_value();
 
 		// If no value is set, make sure it's just passed through.
Index: includes/server-timing/defaults.php
===================================================================
--- includes/server-timing/defaults.php	(revision 3088546)
+++ includes/server-timing/defaults.php	(working copy)
@@ -22,13 +22,13 @@
  *
  * @since 1.8.0
  */
-function perflab_register_default_server_timing_before_template_metrics() {
-	$calculate_before_template_metrics = static function () {
+function perflab_register_default_server_timing_before_template_metrics(): void {
+	$calculate_before_template_metrics = static function (): void {
 		// WordPress execution prior to serving the template.
 		perflab_server_timing_register_metric(
 			'before-template',
 			array(
-				'measure_callback' => static function ( $metric ) {
+				'measure_callback' => static function ( $metric ): void {
 					// The 'timestart' global is set right at the beginning of WordPress execution.
 					$metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 );
 				},
@@ -42,7 +42,7 @@
 			perflab_server_timing_register_metric(
 				'before-template-db-queries',
 				array(
-					'measure_callback' => static function ( $metric ) {
+					'measure_callback' => static function ( $metric ): void {
 						// This should never happen, but some odd database implementations may be doing it wrong.
 						if ( ! isset( $GLOBALS['wpdb']->queries ) || ! is_array( $GLOBALS['wpdb']->queries ) ) {
 							return;
@@ -80,7 +80,7 @@
 	);
 	add_action(
 		'perflab_server_timing_send_header',
-		static function () use ( $calculate_before_template_metrics ) {
+		static function () use ( $calculate_before_template_metrics ): void {
 			if ( ! perflab_server_timing_use_output_buffer() ) {
 				$calculate_before_template_metrics();
 			}
@@ -106,7 +106,7 @@
 				perflab_server_timing_register_metric(
 					'load-alloptions-query',
 					array(
-						'measure_callback' => static function ( $metric ) {
+						'measure_callback' => static function ( $metric ): void {
 							$metric->measure_before();
 							add_filter(
 								'pre_cache_alloptions',
@@ -135,7 +135,7 @@
  *
  * @since 1.8.0
  */
-function perflab_register_default_server_timing_template_metrics() {
+function perflab_register_default_server_timing_template_metrics(): void {
 	// Template-related metrics can only be recorded if output buffering is used.
 	if ( ! perflab_server_timing_use_output_buffer() ) {
 		return;
@@ -148,7 +148,7 @@
 			perflab_server_timing_register_metric(
 				'template',
 				array(
-					'measure_callback' => static function ( $metric ) {
+					'measure_callback' => static function ( Perflab_Server_Timing_Metric $metric ): void {
 						$metric->measure_before();
 						add_action( 'perflab_server_timing_send_header', array( $metric, 'measure_after' ), PHP_INT_MAX );
 					},
@@ -163,12 +163,12 @@
 
 	add_action(
 		'perflab_server_timing_send_header',
-		static function () {
+		static function (): void {
 			// WordPress total load time.
 			perflab_server_timing_register_metric(
 				'total',
 				array(
-					'measure_callback' => static function ( $metric ) {
+					'measure_callback' => static function ( $metric ): void {
 						// The 'timestart' global is set right at the beginning of WordPress execution.
 						$metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 );
 					},
@@ -182,12 +182,12 @@
 	if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
 		add_action(
 			'perflab_server_timing_send_header',
-			static function () {
+			static function (): void {
 				// WordPress database query time within template.
 				perflab_server_timing_register_metric(
 					'template-db-queries',
 					array(
-						'measure_callback' => static function ( $metric ) {
+						'measure_callback' => static function ( $metric ): void {
 							// This global should typically be set when this is called, but check just in case.
 							if ( ! isset( $GLOBALS['perflab_query_time_before_template'] ) ) {
 								return;
@@ -225,7 +225,7 @@
  *
  * @since 2.6.0
  */
-function perflab_register_additional_server_timing_metrics_from_setting() {
+function perflab_register_additional_server_timing_metrics_from_setting(): void {
 	$options = (array) get_option( PERFLAB_SERVER_TIMING_SETTING, array() );
 
 	$hooks_to_measure = array();
@@ -265,7 +265,7 @@
 	 */
 	add_action(
 		'all',
-		static function ( $hook_name ) use ( $hooks_to_measure ) {
+		static function ( $hook_name ) use ( $hooks_to_measure ): void {
 			if ( ! isset( $hooks_to_measure[ $hook_name ] ) ) {
 				return;
 			}
@@ -277,11 +277,11 @@
 				return;
 			}
 
-			$measure_callback = static function ( $metric ) use ( $hook_name, $hook_type ) {
+			$measure_callback = static function ( $metric ) use ( $hook_name, $hook_type ): void {
 				$metric->measure_before();
 
 				if ( 'action' === $hook_type ) {
-					$cb = static function () use ( $metric, $hook_name, &$cb ) {
+					$cb = static function () use ( $metric, $hook_name, &$cb ): void {
 						$metric->measure_after();
 						remove_action( $hook_name, $cb, PHP_INT_MAX );
 					};
Index: includes/server-timing/load.php
===================================================================
--- includes/server-timing/load.php	(revision 3088546)
+++ includes/server-timing/load.php	(working copy)
@@ -18,6 +18,8 @@
 define( 'PERFLAB_SERVER_TIMING_SETTING', 'perflab_server_timing_settings' );
 define( 'PERFLAB_SERVER_TIMING_SCREEN', 'perflab-server-timing' );
 
+require_once __DIR__ . '/hooks.php';
+
 /**
  * Provides access the Server-Timing API.
  *
@@ -27,7 +29,7 @@
  *
  * @since 1.8.0
  */
-function perflab_server_timing() {
+function perflab_server_timing(): Perflab_Server_Timing {
 	static $server_timing;
 
 	if ( null === $server_timing ) {
@@ -48,9 +50,19 @@
 
 	return $server_timing;
 }
-add_action( 'wp_loaded', 'perflab_server_timing' );
 
 /**
+ * Initializes the Server-Timing API.
+ *
+ * @since 3.1.0
+ */
+function perflab_server_timing_init(): void {
+	perflab_server_timing();
+}
+
+add_action( 'wp_loaded', 'perflab_server_timing_init' );
+
+/**
  * Registers a metric to calculate for the Server-Timing header.
  *
  * This method must be called before the {@see 'perflab_server_timing_send_header'} hook.
@@ -57,8 +69,8 @@
  *
  * @since 1.8.0
  *
- * @param string $metric_slug The metric slug.
- * @param array  $args        {
+ * @param string                                                $metric_slug The metric slug.
+ * @param array{measure_callback: callable, access_cap: string} $args        {
  *     Arguments for the metric.
  *
  *     @type callable $measure_callback The callback that initiates calculating the metric value. It will receive
@@ -69,7 +81,7 @@
  *                                      needs to be set to "exist".
  * }
  */
-function perflab_server_timing_register_metric( $metric_slug, array $args ) {
+function perflab_server_timing_register_metric( string $metric_slug, array $args ): void {
 	perflab_server_timing()->register_metric( $metric_slug, $args );
 }
 
@@ -80,7 +92,7 @@
  *
  * @return bool True if an output buffer should be used, false otherwise.
  */
-function perflab_server_timing_use_output_buffer() {
+function perflab_server_timing_use_output_buffer(): bool {
 	return perflab_server_timing()->use_output_buffer();
 }
 
@@ -93,9 +105,9 @@
  * @param string   $metric_slug The metric slug to use within the Server-Timing header.
  * @param string   $access_cap  Capability required to view the metric. If this is a public metric, this needs to be
  *                              set to "exist".
- * @return callable Callback function that will run $callback and measure its execution time once called.
+ * @return Closure Callback function that will run $callback and measure its execution time once called.
  */
-function perflab_wrap_server_timing( $callback, $metric_slug, $access_cap ) {
+function perflab_wrap_server_timing( callable $callback, string $metric_slug, string $access_cap ): Closure {
 	return static function ( ...$callback_args ) use ( $callback, $metric_slug, $access_cap ) {
 		// Gain access to Perflab_Server_Timing_Metric instance.
 		$server_timing_metric = null;
@@ -106,7 +118,7 @@
 			perflab_server_timing_register_metric(
 				$metric_slug,
 				array(
-					'measure_callback' => static function ( $metric ) use ( &$server_timing_metric ) {
+					'measure_callback' => static function ( $metric ) use ( &$server_timing_metric ): void {
 						$server_timing_metric = $metric;
 					},
 					'access_cap'       => $access_cap,
@@ -134,11 +146,26 @@
 }
 
 /**
+ * Gets default value for server timing setting.
+ *
+ * @since 3.1.0
+ *
+ * @return array{benchmarking_actions: string[], benchmarking_filters: string[], output_buffering: bool} Default value.
+ */
+function perflab_get_server_timing_setting_default_value(): array {
+	return array(
+		'benchmarking_actions' => array(),
+		'benchmarking_filters' => array(),
+		'output_buffering'     => false,
+	);
+}
+
+/**
  * Registers the Server-Timing setting.
  *
  * @since 2.6.0
  */
-function perflab_register_server_timing_setting() {
+function perflab_register_server_timing_setting(): void {
 	register_setting(
 		PERFLAB_SERVER_TIMING_SCREEN,
 		PERFLAB_SERVER_TIMING_SETTING,
@@ -145,7 +172,7 @@
 		array(
 			'type'              => 'object',
 			'sanitize_callback' => 'perflab_sanitize_server_timing_setting',
-			'default'           => array(),
+			'default'           => perflab_get_server_timing_setting_default_value(),
 		)
 	);
 }
@@ -156,22 +183,18 @@
  *
  * @since 2.6.0
  *
- * @param mixed $value Server-Timing setting value.
- * @return array Sanitized Server-Timing setting value.
+ * @param array|mixed $value Server-Timing setting value.
+ * @return array{benchmarking_actions: string[], benchmarking_filters: string[], output_buffering: bool} Sanitized Server-Timing setting value.
  */
-function perflab_sanitize_server_timing_setting( $value ) {
-	static $allowed_keys = array(
-		'benchmarking_actions' => true,
-		'benchmarking_filters' => true,
-		'output_buffering'     => true,
-	);
-
+function perflab_sanitize_server_timing_setting( $value ): array {
 	if ( ! is_array( $value ) ) {
-		return array();
+		$value = array();
 	}
+	$value = wp_array_slice_assoc(
+		array_merge( perflab_get_server_timing_setting_default_value(), $value ),
+		array_keys( perflab_get_server_timing_setting_default_value() )
+	);
 
-	$value = array_intersect_key( $value, $allowed_keys );
-
 	/*
 	 * Ensure that every element is an indexed array of hook names.
 	 * Any duplicates across a group of hooks are removed.
@@ -184,7 +207,7 @@
 			array_unique(
 				array_filter(
 					array_map(
-						static function ( $hookname ) {
+						static function ( string $hook_name ): string {
 							/*
 							 * Allow any characters except whitespace.
 							 * While most hooks use a limited set of characters, hook names in plugins are not
@@ -191,10 +214,10 @@
 							 * restricted to them, therefore the sanitization does not limit the characters
 							 * used.
 							 */
-							return preg_replace(
+							return (string) preg_replace(
 								'/\s/',
 								'',
-								sanitize_text_field( $hookname )
+								sanitize_text_field( $hook_name )
 							);
 						},
 						$hooks
@@ -204,7 +227,12 @@
 		);
 	}
 
-	$value['output_buffering'] = ! empty( $value['output_buffering'] );
+	$value['output_buffering'] = (bool) $value['output_buffering'];
 
+	/**
+	 * Validated value.
+	 *
+	 * @var array{benchmarking_actions: string[], benchmarking_filters: string[], output_buffering: bool} $value
+	 */
 	return $value;
 }
Index: includes/server-timing/object-cache.copy.php
===================================================================
--- includes/server-timing/object-cache.copy.php	(revision 3088546)
+++ includes/server-timing/object-cache.copy.php	(working copy)
@@ -47,7 +47,7 @@
 	 *
 	 * @since 1.8.0
 	 */
-	function perflab_load_server_timing_api_from_dropin() {
+	function perflab_load_server_timing_api_from_dropin(): void {
 		if ( defined( 'PERFLAB_DISABLE_SERVER_TIMING' ) && PERFLAB_DISABLE_SERVER_TIMING ) {
 			return;
 		}
Index: includes/site-health/audit-autoloaded-options/helper.php
===================================================================
--- includes/site-health/audit-autoloaded-options/helper.php	(revision 3088546)
+++ includes/site-health/audit-autoloaded-options/helper.php	(working copy)
@@ -15,9 +15,9 @@
  *
  * @since 1.0.0
  *
- * @return array
+ * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
  */
-function perflab_aao_autoloaded_options_test() {
+function perflab_aao_autoloaded_options_test(): array {
 
 	$autoloaded_options_size  = perflab_aao_autoloaded_options_size();
 	$autoloaded_options_count = count( wp_load_alloptions() );
@@ -73,7 +73,7 @@
 	$result['description'] = apply_filters( 'perflab_aao_autoloaded_options_limit_description', $result['description'] );
 
 	$result['actions'] = sprintf(
-	/* translators: 1: HelpHub URL. 2: Link description. */
+		/* translators: 1: HelpHub URL. 2: Link description. */
 		'<p><a target="_blank" href="%1$s">%2$s</a></p>',
 		esc_url( __( 'https://wordpress.org/support/article/optimization/#autoloaded-options', 'performance-lab' ) ),
 		__( 'More info about performance optimization', 'performance-lab' )
@@ -95,24 +95,18 @@
  *
  * @since 1.0.0
  *
- * @global wpdb $wpdb WordPress database abstraction object.
- *
  * @return int autoloaded data in bytes.
  */
-function perflab_aao_autoloaded_options_size() {
-	global $wpdb;
+function perflab_aao_autoloaded_options_size(): int {
+	$all_options = wp_load_alloptions();
 
-	$autoload_values = perflab_aao_get_autoload_values_to_autoload();
+	$total_length = 0;
 
-	return (int) $wpdb->get_var(
-		$wpdb->prepare(
-			sprintf(
-				"SELECT SUM(LENGTH(option_value)) FROM $wpdb->options WHERE autoload IN (%s)",
-				implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) )
-			),
-			$autoload_values
-		)
-	);
+	foreach ( $all_options as $option_name => $option_value ) {
+		$total_length += strlen( $option_value );
+	}
+
+	return $total_length;
 }
 
 /**
@@ -120,12 +114,9 @@
  *
  * @since 1.5.0
  *
- * @global wpdb $wpdb WordPress database abstraction object.
- *
- * @return array Autoloaded data as option names and their sizes.
+ * @return array<object{option_name: string, option_value_length: int}> Autoloaded data as option names and their sizes.
  */
-function perflab_aao_query_autoloaded_options() {
-	global $wpdb;
+function perflab_aao_query_autoloaded_options(): array {
 
 	/**
 	 * Filters the threshold for an autoloaded option to be considered large.
@@ -141,17 +132,27 @@
 	 */
 	$option_threshold = apply_filters( 'perflab_aao_autoloaded_options_table_threshold', 100 );
 
-	$autoload_values = perflab_aao_get_autoload_values_to_autoload();
+	$all_options = wp_load_alloptions();
 
-	return $wpdb->get_results(
-		$wpdb->prepare(
-			sprintf(
-				"SELECT option_name, LENGTH(option_value) AS option_value_length FROM {$wpdb->options} WHERE autoload IN (%s)",
-				implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) )
-			) . ' AND LENGTH(option_value) > %d ORDER BY option_value_length DESC LIMIT 20',
-			array_merge( $autoload_values, array( $option_threshold ) )
-		)
+	$large_options = array();
+
+	foreach ( $all_options as $option_name => $option_value ) {
+		if ( strlen( $option_value ) > $option_threshold ) {
+			$large_options[] = (object) array(
+				'option_name'         => $option_name,
+				'option_value_length' => strlen( $option_value ),
+			);
+		}
+	}
+
+	usort(
+		$large_options,
+		static function ( $a, $b ) {
+			return $b->option_value_length - $a->option_value_length;
+		}
 	);
+
+	return array_slice( $large_options, 0, 20 );
 }
 
 /**
@@ -161,7 +162,7 @@
  *
  * @return string HTML formatted table.
  */
-function perflab_aao_get_autoloaded_options_table() {
+function perflab_aao_get_autoloaded_options_table(): string {
 	$autoload_summary = perflab_aao_query_autoloaded_options();
 
 	$html_table = sprintf(
@@ -197,29 +198,36 @@
  *
  * @since 3.0.0
  *
- * @global wpdb $wpdb WordPress database abstraction object.
- *
  * @return string HTML formatted table.
  */
-function perflab_aao_get_disabled_autoloaded_options_table() {
-	global $wpdb;
-
+function perflab_aao_get_disabled_autoloaded_options_table(): string {
 	$disabled_options = get_option( 'perflab_aao_disabled_options', array() );
 
-	if ( empty( $disabled_options ) ) {
+	if ( ! is_array( $disabled_options ) ) {
 		return '';
 	}
 
-	$disabled_options_summary = $wpdb->get_results(
-		$wpdb->prepare(
-			sprintf(
-				"SELECT option_name, LENGTH(option_value) AS option_value_length FROM $wpdb->options WHERE option_name IN (%s) ORDER BY option_value_length DESC",
-				implode( ',', array_fill( 0, count( $disabled_options ), '%s' ) )
-			),
-			$disabled_options
-		)
-	);
+	$disabled_options_summary = array();
+	wp_prime_option_caches( $disabled_options );
 
+	foreach ( $disabled_options as $option_name ) {
+		if ( ! is_string( $option_name ) ) {
+			continue;
+		}
+		$option_value = get_option( $option_name );
+
+		if ( false !== $option_value ) {
+			$option_length                            = strlen( maybe_serialize( $option_value ) );
+			$disabled_options_summary[ $option_name ] = $option_length;
+		}
+	}
+
+	if ( count( $disabled_options_summary ) === 0 ) {
+		return '';
+	}
+
+	arsort( $disabled_options_summary );
+
 	$html_table = sprintf(
 		'<p>%s</p><table class="widefat striped"><thead><tr><th scope="col">%s</th><th scope="col">%s</th><th scope="col">%s</th></tr></thead><tbody>',
 		__( 'The following table shows the options for which you have previously disabled Autoload.', 'performance-lab' ),
@@ -229,13 +237,14 @@
 	);
 
 	$nonce = wp_create_nonce( 'perflab_aao_update_autoload' );
-	foreach ( $disabled_options_summary as $value ) {
+
+	foreach ( $disabled_options_summary as $option_name => $option_length ) {
 		$url            = esc_url_raw(
 			add_query_arg(
 				array(
 					'action'      => 'perflab_aao_update_autoload',
 					'_wpnonce'    => $nonce,
-					'option_name' => $value->option_name,
+					'option_name' => $option_name,
 					'autoload'    => 'true',
 				),
 				admin_url( 'site-health.php' )
@@ -242,8 +251,9 @@
 			)
 		);
 		$disable_button = sprintf( '<a class="button" href="%s">%s</a>', esc_url( $url ), esc_html__( 'Revert to Autoload', 'performance-lab' ) );
-		$html_table    .= sprintf( '<tr><td>%s</td><td>%s</td><td>%s</td></tr>', esc_html( $value->option_name ), size_format( $value->option_value_length, 2 ), $disable_button );
+		$html_table    .= sprintf( '<tr><td>%s</td><td>%s</td><td>%s</td></tr>', esc_html( $option_name ), size_format( $option_length, 2 ), $disable_button );
 	}
+
 	$html_table .= '</tbody></table>';
 
 	return $html_table;
@@ -254,9 +264,9 @@
  *
  * @since 3.0.0
  *
- * @return array List of autoload values.
+ * @return string[] List of autoload values.
  */
-function perflab_aao_get_autoload_values_to_autoload() {
+function perflab_aao_get_autoload_values_to_autoload(): array {
 	if ( function_exists( 'wp_autoload_values_to_autoload' ) ) {
 		return wp_autoload_values_to_autoload();
 	}
Index: includes/site-health/audit-autoloaded-options/hooks.php
===================================================================
--- includes/site-health/audit-autoloaded-options/hooks.php	(revision 3088546)
+++ includes/site-health/audit-autoloaded-options/hooks.php	(working copy)
@@ -15,10 +15,10 @@
  *
  * @since 1.0.0
  *
- * @param array $tests Site Health Tests.
- * @return array
+ * @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
+ * @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
  */
-function perflab_aao_add_autoloaded_options_test( $tests ) {
+function perflab_aao_add_autoloaded_options_test( array $tests ): array {
 	$tests['direct']['autoloaded_options'] = array(
 		'label' => __( 'Autoloaded options', 'performance-lab' ),
 		'test'  => 'perflab_aao_autoloaded_options_test',
@@ -32,7 +32,7 @@
  *
  * @since 3.0.0
  */
-function perflab_aao_register_admin_actions() {
+function perflab_aao_register_admin_actions(): void {
 	add_action( 'admin_action_perflab_aao_update_autoload', 'perflab_aao_handle_update_autoload' );
 }
 add_action( 'admin_init', 'perflab_aao_register_admin_actions' );
@@ -42,7 +42,7 @@
  *
  * @since 3.0.0
  */
-function perflab_aao_handle_update_autoload() {
+function perflab_aao_handle_update_autoload(): void {
 	check_admin_referer( 'perflab_aao_update_autoload' );
 
 	if ( ! isset( $_GET['option_name'], $_GET['autoload'] ) ) {
@@ -50,7 +50,7 @@
 	}
 
 	$option_name = sanitize_text_field( wp_unslash( $_GET['option_name'] ) );
-	$autoload    = isset( $_GET['autoload'] ) ? rest_sanitize_boolean( $_GET['autoload'] ) : false;
+	$autoload    = rest_sanitize_boolean( $_GET['autoload'] );
 
 	if ( ! current_user_can( 'manage_options' ) ) {
 		wp_die( esc_html__( 'Permission denied.', 'performance-lab' ) );
@@ -96,7 +96,7 @@
  *
  * @global string $pagenow The filename of the current screen.
  */
-function perflab_aao_admin_notices() {
+function perflab_aao_admin_notices(): void {
 	if ( 'site-health.php' !== $GLOBALS['pagenow'] ) {
 		return;
 	}
Index: includes/site-health/audit-enqueued-assets/helper.php
===================================================================
--- includes/site-health/audit-enqueued-assets/helper.php	(revision 3088546)
+++ includes/site-health/audit-enqueued-assets/helper.php	(working copy)
@@ -15,15 +15,19 @@
  *
  * @since 1.0.0
  *
- * @return array
+ * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string}|array{omitted: true} Result.
  */
-function perflab_aea_enqueued_js_assets_test() {
+function perflab_aea_enqueued_js_assets_test(): array {
 	/**
 	 * If the test didn't run yet, deactivate.
 	 */
 	$enqueued_scripts = perflab_aea_get_total_enqueued_scripts();
-	if ( false === $enqueued_scripts ) {
-		return array();
+	$bytes_enqueued   = perflab_aea_get_total_size_bytes_enqueued_scripts();
+	if ( false === $enqueued_scripts || false === $bytes_enqueued ) {
+		// The return value is validated in JavaScript at:
+		// <https://github.com/WordPress/wordpress-develop/blob/d1e0a6241dcc34f4a5ed464a741116461a88d43b/src/js/_enqueues/admin/site-health.js#L65-L114>
+		// If the value lacks the required keys of test, label, and description then it is omitted.
+		return array( 'omitted' => true );
 	}
 
 	$result = array(
@@ -45,7 +49,7 @@
 						'performance-lab'
 					),
 					$enqueued_scripts,
-					size_format( perflab_aea_get_total_size_bytes_enqueued_scripts() )
+					size_format( $bytes_enqueued )
 				)
 			)
 		),
@@ -71,7 +75,7 @@
 	 */
 	$scripts_size_threshold = apply_filters( 'perflab_aea_enqueued_scripts_byte_size_threshold', 300000 );
 
-	if ( $enqueued_scripts > $scripts_threshold || perflab_aea_get_total_size_bytes_enqueued_scripts() > $scripts_size_threshold ) {
+	if ( $enqueued_scripts > $scripts_threshold || $bytes_enqueued > $scripts_size_threshold ) {
 		$result['status'] = 'recommended';
 
 		$result['description'] = sprintf(
@@ -86,7 +90,7 @@
 						'performance-lab'
 					),
 					$enqueued_scripts,
-					size_format( perflab_aea_get_total_size_bytes_enqueued_scripts() )
+					size_format( $bytes_enqueued )
 				)
 			)
 		);
@@ -109,15 +113,17 @@
  *
  * @since 1.0.0
  *
- * @return array
+ * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string}|array{omitted: true} Result.
  */
-function perflab_aea_enqueued_css_assets_test() {
-	/**
-	 * If the test didn't run yet, deactivate.
-	 */
+function perflab_aea_enqueued_css_assets_test(): array {
+	// Omit if the test didn't run yet, omit.
 	$enqueued_styles = perflab_aea_get_total_enqueued_styles();
-	if ( false === $enqueued_styles ) {
-		return array();
+	$bytes_enqueued  = perflab_aea_get_total_size_bytes_enqueued_styles();
+	if ( false === $enqueued_styles || false === $bytes_enqueued ) {
+		// The return value is validated in JavaScript at:
+		// <https://github.com/WordPress/wordpress-develop/blob/d1e0a6241dcc34f4a5ed464a741116461a88d43b/src/js/_enqueues/admin/site-health.js#L65-L114>
+		// If the value lacks the required keys of test, label, and description then it is omitted.
+		return array( 'omitted' => true );
 	}
 	$result = array(
 		'label'       => __( 'Enqueued styles', 'performance-lab' ),
@@ -138,7 +144,7 @@
 						'performance-lab'
 					),
 					$enqueued_styles,
-					size_format( perflab_aea_get_total_size_bytes_enqueued_styles() )
+					size_format( $bytes_enqueued )
 				)
 			)
 		),
@@ -178,7 +184,7 @@
 						'performance-lab'
 					),
 					$enqueued_styles,
-					size_format( perflab_aea_get_total_size_bytes_enqueued_styles() )
+					size_format( $bytes_enqueued )
 				)
 			)
 		);
@@ -206,7 +212,7 @@
 function perflab_aea_get_total_enqueued_scripts() {
 	$enqueued_scripts      = false;
 	$list_enqueued_scripts = get_transient( 'aea_enqueued_front_page_scripts' );
-	if ( $list_enqueued_scripts ) {
+	if ( is_array( $list_enqueued_scripts ) ) {
 		$enqueued_scripts = count( $list_enqueued_scripts );
 	}
 	return $enqueued_scripts;
@@ -222,10 +228,12 @@
 function perflab_aea_get_total_size_bytes_enqueued_scripts() {
 	$total_size            = false;
 	$list_enqueued_scripts = get_transient( 'aea_enqueued_front_page_scripts' );
-	if ( $list_enqueued_scripts ) {
+	if ( is_array( $list_enqueued_scripts ) ) {
 		$total_size = 0;
 		foreach ( $list_enqueued_scripts as $enqueued_script ) {
-			$total_size += $enqueued_script['size'];
+			if ( is_array( $enqueued_script ) && array_key_exists( 'size', $enqueued_script ) && is_int( $enqueued_script['size'] ) ) {
+				$total_size += $enqueued_script['size'];
+			}
 		}
 	}
 	return $total_size;
@@ -257,10 +265,12 @@
 function perflab_aea_get_total_size_bytes_enqueued_styles() {
 	$total_size           = false;
 	$list_enqueued_styles = get_transient( 'aea_enqueued_front_page_styles' );
-	if ( $list_enqueued_styles ) {
+	if ( is_array( $list_enqueued_styles ) ) {
 		$total_size = 0;
 		foreach ( $list_enqueued_styles as $enqueued_style ) {
-			$total_size += $enqueued_style['size'];
+			if ( is_array( $enqueued_style ) && array_key_exists( 'size', $enqueued_style ) && is_int( $enqueued_style['size'] ) ) {
+				$total_size += $enqueued_style['size'];
+			}
 		}
 	}
 	return $total_size;
@@ -276,7 +286,7 @@
  * @param string $resource_url URl resource link.
  * @return string Returns absolute path to the resource.
  */
-function perflab_aea_get_path_from_resource_url( $resource_url ) {
+function perflab_aea_get_path_from_resource_url( string $resource_url ): string {
 	if ( ! $resource_url ) {
 		return '';
 	}
Index: includes/site-health/audit-enqueued-assets/hooks.php
===================================================================
--- includes/site-health/audit-enqueued-assets/hooks.php	(revision 3088546)
+++ includes/site-health/audit-enqueued-assets/hooks.php	(working copy)
@@ -19,7 +19,7 @@
  *
  * @global WP_Scripts $wp_scripts
  */
-function perflab_aea_audit_enqueued_scripts() {
+function perflab_aea_audit_enqueued_scripts(): void {
 	if ( ! is_admin() && is_front_page() && current_user_can( 'view_site_health_checks' ) && false === get_transient( 'aea_enqueued_front_page_scripts' ) ) {
 		global $wp_scripts;
 		$enqueued_scripts = array();
@@ -64,7 +64,7 @@
  *
  * @global WP_Styles $wp_styles The WP_Styles current instance.
  */
-function perflab_aea_audit_enqueued_styles() {
+function perflab_aea_audit_enqueued_styles(): void {
 	if ( ! is_admin() && is_front_page() && current_user_can( 'view_site_health_checks' ) && false === get_transient( 'aea_enqueued_front_page_styles' ) ) {
 		global $wp_styles;
 		$enqueued_styles = array();
@@ -108,10 +108,10 @@
  *
  * @since 1.0.0
  *
- * @param array $tests Site Health Tests.
- * @return array
+ * @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
+ * @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
  */
-function perflab_aea_add_enqueued_assets_test( $tests ) {
+function perflab_aea_add_enqueued_assets_test( array $tests ): array {
 	$tests['direct']['enqueued_js_assets']  = array(
 		'label' => __( 'JS assets', 'performance-lab' ),
 		'test'  => 'perflab_aea_enqueued_js_assets_test',
@@ -131,7 +131,7 @@
  *
  * @since 1.0.0
  */
-function perflab_aea_clean_aea_audit_action() {
+function perflab_aea_clean_aea_audit_action(): void {
 	if ( isset( $_GET['action'] ) && 'clean_aea_audit' === $_GET['action'] && current_user_can( 'view_site_health_checks' ) ) {
 		check_admin_referer( 'clean_aea_audit' );
 		perflab_aea_invalidate_cache_transients();
@@ -145,7 +145,7 @@
  *
  * @since 1.0.0
  */
-function perflab_aea_invalidate_cache_transients() {
+function perflab_aea_invalidate_cache_transients(): void {
 	delete_transient( 'aea_enqueued_front_page_scripts' );
 	delete_transient( 'aea_enqueued_front_page_styles' );
 }
Index: includes/site-health/load.php
===================================================================
--- includes/site-health/load.php	(revision 3088546)
+++ includes/site-health/load.php	(working copy)
@@ -21,3 +21,7 @@
 // WebP Support site health check.
 require_once __DIR__ . '/webp-support/helper.php';
 require_once __DIR__ . '/webp-support/hooks.php';
+
+// AVIF Support site health check.
+require_once __DIR__ . '/avif-support/helper.php';
+require_once __DIR__ . '/avif-support/hooks.php';
Index: includes/site-health/webp-support/helper.php
===================================================================
--- includes/site-health/webp-support/helper.php	(revision 3088546)
+++ includes/site-health/webp-support/helper.php	(working copy)
@@ -15,9 +15,9 @@
  *
  * @since 1.0.0
  *
- * @return array
+ * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
  */
-function webp_uploads_check_webp_supported_test() {
+function webp_uploads_check_webp_supported_test(): array {
 	$result = array(
 		'label'       => __( 'Your site supports WebP', 'performance-lab' ),
 		'status'      => 'good',
Index: includes/site-health/webp-support/hooks.php
===================================================================
--- includes/site-health/webp-support/hooks.php	(revision 3088546)
+++ includes/site-health/webp-support/hooks.php	(working copy)
@@ -15,10 +15,10 @@
  *
  * @since 1.0.0
  *
- * @param array $tests Site Health Tests.
- * @return array
+ * @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
+ * @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
  */
-function webp_uploads_add_is_webp_supported_test( $tests ) {
+function webp_uploads_add_is_webp_supported_test( array $tests ): array {
 	$tests['direct']['webp_supported'] = array(
 		'label' => __( 'WebP Support', 'performance-lab' ),
 		'test'  => 'webp_uploads_check_webp_supported_test',
Index: load.php
===================================================================
--- load.php	(revision 3088546)
+++ load.php	(working copy)
@@ -4,8 +4,8 @@
  * Plugin URI: https://github.com/WordPress/performance
  * Description: Performance plugin from the WordPress Performance Team, which is a collection of standalone performance features.
  * Requires at least: 6.4
- * Requires PHP: 7.0
- * Version: 3.0.0
+ * Requires PHP: 7.2
+ * Version: 3.1.0
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -19,7 +19,7 @@
 	exit; // Exit if accessed directly.
 }
 
-define( 'PERFLAB_VERSION', '3.0.0' );
+define( 'PERFLAB_VERSION', '3.1.0' );
 define( 'PERFLAB_MAIN_FILE', __FILE__ );
 define( 'PERFLAB_PLUGIN_DIR_PATH', plugin_dir_path( PERFLAB_MAIN_FILE ) );
 define( 'PERFLAB_SCREEN', 'performance-lab' );
@@ -48,7 +48,7 @@
  * @since 2.9.0 The generator tag now includes the active standalone plugin slugs.
  * @since 3.0.0 The generator tag no longer includes module slugs.
  */
-function perflab_get_generator_content() {
+function perflab_get_generator_content(): string {
 	$active_plugins = array();
 	foreach ( perflab_get_standalone_plugin_version_constants() as $plugin_slug => $constant_name ) {
 		if ( defined( $constant_name ) && ! str_starts_with( constant( $constant_name ), 'Performance Lab ' ) ) {
@@ -71,7 +71,7 @@
  *
  * @since 1.1.0
  */
-function perflab_render_generator() {
+function perflab_render_generator(): void {
 	$content = perflab_get_generator_content();
 
 	echo '<meta name="generator" content="' . esc_attr( $content ) . '">' . "\n";
@@ -85,7 +85,7 @@
  *
  * @return array<string, array{'constant': string, 'experimental'?: bool}> Associative array of $plugin_slug => $plugin_data pairs.
  */
-function perflab_get_standalone_plugin_data() {
+function perflab_get_standalone_plugin_data(): array {
 	/*
 	 * Alphabetically sorted list of plugin slugs and their data.
 	 * Supported keys per plugin are:
@@ -125,7 +125,7 @@
  *
  * @return array<string, string> Map of plugin slug and the version constant used.
  */
-function perflab_get_standalone_plugin_version_constants() {
+function perflab_get_standalone_plugin_version_constants(): array {
 	return wp_list_pluck( perflab_get_standalone_plugin_data(), 'constant' );
 }
 
@@ -144,7 +144,7 @@
  *
  * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
  */
-function perflab_maybe_set_object_cache_dropin() {
+function perflab_maybe_set_object_cache_dropin(): void {
 	global $wp_filesystem;
 
 	// Bail if Server-Timing is disabled entirely.
@@ -255,7 +255,7 @@
  *
  * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
  */
-function perflab_maybe_remove_object_cache_dropin() {
+function perflab_maybe_remove_object_cache_dropin(): void {
 	global $wp_filesystem;
 
 	// Bail if disabled via constant.
@@ -298,7 +298,7 @@
  *
  * @global $plugin_page
  */
-function perflab_no_access_redirect_module_to_performance_feature_page() {
+function perflab_no_access_redirect_module_to_performance_feature_page(): void {
 	global $plugin_page;
 
 	if ( 'perflab-modules' !== $plugin_page ) {
@@ -319,7 +319,7 @@
  *
  * @since 3.0.0
  */
-function perflab_cleanup_option() {
+function perflab_cleanup_option(): void {
 	if ( current_user_can( 'manage_options' ) ) {
 		delete_option( 'perflab_modules_settings' );
 	}
Index: readme.txt
===================================================================
--- readme.txt	(revision 3088546)
+++ readme.txt	(working copy)
@@ -3,8 +3,8 @@
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
-Requires PHP:      7.0
-Stable tag:        3.0.0
+Requires PHP:      7.2
+Stable tag:        3.1.0
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 Tags:              performance, site health, measurement, optimization, diagnostics
@@ -60,6 +60,25 @@
 
 == Changelog ==
 
+= 3.1.0 =
+
+**Enhancements**
+
+* Add progress indicator when activating a feature. ([1190](https://github.com/WordPress/performance/pull/1190))
+* Display plugin settings links in the features screen and fix responsive layout for mobile. ([1208](https://github.com/WordPress/performance/pull/1208))
+* Add plugin dependency support for activating performance features. ([1184](https://github.com/WordPress/performance/pull/1184))
+* Add support for AVIF image format in site health. ([1177](https://github.com/WordPress/performance/pull/1177))
+* Add server timing to REST API response. ([1206](https://github.com/WordPress/performance/pull/1206))
+* Bump minimum PHP requirement to 7.2. ([1130](https://github.com/WordPress/performance/pull/1130))
+* Refine logic in perflab_install_activate_plugin_callback() to rely only on validated slug. ([1170](https://github.com/WordPress/performance/pull/1170))
+* Improve overall code quality with stricter static analysis checks. ([775](https://github.com/WordPress/performance/issues/775))
+
+**Bug Fixes**
+
+* Avoid passing incomplete data to perflab_render_plugin_card() and show error when plugin directory API query fails. ([1175](https://github.com/WordPress/performance/pull/1175))
+* Do not show admin pointer on the Performance screen and dismiss the pointer when visited. ([1147](https://github.com/WordPress/performance/pull/1147))
+* Fix `WordPress.DB.DirectDatabaseQuery.DirectQuery` warning for Autoloaded Options Health Check. ([1179](https://github.com/WordPress/performance/pull/1179))
+
 = 3.0.0 =
 
 **Enhancements**

Copy link

github-actions bot commented May 18, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: westonruter <westonruter@git.wordpress.org>
Co-authored-by: mukeshpanchal27 <mukesh27@git.wordpress.org>
Co-authored-by: joemcgill <joemcgill@git.wordpress.org>
Co-authored-by: sstopfer <stellastopfer@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Copy link
Member

@mukeshpanchal27 mukeshpanchal27 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog for PHP version bump is missing on all plugins.

plugins/dominant-color-images/readme.txt Show resolved Hide resolved
plugins/embed-optimizer/readme.txt Show resolved Hide resolved
plugins/optimization-detective/readme.txt Show resolved Hide resolved
plugins/speculation-rules/readme.txt Show resolved Hide resolved
plugins/webp-uploads/readme.txt Show resolved Hide resolved
westonruter and others added 2 commits May 20, 2024 07:29
Co-authored-by: Mukesh Panchal <mukeshpanchal27@users.noreply.github.com>
@westonruter westonruter changed the base branch from trunk to release/3.1.0 May 20, 2024 16:16
@westonruter
Copy link
Member Author

✅ Smoke testing of Performance Lab successful:

  • Admin pointer successfully auto-dismissed when landing on the Performance screen.
  • I see the AVIF Site Health test: image
  • I enabled Server-Timing and added the rest_pre_dispatch filter to be timed, and when going to /wp-json/ I see it show up:
    image
  • Responsive layout of Settings screen is working, and Settings links appear:
Screen.recording.2024-05-20.09.37.14.webm
  • Loading spinner appears when activating a feature:
Screen.recording.2024-05-20.09.39.02.webm
  • The settings link appears in the activated admin notice:
Screen.recording.2024-05-20.09.40.49.webm

Copy link
Member

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

@westonruter
Copy link
Member Author

westonruter commented May 20, 2024

✅ Smoke testing other plugins:

  • Auto Sizes continues to include auto in the sizes attribute when lazy-loading.
  • Image Placeholders is generating the dominant color and partially-transparent images are getting data-has-transparent=true.
  • Embed Optimizer: Post Embeds are lazy-loaded and are initially-hidden with visibility:hidden.
  • Optimization Detective: Detection script is being injected, XPath attributes are being added, and od_url_metrics posts are being added. Adding ?optimization_detective_disabled to a URL now disables the functionality. XPath indices are starting at 1 instead of 0.
  • Speculative Loading: Linked images in uploads directory are now successfully omitted from being speculatively loaded.
  • Modern Image Formats: Uploaded images are automatically converted to WebP.

@westonruter westonruter merged commit be0727f into release/3.1.0 May 20, 2024
25 checks passed
@westonruter westonruter deleted the publish/3.1.0 branch May 20, 2024 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
skip changelog PRs that should not be mentioned in changelogs [Type] Documentation Documentation to be added or enhanced
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Prepare 3.1.0 release
3 participants