PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.5.2
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.5.2
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / includes / class-wp-optimize-delay-js.php

class-wp-optimize-delay-js.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.5.2, at includes/class-wp-optimize-delay-js.php

391 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('No direct access allowed');
4
5 if (!class_exists('WP_Optimize_Delay_JS')) :
6 /**
7 * Class WP_Optimize_Delay_JS
8 */
9 class WP_Optimize_Delay_JS {
10
11 /**
12 * @var array $options Options or settings related to delaying JavaScript execution.
13 */
14 private $options;
15
16 /**
17 * Constructor method.
18 *
19 * @return void
20 */
21 private function __construct() {
22 $this->options = wp_optimize_minify_config()->get();
23 }
24
25 /**
26 * Returns singleton instance object
27 *
28 * @return WP_Optimize_Delay_JS
29 */
30 public static function instance() {
31 static $_instance = null;
32 if (null === $_instance) {
33 $_instance = new self();
34 }
35 return $_instance;
36 }
37
38 /**
39 * If delay or preload JavaScript is enabled, process the output.
40 *
41 * @return bool
42 */
43 public function should_process() {
44 return ($this->is_delay_js_enabled() || $this->is_preload_js_enabled()) && !$this->is_edit_mode();
45 }
46
47 /**
48 * Main processing function to manage JavaScript optimizations.
49 * Replaces script tags to make them delayable when the Delay JS option is enabled,
50 * and generates a list of tags to preload scripts when the Preload JavaScript Files option is enabled.
51 *
52 * @param string $buffer The content to be processed.
53 *
54 * @return string
55 */
56 public function process($buffer): string {
57 $buffer = $this->maybe_preload_js($buffer);
58 return $this->maybe_delay_js($buffer);
59 }
60
61 /**
62 * Adds actions to preload JavaScript if the feature is enabled.
63 *
64 * @param string $content
65 * @return string
66 */
67 private function maybe_preload_js($content) {
68 if ($this->is_preload_js_enabled()) {
69 $content = $this->preload_js($content);
70 }
71 return $content;
72 }
73
74 /**
75 * Check if Preload JS is enabled.
76 *
77 * @return bool
78 */
79 private function is_preload_js_enabled() {
80 return $this->options['enable_preload_js'];
81 }
82
83 /**
84 * Updates the content to add preload tags.
85 *
86 * @param string $content
87 * @return string
88 */
89 private function preload_js($content) {
90 $pattern = $this->get_scripts_pattern();
91 $scripts = array();
92 preg_match_all($pattern, $content, $matches);
93 foreach ($matches[1] as $attributes_string) {
94 $attributes = WP_Optimize_Utils::parse_attributes($attributes_string);
95 if (!empty($attributes['src']) && !$this->is_excluded_script($attributes['src'])) {
96 $scripts[] = $attributes['src'];
97 }
98 }
99
100 $updated_content = preg_replace('/\<(head)\>/i', "<$1>\n".$this->generate_preload_scripts_tags($scripts), $content, 1);
101 if (!is_null($updated_content)) {
102 $content = $updated_content;
103 }
104 return $content;
105 }
106
107 /**
108 * Adds actions to delay JavaScript execution if the feature is enabled.
109 *
110 * @param string $content
111 * @return string
112 */
113 private function maybe_delay_js($content) {
114
115 if ($this->is_delay_js_enabled()) {
116 $content = $this->delay_js($content);
117 }
118
119 return $content;
120 }
121
122 /**
123 * Check if Delay JS is enabled.
124 *
125 * @return bool
126 */
127 private function is_delay_js_enabled() {
128 return (bool) $this->options['enable_delay_js'];
129 }
130
131 /**
132 * Updates the content to delay JavaScript execution.
133 *
134 * @param string $content
135 * @return string
136 */
137 private function delay_js($content) {
138 $pattern = $this->get_scripts_pattern();
139 $updated_content = preg_replace_callback($pattern, array($this, 'update_script_tags_callback'), $content);
140 if (!is_null($updated_content)) {
141 $content = $updated_content;
142 }
143
144 return preg_replace('/<\/body>/i', $this->get_delay_js_script() . '</body>', $content);
145 }
146
147
148
149 /**
150 * Gets Delay JS inline script
151 *
152 * @return string
153 */
154 private function get_delay_js_script(): string {
155 $min_or_not_internal = WP_Optimize()->get_min_or_not_internal_string();
156 $script = '<script data-no-delay-js>';
157 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we don't need to escape inline scripts
158 $delay_js = file_get_contents(WPO_PLUGIN_MAIN_PATH . 'js/delay-js' . $min_or_not_internal . '.js');
159 if (false === $delay_js) {
160 $script .= 'console.error("Failed to load delay-js.js");';
161 $script .= '</script>';
162 return $script;
163 }
164 $script .= $delay_js;
165 $script .= '</script>';
166 return $script;
167 }
168
169 /**
170 * Retrieve the list of excluded scripts from the options.
171 *
172 * @return array
173 */
174 private function get_excluded_scripts() {
175 static $exceptions;
176
177 if (!is_null($exceptions)) return $exceptions;
178
179 $exceptions = $this->options['exclude_delay_js'];
180 $exceptions = is_array($exceptions) ? $exceptions : preg_split('#(\n|\r)#', $exceptions);
181 $exceptions = $exceptions ? array_filter(array_map('trim', $exceptions)) : array();
182
183 foreach ($exceptions as &$exception) {
184 $exception = str_replace('*', '.*', $exception);
185 }
186
187 return $exceptions;
188 }
189
190 /**
191 * Checks if minification is enabled for JavaScript files.
192 *
193 * @return boolean
194 */
195 private function is_minification_enabled_for_js() {
196 return isset($this->options['enable_js_minification']) && $this->options['enable_js_minification'];
197 }
198
199 /**
200 * Verify whether the script is included in the excluded scripts list.
201 *
202 * @param string $script - script url
203 * @return bool
204 */
205 private function is_excluded_script($script) {
206 $excluded_scripts = $this->get_excluded_scripts();
207
208 if (empty($excluded_scripts)) return false;
209
210 if ($this->is_minification_enabled_for_js()) {
211 $minified_scripts_to_exclude = $this->get_minified_scripts_to_exclude();
212
213 foreach ($minified_scripts_to_exclude as $minified_script) {
214 if (preg_match('#'.$minified_script.'#i', $script)) return true;
215 }
216 }
217
218 foreach ($excluded_scripts as $exception) {
219 if (preg_match('#'.$exception.'#i', $script)) return true;
220 }
221
222 return false;
223 }
224
225 /**
226 * Returns the list of minified JavaScript scripts to exclude from processing.
227 *
228 * @return array
229 */
230 private function get_minified_scripts_to_exclude() {
231 static $scripts_to_exclude = null;
232 static $minified_files = null;
233
234 if (!is_null($scripts_to_exclude)) {
235 return $scripts_to_exclude;
236 }
237
238 $scripts_to_exclude = array();
239
240 // Get the list of excluded scripts from options.
241 $excluded_scripts = $this->get_excluded_scripts();
242
243 if ($this->is_minification_enabled_for_js()) {
244 if (is_null($minified_files)) {
245 $minified_files = WP_Optimize_Minify_Cache_Functions::get_cached_files(0, false);
246 }
247 } else {
248 $minified_files = array();
249 }
250
251 if (!empty($minified_files['js'])) {
252 foreach ($minified_files['js'] as $file_info) {
253 if (isset($file_info['log']->files)) {
254 $files = get_object_vars($file_info['log']->files);
255
256 // Loop through each file and match it with the exclusion patterns.
257 foreach ($files as $file) {
258 foreach ($excluded_scripts as $exception) {
259 if (preg_match('#' . $exception . '#i', $file->url)) {
260 // Store minified filename into result
261 $scripts_to_exclude[] = $file_info['filename'];
262 break(2);
263 }
264 }
265 }
266 }
267 }
268 }
269
270 return $scripts_to_exclude;
271 }
272
273 /**
274 * Generate HTML code with tags to preload JavaScript files.
275 *
276 * @param array $scripts array with urls to scripts
277 * @return string
278 */
279 private function generate_preload_scripts_tags($scripts) {
280 $tags = array();
281
282 if (!empty($scripts)) {
283 foreach ($scripts as $script) {
284 if ($this->is_excluded_script($script)) continue;
285 $tags[] = '<link rel="preload" href="'.esc_url($script).'" as="script" />';
286 }
287 }
288
289 return implode("\n", $tags);
290 }
291
292 /**
293 * Callback function that updates the script tag if necessary, or keeps it as is when the script is in the excluded list.
294 *
295 * @param array $tag
296 * @return string
297 */
298 private function update_script_tags_callback($tag) {
299 $attributes = WP_Optimize_Utils::parse_attributes($tag[1]);
300 // When the inline script content is not empty, we will change the script type to 'text/plain'.
301 $change_type = '' !== trim($tag[2]);
302
303 if (!empty($attributes['src']) && $this->is_excluded_script($attributes['src'])) {
304 return $tag[0];
305 } else {
306 return $this->build_delay_js_tag($attributes, $change_type, $tag[2]);
307 }
308 }
309
310 /**
311 * Updates tag attributes by changing src="value" to data-src="value". If $change_type is true,
312 * it also adds data-type="value" with the original type attribute value, and sets the original
313 * type value to 'text/plain'.
314 *
315 * @param array $attributes
316 * @param bool $change_type
317 * @param string $inline_script Inline script content
318 * @return string
319 */
320 private function build_delay_js_tag($attributes, $change_type, $inline_script) {
321 if (!isset($attributes['data-no-delay-js'])) {
322 // Change 'src' attribute to 'data-src' attr for external scripts
323 if (!empty($attributes['src'])) {
324 $attributes['data-src'] = $attributes['src'];
325 unset($attributes['src']);
326 }
327
328 // Add data-type="value" with the original type attribute value and set type value to 'text/plain' for inline scripts
329 if ($change_type && (empty($attributes['type']) || $this->is_supported_type($attributes['type']))) {
330 $attributes['data-type'] = empty($attributes['type']) ? 'text/javascript' : $attributes['type'];
331 $attributes['type'] = 'text/plain';
332 }
333 }
334
335 return '<script '.WP_Optimize_Utils::build_attributes($attributes).'>'.$inline_script.'</script>';
336 }
337
338 /**
339 * Checks if the script type is in the supported list of types.
340 *
341 * @param string $type
342 * @return bool
343 */
344 private function is_supported_type($type) {
345 $supported_types = array(
346 'text/javascript',
347 'module',
348 'application/javascript',
349 'application/ecmascript',
350 'application/x-ecmascript',
351 'application/x-javascript',
352 'text/x-ecmascript',
353 'text/x-javascript',
354 'text/javascript1.0',
355 'text/javascript1.1',
356 'text/javascript1.2',
357 'text/javascript1.3',
358 'text/javascript1.4',
359 'text/javascript1.5',
360 'text/jscript',
361 'text/livescript',
362 'text/ecmascript',
363 );
364
365 $type = strtolower($type);
366
367 return in_array($type, $supported_types);
368 }
369
370 /**
371 * Regular expression to get scripts.
372 *
373 * @return string
374 */
375 private function get_scripts_pattern() {
376 return '/<script(.*)>(.*)<\/script>/Uis';
377 }
378
379 /**
380 * Determines whether the page is in edit mode using page builders
381 * Beaver Builder, Divi Theme, Elementor, and Oxygen Builder
382 *
383 * @return bool
384 */
385 private function is_edit_mode() {
386 return isset($_GET['fl_builder']) || isset($_GET['et_fb']) || isset($_GET['elementor-preview']) || isset($_GET['oxygen']) || isset($_GET['ct_builder']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are not using any global values, only checking for existence
387 }
388 }
389
390 endif;
391