PluginProbe
Optimization Detective / trunk
Optimization Detective vtrunk
1.0.0-beta7 trunk 0.1.0 0.1.1 0.2.0 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0 0.9.0 1.0.0-beta1 1.0.0-beta2 1.0.0-beta3 1.0.0-beta4 1.0.0-beta5 1.0.0-beta6
optimization-detective / helper.php

helper.php in Optimization Detective trunk, at helper.php

416 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Helper functions for Optimization Detective.
4 *
5 * @package optimization-detective
6 * @since 0.1.0
7 */
8
9 declare( strict_types = 1 );
10
11 // @codeCoverageIgnoreStart
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15 // @codeCoverageIgnoreEnd
16
17 /**
18 * Initializes extensions for Optimization Detective.
19 *
20 * @since 0.7.0
21 * @access private
22 */
23 function od_initialize_extensions(): void {
24 /**
25 * Fires when extensions to Optimization Detective can be loaded and initialized.
26 *
27 * @since 0.7.0
28 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Action%3A%20od_init
29 *
30 * @param string $version Optimization Detective version.
31 */
32 do_action( 'od_init', OPTIMIZATION_DETECTIVE_VERSION );
33 }
34
35 /**
36 * Generates a media query for the provided minimum and maximum viewport widths.
37 *
38 * This helper function is available for extensions to leverage when manually printing STYLE rules via
39 * {@see OD_HTML_Tag_Processor::append_head_html()} or {@see OD_HTML_Tag_Processor::append_body_html()}
40 *
41 * @since 0.7.0
42 *
43 * @param int<0, max>|null $minimum_viewport_width Minimum viewport width (exclusive).
44 * @param int<1, max>|null $maximum_viewport_width Maximum viewport width (inclusive).
45 * @return non-empty-string|null Media query, or null if the min/max were both unspecified or invalid.
46 */
47 function od_generate_media_query( ?int $minimum_viewport_width, ?int $maximum_viewport_width ): ?string {
48 if ( is_int( $minimum_viewport_width ) && is_int( $maximum_viewport_width ) && $minimum_viewport_width >= $maximum_viewport_width ) {
49 _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width cannot be greater than or equal to the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' );
50 return null;
51 }
52 $has_min_width = ( null !== $minimum_viewport_width && $minimum_viewport_width > 0 );
53 $has_max_width = ( null !== $maximum_viewport_width && PHP_INT_MAX !== $maximum_viewport_width ); // Note: The use of PHP_INT_MAX is obsolete.
54 if ( $has_min_width && $has_max_width ) {
55 return sprintf( '(%dpx < width <= %dpx)', $minimum_viewport_width, $maximum_viewport_width );
56 } elseif ( $has_min_width ) {
57 return sprintf( '(%dpx < width)', $minimum_viewport_width );
58 } elseif ( $has_max_width ) {
59 return sprintf( '(width <= %dpx)', $maximum_viewport_width );
60 } else {
61 return null;
62 }
63 }
64
65 /**
66 * Gets the reasons why Optimization Detective is disabled for the current response.
67 *
68 * @since 1.0.0
69 * @access private
70 *
71 * @return array{
72 * is_search?: string,
73 * is_embed?: string,
74 * is_preview?: string,
75 * is_customize_preview?: string,
76 * non_get_request?: string,
77 * no_cache_purge_post_id?: string,
78 * filter_disabled?: string,
79 * rest_api_unavailable?: string,
80 * query_param_disabled?: string
81 * } Array of disabled reason codes and their messages.
82 */
83 function od_get_disabled_reasons(): array {
84 $disabled_flags = array(
85 'is_search' => false,
86 'is_embed' => false,
87 'is_preview' => false,
88 'is_customize_preview' => false,
89 'non_get_request' => false,
90 'no_cache_purge_post_id' => false,
91 );
92
93 // Disable the search template since there is no predictability in whether posts in the loop will have featured images assigned or not. If a
94 // theme template for search results doesn't even show featured images, then this wouldn't be an issue.
95 if ( is_search() ) {
96 $disabled_flags['is_search'] = true;
97 }
98
99 // Avoid optimizing embed responses because the Post Embed iframes include a sandbox attribute with the value of
100 // "allow-scripts" but without "allow-same-origin". This can result in an error in the console:
101 // > Access to script at '.../detect.js?ver=0.4.1' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
102 // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway).
103 if ( is_embed() ) {
104 $disabled_flags['is_embed'] = true;
105 }
106
107 // Skip posts that aren't published yet.
108 if ( is_preview() ) {
109 $disabled_flags['is_preview'] = true;
110 }
111
112 // Disable in Customizer preview since injection of inline-editing controls can interfere with XPath. Optimization is also not necessary in this context.
113 if ( is_customize_preview() ) {
114 $disabled_flags['is_customize_preview'] = true;
115 }
116
117 // Disable for POST responses since they cannot, by definition, be cached.
118 if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
119 $disabled_flags['non_get_request'] = true;
120 }
121
122 // Disable when there is no post ID available for cache purging. Page caching plugins can only reliably be told to invalidate a cached page when a post is available to trigger
123 // the relevant actions on.
124 if ( null === od_get_cache_purge_post_id() ) {
125 $disabled_flags['no_cache_purge_post_id'] = true;
126 }
127
128 // Check if any flags are set to true.
129 $has_disabled_flags = count( array_filter( $disabled_flags ) ) > 0;
130
131 /**
132 * Filters whether the current response can be optimized.
133 *
134 * @since 0.1.0
135 * @since 1.0.0 Added $disabled_flags parameter
136 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_can_optimize_response
137 *
138 * @param bool $can_optimize Whether response can be optimized.
139 * @param array{
140 * is_search: bool,
141 * is_embed: bool,
142 * is_preview: bool,
143 * is_customize_preview: bool,
144 * non_get_request: bool,
145 * no_cache_purge_post_id: bool
146 * } $disabled_flags Flags indicating which conditions are disabling optimization.
147 */
148 $can_optimize = (bool) apply_filters( 'od_can_optimize_response', ! $has_disabled_flags, $disabled_flags );
149
150 $reasons = array();
151 if ( ! $can_optimize ) {
152 $reason_messages = array(
153 'is_search' => __( 'Page is not optimized because it is a search results page.', 'optimization-detective' ),
154 'is_embed' => __( 'Page is not optimized because it is an embed.', 'optimization-detective' ),
155 'is_preview' => __( 'Page is not optimized because it is a preview.', 'optimization-detective' ),
156 'is_customize_preview' => __( 'Page is not optimized because it is a customize preview.', 'optimization-detective' ),
157 'non_get_request' => __( 'Page is not optimized because it is not a GET request.', 'optimization-detective' ),
158 'no_cache_purge_post_id' => __( 'Page is not optimized because there is no post ID available for cache purging.', 'optimization-detective' ),
159 );
160
161 $reasons = array_intersect_key( $reason_messages, array_filter( $disabled_flags ) );
162
163 // If no technical reasons but optimization still disabled, it's because of the filter.
164 if ( 0 === count( $reasons ) ) {
165 $reasons['filter_disabled'] = __( 'Page is not optimized because the od_can_optimize_response filter returned false.', 'optimization-detective' );
166 }
167 }
168
169 if ( od_is_rest_api_unavailable() && ! ( wp_get_environment_type() === 'local' && ! function_exists( 'tests_add_filter' ) ) ) {
170 $reasons['rest_api_unavailable'] = __( 'Page is not optimized because the REST API for storing URL Metrics is not available.', 'optimization-detective' );
171 }
172
173 if ( isset( $_GET['optimization_detective_disabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
174 $reasons['query_param_disabled'] = __( 'Page is not optimized because the URL has the optimization_detective_disabled query parameter.', 'optimization-detective' );
175 }
176
177 return $reasons;
178 }
179
180 /**
181 * Displays the HTML generator META tag for the Optimization Detective plugin.
182 *
183 * See {@see 'wp_head'}.
184 *
185 * @since 0.1.0
186 * @access private
187 */
188 function od_render_generator_meta_tag(): void {
189 // Use the plugin slug as it is immutable.
190 $content = 'optimization-detective ' . OPTIMIZATION_DETECTIVE_VERSION;
191
192 // Add any reasons why Optimization Detective is disabled.
193 $disabled_reasons = od_get_disabled_reasons();
194 if ( count( $disabled_reasons ) > 0 ) {
195 $flags = array_keys( $disabled_reasons );
196 $content .= '; ' . implode( '; ', $flags );
197 }
198
199 echo '<meta name="generator" content="' . esc_attr( $content ) . '">' . "\n";
200 }
201
202 /**
203 * Adds an Extensions link to the plugin row meta for Optimization Detective.
204 *
205 * This link directs users to the plugin directory to discover extensions that
206 * provide optimization functionality using the Optimization Detective plugin.
207 *
208 * @since 1.0.0
209 * @access private
210 *
211 * @param string[]|mixed $plugin_meta The plugin's metadata.
212 * @param string $plugin_file Plugin file.
213 * @return string[] Updated plugin metadata.
214 */
215 function od_render_extensions_meta_link( $plugin_meta, string $plugin_file ): array {
216 if ( ! is_array( $plugin_meta ) ) {
217 $plugin_meta = array();
218 }
219 if ( 'optimization-detective/load.php' !== $plugin_file || ! current_user_can( 'install_plugins' ) ) {
220 return $plugin_meta;
221 }
222
223 /* @noinspection HtmlUnknownTarget */
224 $extensions_link = sprintf(
225 '<a href="%s">%s</a>',
226 esc_url( admin_url( 'plugin-install.php?s=optimization-detective&tab=search&type=tag' ) ),
227 esc_html__( 'Extensions', 'optimization-detective' )
228 );
229
230 $plugin_meta[] = $extensions_link;
231 return $plugin_meta;
232 }
233
234 /**
235 * Checks for active extension plugins for Optimization Detective.
236 *
237 * @since 1.0.0
238 * @access private
239 *
240 * @return string[] List of active extension plugin files.
241 */
242 function od_get_active_extensions(): array {
243 $installed_plugins = get_plugins();
244 $active_extensions = array();
245
246 foreach ( $installed_plugins as $plugin_slug => $plugin_data ) {
247 if ( isset( $plugin_data['RequiresPlugins'] ) && is_string( $plugin_data['RequiresPlugins'] ) ) {
248 $required_plugins = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) );
249 if ( in_array( 'optimization-detective', $required_plugins, true ) && is_plugin_active( $plugin_slug ) ) {
250 $active_extensions[] = $plugin_slug;
251 }
252 }
253 }
254
255 // Check for plugins without Requires Plugins header but known to be extensions.
256 $suggesting_extensions = array(
257 'embed-optimizer/load.php',
258 );
259 foreach ( $suggesting_extensions as $extension ) {
260 if ( isset( $installed_plugins[ $extension ] ) && is_plugin_active( $extension ) ) {
261 $active_extensions[] = $extension;
262 }
263 }
264
265 return array_values( array_unique( $active_extensions ) );
266 }
267
268 /**
269 * Renders an inline admin notice prompting the user to install or activate extensions for Optimization Detective.
270 *
271 * @since 1.0.0
272 * @access private
273 */
274 function od_maybe_render_installed_extensions_admin_notice(): void {
275 if ( ! current_user_can( 'activate_plugins' ) ) {
276 return;
277 }
278 $active_extensions = od_get_active_extensions();
279 if ( count( $active_extensions ) > 0 ) {
280 return;
281 }
282
283 $message = sprintf(
284 '<summary style="margin: 0.5em 0">%s</summary>',
285 esc_html__( 'Optimization Detective is a framework plugin which requires extensions.', 'optimization-detective' )
286 );
287
288 $message .= '<p>' . esc_html__( 'This plugin doesn&#8217;t provide standalone functionality; it is a framework that requires extension plugins to implement optimizations. Please install and activate one or more of the following extensions:', 'optimization-detective' ) . '</p>';
289
290 $featured_extensions = array(
291 'image-prioritizer' => array(
292 'name' => __( 'Image Prioritizer', 'optimization-detective' ),
293 'description' => __( 'Prioritizes the loading of images and videos based on how visible they are to actual visitors; adds fetchpriority and applies lazy-loading.', 'optimization-detective' ),
294 'url' => admin_url( 'plugin-install.php?tab=plugin-information&plugin=image-prioritizer&TB_iframe=true&width=772' ),
295 ),
296 'embed-optimizer' => array(
297 'name' => __( 'Embed Optimizer', 'optimization-detective' ),
298 'description' => __( 'Optimizes the performance of embeds through lazy-loading, adding dns-prefetch links, and reserving space to reduce layout shifts.', 'optimization-detective' ),
299 'url' => admin_url( 'plugin-install.php?tab=plugin-information&plugin=embed-optimizer&TB_iframe=true&width=772' ),
300 ),
301 );
302
303 $message .= '<table class="widefat" style="margin-bottom: 11px;"><tbody>';
304 foreach ( $featured_extensions as $featured_extension ) {
305 /* @noinspection HtmlUnknownTarget */
306 $message .= sprintf(
307 '<tr>
308 <td><strong>%s</strong></td>
309 <td>%s</td>
310 </tr>',
311 current_user_can( 'install_plugins' ) ?
312 sprintf( '<a href="%s" class="thickbox open-plugin-details-modal">%s</a>', esc_url( $featured_extension['url'] ), esc_html( $featured_extension['name'] ) ) :
313 esc_html( $featured_extension['name'] ),
314 esc_html( $featured_extension['description'] )
315 );
316 }
317 $message .= '</tbody></table>';
318 $message = "<details>$message</details>";
319
320 $notice = wp_get_admin_notice(
321 $message,
322 array(
323 'type' => 'info',
324 'additional_classes' => array( 'inline' ),
325 'paragraph_wrap' => false,
326 )
327 );
328
329 if ( current_user_can( 'install_plugins' ) ) {
330 add_thickbox();
331 }
332 echo wp_kses( $notice, wp_kses_allowed_html( 'post' ) );
333 }
334
335 /**
336 * Renders a paragraph of links to the plugin's documentation on GitHub.
337 *
338 * @since 1.0.0
339 * @access private
340 */
341 function od_render_documentation_links(): void {
342 echo '<p>';
343 /* @noinspection HtmlUnknownTarget */
344 echo wp_kses_post(
345 sprintf(
346 /* translators: 1: project documentation URL, 2: introduction URL, 3: code reference URL, 4: extensions list URL. */
347 __( 'The <a href="%1$s" target="_blank">project documentation</a> is available on GitHub, including an <a href="%2$s" target="_blank">introduction</a>, <a href="%3$s" target="_blank">code reference</a>, and a list of <a href="%4$s" target="_blank">extensions</a>.', 'optimization-detective' ),
348 esc_url( 'https://github.com/WordPress/performance/tree/trunk/plugins/optimization-detective/docs' ),
349 esc_url( 'https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/introduction.md' ),
350 esc_url( 'https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md' ),
351 esc_url( 'https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/extensions.md' )
352 )
353 );
354 echo '</p>';
355 }
356
357 /**
358 * Displays an inline admin notice on the plugin row if no extensions are installed and active.
359 *
360 * @since 1.0.0
361 * @access private
362 *
363 * @param non-empty-string $plugin_file Plugin file.
364 */
365 function od_render_installed_extensions_admin_notice_in_plugin_row( string $plugin_file ): void {
366 if ( 'optimization-detective/load.php' !== $plugin_file ) {
367 return;
368 }
369 od_maybe_render_installed_extensions_admin_notice();
370 od_render_documentation_links();
371 }
372
373 /**
374 * Gets the path to a script or stylesheet.
375 *
376 * @since 0.9.0
377 * @access private
378 *
379 * @param string $src_path Source path, relative to the plugin root.
380 * @param string|null $min_path Minified path. If not supplied, then '.min' is injected before the file extension in the source path.
381 * @return string URL to script or stylesheet.
382 *
383 * @noinspection PhpDocMissingThrowsInspection
384 */
385 function od_get_asset_path( string $src_path, ?string $min_path = null ): string {
386 if ( null === $min_path ) {
387 // Note: wp_scripts_get_suffix() is not used here because we need access to both the source and minified paths.
388 $min_path = (string) preg_replace( '/(?=\.\w+$)/', '.min', $src_path );
389 }
390
391 $force_src = false;
392 if ( WP_DEBUG && ! file_exists( trailingslashit( __DIR__ ) . $min_path ) ) {
393 $force_src = true;
394 /**
395 * No WP_Exception is thrown by wp_trigger_error() since E_USER_ERROR is not passed as the error level.
396 *
397 * @noinspection PhpUnhandledExceptionInspection
398 */
399 wp_trigger_error(
400 __FUNCTION__,
401 sprintf(
402 /* translators: %s is the minified asset path */
403 __( 'Minified asset has not been built: %s', 'optimization-detective' ),
404 $min_path
405 ),
406 E_USER_WARNING
407 );
408 }
409
410 if ( SCRIPT_DEBUG || $force_src ) {
411 return $src_path;
412 }
413
414 return $min_path;
415 }
416