PluginProbe
Plugin Check (PCP) / 1.5.0
Plugin Check (PCP) v1.5.0
2.1.0 trunk 0.1 0.2.0 0.2.1 0.2.2 0.2.3 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 ci-artifacts
plugin-check / includes / Checker / Checks / Performance / Non_Blocking_Scripts_Check.php

Non_Blocking_Scripts_Check.php in Plugin Check (PCP) 1.5.0, at includes/Checker/Checks/Performance/Non_Blocking_Scripts_Check.php

270 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Non_Blocking_Scripts_Check.
4 *
5 * @package plugin-check
6 */
7
8 namespace WordPress\Plugin_Check\Checker\Checks\Performance;
9
10 use Exception;
11 use WordPress\Plugin_Check\Checker\Check_Categories;
12 use WordPress\Plugin_Check\Checker\Check_Result;
13 use WordPress\Plugin_Check\Checker\Checks\Abstract_Runtime_Check;
14 use WordPress\Plugin_Check\Checker\Preparations\Demo_Posts_Creation_Preparation;
15 use WordPress\Plugin_Check\Checker\With_Shared_Preparations;
16 use WordPress\Plugin_Check\Traits\Amend_Check_Result;
17 use WordPress\Plugin_Check\Traits\Stable_Check;
18 use WordPress\Plugin_Check\Traits\URL_Aware;
19
20 /**
21 * Check for non-blocking scripts.
22 *
23 * @since 1.1.0
24 */
25 class Non_Blocking_Scripts_Check extends Abstract_Runtime_Check implements With_Shared_Preparations {
26
27 use Amend_Check_Result;
28 use Stable_Check;
29 use URL_Aware;
30
31 /**
32 * List of viewable post types.
33 *
34 * @since 1.1.0
35 * @var array
36 */
37 private $viewable_post_types;
38
39 /**
40 * Gets the categories for the check.
41 *
42 * Every check must have at least one category.
43 *
44 * @since 1.1.0
45 *
46 * @return array The categories for the check.
47 */
48 public function get_categories() {
49 return array( Check_Categories::CATEGORY_PERFORMANCE );
50 }
51
52 /**
53 * Runs this preparation step for the environment and returns a cleanup function.
54 *
55 * @since 1.1.0
56 *
57 * @return callable Cleanup function to revert any changes made here.
58 *
59 * @throws Exception Thrown when preparation fails.
60 */
61 public function prepare() {
62 $orig_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null;
63
64 // Backup the original values for the global state.
65 $this->backup_globals();
66
67 return function () use ( $orig_scripts ) {
68 if ( is_null( $orig_scripts ) ) {
69 unset( $GLOBALS['wp_scripts'] );
70 } else {
71 $GLOBALS['wp_scripts'] = $orig_scripts;
72 }
73
74 $this->restore_globals();
75 };
76 }
77
78 /**
79 * Returns an array of shared preparations for the check.
80 *
81 * @since 1.1.0
82 *
83 * @return array Returns a map of $class_name => $constructor_args pairs. If the class does not
84 * need any constructor arguments, it would just be an empty array.
85 */
86 public function get_shared_preparations() {
87 $demo_posts = array_map(
88 static function ( $post_type ) {
89 return array(
90 'post_title' => "Demo {$post_type} post",
91 'post_content' => 'Test content',
92 'post_type' => $post_type,
93 'post_status' => 'publish',
94 );
95 },
96 $this->get_viewable_post_types()
97 );
98
99 return array(
100 Demo_Posts_Creation_Preparation::class => array( $demo_posts ),
101 );
102 }
103
104 /**
105 * Runs the check on the plugin and amends results.
106 *
107 * @since 1.1.0
108 *
109 * @param Check_Result $result The check results to amend and the plugin context.
110 */
111 public function run( Check_Result $result ) {
112 $this->run_for_urls(
113 $this->get_urls(),
114 function ( $url ) use ( $result ) {
115 $this->check_url( $result, $url );
116 }
117 );
118 }
119
120 /**
121 * Gets the list of URLs to run this check for.
122 *
123 * @since 1.1.0
124 *
125 * @return array List of URL strings (either full URLs or paths).
126 *
127 * @throws Exception Thrown when a post type URL cannot be retrieved.
128 */
129 protected function get_urls() {
130 $urls = array( home_url() );
131
132 foreach ( $this->get_viewable_post_types() as $post_type ) {
133 $posts = get_posts(
134 array(
135 'posts_per_page' => 1,
136 'post_type' => $post_type,
137 'post_status' => array( 'publish', 'inherit' ),
138 )
139 );
140
141 if ( ! isset( $posts[0] ) ) {
142 throw new Exception(
143 sprintf(
144 /* translators: %s: The Post Type name. */
145 __( 'Unable to retrieve post URL for post type: %s', 'plugin-check' ),
146 $post_type
147 )
148 );
149 }
150
151 $urls[] = get_permalink( $posts[0] );
152 }
153
154 return $urls;
155 }
156
157 /**
158 * Amends the given result by running the check for the given URL.
159 *
160 * @since 1.1.0
161 *
162 * @param Check_Result $result The check result to amend, including the plugin context to check.
163 * @param string $url URL to run the check for.
164 *
165 * @throws Exception Thrown when the check fails with a critical error (unrelated to any errors detected as part of
166 * the check).
167 *
168 * @SuppressWarnings(PHPMD.NPathComplexity)
169 */
170 protected function check_url( Check_Result $result, $url ) {
171 // Reset the WP_Scripts instance.
172 unset( $GLOBALS['wp_scripts'] );
173
174 // Run the 'wp_enqueue_script' action, wrapped in an output buffer in case of any callbacks printing scripts
175 // directly. This is discouraged, but some plugins or themes are still doing it.
176 ob_start();
177 wp_enqueue_scripts();
178 wp_scripts()->do_head_items();
179 wp_scripts()->do_footer_items();
180 ob_end_clean();
181
182 foreach ( wp_scripts()->done as $handle ) {
183 $script = wp_scripts()->registered[ $handle ];
184
185 // TODO: Somehow detect inline scripts added by the plugin that don't have a `src`.
186
187 if ( ! $script->src || strpos( $script->src, $result->plugin()->url() ) !== 0 ) {
188 continue;
189 }
190
191 if ( ! empty( $script->extra['strategy'] ) ) {
192 continue;
193 }
194
195 $script_path = str_replace( $result->plugin()->url(), $result->plugin()->path(), $script->src );
196
197 if ( ! in_array( $handle, wp_scripts()->in_footer, true ) ) {
198 $this->add_result_warning_for_file(
199 $result,
200 sprintf(
201 /* translators: 1: tested URL. 2: the script handle. 3: 'defer'. 4: 'async' */
202 __( 'This script on %1$s (with handle %2$s) is potentially blocking. Consider a %3$s or %4$s script strategy or moving it to the footer.', 'plugin-check' ),
203 $url,
204 $handle,
205 'defer',
206 'async'
207 ),
208 'NonBlockingScripts.BlockingHeadScript',
209 $script_path
210 );
211 } else {
212 $this->add_result_warning_for_file(
213 $result,
214 sprintf(
215 /* translators: 1: tested URL. 2: the script handle. 3: 'defer'. 4: 'async' */
216 __( 'This script on %1$s (with handle %2$s) is loaded in the footer. Consider a %3$s or %4$s script loading strategy instead.', 'plugin-check' ),
217 $url,
218 $handle,
219 'defer',
220 'async'
221 ),
222 'NonBlockingScripts.NoStrategy',
223 $script_path
224 );
225 }
226 }
227 }
228
229 /**
230 * Returns an array of viewable post types.
231 *
232 * @since 1.1.0
233 *
234 * @return array Array of viewable post type slugs.
235 */
236 private function get_viewable_post_types() {
237 if ( ! is_array( $this->viewable_post_types ) ) {
238 $this->viewable_post_types = array_filter( get_post_types(), 'is_post_type_viewable' );
239 }
240
241 return $this->viewable_post_types;
242 }
243
244 /**
245 * Gets the description for the check.
246 *
247 * Every check must have a short description explaining what the check does.
248 *
249 * @since 1.1.0
250 *
251 * @return string Description.
252 */
253 public function get_description(): string {
254 return __( 'Checks whether scripts and styles are enqueued using a recommended loading strategy.', 'plugin-check' );
255 }
256
257 /**
258 * Gets the documentation URL for the check.
259 *
260 * Every check must have a URL with further information about the check.
261 *
262 * @since 1.1.0
263 *
264 * @return string The documentation URL.
265 */
266 public function get_documentation_url(): string {
267 return __( 'https://developer.wordpress.org/plugins/', 'plugin-check' );
268 }
269 }
270