PluginProbe ʕ •ᴥ•ʔ
Oxyplug Preload / trunk
Oxyplug Preload vtrunk
2.2.1 2.2.0 trunk 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.5
oxyplug-preload / oxy-preload.php
oxyplug-preload Last commit date
assets 1 week ago lang 2 weeks ago oxy-preload.php 1 week ago readme.txt 1 week ago uninstall.php 1 week ago
oxy-preload.php
891 lines
1 <?php
2 /**
3 * Plugin Name: Oxyplug Preload
4 * Plugin URI: https://www.oxyplug.com/products/oxy-preload
5 * Description: Preload post/page featured images and product images to enhance the Largest Contentful Paint (LCP) and achieve a better Core Web Vitals (CWV) score in Google's Lighthouse. Additionally, the tool supports preloading fonts, CSS, and JavaScript files when specified manually, allowing for even greater optimization of page load performance.
6 * Version: 2.2.1
7 * Author: Oxyplug
8 * Author URI: https://www.oxyplug.com
9 * Requires PHP: 7.4
10 * Requires at least: 4.9
11 * Tested up to: 7.0
12 * Text Domain: oxyplug-preload
13 * Domain Path: /lang/
14 * License: GPL v2 or later
15 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
16 *
17 * Copyright 2025 Oxyplug
18 */
19
20 if (!defined('ABSPATH')) {
21 exit;
22 }
23
24 /**
25 * Class OxyPreload
26 */
27 class OxyPreload
28 {
29 protected string $imgurl;
30 protected string $srcset;
31 protected string $sizes;
32 const OXYPLUG_PRELOAD_VERSION = '2.1.5';
33
34 public function __construct()
35 {
36 if (!defined('FS_CHMOD_FILE')) {
37 define('FS_CHMOD_FILE', 0644);
38 }
39
40 // Init on activate
41 register_activation_hook(__FILE__, array($this, 'activate_it'));
42 register_deactivation_hook(__FILE__, array($this, 'deactivate_it'));
43 add_action('admin_init', array($this, 'init'));
44
45 // Load translations from the bundled /lang directory
46 add_action('init', array($this, 'oxyplug_preload_load_textdomain'));
47
48 // Add preload tag
49 add_action('plugins_loaded', array($this, 'check_required_plugin'));
50
51 // Save preloads
52 add_action('wp_ajax_oxyplug_preload_save_preloads', array($this, 'oxyplug_preload_save_preloads'));
53
54 // Add menu
55 add_action('admin_menu', array($this, 'add_menu'));
56
57 // Add settings in plugins page
58 add_filter('plugin_action_links', array($this, 'add_settings'), 10, 3);
59
60 // Add necessities in admin head
61 add_action('admin_head', array($this, 'admin_head'));
62
63 // Add admin assets
64 add_action('admin_enqueue_scripts', array($this, 'add_admin_assets'));
65 }
66
67 /**
68 * @return void
69 */
70 public function activate_it()
71 {
72 // Enable preloading `featured image` by default
73 $preload_featured_image = $this->oxyplug_preload_get_option('_oxyplug_preload_featured_image');
74 if (empty($preload_featured_image)) {
75 $this->oxyplug_preload_update_option('_oxyplug_preload_featured_image', 'true');
76 }
77
78 // Set a transient to indicate an update has occurred
79 set_transient('oxyplug_preload_updated', true, 30);
80 }
81
82 /**
83 * Remove the static `.htaccess` preload block on deactivation.
84 *
85 * The preload `Link` headers live in `.htaccess`, so they would keep being
86 * emitted while the plugin is inactive. Strip the block on deactivation, but
87 * leave the `_oxyplug_preload_*` options intact so settings survive a
88 * reactivation (the block is regenerated when preloads are next saved).
89 *
90 * @return void
91 */
92 public function deactivate_it()
93 {
94 $htaccess_path = ABSPATH . '.htaccess';
95
96 global $wp_filesystem;
97 if (!$wp_filesystem) {
98 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
99 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
100 $wp_filesystem = new \WP_Filesystem_Direct(null);
101 }
102
103 if (!$wp_filesystem->exists($htaccess_path)
104 || !$wp_filesystem->is_readable($htaccess_path)
105 || !$wp_filesystem->is_writable($htaccess_path)) {
106 return;
107 }
108
109 $current_content = $wp_filesystem->get_contents($htaccess_path);
110 if ($current_content === false) {
111 return;
112 }
113
114 $cleaned = $this->strip_oxyplug_section($current_content);
115
116 if ($cleaned !== $current_content) {
117 $wp_filesystem->put_contents($htaccess_path, rtrim($cleaned) . "\n");
118 }
119 }
120
121 /**
122 * Load the plugin text domain so bundled translations in /lang are applied.
123 *
124 * @return void
125 */
126 public function oxyplug_preload_load_textdomain()
127 {
128 load_plugin_textdomain('oxyplug-preload', false, dirname(plugin_basename(__FILE__)) . '/lang/');
129 }
130
131 public function init()
132 {
133 // Check if we need to perform update tasks
134 if (get_transient('oxyplug_preload_updated')) {
135 // Delete the transient
136 delete_transient('oxyplug_preload_updated');
137
138 // Update .htaccess with improved rules if needed
139 $preloads = $this->oxyplug_preload_get_option('_oxyplug_preload_preloads', array());
140
141 if (!empty($preloads)) {
142 // Prepare data for htaccess generation
143 $htaccess_preloads = array();
144
145 foreach ($preloads as $type => $urls) {
146 if (!isset($htaccess_preloads[$type])) {
147 $htaccess_preloads[$type] = array();
148 }
149
150 foreach ($urls as $url) {
151 $htaccess_preloads[$type][] = $url;
152 }
153 }
154
155 // Generate htaccess content
156 $htaccess_content = $this->generate_htaccess_content($htaccess_preloads);
157
158 // Update .htaccess file
159 $this->update_htaccess($htaccess_content);
160 }
161 }
162 }
163
164 /**
165 * @return void
166 */
167 public function check_required_plugin()
168 {
169 // is_plugin_active() lives in wp-admin/includes/plugin.php, which is not
170 // loaded on the front end. Use the active_plugins option directly so this
171 // works on public requests without a fatal "undefined function" error.
172 $active_plugins = (array)get_option('active_plugins', array());
173 if (!in_array('oxyplug-image/index.php', $active_plugins, true)) {
174 add_action('wp_head', array($this, 'add_preload_tag'));
175 }
176 }
177
178 /**
179 * @return void
180 */
181 public function add_preload_tag()
182 {
183 $preload_featured_image = $this->oxyplug_preload_get_option('_oxyplug_preload_featured_image') == 'true';
184 if ($preload_featured_image) {
185 if (is_single() || is_page()) {
186 // Use a single, consistent image size for the href, srcset and sizes
187 // hints. Mismatched sizes make the browser download the preloaded image
188 // AND the rendered one, wasting bandwidth and hurting LCP.
189 $size = apply_filters('oxyplug_preload_image_size', 'post-thumbnail');
190
191 $thumbnail_id = (int)(get_post_thumbnail_id());
192 if ($thumbnail_id > 0) {
193 $this->imgurl = get_the_post_thumbnail_url(null, $size);
194 } else if (function_exists('wc_get_product')) {
195 if ($product = wc_get_product(get_the_id())) {
196 $attachment_ids = $product->get_gallery_image_ids();
197 if (sizeof($attachment_ids) > 0) {
198 $thumbnail_id = reset($attachment_ids);
199 $this->imgurl = wp_get_attachment_image_url($thumbnail_id, $size);
200 }
201 }
202 }
203
204 if ($thumbnail_id) {
205 $this->srcset = wp_get_attachment_image_srcset($thumbnail_id, $size);
206 $this->sizes = wp_get_attachment_image_sizes($thumbnail_id, $size);
207 ?>
208 <link rel="preload"
209 as="image"
210 href="<?php echo esc_url($this->imgurl) ?>"
211 imagesrcset="<?php echo esc_attr($this->srcset) ?>"
212 imagesizes="<?php echo esc_attr($this->sizes) ?>"
213 fetchpriority="high">
214 <?php
215 }
216 }
217 }
218 }
219
220 /**
221 * @return void
222 */
223 public function admin_head()
224 {
225 // Load only in specific pages
226 $screen = get_current_screen();
227 if ($screen && $screen->base == 'tools_page_oxyplug-preload-settings') {
228 $component_path = plugins_url('assets/js/dist/', __FILE__);
229 $components = array(
230 'tools_page_oxyplug-preload-settings' => array(
231 'outlined-text-field',
232 'icon',
233 'icon-button',
234 'outlined-button',
235 'filled-button',
236 'divider',
237 'switch'
238 ),
239 );
240 $components = wp_json_encode($components[$screen->base]);
241 ?>
242 <link rel="preconnect" href="https://fonts.googleapis.com">
243 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
244 <link href="https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap" rel="stylesheet">
245 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
246
247 <script type="module">
248 // Load MD3 components
249 (async function () {
250 const OXYPLUG_PRELOAD_VERSION = '<?php echo self::OXYPLUG_PRELOAD_VERSION ?>'
251 const components = '<?php echo $components ?>'
252 const component_path = '<?php echo $component_path ?>'
253
254 for (const component of JSON.parse(components)) {
255 await import(`${component_path}${component}.js?ver=${OXYPLUG_PRELOAD_VERSION}`);
256 }
257 })();
258 </script>
259 <?php }
260 }
261
262 /**
263 * @param string $hook_suffix Current admin page hook suffix.
264 *
265 * @return void
266 */
267 public function add_admin_assets($hook_suffix = '')
268 {
269 // Only load assets on the plugin's settings screen, not on every admin page.
270 if ($hook_suffix !== 'tools_page_oxyplug-preload-settings') {
271 return;
272 }
273
274 wp_register_script('oxyplug-preload-admin-script', plugins_url('assets/js/admin-script.js', __FILE__), array('jquery'), self::OXYPLUG_PRELOAD_VERSION);
275 wp_enqueue_script('oxyplug-preload-admin-script');
276
277 wp_register_style(
278 'oxyplug-preload-admin-style',
279 plugins_url('assets/css/admin-style.css', __FILE__),
280 array(),
281 self::OXYPLUG_PRELOAD_VERSION
282 );
283 wp_enqueue_style('oxyplug-preload-admin-style');
284
285 wp_localize_script(
286 'oxyplug-preload-admin-script',
287 'oxyplug_preload_defines',
288 array(
289 'trans' => array(
290 'invalid_url' => __('Invalid URL', 'oxyplug-preload'),
291 )
292 )
293 );
294 }
295
296 /**
297 * @return void
298 */
299 public function add_menu()
300 {
301 add_submenu_page(
302 'tools.php',
303 'Oxyplug Preload',
304 'Oxyplug Preload',
305 'manage_options',
306 'oxyplug-preload-settings',
307 array($this, 'oxyplug_preload_settings')
308 );
309 }
310
311 /**
312 * @param $actions
313 * @param $plugin_file
314 * @param $plugin_data
315 *
316 * @return mixed
317 */
318 public function add_settings($actions, $plugin_file, $plugin_data)
319 {
320 if (isset($plugin_data['slug']) && $plugin_data['slug'] == 'oxyplug-preload') {
321 $href = admin_url('tools.php?page=oxyplug-preload-settings');
322
323 $actions['Settings'] = '<a href="' . $href . '">' . __('Settings', 'oxyplug-preload') . '</a>';
324 }
325
326 return $actions;
327 }
328
329 /**
330 * @return void
331 */
332 public function oxyplug_preload_settings()
333 {
334 $preloads = $this->oxyplug_preload_get_option('_oxyplug_preload_preloads', array()); ?>
335
336 <div class="oxyplug-preload-admin-page">
337 <section class="oxyplug-preload-admin-head">
338 <h1 class="oxyplug-preload-head-title">
339 <span class="oxyplug-preload-brand-highlight">
340 <?php esc_html_e('Oxyplug Preload', 'oxyplug-preload') ?>
341 </span>
342 <span>|</span>
343 <span>
344 <?php esc_html_e('Settings', 'oxyplug-preload') ?>
345 </span>
346 </h1>
347
348 <div class="oxyplug-preload-need-help">
349 <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
350 <path
351 d="M7.59161 31.5676L8.1794 30.7586L7.59161 31.5676ZM5.93237 29.9084L6.74139 29.3206L5.93237 29.9084ZM30.0676 29.9084L29.2586 29.3206L30.0676 29.9084ZM28.4084 31.5676L27.8206 30.7586L28.4084 31.5676ZM28.4084 4.43237L28.9962 3.62336L28.4084 4.43237ZM30.0676 6.09161L29.2586 6.6794L30.0676 6.09161ZM7.59161 4.43237L8.1794 5.24139L7.59161 4.43237ZM5.93237 6.09161L6.74139 6.6794L5.93237 6.09161ZM19.7192 9.89296L19.8756 10.8807L19.7192 9.89296ZM17.3727 9.89296L17.2163 10.8807L17.3727 9.89296ZM12 23C11.4477 23 11 23.4477 11 24C11 24.5523 11.4477 25 12 25V23ZM24 25C24.5523 25 25 24.5523 25 24C25 23.4477 24.5523 23 24 23V25ZM12 17C11.4477 17 11 17.4477 11 18C11 18.5523 11.4477 19 12 19V17ZM16.5 19C17.0523 19 17.5 18.5523 17.5 18C17.5 17.4477 17.0523 17 16.5 17V19ZM30.5 16.5V19.5H32.5V16.5H30.5ZM5.5 19.5V16.5H3.5V19.5H5.5ZM18 32C15.1654 32 13.1198 31.9986 11.5336 31.8268C9.9661 31.6569 8.96626 31.3303 8.1794 30.7586L7.00383 32.3766C8.18845 33.2373 9.58051 33.6269 11.3182 33.8151C13.0371 34.0014 15.21 34 18 34V32ZM3.5 19.5C3.5 22.29 3.49863 24.4629 3.68486 26.1818C3.87313 27.9195 4.26267 29.3116 5.12336 30.4962L6.74139 29.3206C6.1697 28.5337 5.84306 27.5339 5.67323 25.9664C5.50137 24.3802 5.5 22.3346 5.5 19.5H3.5ZM8.1794 30.7586C7.62758 30.3577 7.14231 29.8724 6.74139 29.3206L5.12336 30.4962C5.64763 31.2178 6.28222 31.8524 7.00383 32.3766L8.1794 30.7586ZM30.5 19.5C30.5 22.3346 30.4986 24.3802 30.3268 25.9664C30.1569 27.5339 29.8303 28.5337 29.2586 29.3206L30.8766 30.4962C31.7373 29.3116 32.1269 27.9195 32.3151 26.1818C32.5014 24.4629 32.5 22.29 32.5 19.5H30.5ZM18 34C20.79 34 22.9629 34.0014 24.6818 33.8151C26.4195 33.6269 27.8115 33.2373 28.9962 32.3766L27.8206 30.7586C27.0337 31.3303 26.0339 31.6569 24.4664 31.8268C22.8802 31.9986 20.8346 32 18 32V34ZM29.2586 29.3206C28.8577 29.8724 28.3724 30.3577 27.8206 30.7586L28.9962 32.3766C29.7178 31.8524 30.3524 31.2178 30.8766 30.4962L29.2586 29.3206ZM32.5 16.5C32.5 13.71 32.5014 11.5371 32.3151 9.81818C32.1269 8.08051 31.7373 6.68845 30.8766 5.50383L29.2586 6.6794C29.8303 7.46626 30.1569 8.4661 30.3268 10.0336C30.4986 11.6198 30.5 13.6654 30.5 16.5H32.5ZM27.8206 5.24139C28.3724 5.64231 28.8577 6.12758 29.2586 6.6794L30.8766 5.50383C30.3524 4.78222 29.7178 4.14763 28.9962 3.62336L27.8206 5.24139ZM5.5 16.5C5.5 13.6654 5.50137 11.6198 5.67323 10.0336C5.84306 8.4661 6.1697 7.46626 6.74139 6.6794L5.12336 5.50383C4.26267 6.68845 3.87313 8.08051 3.68486 9.81818C3.49863 11.5371 3.5 13.71 3.5 16.5H5.5ZM7.00383 3.62336C6.28222 4.14763 5.64763 4.78222 5.12336 5.50383L6.74139 6.6794C7.14231 6.12758 7.62758 5.64231 8.1794 5.24139L7.00383 3.62336ZM19.5628 8.90528C18.8891 9.01198 18.2028 9.01198 17.5291 8.90528L17.2163 10.8807C18.0972 11.0202 18.9947 11.0202 19.8756 10.8807L19.5628 8.90528ZM12 25H24V23H12V25ZM12 19H16.5V17H12V19ZM26.9539 3.26967C25.0951 5.12711 23.7338 6.46779 22.5558 7.39555C21.3926 8.31159 20.4881 8.75872 19.5628 8.90528L19.8756 10.8807C21.2687 10.66 22.4884 9.99435 23.7932 8.9668C25.0831 7.95097 26.5334 6.51727 28.3676 4.68439L26.9539 3.26967ZM18 4C20.4907 4 22.3761 4.00071 23.8821 4.11906C25.3848 4.23715 26.4116 4.46712 27.2105 4.86993L28.111 3.08413C26.9722 2.50991 25.6443 2.25137 24.0388 2.1252C22.4366 1.99929 20.4605 2 18 2V4ZM27.2105 4.86993C27.4269 4.97906 27.6289 5.1021 27.8206 5.24139L28.9962 3.62336C28.7158 3.41966 28.4216 3.24078 28.111 3.08413L27.2105 4.86993ZM8.40124 4.3614C10.3389 6.29902 11.8529 7.81034 13.1857 8.87708C14.5333 9.95562 15.7832 10.6537 17.2163 10.8807L17.5291 8.90528C16.5773 8.75451 15.6476 8.28574 14.4355 7.31562C13.2086 6.33371 11.7829 4.91456 9.81542 2.94716L8.40124 4.3614ZM18 2C15.8443 2 14.0633 1.99964 12.5843 2.08338C11.1063 2.16707 9.85915 2.33736 8.78216 2.70897L9.4345 4.59959C10.2537 4.31692 11.2865 4.16007 12.6974 4.08019C14.1073 4.00036 15.824 4 18 4V2ZM8.78216 2.70897C8.1318 2.93337 7.54439 3.23061 7.00383 3.62336L8.1794 5.24139C8.54517 4.97564 8.95294 4.76575 9.4345 4.59959L8.78216 2.70897Z"
352 fill="#2D2D2D"></path>
353 </svg>
354 <div>
355 <span><?php esc_html_e('Need Help Or Have Questions?', 'oxyplug-preload') ?></span>
356 <a class="oxyplug-preload-a" href="https://www.oxyplug.com/docs/oxy-preload/" target="_blank">
357 <?php esc_html_e('Check our documentation.', 'oxyplug-preload') ?>
358 </a>
359 </div>
360 </div>
361 </section>
362
363 <div class="oxyplug-preload-in-row">
364 <div class="oxyplug-preload-card">
365 <h2>
366 <?php esc_html_e('Script Preload', 'oxyplug-preload') ?>
367 <i class="dashicons dashicons-editor-help oxyplug-preload-has-tooltip"
368 data-tooltip="<?php esc_attr_e('Preload scripts', 'oxyplug-preload') ?>"
369 data-href="https://www.oxyplug.com/docs/oxy-preload/settings/?utm_source=plugin-settings&utm_medium=wordpress&utm_campaign=oxyplug-preload#preload-settings"
370 data-href-text="<?php esc_attr_e('Learn More', 'oxyplug-preload'); ?>"></i>
371 </h2>
372
373 <?php if (empty($preloads['script'])): ?>
374 <div class="oxyplug-preload-input-wrap">
375 <md-outlined-text-field class="oxyplug-preload-text-field has-clear-button"
376 name="preloads[script][]"
377 label="<?php esc_attr_e('Script URL', 'oxyplug-preload'); ?>"
378 placeholder="https://www.example.com/wp-content/my-script.js"
379 type="url">
380 <md-icon-button toggle slot="trailing-icon" type="button">
381 <md-icon>cancel</md-icon>
382 </md-icon-button>
383 </md-outlined-text-field>
384 <md-icon-button class="oxyplug-preload-remove-url"
385 toggle
386 slot="trailing-icon"
387 type="button"
388 style="display:none">
389 <md-icon>delete</md-icon>
390 </md-icon-button>
391 </div>
392 <?php else: ?>
393 <?php foreach ($preloads['script'] as $index => $link): ?>
394 <div class="oxyplug-preload-input-wrap">
395 <md-outlined-text-field class="oxyplug-preload-text-field has-clear-button"
396 name="preloads[script][]"
397 value="<?php echo esc_url($link) ?>"
398 label="Script URL"
399 placeholder="https://www.example.com/wp-content/my-script.js"
400 type="url">
401 <md-icon-button toggle slot="trailing-icon" type="button">
402 <md-icon>cancel</md-icon>
403 </md-icon-button>
404 </md-outlined-text-field>
405 <md-icon-button class="oxyplug-preload-remove-url"
406 toggle
407 slot="trailing-icon"
408 type="button"
409 <?php if ($index == 0): ?> style="display:none" <?php endif; ?>>
410 <md-icon>delete</md-icon>
411 </md-icon-button>
412 </div>
413 <?php endforeach; ?>
414 <?php endif; ?>
415 <md-outlined-button class="oxyplug-preload-add-more">
416 <?php esc_html_e('Add More Script URL', 'oxyplug-preload') ?>
417 </md-outlined-button>
418
419 </div>
420 <div class="oxyplug-preload-card">
421 <h2>
422 <?php esc_html_e('Style Preload', 'oxyplug-preload') ?>
423 <i class="dashicons dashicons-editor-help oxyplug-preload-has-tooltip"
424 data-tooltip="<?php esc_attr_e('Preload scripts', 'oxyplug-preload') ?>"
425 data-href="https://www.oxyplug.com/docs/oxy-preload/settings/?utm_source=plugin-settings&utm_medium=wordpress&utm_campaign=oxyplug-preload#preload-settings"
426 data-href-text="<?php esc_attr_e('Learn More', 'oxyplug-preload'); ?>"></i>
427 </h2>
428
429 <?php if (empty($preloads['style'])): ?>
430 <div class="oxyplug-preload-input-wrap">
431 <md-outlined-text-field class="oxyplug-preload-text-field has-clear-button"
432 name="preloads[style][]"
433 label="<?php esc_attr_e('Style URL', 'oxyplug-preload'); ?>"
434 placeholder="https://www.example.com/wp-content/my-style.css"
435 type="url">
436 <md-icon-button toggle slot="trailing-icon" type="button">
437 <md-icon>cancel</md-icon>
438 </md-icon-button>
439 </md-outlined-text-field>
440 <md-icon-button class="oxyplug-preload-remove-url"
441 toggle
442 slot="trailing-icon"
443 type="button"
444 style="display:none">
445 <md-icon>delete</md-icon>
446 </md-icon-button>
447 </div>
448 <?php else: ?>
449 <?php foreach ($preloads['style'] as $index => $link): ?>
450 <div class="oxyplug-preload-input-wrap">
451 <md-outlined-text-field class="oxyplug-preload-text-field has-clear-button"
452 name="preloads[style][]"
453 value="<?php echo esc_url($link) ?>"
454 label="<?php esc_attr_e('Style URL', 'oxyplug-preload'); ?>"
455 placeholder="https://www.example.com/wp-content/my-style.css"
456 type="url">
457 <md-icon-button toggle slot="trailing-icon" type="button">
458 <md-icon>cancel</md-icon>
459 </md-icon-button>
460 </md-outlined-text-field>
461 <md-icon-button class="oxyplug-preload-remove-url"
462 toggle
463 slot="trailing-icon"
464 type="button"
465 <?php if ($index == 0): ?> style="display:none" <?php endif; ?>>
466 <md-icon>delete</md-icon>
467 </md-icon-button>
468 </div>
469 <?php endforeach; ?>
470 <?php endif; ?>
471 <md-outlined-button class="oxyplug-preload-add-more">
472 <?php esc_html_e('Add More Style URL', 'oxyplug-preload') ?>
473 </md-outlined-button>
474
475 </div>
476 </div>
477
478 <div class="oxyplug-preload-in-row">
479 <div class="oxyplug-preload-card">
480 <h2>
481 <?php esc_html_e('Font Preload', 'oxyplug-preload') ?>
482 <i class="dashicons dashicons-editor-help oxyplug-preload-has-tooltip"
483 data-tooltip="<?php esc_attr_e('Preload fonts', 'oxyplug-preload') ?>"
484 data-href="https://www.oxyplug.com/docs/oxy-preload/settings/?utm_source=plugin-settings&utm_medium=wordpress&utm_campaign=oxyplug-preload#preload-settings"
485 data-href-text="<?php esc_attr_e('Learn More', 'oxyplug-preload'); ?>"></i>
486 </h2>
487
488 <?php if (empty($preloads['font'])): ?>
489 <div class="oxyplug-preload-input-wrap">
490 <md-outlined-text-field class="oxyplug-preload-text-field has-clear-button"
491 name="preloads[font][]"
492 label="<?php esc_attr_e('Font URL', 'oxyplug-preload'); ?>"
493 placeholder="https://www.example.com/wp-content/my-font.woff2"
494 type="url">
495 <md-icon-button toggle slot="trailing-icon" type="button">
496 <md-icon>cancel</md-icon>
497 </md-icon-button>
498 </md-outlined-text-field>
499 <md-icon-button class="oxyplug-preload-remove-url"
500 toggle
501 slot="trailing-icon"
502 type="button"
503 style="display:none">
504 <md-icon>delete</md-icon>
505 </md-icon-button>
506 </div>
507 <?php else: ?>
508 <?php foreach ($preloads['font'] as $index => $link): ?>
509 <div class="oxyplug-preload-input-wrap">
510 <md-outlined-text-field class="oxyplug-preload-text-field has-clear-button"
511 name="preloads[font][]"
512 value="<?php echo esc_url($link) ?>"
513 label="<?php esc_attr_e('Font URL', 'oxyplug-preload'); ?>"
514 placeholder="https://www.example.com/wp-content/my-font.woff2"
515 type="url">
516 <md-icon-button toggle slot="trailing-icon" type="button">
517 <md-icon>cancel</md-icon>
518 </md-icon-button>
519 </md-outlined-text-field>
520 <md-icon-button class="oxyplug-preload-remove-url"
521 toggle
522 slot="trailing-icon"
523 type="button"
524 <?php if ($index == 0): ?> style="display:none" <?php endif; ?>>
525 <md-icon>delete</md-icon>
526 </md-icon-button>
527 </div>
528 <?php endforeach; ?>
529 <?php endif; ?>
530 <md-outlined-button class="oxyplug-preload-add-more">
531 <?php esc_html_e('Add More Font URL', 'oxyplug-preload') ?>
532 </md-outlined-button>
533 </div>
534 <div class="oxyplug-preload-card oxyplug-preload-self-height">
535 <h2>
536 <?php esc_html_e('Featured Image Preload', 'oxyplug-preload') ?>
537 <i class="dashicons dashicons-editor-help oxyplug-preload-has-tooltip"
538 data-tooltip="<?php esc_attr_e('Preload featured image', 'oxyplug-preload') ?>"
539 data-href="https://www.oxyplug.com/docs/oxy-preload/settings/?utm_source=plugin-settings&utm_medium=wordpress&utm_campaign=oxyplug-preload#preload-settings"
540 data-href-text="<?php esc_attr_e('Learn More', 'oxyplug-preload'); ?>"></i>
541 </h2>
542
543 <div class="oxyplug-preload-switch-wrap">
544 <md-switch icons
545 name="featured_image_preload" <?php if ($this->oxyplug_preload_get_option('_oxyplug_preload_featured_image', 'true') == 'true'): ?> selected <?php endif ?>></md-switch>
546 <span><?php esc_html_e('Preload featured image automatically? (No need to add URLs)', 'oxyplug-preload'); ?></span>
547 </div>
548 </div>
549 </div>
550
551 <md-divider class="oxyplug-preload-horizontal-divider"></md-divider>
552
553 <md-filled-button id="oxyplug-preload-save-preloads"
554 class="oxyplug-preload-has-loading"
555 data-nonce="<?php echo esc_attr(wp_create_nonce('oxyplug_preload_save_preloads')) ?>">
556 <?php esc_html_e('Save', 'oxyplug-preload'); ?>
557 </md-filled-button>
558 <div class="oxyplug-preload-spinner-wrap">
559 <div class="oxyplug-preload-spinner"></div>
560 <p><?php esc_html_e('Saving...Please wait.', 'oxyplug-preload'); ?></p>
561 </div>
562
563 </div>
564 <?php }
565
566 /**
567 * @return void
568 */
569 public function oxyplug_preload_save_preloads()
570 {
571 if (!empty($_POST['oxyplug_preload_save_preloads_nonce'])) {
572 $sanitized_nonce = sanitize_text_field(wp_unslash($_POST['oxyplug_preload_save_preloads_nonce']));
573 if (wp_verify_nonce($sanitized_nonce, 'oxyplug_preload_save_preloads')) {
574
575 // A nonce proves intent, not authorization — require the capability too.
576 if (!current_user_can('manage_options')) {
577 wp_send_json(array('messages' => array(esc_html__('Permission denied.', 'oxyplug-preload'))), 403);
578 }
579
580 // Featured image preload
581 $preload_featured_image = empty($_POST['featured_image_preload']) ? 'false' : 'true';
582 $this->oxyplug_preload_update_option('_oxyplug_preload_featured_image', $preload_featured_image);
583
584 // CSS/JS/Font preload
585 if (!empty($_POST['preloads']) && is_array($_POST['preloads'])) {
586 $valid_links = array();
587 $htaccess_preloads = array();
588 $link_regex = '/^(https?:\/\/([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/[-a-zA-Z0-9@:%._+~#=]*)*(\?[a-zA-Z0-9=&._-]*)?)?$/';
589
590 foreach ($_POST['preloads'] as $key => $links) {
591 if (in_array((string)($key), array('script', 'style', 'font'))) {
592 foreach ($links as $link) {
593 $is_valid = preg_match($link_regex, $link);
594 if (!empty(trim($link)) && $is_valid) {
595 $md5link = md5($link);
596 if (!isset($valid_links[$key][$md5link])) {
597 $escaped_url = esc_url($link);
598 $valid_links[$key][$md5link] = $escaped_url;
599
600 // Store for htaccess generation
601 if (!isset($htaccess_preloads[$key])) {
602 $htaccess_preloads[$key] = array();
603 }
604 $htaccess_preloads[$key][] = $escaped_url;
605 }
606 }
607 }
608
609 if (isset($valid_links[$key])) {
610 $valid_links[$key] = array_values($valid_links[$key]);
611 }
612 }
613 }
614
615 // Generate htaccess content
616 $htaccess_content = $this->generate_htaccess_content($htaccess_preloads);
617
618 // Add preload URLs to .htaccess file
619 $write_to_htaccess_result = $this->update_htaccess($htaccess_content);
620 if ($write_to_htaccess_result !== true) {
621 wp_send_json(array('messages' => array($write_to_htaccess_result)), 500);
622 }
623
624 // Insert preload URLs into the database
625 $this->oxyplug_preload_update_option('_oxyplug_preload_preloads', $valid_links);
626 }
627
628 wp_send_json_success(array('messages' => array(esc_html__('Successfully saved.', 'oxyplug-preload'))), 200);
629 }
630
631 wp_send_json(array('messages' => array(esc_html__('Wrong wpnonce. Refresh the page.', 'oxyplug-preload'))), 403);
632 }
633 }
634
635 /**
636 * Remove the `# BEGIN Oxyplug Preload ... # END Oxyplug Preload` block from
637 * a chunk of .htaccess content.
638 *
639 * This is the single source of truth for the marker pattern, shared by
640 * update_htaccess() and deactivate_it(). On a regex failure the original
641 * content is returned unchanged so a corrupt match never blanks the file.
642 *
643 * @param string $content
644 *
645 * @return string
646 */
647 private function strip_oxyplug_section($content): string
648 {
649 $pattern = '/\n*# BEGIN Oxyplug Preload\n.*?# END Oxyplug Preload\n*/s';
650 $stripped = preg_replace($pattern, '', $content);
651
652 return $stripped === null ? $content : $stripped;
653 }
654
655 /**
656 * Generate .htaccess content for preloading
657 *
658 * @param array $preloads Array of preload URLs grouped by type
659 * @return string Generated .htaccess content
660 */
661 private function generate_htaccess_content($preloads)
662 {
663 $htaccess_content = '';
664
665 // Add preload directives
666 foreach ($preloads as $type => $urls) {
667 foreach ($urls as $url) {
668 $htaccess_content .= " Header append Link \"<$url>; rel=preload; as=$type";
669 if ($type == 'font') {
670 $mime = $this->font_mime_type($url);
671 if ($mime) {
672 $htaccess_content .= "; type=$mime";
673 }
674 $htaccess_content .= '; crossorigin';
675 }
676 $htaccess_content .= "\"\n";
677 }
678 }
679
680 if (!empty($htaccess_content)) {
681 $htaccess_content =
682 ' <FilesMatch "index\.(html|htm|php)$">' . "\n" .
683 ' <IfModule mod_headers.c>' . "\n" .
684 $htaccess_content .
685 ' </IfModule>' . "\n" .
686 ' </FilesMatch>' . "\n";
687 }
688
689 return $htaccess_content;
690 }
691
692 /**
693 * Resolve a font URL's MIME type from its file extension.
694 *
695 * Declaring `type=` on a font preload lets the browser skip the hint when it
696 * can't use that format, and avoids the double-fetch some browsers do when
697 * the type is omitted.
698 *
699 * @param string $url
700 *
701 * @return string Empty string when the extension is unknown.
702 */
703 private function font_mime_type($url): string
704 {
705 $extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? $url, PATHINFO_EXTENSION));
706
707 $mime_types = array(
708 'woff2' => 'font/woff2',
709 'woff' => 'font/woff',
710 'ttf' => 'font/ttf',
711 'otf' => 'font/otf',
712 'eot' => 'application/vnd.ms-fontobject',
713 );
714
715 return $mime_types[$extension] ?? '';
716 }
717
718 /**
719 * @param $htaccess_content
720 *
721 * @return string|true
722 */
723 private function update_htaccess($htaccess_content)
724 {
725 $htaccess_path = ABSPATH . '.htaccess';
726 global $wp_filesystem;
727
728 if (!$wp_filesystem) {
729 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
730 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
731 $wp_filesystem = new \WP_Filesystem_Direct(null);
732 }
733
734 if (!$wp_filesystem->exists($htaccess_path) && $this->is_server(array('iis', 'nginx'))) {
735 wp_send_json(array('messages' => array(esc_html__('Your server does not support .htaccess file.', 'oxyplug-preload'))), 422);
736 }
737
738 if ($wp_filesystem->is_writable($htaccess_path) && $wp_filesystem->is_readable($htaccess_path)) {
739 $htaccess_backup_path = $htaccess_path . '.oxybackup';
740
741 try {
742 // Read current content
743 $current_content = $wp_filesystem->get_contents($htaccess_path);
744 if ($current_content === false) {
745 throw new Exception(esc_html__('Could not read .htaccess file.', 'oxyplug-preload'));
746 }
747
748 // Remove existing Oxyplug Preload section
749 $current_content = rtrim($this->strip_oxyplug_section($current_content));
750
751 if (!empty($htaccess_content)) {
752 // Create new section with headers
753 $section = "\n\n# BEGIN Oxyplug Preload\n";
754 $section .= $htaccess_content;
755 $section .= "# END Oxyplug Preload\n";
756
757 // Append the new section to the content
758 $htaccess_content = $current_content . $section;
759 } else {
760 $htaccess_content = $current_content;
761 }
762
763 // Create backup before making changes
764 if (!$wp_filesystem->copy($htaccess_path, $htaccess_backup_path, true)) {
765 throw new Exception(esc_html__('Failed to create .htaccess backup file.', 'oxyplug-preload'));
766 }
767
768 // Write new content
769 if (!$wp_filesystem->put_contents($htaccess_path, $htaccess_content)) {
770 throw new Exception(esc_html__('Failed to write new .htaccess content.', 'oxyplug-preload'));
771 }
772
773 // Quick site check for errors or 500 status
774 if (!$this->check_site_availability()) {
775 throw new Exception(esc_html__('Site check failed after .htaccess update.', 'oxyplug-preload'));
776 }
777
778 // Success - clean up backup
779 $wp_filesystem->delete($htaccess_backup_path);
780 return true;
781
782 } catch (Exception $e) {
783 // Restore backup if available
784 if ($wp_filesystem->exists($htaccess_backup_path)) {
785 $wp_filesystem->copy($htaccess_backup_path, $htaccess_path, true);
786 $wp_filesystem->delete($htaccess_backup_path);
787 }
788
789 return esc_html__('Oxyplug Preload .htaccess update failed: ' . $e->getMessage(), 'oxyplug-preload');
790 }
791 }
792
793 return esc_html__('Oxyplug Preload .htaccess update failed.', 'oxyplug-preload');
794 }
795
796 /**
797 * @return bool
798 */
799 private function check_site_availability(): bool
800 {
801 // Try REST url first (fastest)
802 $rest_url = get_rest_url();
803 $response = wp_remote_head($rest_url, [
804 'timeout' => 5,
805 'redirection' => 0,
806 'sslverify' => false
807 ]);
808
809 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) < 500) {
810 return true;
811 }
812
813 // Fallback to admin-ajax.php
814 $response = wp_remote_head(admin_url('admin-ajax.php'), [
815 'timeout' => 5,
816 'redirection' => 0,
817 'sslverify' => false
818 ]);
819
820 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) < 500) {
821 return true;
822 }
823
824 // Last resort - check homepage
825 $response = wp_remote_head(home_url(), [
826 'timeout' => 5,
827 'redirection' => 0,
828 'sslverify' => false
829 ]);
830
831 return !is_wp_error($response) && wp_remote_retrieve_response_code($response) < 500;
832 }
833
834 /**
835 * @param array $servers
836 *
837 * @return bool
838 */
839 private function is_server(array $servers): bool
840 {
841 $server = '';
842 $server_software = strtolower(sanitize_text_field($_SERVER['SERVER_SOFTWARE'] ?? ''));
843 if (strpos($server_software, 'apache') !== false) {
844 $server = 'apache';
845 } elseif (strpos($server_software, 'litespeed') !== false) {
846 $server = 'litespeed';
847 } elseif (strpos($server_software, 'nginx') !== false) {
848 $server = 'nginx';
849 } elseif (strpos($server_software, 'microsoft-iis') !== false || strpos($server_software, 'expressiondevserver') !== false) {
850 $server = 'iis';
851 }
852
853 return in_array($server, $servers);
854 }
855
856 /**
857 * @param $option_name
858 * @param $default
859 *
860 * @return false|mixed|void
861 */
862 protected function oxyplug_preload_get_option($option_name, $default = false)
863 {
864 if (is_multisite()) {
865 $network_id = get_current_blog_id();
866
867 return get_network_option($network_id, $option_name, $default);
868 }
869
870 return get_option($option_name, $default);
871 }
872
873 /**
874 * @param $option_name
875 * @param $option_value
876 *
877 * @return void
878 */
879 protected function oxyplug_preload_update_option($option_name, $option_value): void
880 {
881 if (is_multisite()) {
882 update_network_option(get_current_blog_id(), $option_name, $option_value);
883 } else {
884 update_option($option_name, $option_value);
885 }
886 }
887 }
888
889 new OxyPreload();
890
891