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 |