D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
proc
/
self
/
root
/
var
/
tmp
/
Filename :
phpUv9nuq
back
Copy
guest.vary.php 0000644 00000000261 15162130311 0007354 0 ustar 00 <?php /** * Lightweight script to update guest mode vary * * @since 4.1 */ require 'lib/guest.cls.php'; $guest = new \LiteSpeed\Lib\Guest(); $guest->update_guest_vary(); phpcs.xml.dist 0000644 00000004074 15162130312 0007344 0 ustar 00 <?xml version="1.0"?> <ruleset name="LiteSpeed Cache Coding Standards"> <description>Apply LiteSpeed Cache Coding Standards to all plugin files</description> <!-- ############################################################################# COMMAND LINE ARGUMENTS https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-Ruleset ############################################################################# --> <!-- Only scan PHP files --> <arg name="extensions" value="php"/> <!-- Cache scan results to use for unchanged files on future scans --> <arg name="cache" value=".cache/phpcs.json"/> <!-- Set memory limit to 512M Ref: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#specifying-phpini-settings --> <ini name="memory_limit" value="512M"/> <!-- Remove unwanted prefix from filepaths --> <arg name="basepath" value="./"/> <!-- Check max 20 files in parallel --> <arg name="parallel" value="20"/> <!-- Show sniff codes in all reports --> <arg value="ps"/> <!-- ############################################################################# FILE SELECTION Set which files will be subject to the scans executed using this ruleset. ############################################################################# --> <file>.</file> <!-- Exclude any wordpress folder in the current directory --> <exclude-pattern type="relative">^wordpress/*</exclude-pattern> <!-- Directories and third-party library exclusions --> <exclude-pattern>/node_modules/*</exclude-pattern> <exclude-pattern>/vendor/*</exclude-pattern> <!-- ############################################################################# SET UP THE RULESET ############################################################################# --> <!-- Check PHP v7.2 and all newer versions --> <config name="testVersion" value="7.2-"/> <rule ref="PHPCompatibility"> <!-- Exclude false positives --> <!-- array_key_firstFound is defined in lib/php-compatibility.func.php --> <exclude name="PHPCompatibility.FunctionUse.NewFunctions.array_key_firstFound" /> </rule> </ruleset> litespeed-cache.php 0000644 00000015502 15162130314 0010273 0 ustar 00 <?php /** * Plugin Name: LiteSpeed Cache * Plugin URI: https://www.litespeedtech.com/products/cache-plugins/wordpress-acceleration * Description: High-performance page caching and site optimization from LiteSpeed * Version: 7.1 * Author: LiteSpeed Technologies * Author URI: https://www.litespeedtech.com * License: GPLv3 * License URI: http://www.gnu.org/licenses/gpl.html * Text Domain: litespeed-cache * Domain Path: /lang * * Copyright (C) 2015-2025 LiteSpeed Technologies, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ defined('WPINC') || exit(); if (defined('LSCWP_V')) { return; } !defined('LSCWP_V') && define('LSCWP_V', '7.1'); !defined('LSCWP_CONTENT_DIR') && define('LSCWP_CONTENT_DIR', WP_CONTENT_DIR); !defined('LSCWP_DIR') && define('LSCWP_DIR', __DIR__ . '/'); // Full absolute path '/var/www/html/***/wp-content/plugins/litespeed-cache/' or MU !defined('LSCWP_BASENAME') && define('LSCWP_BASENAME', 'litespeed-cache/litespeed-cache.php'); //LSCWP_BASENAME='litespeed-cache/litespeed-cache.php' /** * This needs to be before activation because admin-rules.class.php need const `LSCWP_CONTENT_FOLDER` * This also needs to be before cfg.cls init because default cdn_included_dir needs `LSCWP_CONTENT_FOLDER` * @since 5.2 Auto correct protocol for CONTENT URL */ $WP_CONTENT_URL = WP_CONTENT_URL; $home_url = home_url('/'); if (substr($WP_CONTENT_URL, 0, 5) == 'http:' && substr($home_url, 0, 5) == 'https') { $WP_CONTENT_URL = str_replace('http://', 'https://', $WP_CONTENT_URL); } !defined('LSCWP_CONTENT_FOLDER') && define('LSCWP_CONTENT_FOLDER', str_replace($home_url, '', $WP_CONTENT_URL)); // `wp-content` !defined('LSWCP_PLUGIN_URL') && define('LSWCP_PLUGIN_URL', plugin_dir_url(__FILE__)); // Full URL path '//example.com/wp-content/plugins/litespeed-cache/' /** * Static cache files consts * @since 3.0 */ !defined('LITESPEED_DATA_FOLDER') && define('LITESPEED_DATA_FOLDER', 'litespeed'); !defined('LITESPEED_STATIC_URL') && define('LITESPEED_STATIC_URL', $WP_CONTENT_URL . '/' . LITESPEED_DATA_FOLDER); // Full static cache folder URL '//example.com/wp-content/litespeed' !defined('LITESPEED_STATIC_DIR') && define('LITESPEED_STATIC_DIR', LSCWP_CONTENT_DIR . '/' . LITESPEED_DATA_FOLDER); // Full static cache folder path '/var/www/html/***/wp-content/litespeed' !defined('LITESPEED_TIME_OFFSET') && define('LITESPEED_TIME_OFFSET', get_option('gmt_offset') * 60 * 60); // Placeholder for lazyload img !defined('LITESPEED_PLACEHOLDER') && define('LITESPEED_PLACEHOLDER', 'data:image/gif;base64,R0lGODdhAQABAPAAAMPDwwAAACwAAAAAAQABAAACAkQBADs='); // Auto register LiteSpeed classes require_once LSCWP_DIR . 'autoload.php'; // Define CLI if ((defined('WP_CLI') && WP_CLI) || PHP_SAPI == 'cli') { !defined('LITESPEED_CLI') && define('LITESPEED_CLI', true); // Register CLI cmd if (method_exists('WP_CLI', 'add_command')) { WP_CLI::add_command('litespeed-option', 'LiteSpeed\CLI\Option'); WP_CLI::add_command('litespeed-purge', 'LiteSpeed\CLI\Purge'); WP_CLI::add_command('litespeed-online', 'LiteSpeed\CLI\Online'); WP_CLI::add_command('litespeed-image', 'LiteSpeed\CLI\Image'); WP_CLI::add_command('litespeed-debug', 'LiteSpeed\CLI\Debug'); WP_CLI::add_command('litespeed-presets', 'LiteSpeed\CLI\Presets'); WP_CLI::add_command('litespeed-crawler', 'LiteSpeed\CLI\Crawler'); } } // Server type if (!defined('LITESPEED_SERVER_TYPE')) { if (isset($_SERVER['HTTP_X_LSCACHE']) && $_SERVER['HTTP_X_LSCACHE']) { define('LITESPEED_SERVER_TYPE', 'LITESPEED_SERVER_ADC'); } elseif (isset($_SERVER['LSWS_EDITION']) && strpos($_SERVER['LSWS_EDITION'], 'Openlitespeed') === 0) { define('LITESPEED_SERVER_TYPE', 'LITESPEED_SERVER_OLS'); } elseif (isset($_SERVER['SERVER_SOFTWARE']) && $_SERVER['SERVER_SOFTWARE'] == 'LiteSpeed') { define('LITESPEED_SERVER_TYPE', 'LITESPEED_SERVER_ENT'); } else { define('LITESPEED_SERVER_TYPE', 'NONE'); } } // Checks if caching is allowed via server variable if (!empty($_SERVER['X-LSCACHE']) || LITESPEED_SERVER_TYPE === 'LITESPEED_SERVER_ADC' || defined('LITESPEED_CLI')) { !defined('LITESPEED_ALLOWED') && define('LITESPEED_ALLOWED', true); } // ESI const definition if (!defined('LSWCP_ESI_SUPPORT')) { define('LSWCP_ESI_SUPPORT', LITESPEED_SERVER_TYPE !== 'LITESPEED_SERVER_OLS' ? true : false); } if (!defined('LSWCP_TAG_PREFIX')) { define('LSWCP_TAG_PREFIX', substr(md5(LSCWP_DIR), -3)); } /** * Handle exception */ if (!function_exists('litespeed_exception_handler')) { function litespeed_exception_handler($errno, $errstr, $errfile, $errline) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } } /** * Overwrite the WP nonce funcs outside of LiteSpeed namespace * @since 3.0 */ if (!function_exists('litespeed_define_nonce_func')) { function litespeed_define_nonce_func() { /** * If the nonce is in none_actions filter, convert it to ESI */ function wp_create_nonce($action = -1) { if (!defined('LITESPEED_DISABLE_ALL') || !LITESPEED_DISABLE_ALL) { $control = \LiteSpeed\ESI::cls()->is_nonce_action($action); if ($control !== null) { $params = array( 'action' => $action, ); return \LiteSpeed\ESI::cls()->sub_esi_block('nonce', 'wp_create_nonce ' . $action, $params, $control, true, true, true); } } return wp_create_nonce_litespeed_esi($action); } /** * Ori WP wp_create_nonce */ function wp_create_nonce_litespeed_esi($action = -1) { $uid = get_current_user_id(); if (!$uid) { /** This filter is documented in wp-includes/pluggable.php */ $uid = apply_filters('nonce_user_logged_out', $uid, $action); } $token = wp_get_session_token(); $i = wp_nonce_tick(); return substr(wp_hash($i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), -12, 10); } } } /** * Begins execution of the plugin. * * @since 1.0.0 */ if (!function_exists('run_litespeed_cache')) { function run_litespeed_cache() { //Check minimum PHP requirements, which is 7.2 at the moment. if (version_compare(PHP_VERSION, '7.2.0', '<')) { return; } //Check minimum WP requirements, which is 5.3 at the moment. if (version_compare($GLOBALS['wp_version'], '5.3', '<')) { return; } \LiteSpeed\Core::cls(); } run_litespeed_cache(); } changelog.txt 0000644 00000405261 15162130315 0007241 0 ustar 00 = 5.6 - Aug 1 2023 = * 🌱**Page Optimize** New JS Delay Includes option. (Mitchell Krog/Gerard Reches/Ignacy Hołoga) * **Crawler** Sitemap can use search for URL now. * **GUI** Restrict the scope of balloon CSS rules to avoid conflicts. (#567) * **Object Cache** Detect Memcached in more situations. (#568) * **API** Support `litespeed_purged_front` hook. (Umberto Fiorelli) = 5.5.1 - Jul 19 2023 = * 🐞**Image Optimization** Fixed a bug where WebP replacements couldn't be pulled without optimizing the original images. * 🐞**Image Optimization** Invalid images will now be removed when sending requests to the server. (#138993) * **Cloud** Added support for error codes `unpulled_images` and `blocklisted`. (Tynan) = 5.5 - Jun 20 2023 = * 🌱**Crawler** Can now use multiple sitemaps. (Tobolo/Tim Nolte) * 🌱**Crawler** Now runs asynchronously when manually invoked. * 🌱**Crawler** Now runs asynchronously when invoked from cron. * 🐞**Crawler** Fixed the realtime status bug when crawling. * **Crawler** Summary page now displays server load. (Ruikai) * 🐞**Page Optimize** Fixed an issue where UCSS could not be generated for error pages. (james58899) #556 * 🌱**Image Optimize** Now pulls images asynchronously. * **Image Optimize** Now prevents concurrent requests via a locking mechanism. * **Image Optimize** The process can now bypass invalid image records and continue. * 🐞**Image Optimize** Fixed an issue where images ready for optimization might have to wait for new images to be added before sending the request. * **Cloud** Replaced dashboard links with login/link to my.quic.cloud actions. * **GUI** Added indicators to show when certain options are passively enabled by Guest Mode. * **Htaccess** Added a noabort rule to support asynchronous crawling. * **Htaccess** The "Do Not Cache User Agents" option is now case-insensitive. (Ellen Dabo) * **General** The "Server IP" option now allows IPv4 format only. (Ruikai) * **Misc** Every page's closing HTML comments now displays UCSS/CCSS status. * **Object** Fixed a warning for null get_post_type_object. * **Object** Object_Cache::delete now always returns a boolean value. * **Cache** Fixed advanced-cache.php file warnings for WordPress versions less than 5.3. * **Debug** Added debug logging to record the plugin's total processing time. * **API** HTML minification can now be bypassed via the litespeed_html_min filter. = 5.4 - Apr 19 2023 = * **Image Optimize** Refactored DB storage for this feature. * **Image Optimize** Reduced DB table size. * **Image Optimize** Existing `img_optm` DB tables will have their data gradually transitioned to the new storage format with this update. Once an `img_optm` table is empty, it won't be used anymore. * **Page Optimize** Enabled WebP support for Googlebot User Agent. = 5.3.3 - Feb 22 2023 = * **Page Optimize** Excluded Jetpack stats JS. * **DB Optimize** Fixed DB Optm SQL for revision postmeta. * **Cache** Fixed an undefined array key warning. * **Purge** Prevented undefined array key warning when widgets are disabled. * **Object** Fixed dynamic property deprecation warnings. * **Admin** Safely redirect to homepage if referer is unknown. * **Activation** Check that item slug exists first. * **Cache** Prevented cache header to send globally if header part already closed. * **CSS** Improved string handling for CSS minifier. * **Debug** Fixed undefined array key warnings. * **Misc** Fixed implicit conversion in random string generation function `Str::rrand`. = 5.3.2 - Jan 10 2023 = * **Object** Fixed object cache lib incr, decr functions (thanks bdrbros/DANIEL) #516 * **Database Optimize** Database optimizer now handles postmeta when cleaning revisions #515 * **Cache** Made nocache the default for 4xx/5xx response codes. * **Cache** Default cache TTL settings removed for 403 response code, changed to 10 mins for 500 response code. * **GUI** Added a description for the redetect nodes function. * **GUI** Added a description for the refresh button sync function. = 5.3.1 - Dec 12 2022 = * **CLI** Presets feature is now usable from the CLI. (xLukii) * **CLI** Added 'import_remote' for litespeed-option to enable importing options from URLs. (xLukii) * **Cache** Added LiteSpeed headers to site health check for full page cache. * **Crawler* Fixed unauthorized crawler toggle operation. (#CVE-2022-46800) * **UCSS** Fixed a bug where items weren't added back to the UCSS queue after purging. * **Page Optimize** Fixed a bug where generated CSS would return 404 after upgrading via CLI. * **3rd** Fixed a bug where a WooCommerce session doesn't exist when checking cart, notices (Jason Levy/Gilles) * **GUI** Made LiteSpeed admin notice icon grayscale to avoid distraction. (martinsauter) * **GUI** Fixed RTL style for notification icon. * **API** Added a new hook `litespeed_optm_uri_exc` to exclude URI from page optimization. * **API** Excluded `.well-known` path from page optimization. = 5.3 - Oct 31 2022 = * 🌱**Presets** New `Presets` feature and menu item. * 🌱**UCSS** New option `UCSS File Excludes and Inline` to increase page score. (Ankit) * **UCSS** When UCSS is purged, automatically append URL to UCSS generation queue. (Ankit) * **Page Optimize** Removed a redundant `defer` attribute from Lazy Load image library usage. (#928019) * **Image Optimize** Dropped `Create WebP Versions` setting. Will automatically enable when `Image WebP Replacement` is activated. * **Cloud** Fixed a bug where internal updates were delayed for API keys. * **Cloud** Improved auto alias feature by waiting for second request from alias domain validation before removing a pending alias. * **Purge** Automatically Purge All when plugin auto update is done. * **Purge** Fixed a potential PHP8 error that occurred when removing unused widgets. (acsnaterse) * **Cache** Fixed an infinite 301 redirection caused by UTM-encoded link. * **CLI** Added syntax examples for values that include line breaks (xLukii) * **CLI** Purge requests will now be included with the original request to avoid potential CSS/JS 404 issues. * **ESI** Check all user roles for cache vary and page optimization excludes. * **GUI** Added a LiteSpeed icon to admin message banners to indicate the banners are from our plugin. (Michael D) * **Crawler** Fixed a cache-miss issue that occurred when Guest Mode was ON and WebP Replacement was OFF. * **3rd** Remove WooCommerce private cache. * **3rd** Removed LiteSpeed metabox from ACF field group edit page. (keepmovingdk) = 5.2.1 - Sep 7 2022 = * 🐞**Core** Fixed a fatal error that occurred when uninstalling. (#894556 Hostinger) * **Dash** Show partner info on the dashboard for partner-tier QC accounts. * **UCSS** Auto-purge UCSS on post update. (Ankit) * 🕸️**Crawler** Respect the `LITESPEED_CRAWLER_DISABLE_BLOCKLIST` constant for unexpected results too. (Abe) = 5.2 - Aug 17 2022 = * 🌱**UCSS** Added UCSS message queue to improve service quality and reliability * 🐞**VPI** Fixed conflict w/ image lazyload; used HTML before image lazyload to avoid invalid `data:base64` results. * **VPI** Changed VPI Cron default setting to OFF. * **VPI** Automatically resend requests when VPI result contains invalid `data:` image value. * **Conf** Fixed an issue with URI Excludes, where paths using both ^ and $ were not correctly excluded (Eric/Abe) * **Conf** Auto corrected `WP_CONTENT_URL` protocol if it was explicitly set to `http://`. * **Cloud** No longer sync the configuration to QUIC.cloud if configuration is unchanged. * **Cloud** Appended home_url value into synced configuration data for wp-content folder path correction. * 🕸️**Crawler** Improved compatibility with server `open_basedir` PHP setting limit when detecting load before crawling. (Tom Robak/mmieszalski) = 5.1 - Aug 1 2022 = * 🌱**Toolbox** Debug log can now show Purge/Crawler logs as well. (Tynan) * **UCSS** Prepared for future message queue. * **UCSS** Moved UCSS class to its own file. * **3rd** Added 3rd-party support for WC PDF Product Vouchers. (Tynan) * **Core** Fixed potential PHP warning when saving summary data. (Sarah Richardson) * **Purge** Purge can now clear the summary correctly. (Kevin) * **VPI** Added `queue_k` to API notification. = 5.0.1 - Jul 27 2022 = * 🐞**Cloud** Fixed a potential PHP error that could occur with the cloud service summary. (Bruno Cantuaria) * **3rd** Added Autoptimize back to compatibility list. = 5.0.0.1 - Jul 26 2022 = * 🔥🐞**Cloud** Fixed an issue with the cloud request timestamp update which causes a usage sync failure. (great thanks to Kevin) = 5.0 - Jul 25 2022 = * 🌱**VPI** Added Viewport Images feature to LiteSpeed Options metabox on Post Edit page. * 🌱**CDN** Added Auto CDN Setup feature for simple QUIC.cloud CDN setup. (Kevin) * 🌱**Page Optimize** Automatically cache remote CSS/JS files when fetching for optimization (Lauren) * 🌱**Cache** Added LiteSpeed Options for page-level cache control on Post Edit page. (denisgomesfranco) * 🌱**Cloud** Auto Alias feature. * 🌱**Debug** Added `Debug String Excludes` option. (Hanna) * 🌱**UCSS** Added `Purge this page - UCSS` option to Admin Bar dropdown menu. (Ankit) * 🌱**Guest** Added `litespeed_guest_off=1` URL query string parameter to bypass Guest Mode. (cbdfactum) * 🐞**Page Optimize** Fixed an issue where CSS anchors could be wrongly converted to a full path when minifying. (Tynan) * **Page Optimize** Bypass CCSS/UCSS generation when a self-crawled CSS resource returns a 404 code. (Abe) * **Object** Allow `LSCWP_OBJECT_CACHE` predefined to turn off Object Cache. (knutsp) * **Data** Fixed an issue where empty version tags in the database repeatedly toggled the upgrade banner and reset settings to default. * **Purge** Fixed an issue where the site's index page could be purged upon deletion of an unviewable post. (Kevin) * **Toolbox** Added `View site before optimization` button under `Debug` tab. (Ryan D) * **Admin** Switch to using the `DONOTCACHEPAGE` constant to indicated WP-Admin pages are not cacheable. * **Admin** Moved no-cache header to very beginning to avoid caching unexpected exits. * **Cloud** Added message queue service for VPI. (Abe) * **Cloud** Bypassed 503 error nodes from node redetection process. (Abe) * **Cloud** Fixed a failure to detect `out_of_quota`. (Lauren) * **Cloud** Added ability to display dismissable banners generated by QUIC.cloud. * 🕸️**Crawler** Added realtime load detection before crawl. * 🕸️**Crawler** Adjusted crawler behavior for Divi pages to allow for Divi's CCSS generation process. (miketemby) * 🕸️**API** PHP constant `LITESPEED_CRAWLER_DISABLE_BLOCKLIST` and filter `litespeed_crawler_disable_blocklist` to disable blocklist. (Tobolo) * **CDN** Automatically add a trailing slash to `CDN URL` and `Original URLs` if user didn't provide one. (Lucas) * **Cache** When a URL redirects to a URL with a query string, consider these as different for caching purposes. (Shivam) * **Media** Added ability to disable lazyload from the LiteSpeed Options metabox on Post Edit page. * **Media** Added new default values to `WebP Attribute to Replace` setting for WPBakery and Slider Revolution. (JibsouX) * **Image Optimize** Dropped redundant `Page Speed` user agent when serving WebP images. (serpentdriver) * **GUI** Fixed an issue where manually dismissable admin messages were instead being treated as one-time messages. (Tynan Beatty) * **GUI** Fixed an issue where subsequent admin alerts would overwrite existing alerts in the queue. (Kevin/Tynan) * **GUI** Updated time offset in log. (Ruikai #PR444 #PR445) * **GUI** Added `litespeed_media_ignore_remote_missing_sizes` API description. * **CCSS** Fixed an issue where CCSS was unexpectedly bypassed if `CSS Combine` was OFF and `UCSS Inline` was ON. (Ruikai) * **Debug** Added response headers to debug log. (Kevin) = 4.6 - Mar 29 2022 = * **Page Optimize** Improved compatibility for JS Delay. * 🐞**Page Optimize** Fixed an issue for network subsites that occurred when only CSS/JS Minify are enabled. * **Localization** Added query string compatibility for Resource URLs. * **Vary** Fixed a potential PHP warning when server variable `REQUEST_METHOD` is not detected. * **Cache** Guest Mode now respects Cache Excludes settings. * **GUI** Added warning notice when enabling `Localize Resources` feature; each localized JS resource requires thorough testing! * **GUI** Fixed a PHP Deprecated warning that occurred with the Mobile Cache User Agent setting on PHP v8.1+. (jrmora) * **Conf** Removed Google related scripts from default `Localization Files` value. * **Media** WordPress core Lazy Load feature is now automatically disabled when LiteSpeed Lazy Load Images option is enabled. (VR51 #Issue440) * 🐞**API** Filter `litespeed_ucss_per_pagetype` for UCSS now also applies to CSS Combine to avoid UCSS failure. (Ankit) * **API** Added a filter `litespeed_media_ignore_remote_missing_sizes` to disable auto detection for remote images that are missing dimensions. (Lucas) = 4.5.0.1 - Feb 24 2022 = * 🔥🐞**Media** Fixed an issue where lazy-loaded images would disappear when using custom CSS image loading effects. = 4.5 - Feb 23 2022 = * 🌱**Page Optimize** Localization is back. * **Guest** Fixed organic traffic issue as different browsers may fail to set `document.referrer`. * **Image Optimize** Improved wp_postmeta table compatibility when gathering images. (Thanks to Thomas Stroemme) * 🐞**Page Optimize** Fixed a potential CSS/JS 404 issue for existing records that have been marked as expired. * **ESI** `LITESPEED_ESI_OFF` now affects `litespeed_esi_url` API filter too. * **Guest** Added a check to determine if Guest Mode is blocked by a third-party, and display warning if it is (Ruikai) * **Guest** To support WP sites with multiple domains, Guest Mode detection URL no longer uses domain. * **Report** Network now shows Toolbox page when having a large number of subsites. * **DB Optimize** Reduced default subsites count from 10 to 3 under Network Admin -> DB Optimize page to avoid timeout. * **Cloud** Fixed potential `lack_of_token` error when requesting domain key for cases where local summary value was not historically included in the array. * **Cloud** Fixed a PHP fatal error that occurred when encountering a frequency issue under CLI. (Dean Taylor #Issue410) * **Avatar** Force gravatar cache refresh in browsers and on CDN (rafaucau #PR430) * **API** New filter `litespeed_purge_ucss` to purge a single page UCSS. (#376681) * **API** New filter `litespeed_ucss_per_pagetype` for UCSS per page type generation. (Ankit) * **GUI** Replaced some GUI text and settings with more inclusive language (kebbet #PR437 #PR435) * **3rd** Excluded `WP Statistics` from inline JS optimize. (Ryan D) * **3rd** Added API filter `litespeed_3rd_aelia_cookies` for Aelia CurrencySwitcher. * **Media** Updated image lazyload library to 17.5.0. = 4.4.7 - Jan 11 2022 = * **Page Optimize** Dropped `Inline Lazy Load Images Library` option. Now will always inline lazyload library. (Ankit) * **3rd** Prevented JavaScript files from being appended to Rank Math SEO sitemap. * **Purge** Dropped default stale purge when purging a post. * **Cloud** Dropped unused API calls. * **Cloud** Dropped redundant IP validation in API calls. = 4.4.6 - Dec 27 2022 = * **Guest** Restored `document.referrer` for organic traffic purposes when Guest Mode is enabled. (michelefns) * **Image Optimize** Fixed a potential PHP notice when uploading images in WP w/ PHP7.4+. (titsmaker) * **ESI** Fixed an issue where ESI settings were not updated on customized widgets(#422 Abe) * **3rd** Reverted ESI Adminbar change on Elementor front pages for backward compatibility (#423 Abe) * **3rd** Fixed an issue where disabling ESI potential caused a PHP warning when using `Perfmatters`. (Jeffrey Zhang) * **Misc** Check whether HTTP_REFERER is set or not before using it in Router class. (#425 Abe) = 4.4.5 - Dec 1 2021 = * **Data** Fixed potential PHP notice when generating CSS/JS optimized files w/ PHP v7.4+. (Sarah Richardson/silencedgd/slr1979) * **API** Added `LITESPEED_ESI_OFF` constant to disable ESI, when defined before the WP `init` hook. * **API** Added `LSCWP_DEBUG_PATH` constant to specify debug log path. (khanh-nt) * 🐞**GUI** Fixed an issue where admin messages were not displayed. (Daniel McD) * **CDN** Used WP remote function to communicate w/ Cloudflare per WP guidance. * **3rd** Added compatibility for Perfmatters plugin's script manager (#417 Abe) * **3rd** Added compatibility for Elementor's Editor button when ESI is on (#418 Abe) = 4.4.4 - Nov 23 2021 = * **Page Optimize** Delay deletion of outdated CSS/JS files for a default of 20 days to avoid 404 errors with cached search engine copies. * **Cache** When caching, no longer send a purge request for CSS/JS removal to avoid cache engine conflicts. * 🐞**Core** Optimized SQL queries while autoloading if expected options are missing; reduced by 7 and 3 queries on backend and frontend respectively. (#396425 Jackson) * **Page Optimize** Fixed a 404 issue that occurred when upgrading the plugin manually, with a package upload or through the plugin manager. (Tobolo/Małgorzata/Abe) * **API** Added `litespeed_ccss_url` and `litespeed_ucss_url` API to manipulate the request URL for CCSS and UCSS. * **REST** Fixed a potential warning when detecting cacheable status on REST call. (rafaucau) * **OLS** Fixed an issue where the `COOKIEHASH` constant was undefined when used with OpenLiteSpeed as an MU plugin or with network activation. * **3rd** Sanitized POST data for nextgengallery. * **Cloud** Sanitized GET data when linking to QUIC.cloud. (#591762 WPScan) = 4.4.3 - Oct 13 2021 = * 🐞**Media** Fixed an issue where WebP is served erroneously under Guest Mode on older versions of Safari. (hash73) * 🐞**Media** Reverted regex change to fix `Lazy Load Image Parent Class Name Excludes` failure. (thpstock) * **Purge** Disabled `Purge Delay` in the optimization process by default. * **Conf** Dropped `.htaccess Path Settings` options for security concern. (WP) * **Conf** Dropped `CSS HTTP/2 Push`/`JS HTTP/2 Push` options. (Kevin) * **Conf** Set `Guest Optimization` default to OFF. * **Conf** Set `CCSS Per URL` default to OFF to avoid consuming more quota than intended after upgrade to v4. (n111) * **Object** Fixed an issue with Object Cache warnings during upgrade, when Guest Mode is enabled. * ☁️**Cloud** Fixed an issue with PHP notices when inquiring about quota usage for a service not currently in use. * **GUI** Added GO detail warning. (n111) * **GUI** Moved "quota will be still in use" warning from Guest Mode to Guest Optimization section. * **API** Added `LITESPEED_CFG_HTACCESS` PHP Constant to specify .htaccess path. * **API** Added `litespeed_qs_forbidden` hook to bypass `?LSCWP_CTRL=` query string. (minhduc) * **API** Added `litespeed_delay_purge` hook to delay the following Purge header until the next request. * **API** Added `litespeed_wpconfig_readonly` hook to disable `WP_CACHE` constant update based on the wp-config.php file. (#633545) = 4.4.2 - Sep 23 2021 = * **Purge** In order to clear pages containing 404 CSS/JS, the purge header will always be sent even in cases where purge must be delayed. * 🐞**Purge** Fixed a potential PHP warning caused when generating different optimized filenames. * **Cron** Dropped unnecessary HTML response in cron which sometimes resulted in wp-cron report email. (Gilles) * **Page Optimize** Purge caused by CSS/JS file deletion will now be silent. * **Page Optimize** Fixed an issue where the homepage failed to purge when addressing the 404 CSS/JS issue. * **Avatar** Fixed potential localized Avatar folder creation warning. (mattk0220/josebab) * **API** Added filter `litespeed_optm_html_after_head` to move all optimized code(UCSS/CCSS/Combined CSS/Combined JS) to be right before the `</head>` tag. (ducpl/Kris Regmi) * **Debug** Under debug mode, cache/purge tags will be plaintext. = 4.4.1 - Sep 16 2021 = * 🐞**ESI** Fixed ESI failure on non-cached pages caused by `DONOTCACHEPAGE` constant. * 🐞**Page Optimize** Fixed an issue where the minified CSS/JS file failed to update when the file was changed. (ceap80) * 🐞**Page Optimize** Fixed an issue where the combined CSS/JS file randomly returned a 404 error when visiting the same URL with different query strings. (Abe) * **API** Added `litespeed_const_DONOTCACHEPAGE` hook to control the cache-or-not result of the `DONOTCACHEPAGE` constant. = 4.4 - Sep 8 2021 = * 🌱**Crawler** Added the ability to enable or disable specific crawlers. (⭐ Contributed by Astrid Wang #PR390) * 🌱**UCSS** Added `UCSS Inline` option. (Ankit). * 🌱**UCSS** Added `UCSS URI Excludes` option. (RC Verma). * 🐞**Page Optimize** Fixed an issue where combined CSS/JS files would potentially return 404 errors after a Purge All. (Special thanks to Abe & Ruikai) * **Page Optimize** Minimized the potential for 404 errors by query string when Purging All. * **Page Optimize** Dropped redundant query strings for minified CSS/JS files. * **Conf** Ugrade configuration safely to avoid the issue of new functions not being found in old codebase. * **Conf** Configuration upgrade process now adds a notification to admin pages and disables configuration save until upgrade is complete. (Lisa) * **JS** Fixed an issue where JS Defer caused a `litespeed_var_1_ is not defined` error when enabled w/ ESI options. (Tobolo) * 🐞**JS** Fixed an issue where `JS Delay` doesn't work for combined JS when `JS Combine` is enabled. (Special thanks to Joshua & Ankit) * **JS** `JS Delay` now will continue loading JS, even if there is an error in the current JS loading process. * 🐞**CCSS** If CCSS fails to generate, Load CSS Asynchronously will now be disabled. (Stars #54074166) * 🐞**UCSS** If UCSS generation fails the generated error will no longer be served inside the file. (Ryan D) * **Log** Updated the Debug log to use less code for prefix. * **3rd** Always respect `DONOTCACHEPAGE` constant definition to fix DIVI dynamic css calculation process. = 4.3 - Aug 16 2021 = * **UCSS** Separated UCSS Purge from CCSS Purge. (⭐ Contributed by Alice Tang #PR388) * 🐞**Cloud** Fixed an issue where CCSS/UCSS quota data failed to update locally. * **Cloud** Added server load as a factor when detecting node availability. * **Cloud** Improved the speed of checking daily quota and showing the related error message. * **Cloud** Added ability to re-detect node availability if the current node is responding w/ a heavy load code. * **Cloud** CCSS/UCSS/LQIP queue now exits immediately when quota is depleted. * **Cloud** Replaced separate `d/regionnodes` with a single `d/nodes` in the node list API for image optimization. * **LQIP** Fixed an issue with LQIP network compatibility. (⭐ Contributed by Alice Tang #PR387) * **GUEST** JS no longer preloads for Guest Optimization. (Ankit) * 🐞**Data** Fixed an issue where deleting the `cssjs` data folder causes a failure in the upgrade process. (Joshua #PR391) * **GUI** Fixed a potential dashboard PHP warning when no queue existed. (jrmora) * **GUI** Added daily quota on dashboard. * **GUI** Added downgrade warning to Toolbox -> Beta Test. * **GUI** Tuned `.litespeed-desc` class to full width in CSS. * **Conf** `Preserve EXIF/XMP data` now defaults to ON due to copyright concerns. (Tobolo) * 🐞**3rd** Fixed a PHP warning when using Google AMP w/ /amp as structure. (thanhstran98) = 4.2 - Jul 29 2021 = * **Cloud** Auto redirect to a new node if the current node is not available anymore. * **Cloud** Combined CCSS/UCSS to sub services of Page Optimization. * **Cloud** Added a daily quota rate limit to help mitigate the heavy service load at the beginning of the month. * **Cloud** Cached the node IP list in order to speed up security check. (Lucas) * 🐞**GUEST** Fixed an issue where Guest Mode remained enabled even when the UA setting is empty. (Stars) * **GUEST** Guest Mode will no longer cache POST requests. * **UCSS** Purging CSS/JS now purges the UCSS queue as well, to avoid failure when generating UCSS. * **UCSS** Separated service entry `UCSS` from `CCSS`. * **CCSS** Simplified `load_queue/save_queue/build_filepath_prefix` functions. (⭐ Contributed by Alice Tang #PR373) * **CCSS** If CCSS request fails, details are now saved in the CSS file. * **CCSS** Renamed CCSS ID in inline HTML from `litespeed-optm-css-rules` to `litespeed-ccss`. (Alice) * **Page Optimize** CCSS/UCSS now supports Cloud queue/notify for asynchronous generation. * **Page Optimize** Simplified CCSS/UCSS generation function. * **Page Optimize** Added the ability to cancel CCSS/UCSS Cloud requests. * **Page Optimize** Unnecessary quesry strings will now be dropped from CSS/JS combined files. * **Crawler** Reset position now resets crawler running status too. * **REST** Cloud request to REST will now detect whether an IP in in the Cloud IP list for security reasons. * **Object** Enhanced Object Cache compatibility for `CONF_FILE` constant detection. * **API** Added shorter alias `litespeed_tag` and other similar aliases for Cache Tag API. * **API** Renamed `LITESPEED_BYPASS_OPTM` to `LITESPEED_NO_OPTM` for Page Optimization. * **Toolbox** Dropped v3.6.4- versions in Beta Test as they will cause a fatal error in downgrade. * **GUI** Added shortcut links to each section on the Dashboard. * **GUI** Added UCSS whitelist usage description. (wyb) * **GUI** Showed the default recommended values for Guest Mode UA/IPs. * **3rd** Fixed AMP plugin compatibility. (⭐ Contributed by Alice Tang #PR368) * **3rd** Bypassed all page optimization including CDN/WebP for AMP pages. * **3rd** Improved compatibility with All in One SEO plugin sitemap. (arnaudbroes/flschaves #Issue372) * **3rd** Added wsform nonce. (#365 cstrouse) * **3rd** Added Easy Digital Download (EDD) & WP Menu Cart nonce. (#PR366 AkramiPro) * **3rd** Improved compatibility w/ Restrict Content Pro. (Abe #PR370) * **3rd** Improved compatibility w/ Gravity Forms. (Ruikai #371) = 4.1 - Jun 25 2021 = * 🌱**UCSS/CCSS/LQIP** Moved queue storage to file system from database wp-options table to lessen the IO load. (#633504) * 🌱**3rd** Added an option to disable ESI for the WooCommerce Cart. (#358 Anna Feng/Astrid Wang) * **ESI** Fixed an ESI nonce issue introduced in v4.0. (Andrew Choi) * **Object** Used new `.litespeed_conf.dat` instead of `.object-cache.ini` for object cache configuration storage. * **Conf** Now updating related files after plugin upgrade and not just after activation. * 🌱**Guest** Added a Guest Mode JS Excludes option. (Ankit/Mamac/Rcverma) * **Guest** Guest Mode now uses a lightweight script to update guest vary for reduced server load. * **Guest** Guest Mode now adds missing image dimensions. * **Guest** Guest vary will no longer update if there's already a vary in place to address the infinite loop caused by CloudFlare's incorrect cache control setting for PHP. * **Guest** Guest vary update request will no longer be sent if `lscache_vary` is already set. * **Guest** Added a Configurable Guest Mode UA/IP under the Tuning tab in the General menu. * **Guest** Guest Mode now allows cron to be hooked, even when UCSS/CCSS options are off. (#338437 Stars) * **Guest** Simplified the vary generation process under Guest Mode. * **Guest** Added a Guest Mode HTML comment for easier debugging. (Ruikai) * **Guest** Guest vary update ajax now bypasses potential POST cache. * **CCSS** Added back the options `Separate CCSS Cache Post Types` and `Separate CCSS Cache URIs`. (Joshua/Ankit) * **CCSS** CCSS/UCSS queue is now limited to a maximum of 500 entries. * **Control** The cache control constant `LSCACHE_NO_CACHE` will now have a higher priority than the Forced Public Cache setting. * **Crawler** The Crawler can now crawl Guest Mode pages. * **Crawler** Fixed a potential XSS vulnerability in the Crawler settings. (#927355) * **Crawler** The Crawler now supports a cookie value of `_null`. (Tobolo) * **Media** Updated the default value for the Responsive Placeholder SVG to be transparent. * **Media** WebP images in the background may now be served in Guest Mode. * **Media** WebP images in CSS may now be bypassed if the requesting Guest Mode client doesn't support WebP. * **Media** Fixed empty default image placeholder under Guest Mode. * 🐞**Image Optimize** Changed the missing `$_POST` to `$post_data` so the database status is properly updated. (#345 Lucas) * **Import** Export file is now readable to allow importing of partial configurations. (Ryan D/Joshua) * **Page Optimize** Fixed W3 validator errors in Guest Mode. (#61393817) * **3rd** A fatal WooCommerce error is no longer triggered by a custom theme reusing a previous LSCWP cache detection tag. * **3rd** AMP may now bypass Guest Mode automatically. * **Localize** Dropped the `Localize Resources` option as Guest Mode is a sufficient replacement. (Note: Due to user feedback during the development period, we have decided to reinstate this option in a future version.) * **Cloud** Changed the WP API url. * **Lang** Corrected a missing language folder. * **GUI** Added a CCSS/UCSS loading page visualization. (⭐ Contributed by Astrid Wang & Anna Feng #PR360) * **GUI** Added a warning to indicate when Guest Mode CCSS/UCSS quota is in use. (Contributed by Astrid Wang & Anna Feng #PR361) * **GUI** Added a `litespeed-info` text color. (Astrid Wang) * **GUI** Implemented various UI/UX improvements. (Joshua/Lisa) * **GUI** Duplicate cloud service messages with the same content will only display once now. (Marc Dahl) * **GUI** Added a WebP replacement warning for Guest Mode Optimization if WebP replacement is off. * **Misc** Dropped `wp_assets` from distribution to reduce the package size. (lowwebtech) * **Misc** Increased the new version and score detection intervals. * **Misc** Optimized WP Assets images. (#352 lowwebtech) * **Debug** Dropped the redundant error_log debug info. = 4.0 - Apr 30 2021 = * 🌱🌱🌱**Guest** Introduced `Guest Mode` for instantly cacheable content on the first visit. * 🌱**UCSS** Added a new service: `Unique CSS`, to drop unused CSS from elements from combined CSS * 🌱**CCSS** Added `HTML Lazyload` option. (Ankit) * 🌱**CCSS** Added `CCSS Per URL` option to allow Critical CSS to be generated for each page instead of for each Post Type. * 🌱**Media** Added `Add Missing Sizes` setting for improving Cumulative Layout Shift. (Fahim) * 🌱**JS** Switched to new JS minification library for better compression and compatibility w/ template literals. (LuminSol) * **Media** WebP may now be replaced in CSS. * **Media** Can now drop image tags in noscript to avoid lazyload. (Abe #314 /mattthomas-photography) * **Media** Bypass optimization if a page is not cacheable. * **Image Optimize** Auto hook to `wp_update_attachment_metadata` to automate image gathering process, and to handle the new thumbnail generation after images are uploaded. (smerriman). * **Image Optimize** Repeated image thumbnails won't be gathered anymore. * **Image Optimize** Simplified the rescan/gather/upload_hook for existing image detection. * **Image Optimize** Fixed the duplicated optimize size records in the postmeta table. (Abe #315) * **Image Optimize** Allow either JSON POST request or normal form request in `notify_img`. (Lucas #313) * **Image Optimize** Optimized SQL query for better efficiency. (lucas/Lauren) * **Image Optimize** Fixed issue where rescan mass created duplicate images. (#954399) * **Image Optimize** Image optimization pie will not show 100% anymore if there is still a small amount in the unfinished queue. * **Image Optimize** WebP generation defaults to ON for Guest Mode. * **Image Optimize** `Priority Line` package now can have smaller request interval. * **ESI** Disable ESI when page is not cacheable. (titsmaker) * **ESI** Fixed an issue where Divi was disabling all in edit mode, but couldn't disable ESI. (Abe) * **ESI** ESI init moved under `init` hook from `plugin_loaded` hook. * **CDN** Add basic support for CloudFlare API Tokens (Abe #320) * **CSS** Simplified `Font Display Optimization` option. * **CSS** Fixed manual cron timeout issue. (jesse Distad) * **CSS** Inline CSS may now use `data-no-optimize` to be excluded from optimization. (popaionut) * **JS** Combined `Load JS Defer` and `Load Inline JS Defer` options. * **JS** Forced async to defer. * **JS** Moved Google Analytics JS from constant default to setting default for removal. * **JS** Fixed potential JS parsing issue caused by JS src being changed to data-src by other plugins. (ankit) * **JS** Excluded spotlight from JS optimize. (tobolo) * **CCSS** Fixed CCSS/UCSS manual cron timeout issue. * **CCSS** Only 10 items will be kept for CCSS history. * **CCSS** The appearance of CCSS Purge in the topbar menu will be determined by the existence of CCSS cache, and not the setting only. * **CCSS** To avoid stuck queues when the current request keeps failing, the CCSS queue will always drop once requested. * **CCSS** CCSS will no longer hide adminbar. * **CCSS** CCSS may now be separate for network subsites. (Joshua) * **CCSS** Gave CCSS a unique filename per URL per user role per subsite. * **CCSS** Dropped `Separate CCSS Cache Post Types` option. * **CCSS** Dropped `Separate CCSS Cache URIs` option. * **CCSS** Subsites purge Avatar/CSS/JS/CCSS will not affect the whole network anymore. * **CCSS** Implemented a better queue list for CCSS that auto collapses if there are more than 20 entries, and shows the total on top. * **CSSJS** Now using separate CSS and JS folders instead of `cssjs`. * **CSSJS** Automatically purge cache after CCSS is generated. * **Network** Dropped network CSS/JS rewrite rules. * **Cache** Send cache tag header whenever adding a tag to make it effective in the page optimization process. * **Core** Used hook for buffer optimization; Used `init()` instead of `constructor`. * **Object** Used `cls` instead of `get_instance` for init. * **Cloud** Replaced one-time message with a dismissible-only message when the domain key has been automatically cleared due to domain/key dismatch. * **API** Dropped function `hook_vary_add()`. * **API** Dropped function `vary_add()`. * **API** Dropped function `filter_vary_cookies()`. * **API** Dropped function `hook_vary()`. * **API** Dropped action `litespeed_vary_add`. * **API** Dropped filter `litespeed_api_vary`. * **API** Use `litespeed_vary_curr_cookies` and `litespeed_vary_cookies` for Vary cookie operations instead. * **API** Dropped action `litespeed_vary_append`. * **Vary** 3rd party vary cookies will not append into .htaccess anymore but only present in response vary header if in use. * **Vary** Dropped function `append()`. * **Vary** Commenter cookie is now considered cacheable. * **Crawler** Minor update to crawler user agent to accommodate mobile_detect.php (Abe #304) * **Data** Added a table truncate function. * **Data** Added new tables url & url_file. * **Data** Dropped cssjs table. * **Data** Options/Summary data is now stored in JSON format to speed up backend visit. (#233250) * **Data** Default `CSS Combine External and Inline` and `JS Combine External and Inline` to On for new installations for better compatibility. * **Purge** Fixed potential purge warning for certain themes. * **Purge** Purge will be stored for next valid visit to trigger if it is initially generated by CLI. * **Page Optimize** `CSS Combine`/`JS Combine` will now share the same file if the contents are the same. Limited disk usage for better file usage and fewer issues with random string problems. * **Page Optimize** Dropped option CSS/JS Cache TTL. * **Page Optimize** Bypass optimization if page not cacheable. * **Page Optimize** Purge CSS/JS will purge the `url_file` table too. * **Page Optimize** Optionally store a vary with a shorter value. * **Page Optimize** Removing query strings will no longer affect external assets. (ankit) * **Page Optimize** Better regex for optimization parsing. * **Page Optimize** Eliminated w3 validator for DNS prefetch and duplicated ID errors. (sumit Pandey) * **Page Optimize** New Optimization for Guest Only option under Tuning. * **Page Optimize** Now forbidding external link redirection for localization. * **Debug** Implemented a better debug format for the 2nd parameter in the log. * **GUI** Bypass page score banner when score is not detected (both 0). (ankit) * **GUI** Fixed deprecated JQuery function warning in WP-Admin. (krzxsiek) = 3.6.4 - Mar 15 2021 = * **Toolbox** Fixed Beta Test upgrade error when upgrading to v3.7+. = 3.6.3 - Mar 10 2021 = * **Core** Fixed potential upgrade failure when new versions have changes in activation related functions. * **Core** Upgrade process won't get deactivated anymore on Network setup. = 3.6.2 - Feb 1 2021 = * **Page Optimize** Fixed an issue where network purge CSS/JS caused 404 errors for subsites. * **Page Optimize** Fixed an issue where purge CSS/JS only caused 404 errors. * **Page Optimize** Added a notice for CSS/JS data detection and potential random string issue. * **Page Optimize** Limited localization resources to specified .js only. (closte #292/ormonk) * **JS** Data src may now be bypassed from JS Combine. (ankit) * **CLI** Fixed a message typo in Purge. (flixwatchsupport) * **Browser** Added font/otf to Browser Cache expire list. (ruikai) * **Data** Updated data files to accept PR from dev branch only. * **3rd** Add data-view-breakpoint-pointer to js_excludes.txt for the Events Calendar plugin. (therealgilles) * **Cloud** Bypassed invalid requests. * **Doc** CDN Mapping description improvement. (mihai A.) = 3.6.1 - Dec 21 2020 = * **WP** Tested up to WP v5.6. * **WebP** Reverted WebP support on Safari Big Sur and Safari v14.0.1+ due to an inability to detect MacOS versions from UA. (@antomal) * **CDN** Dropped the option `Load JQuery Remotely`. * **CDN** Fixed CDN URL replacement issue in optimized CSS files. (@ankit) * **CDN** Fixed an issue where CDN CLI wouldn't set mapping image/CSS/JS to OFF when `false` was the value. * **CDN** Started using React for CDN Mapping settings. * **GUI** Secured Server IP setting from potential XSS issues. (@WonTae Jang) * **Toolbox** Supported both dev and master branches for Beta Test. Latest version updated to v3.6.1. * **Purge** Purge Pages now can purge non-archive pages too. * **Admin** Simplified the admin JS. * **Admin** Limited crawler-related react JS to crawler page only. = 3.6 - Dec 14 2020 = * 🌱**WebP** Added WebP support on Safari Big Sur or Safari v14.0.1+. (@ruikai) * 🐞**Config** Fixed an issue where new installations were not getting the correct default .htaccess content. * **Crawler** Will auto bypass empty sub-sitemap instead of throwing an exception. (@nanoprobes @Tobolo) * **Crawler** Now using React for Cookie Simulation settings instead of Vue.js. Dropped Vue.js. * **Crawler** Dropped `Sitemap Generation` (will only use 3rd party sitemap for crawler). * **CSS** Added `CSS Combine External and Inline` option for backward compatibility. (@lisa) * **Object** Forbid .object-cache.ini visits. (@Tarik) * **Page Optimize** Dropped `Remove Comments` option to avoid combine error. * **CSS** Added a predefined CSS exclude file `data/css_excludes.txt`. * **CSS** Excluded Flatsome theme random inline CSS from combine. * **CSS** Excluded WoodMart theme from combine. (@moemauphie) * **Page Optimize** Excluded tagDiv.com Newspaper theme dynamic CSS/JS from CSS/JS Combine. * **CSS** Added predefined JS defer excludes list. (@Shivam) * **JS** `data-no-defer` option now supports inline JS. (@rafaucau) * **Media** Lazyload inline library is now bypassed by JS Combine. * **Admin** Fixed WP-Admin console ID duplicate warnings. * **Cloud** Dropped QUIC.cloud sync options that have long been unused. * **CSS** Dropped `Unique CSS File` option (UCSS will always generate unique file, will use whitelist to group post type to one CSS). * **GUI** Dropped Help tab. * **Toolbox** Added 3.5.2 to version list. = 3.5.2 - Oct 27 2020 = * **CSS** `CSS Combine` is now compatible w/ inline noscript CSS. (@galbaras) * **GUI** Added ability to manually dismiss the JS option reset message in v3.5.1 upgrade process. (#473917) * 🐞**CSS** `CSS Excludes` setting will no longer lose items beginning w/ `#`. (@ankit) * **API** New `litespeed_media_reset` API function for image editing purposes. (@Andro) = 3.5.1 - Oct 20 2020 = * **JS** Inline JS containing nonces can now be combined. * **JS** Reset JS Combine/Defer to OFF when upgrading to avoid breaking sites. * **JS** Added new option JS Combine External and Inline to allow backwards compatibility. * **JS** Added Inline JS Defer option back. (@ankit) * **Page Optimize** Dropped Inline JS Minify option and merged the feature into JS Minify. * **JS** Pre-added jQuery to the default JS excludes/defer list for better layout compatibility for new users. * **JS** Excluded Stripe/PayPal/Google Map from JS optimization. (@FPCSJames) * **JS** Allowed excluded JS to still be HTTP2 pushed. (@joshua) * **CCSS** Critical CSS now can avoid network pollution from other sites. (@ankit) * **Toolbox** Beta Test now displays recent public versions so it is easier to revert to an older version * **Vary** Server environment variable Vary can now be passed to original server from QUIC.cloud for non-LiteSpeed servers. * **ESI** Improved backward compatibility for ESI nonce list. (@zach E) * 🐞**Misc** Fixed failure of upgrade button on plugin news banner and made cosmetic improvements. * **Doc** Added note that LSCWP works with ClassicPress. = 3.5.0.2 - Sep 30 2020 = * This is a temporary revert fix. Code is SAME as v3.4.2. = 3.5.0.1 - Sep 29 2020 = * 🔥🐞**CSS** Fixed print media query issue when having CSS Combine. (@paddy-duncan) = 3.5 - Sep 29 2020 = * **Page Optimize** Refactored CSS/JS optimization. * **Page Optimize** CSS and JS Combine now each save to a single file without memory usage issues. * **CSS** Inline CSS Minify is now a part of CSS Minify, and will respect the original priorities. (thanks to @galbaras) * **JS** JS Combine now generates a single JS file in the footer. (Special thanks to @ankit) * **JS** JS Combine now combines external JS files, too. (Thanks to @ankit) * **JS** JS Deferred Excludes now uses the original path/filename as keywords instead of the minified path/filename, when JS Minify is enabled. * **JS** JS Combine now combines inline JS, too. * **JS** JS Excludes may now be used for inline JS snippet. * **Page Optimize** Inline CSS Minify and Max Combined File Size retired due to changes listed above. * **CSS** Combined CSS Priority retired due to changes listed above. * **JS** Exclude JQuery, Combined JS Priority, Load Inline JS Deferred, and Inline JS Deferred Excludes retired due to changes listed above. * **JS** Predefined data file data/js_excludes.txt now available for JS Excludes. * **ESI** Predefined data file data/esi.nonces.txt now available for ESI Nonces. * **ESI** Remote Fetch ESI Nonces functionality retired. * **API** Added support for new litespeed_esi_nonces filter. * **Object** Object Cache will not try to reconnect after failure to connect in a single process. * **CCSS** Remote read CSS will add the scheme if it is missing from the URL. * **CCSS** CSS will no longer be prepared for a URL if 404 result is detected. * **CCSS** Fixed most failures caused by third party CSS syntax errors. * **CCSS** Remote read CSS will fix the scheme if the URL doesn't have it. * **CCSS** Excluded 404 when preparing CSS before request. * **CCSS** Adjusted CCSS timeout from 180 seconds to 30 seconds. * **Image Optimize** Fixed the delete attachment database error that occurred when not using the image optimization service yet. * **Media** Added iOS 14 WebP support. * **Data** Fixed database creation failure for MySQL v8. * **Cloud** Error code err_key will clear the domain key in order to avoid duplicate invalid requests. * **Network** Fixed issue with object cache password file storage that occurred when resaving the settings. (#302358) * **Misc** Fixed IP detect compatibility w/ Apache. * **GUI** Fixed the description for Do Not Cache Categories. * **Preload** Upgraded Instant Click to a new stable preload library. (@stasonua0) = 3.4.2 - Sep 8 2020 = * **CCSS** Corrected the issue that wrongly appended non-CSS files to CSS in links before sending request. * **3rd** YITH wishlist now sends a combined single sub request for all widgets contained in one page. (LSWS v5.4.9 build 3+ required) * **ESI** Added support for ESI combine feature. * **GUI** Dropped banner notification for missing domain key when domain key is not initialized. * **Log** When QC whitelist check fails, a detailed failure log is now appended. = 3.4.1 - Sep 2 2020 = * 🐞**CCSS** Fixed an issue where dynamically generated CSS failed with `TypeError: Cannot read property type of undefined`. * 🐞**Page Optimize** Fixed CSS optimization compatibility for CSS dynamically generated with PHP. * **Page Optimize** Added the ability to defer JS even when the resource is excluded from other JS optimizations. (@slr1979) * **ESI** Added support for ESI last parameter inline value. * **3rd** YITH Wishlist, when cached for the first time, will no longer send sub requests. = 3.4 - Aug 26 2020 = * 🌱**LQIP** New setting **LQIP Excludes**. * 🌱**LQIP** Added a Clear LQIP Queue button. * 🌱**CCSS** Added a Clear CCSS Queue button. * **CCSS** Fixed an issue which wrongly included preloaded images in CCSS. (@pixtweaks) * **Network** Primary site and subsite settings now display correctly. * **Page Optimize** Noscript tags generated by LSCWP will only be dropped when the corresponding option is enabled. (@ankit) * **DB Optimize** Fixed database optimizer conflicts w/ object cache transient setting. (#752931) * **3rd** Fixed an issue with WooCommerce product purge when order is placed. * **3rd** Improved WooCommerce product comment compatibility with **WooCommerce Photo Reviews Premium** plugin when using ESI. * **CDN** Fixed Remote jQuery compatibility with WordPress v5.5. (@pixtweaks) * **API** New API `litespeed_purge_all_object` and `litespeed_purged_all_object` action hooks. = 3.3.1 - Aug 12 2020 = * 🌱**Page Optimize** New option to Remove Noscript Tags. (@phuc88bmt) * 🐞**LQIP** Fixed a critical bug that bypassed all requests in v3.3. * **LQIP** Requests are now bypassed if domain has no credit left. * **Page Optimize** Inline defer will be bypassed if document listener is detected in the code. (@ssurfer) * **CCSS** Print-only styles will no longer be included in Critical CSS. * **API** Added hooks to Purge action to handle file deletions. (@biati) * **Cloud** Plain permalinks are no longer required for use of cloud services. * **Data** Added an access denial to work with OpenLiteSpeed. (@spenweb #PR228) * **GUI** Spelling and grammar adjustments. (@blastoise186 #PR253) = 3.3 - Aug 6 2020 = * 🌱**Page Optimize** Added a new setting, Inline JS Deferred Excludes. (@ankit) * **Page Optimize** CSS/JS Combine/Minify file versions will be differentiated by query string hash instead of new filename to reduce DB/file system storage. * **Page Optimize** Added the ability to use local copies of external JS files for better control over page score impacts. * **Page Optimize** Improved combination of CSS media queries. (@galbaras) * **Page Optimize** Reprioritized Inline JS Defer to be optimized before encoding, for a significantly smaller result. * **LQIP** Detect if the file exists before sending LQIP request to QUIC.cloud. * **CCSS** Sped up CCSS process significantly by sending HTML and CSS in request. * **CCSS** Improvements to mobile CSS support in CCSS. * **CCSS** Minimize CCSS failures by attempting to automatically fix CSS syntax errors. * **Cloud** Domain Key will be deleted after QUIC.cloud site_not_registered error to avoid endless repeated requests. * **CDN** CDN Original URL will default to WP Site URL if not set. (@ruikai) * **CLI** Global output format `--format=json/yaml/dump` and `--json` support in CLI. (@alya1992) * **CDN** Improved handling of non-image CSS `url()` sources in CDN. (@daniel McD) * 🐞**CDN** Fixed CDN replacement conflict w/ JS/CSS Optimize. (@ankit) * **Crawler** Only reset Crawler waiting queues when crawling begins. (@ruikai) * **Network** Network Enable Cache is no longer reset to ON Use Network Settings in enabled. (@RavanH) * 🐞**Activation** Fixed a PHP warning that appeared during uninstall. (@RavanH) * **Debug** Automatically omit long strings when dumping an array to debug log. * **Report** Subsites report now shows overwritten values along w/ original values. (#52593959) * **REST** Improved WP5.5 REST compatibility. (@oldrup) * **GUI** Server IP setting moved from Crawler menu to General menu. * **GUI** Localize resources moved to Localization tab. * **Config** News option now defaults to ON. = 3.2.4 - Jul 8 2020 = * **Object** New installations no longer get custom data.ini reset, as this could cause lost configuration. (@Eric) * **ESI** Now using `svar` to load nonces more quickly. (@Lauren) * **ESI** Fixed the conflicts between nonces in inline JS and ESI Nonces when Inline JS Deferred is enabled. (@JesseDistad) * 🐞**ESI** Fixed Fetch Latest Predefined Nonce button. * 🐞**Cache** Fixed an issue where mobile visits were not being cached when Cache Mobile was disabled. * **CDN** Bypass CDN constant `LITESPEED_BYPASS_CDN` now will apply to all CDN replacements. * **Router** Dropped `Router::get_uid()` function. * **Crawler** Updated role simulator function for future UCSS usage. * **GUI** Textarea will now automatically adjust the height based on the number of rows input. * **CLI** Fixed an issue that caused WP-Cron to exit when a task errored out. (@DovidLevine @MatthewJohnson) * **Cloud** No longer communcate with QUIC.cloud when Domain Key is not set and Debug is enabled. * **Cloud** Score banner no longer automatically fetches a new score. (@LucasRolff) = 3.2.3.2 - Jun 19 2020 = * 🔥🐞**Page Optimize** Hotfix for CSS/JS minify/combine. (@jdelgadoesteban @martin_bailey) = 3.2.3.1 - Jun 18 2020 = * **API** New filter `litespeed_buffer_before` and `litespeed_buffer_after`. (#PR243 @joejordanbrown) = 3.2.3 - Jun 18 2020 = * 🌱**Page Optimize** Added Unique CSS option for future removal of unused CSS per page. (@moongear) * **Page Optimize** Fixed an issue where Font Optimization could fail when having Load JS Deferred and Load Inline JS Deferred. (#PR241 @joejordanbrown) * 🐞**Page Optimize** Fixed an issue with Font Display Optimization which caused Google Fonts to load incorrectly. (#PR240 @joejordanbrown @haidan) * 🐞**Network** Use Primary Site Configuration setting for network sites now works properly with Object Cache and Browser Cache. (#56175101) * **API** Added filter `litespeed_is_from_cloud` to detect if the current request is from QC or not. (@lechon) * **ESI** ESI Nonce now can fetch latest list with one click. * **GUI** Updated remaining documentation links & some minor UI tweaks. (@Joshua Reynolds) = 3.2.2 - Jun 10 2020 = * 🌱**Purge** Scheduled Purge URLs now supports wildcard. (#427338) * 🌱**ESI** ESI Nonce supports wildcard match now. * **Network** Use Primary Site Settings now can support Domain Key, and override mechanism improved. (@alican532 #96266273) * **Cloud** Debug mode will now have no interval limit for most cloud requests. (@ruikai) * **Conf** Default Purge Stale to OFF. * **GUI** Purge Stale renamed to Serve Stale. * **Data** Predefined nonce list located in `/litespeed-cache/data/esi.nonce.txt`. Pull requests welcome. * **Debug** Limited parameter log length. * 🐞**CDN** Fixed an issue where upgrading lost value of CDN switch setting. (#888668) * **3rd** Caldera Forms ESI Nonce enhancement. (@paconarud16 @marketingsweet) * **3rd** Elementor now purges correctly after post/page updates. * **3rd** Disabled Page Optimization features on AMP to avoid webfont JS inject. (@rahulgupta1985) = 3.2.1 - Jun 1 2020 = * **Cloud** LQIP/CCSS rate limit tweaks. (@ianpegg) * **Admin** Improved frontend Admin Bar menu functionality. (#708642) * **Crawler** Fixed an issue where cleaning up a crawler map with a leftover page number would cause a MySQL error. (@saowp) * **Image Optimize** Added WP default thumbnails to image optimization summary list. (@johnny Nguyen) * **REST** Improved REST compatibility w/ WP4.4-. (#767203) * **GUI** Moved Use Primary Site Configuration to General menu. (@joshua) = 3.2 - May 27 2020 = * **Image Optimize** Major improvements in queue management, scalability, and speed. (@LucasRolff) * **Cloud** Implemented a series of communication enhancements. (@Lucas Rolff) * **Crawler** Enhanced PHP 5.3 compatibility. (@JTS-FIN #230) * **Page Optimize** Appended image template in wpDiscuz script into default lazyload image exclude list. (@philipfaster @szmigieldesign) * **Page Optimize** Eliminated the 404 issue for CSS/JS in server environments with missing SCRIPT_URI. (@ankit) * **Data** ENhanced summary data storage typecasting. = 3.1 - May 20 2020 = * 🌱**Network** Added Debug settings to network level when on network. * 🐞**Purge** Network now can purge all. * 🐞**Network** Fixed issue where saving the network primary site settings failed. * **Network** Moved Beta Test to network level when on network. * 🐞**Cache** Fixed issue in admin where new post editor was wrongly cached for non-admin roles. (@TEKFused) * 🐞**Data** Fixed issue with crawler & img_optm table creation failure. (@berdini @piercand) * 🐞**Core** Improved plugin activation compatibility on Windows 10 #224 (@greenphp) * **Core** Improved compatibility for .htaccess path search. * **Object** Catch RedisException. (@elparts) * Fixed Script URI issue in 3.0.9 #223 (@aonsyed) * **Image Optimize** Show thumbnail size set list in image optimization summary. (@Johnny Nguyen) * **Debug** Parameters will now be logged. = 3.0.9 - May 13 2020 = * **Purge** Comment cache can be successfully purged now. * **Data** Better MySQL charset support for crawler/image optimize table creation. (@Roshan Jonah) * **API** New hook to fire after Purge All. (@salvatorefresta) * **Crawler** Resolve IP for crawler. * **Task** PHP5.3 Cron compatibility fix. * **3rd** Elementor edit mode compatibility. * **Page Optimize** Fixed an issue where Purge Stale returned 404 for next visitor on CSS/JS. * **Page Optimize** Fixed the PHP warning when srcset doesn't have size info inside. (@gvidano) * **Cloud** Fixed the potential PHP warning when applying for the domain key. * **Core** PHP __DIR__ const replacement. (@MathiasReker) = 3.0.8.6 - May 4 2020 = * **CCSS** Bypassed CCSS functionality on frontend when domain key isn't setup yet. * **Cloud** Fixed WP node redetection bug when node expired. (@Joshua Reynolds) * **Crawler** Fixed an issue where URL is wrongly blacklisted when using ADC. = 3.0.8.5 - May 1 2020 = * 🔥🐞**3rd** Hotfix for WPLister critical error due to v3.0.8.4 changes. * **Image Optimize** Unfinished queue now will get more detailed info to indicate the proceeding status on node. * **CLI** Options can now use true/false as value for bool. (@gavin) * **CLI** Detect error if the ID does not exist when get/set an option value. * **Doc** An API comment typo for `litespeed_esi_load-` is fixed. = 3.0.8.4 - Apr 30 2020 = * 🌱**Crawler** New setting: Sitemap timeout. (#364607) * **Image Optimize** Images that fail to optimize are now counted to increase next request limit. * **Cloud** Redetect fastest node every 3 days. * **Cloud** Suppressed auto upgrade version detection error. (@marc Dahl) * **3rd** 3rd party namespace compatibility. (#366352) = 3.0.8.3 - Apr 28 2020 = * **Cloud** Better compatibility for the Link to QUIC.cloud operation. (@Ronei de Sousa Almeida) * **Image Optimize** Automatically clear invalid image sources before sending requests. (@Richard Hordern) = 3.0.8.2 - Apr 27 2020 = * **GUI** Corrected the Request Domain Key wording. = 3.0.8.1 - Apr 27 2020 = * **Object** Object cache compatibility for upgrade from v2.9.9- versions. = 3.0.8 - Apr 27 2020 = * Released v3 on WordPress officially. = 3.0.4 - Apr 23 2020 = * **Cloud** Apply Domain Key now receives error info in next apply action if failed to generate. * **GUI** Apply Domain Key timeout now displays troubleshooting guidance. * **REST** Added /ping and /token to REST GET for easier debug. * **Cache** Dropped `advanced-cache.php` file detection and usage. = 3.0.3 - Apr 21 2020 = * **Conf** Settings from all options (data ini, defined constant, and forced) will be filtered and cast to expected type. * **Upgrade** CDN mapping and other multiple line settings will now migrate correctly when upgrading from v2 to v3. = 3.0.2 - Apr 17 2020 = * **GUI** More guidance on domain key setting page. * **Cloud** Now Apply Domain Key will append the server IP if it exists in Crawler Server IP setting. = 3.0.1 - Apr 16 2020 = * **Data** Increased timeout for database upgrade related to version upgrade. Display a banner while update in progress. * **Page Optimize** All appended HTML attributes now will use double quotes to reduce the conflicts when the optimized resources are in JS snippets. = 3.0 - Apr 15 2020 = * 🌱**Media** LQIP (Low Quality Image Placeholder). * 🌱**Page Optimize** Load Inline JS Deferred Compatibility Mode. (Special thanks to @joe B - AppsON) * 🌱**Cloud** New QUIC.cloud API key setting. * 🌱**ESI** New ESI nonce setting. * 🌱**Media** JPG quality control. (@geckomist) * 🌱**Media** Responsive local SVG placeholder. * 🌱**Discussion** Gravatar warmup cron. * 🌱**DB** Table Engine Converter tool. (@johnny Nguyen) * 🌱**DB** Database summary: Autoload size. (@JohnnyNguyen) * 🌱**DB** Database summary: Autoload entries list. * 🌱**DB** Revisions older than. (@thememasterguru) * 🌱**Cache** Forced public cache setting. (#308207) * 🌱**Crawler** New timeout setting to avoid incorrect blacklist addition. (#900171) * 🌱**Htaccess** Frontend & backend .htaccess path customize. (@jon81) * 🌱**Toolbox** Detailed Heartbeat Control (@K9Heaven) * 🌱**Purge** Purge Stale setting. * 🌱**Page Optimize** Font display optimization. (@Joeee) * 🌱**Page Optimize** Google font URL display optimization. * 🌱**Page Optimize** Load Inline JS deferred. * 🌱**Page Optimize** Store gravatar locally. (@zzTaLaNo1zz @JohnnyNguyen) * 🌱**Page Optimize** DNS prefetch control setting. * 🌱**Page Optimize** Lazy Load Image Parent Class Name Excludes. (@pako69) * 🌱**Page Optimize** Lazy load iframe class excludes. (@vnnloser) * 🌱**Page Optimize** Lazy load exclude URIs. (@wordpress_fan1 @aminaz) * 🌱**GUI** New Dashboard and new menus. * 🌱**Image Optimize** Supported GIF WebP optimization. (@Lucas Rolff) * 🌱**Image Optimize** New workflow for image optimization (Gather first, request second). * 🌱**Image Optimize** The return of Rescan. * 🌱**CLI** Get single option cmd. * 🌱**CLI** QUIC.cloud cmd supported. * 🌱**CLI** CLI can send report now. * 🌱**Health** Page speed and page score now are in dashboard. * 🌱**Conf** Supported consts overwritten of `LITESPEED_CONF__` for all settings. (@menathor) * 🌱**REST** New REST TTL setting. (@thekendog) * 🌱**CDN** New setting `HTML Attribute To Replace`. CDN can now support any HTML attribute to be replaced. (@danushkaj91) * 🌱**Debug** Debug URI includes/excludes settings. * 🌱**Crawler** 🐞 Support for multiple domains in custom sitemap. (@alchem) * 🌱**Crawler** New Crawler dashboard. New sitemap w/ crawler status. New blacklist w/ reason. * 🌱**Media** LQIP minimum dimensions setting. (@Lukasz Szmigiel) * **Crawler** Able to add single rows to blacklist. * **Crawler** Crawler data now saved into database instead of creating new files. * **Crawler** Larger timeout to avoid wrongly added to blacklist. * **Crawler** Manually changed the priority of mobile and WebP. (@rafaucau) * **Browser** Larger Browser Cache TTL for Google Page Score improvement. (@max2348) * **Task** Task refactored. Disabled cron will not show in cron list anymore. * **Task** Speed up task load speed. * **ESI** Added Bloom nonce to ESI for Elegant Themes. * **Cloud** Able to redetect cloud nodes now. * **Img_optm** Fixed stale data in redirected links. * **Lazyload** CSS class `litespeed_lazyloaded` is now appended to HTML body after lazyload is finished. (@Adam Wilson) * **Cache** Default drop qs values. (@gijo Varghese) * **LQIP** Show all LQIP images in Media column. * **CDN** Can now support custom REST API prefix other than wp-json. (#174 @therealgilles) * **IAPI** Used REST for notify/destroy/check_img; Removed callback passive/aggreesive IAPI func * **CSSJS** Saved all static files to litespeed folder; Uninstallation will remove static cache folder too; Reduced .htaccess rules by serving CSS/JS directly. * **Object** Fixed override different ports issue. (@timofeycom #ISSUE178) * **Conf** DB Tables will now only create when activating/upgrading/changing settings. * **DB** Simplified table operation funcs. * **CSSJS** Bypassed CSS/JS generation to return 404 if file is empty (@grubyy) * **CSSJS** Inline JS defer will not conflict with JS inline optm anymore. * **CDN** settings will not be overwritten by primary settings in network anymore. (@rudi Khoury) * **OPcache** Purged all opcache when updating cache file. (@closte #170) * **CLI** CLI cmd renamed. * **CLI** Well-formatted table to show all options. * **Purge** Only purge related posts that have a status of "published" to avoid unnecessary "draft" purges. (@Jakub Knytl) * **GUI** Removed basic/adv mode for settings. Moved non-cache settings to its own menu. * **Htaccess** Protected .htaccess.bk file. Only kept one backup. (@teflonmann) * **Crawler** Crawler cookie now support `_null` as empty value. * **Crawler** Avoid crawler PHP fatal error on Windows OS. (@technisolutions) * **Admin** Simplified admin setting logic. * **Conf** Multi values settings now uniformed to multi lines for easier setting. * **Conf** New preset default data file `data/consts.default.ini`. * **Conf** Config setting renamed and uniformed. * **Conf** Dropped `Conf::option()`. Used `Conf::val()` instead. * **Conf** Improved conf initialization and upgrade conversion workflow. * **Core** Code base refactored. New namespace LiteSpeed. * **API** New API: iframe lazyload exclude filter. * **GUI** human readable seconds. (@MarkCanada) * **API** API refactored. * NOTE: All 3rd party plugins that are using previous APIs, especially `LiteSpeed_Cache_API`, need to be adjusted to the latest one. Same for ESI blocks.* ESI shortcode doesn't change. * **API** New hook `litespeed_update_confs` to settings update. * **API** New Hooks `litespeed_frontend_shortcut` and `litespeed_backend_shortcut` for dropdown menu. (@callaloo) * **API** Removed `litespeed_option_*` hooks. Use `litespeed_force_option` hook insteadly * **API** Renamed `litespeed_force_option` to `litespeed_conf_force`. * **API** Removed function `litespeed_purge_single_post`. * **REST** New rest API to fetch public IP. * **GUI** Hiding Cloudflare/Object Cache/Cloud API key credentials. (@menathor) * **GUI** Renamed all backend link tag from lscache to litespeed. * **GUI** fixed duplicated form tag. * **GUI** Fix cron doc link. (@arnab Mohapatra) * **GUI** Frontend adminbar menu added `Purge All` actions. (@Monarobase) * **GUI** Localized vue.js to avoid CloudFlare cookie. (@politicske) * **GUI** Always show optm column in Media Library for future single row optm operation. (@mikeyhash) * **GUI** Displayed TTL range below the corresponding setting. * **GUI** GUI refactored. * **Debug** Report can now append notes. * **3rd** Default added parallax-image to webp replacement for BB. * **3rd** User Switching plugin compatibility. (@robert Staddon) * **3rd** Beaver Builder plugin compatibility with v3.0. * **3rd** Avada plugin compatibility w/ BBPress. (@pimg) * **3rd** WooCommerce PayPal Checkout Gateway compatibility. (#960642 @Glen Cabusas) * **Network** Fixed potential timeout issue when containing a large volume of sites. (@alican532) * **Debug** `Disable All Features` now will see the warning banner if ON. * **Debug** Dropped `log filters` section. * **Debug** Debug and Tools sections combined into new `Toolbox` section. * 🐞**Crawler** Multi sites will now use separate sitemap even when `Use Primary Site` is ON. (@mrhuynhanh) * 🐞**Img_optm** Fixed large volume image table storage issue. (#328956) * 🐞 **Cloud** Cloud callback hash validation fixed OC conflict. (@pbpiotr) * 🎊 Any user that had the contribution to our WP community or changelog (even just bug report/feedback/suggestion) can apply for extra credits in QUIC.cloud. = 2.9.9.2 - Nov 24 2019 = * 🌱**GUI** New settings to limit News Feed to plugin page only. = 2.9.9.1 - Nov 18 2019 = * 🌱**Env** Environment Report can now append a passwordless link for support access without wp-admin password. * **Admin** The latest v3.0 beta test link may now be shown on the admin page when it's available. * **3rd** Compatibility with [DoLogin Security](https://wordpress.org/plugins/dologin/). * 🐞**ESI** Fixed a failure issue with Vary Group save. (@rafasshop) * 🐞**3rd** In browsers where WebP is not supported, Divi image picker will no longer serve WebP. (@Austin Tinius) = 2.9.9 - Oct 28 2019 = * <strong>Core</strong>: Preload all classes to avoid getting error for upcoming v3.0 upgrade. * <strong>Object</strong>: Improved compatibility with upcoming v3.0 release. * <strong>ESI</strong>: Unlocked ESI for OLS in case OLS is using QUIC.cloud CDN which supports ESI. * <strong>3rd</strong>: Elementor Edit button will now show when ESI enabled. (#PR149 #335322 @maxgorky) * 🐞<strong>Media</strong>: Fixed missing Media optimization column when Admin role is excluded from optimization in settings. (@mikeyhash @pako69 @dgilfillan) = 2.9.8.7 - Oct 11 2019 = * <strong>3rd</strong>: Enhanced WP stateless compatibility. (#PR143) * <strong>3rd</strong>: Fixed a PHP warning caused by previous PR for AMP. (#PR176) = 2.9.8.6 - Sep 24 2019 = * <strong>3rd</strong>: Bypassed page optimizations for AMP. (#359748 #PR169) * <strong>GUI</strong>: Firefox compatibility with radio button state when reloading pages. (#288940 #PR162) * <strong>GUI</strong>: Updated Slack invitation link. (#PR173) = 2.9.8.5 - Aug 21 2019 = * <strong>CCSS</strong>: Removed potential PHP notice when getting post_type. (@amcgiffert) * <strong>CDN</strong>: Bypassed CDN replacement on admin page when adding media to page/post. (@martin_bailey) * 🐞<strong>Media</strong>: Fixed inability to update or destroy postmeta data for child images. (#167713) = 2.9.8.4 - Jul 25 2019 = * <strong>Object</strong>: Increased compatibility with phpredis 5.0. * <strong>Object</strong>: Appended `wc_session_id` to default Do Not Cache Groups setting to avoid issue where WooCommerce cart items were missing when Object Cache is used. NOTE: Existing users must add `wc_session_id` manually! (#895333) * <strong>CSS</strong>: Added null onload handler for CSS async loading. (@joejordanbrown) * 🕷️: Increased crawler timeout to avoid wrongly adding a URL to the blacklist. * <strong>3rd</strong>: WooCommerce Advanced Bulk Edit can now purge cache automatically. = 2.9.8.3 - Jul 9 2019 = * <strong>CSS</strong>: Enhanced the CSS Minify compatibility for CSS with missing closing bracket syntax errors. (@fa508210020) * 🕷️: Crawler now supports both cookie and no-cookie cases. (@tabare) * <strong>CCSS</strong>: Enhanced compatibility with requested pages where meta info size exceeds 8k. (@Joe B) * <strong>CCSS</strong>: No longer processing "font" or "import" directives as they are not considered critical. (@Ankit @Joe B) * <strong>IAPI</strong>: Removed IPv6 from all servers to avoid invalid firewall whitelist. = 2.9.8.2 - Jun 17 2019 = * 🔥🐞 <strong>3rd</strong>: Fixed PHP 5.3 compatibility issue with Facetwp. = 2.9.8.1 - Jun 17 2019 = * <strong>3rd</strong>: Set ESI template hook priority to highest number to prevent ESI conflict with Enfold theme. (#289354) * <strong>3rd</strong>: Improved Facetwp reset button compatibility with ESI. (@emilyel) * <strong>3rd</strong>: Enabled user role change to fix duplicate login issue for plugins that use alternative login processes. (#114165 #717223 @sergiom87) * <strong>GUI</strong>: Wrapped static text with translate function. (@halilemreozen) = 2.9.8 - May 22 2019 = * <strong>Core</strong>: Refactored loading priority so user related functions & optimization features are set after user initialization. (#717223 #114165 #413338) * <strong>Media</strong>: Improved backup file calculation query to prevent out-of-memory issue. * <strong>Conf</strong>: Feed cache now defaults to ON. * <strong>API</strong>: Fully remote attachment compatibility API of image optimization now supported. * 🕷️: Bypassed vary change for crawler; crawler can now simulate default vary cookie. * <strong>ESI</strong>: Refactored ESI widget. Removed `widget_load_get_options()` function. * <strong>ESI</strong>: Changed the input name of widget fields in form. * <strong>3rd</strong>: Elementor can now save ESI widget settings in frontend builder. * <strong>3rd</strong>: WP-Stateless compatibility. * <strong>IAPI</strong>: Image optimization can now successfully finish the destroy process with large volume images with automatic continual mode. * 🐞<strong>CDN</strong>: Fixed issue with Load JQuery Remotely setting where WP 5.2.1 provided an unexpected jQuery version. * 🐞<strong>3rd</strong>: Login process now gets the correct role; fixed double login issue. = 2.9.7.2 - May 2 2019 = * <strong>Conf</strong>: Enhanced compatibility when an option is not properly initialized. * <strong>Conf</strong>: Prevent non-array instance in widget from causing 500 error. (#210407) * <strong>CCSS</strong>: Increase CCSS generation timeout to 60s. * <strong>Media</strong>: Renamed lazyload CSS class to avoid conflicts with other plugins. (@DynamoProd) * <strong>JS</strong>: Improved W3 validator. (@istanbulantik) * <strong>QUIC</strong>: Synced cache tag prefix for static files cache. * <strong>ESI</strong>: Restored query strings to ESI admin bar for accurate rendering. (#977284) * <strong>ESI</strong>: Tweaked ESI init priority to honor LITESPEED_DISABLE_ALL const. ESI will now init after plugin loaded. * 🐞<strong>ESI</strong>: No longer initialize ESI if ESI option is OFF. * <strong>API</strong>: New "Disable All" API function. * <strong>API</strong>: New "Force public cache" API function. * 🐞<strong>Vary</strong>: Fixed an issue with saving vary groups. * 🐞<strong>IAPI</strong>: Fixed an issue where image md5 validation failed due to whitespace in the image path. * 🐞<strong>3rd</strong>: Bypass all optimization/ESI/Cache features when entering Divi Theme Builder frontend editor. * 🐞<strong>3rd</strong>: Fixed an issue where DIVI admin bar exit button didn't work when ESI was ON. = 2.9.7.1 - Apr 9 2019 = * <strong>Purge</script>: Purge All no longer includes Purge CCSS/Placeholder. * <strong>3rd</strong>: Divi Theme Builder no longer experiences nonce expiration issues in the contact form widget. (#475461) = 2.9.7 - Apr 1 2019 = * 🌱🌱🌱 QUIC.cloud CDN feature. Now Apache/Nginx can use LiteSpeed cache freely. = 2.9.6 - Mar 27 2019 = * 🌱<strong>IAPI</strong>: Appended XMP to `Preserve EXIF data` setting. WebP will now honor this setting. (#902219) * <strong>Object</script>: Fixed SASL connection with LSMCD. * <strong>ESI</strong>: Converted ESI URI parameters to JSON; Added ESI validation. * <strong>Import</strong>: Import/Export will now use JSON format. <strong>Please re-export any backed up settings. Previous backup format is no longer recognized.</strong> * <strong>Media</strong>: WebP replacement will honor `Role Excludes` setting now. (@mfazio26) * <strong>Data</strong>: Forbid direct visit to const.default.ini. * <strong>Utility</strong>: Can handle WHM passed in `LITESPEED_ERR` constant now. * <strong>IAPI</strong>: Communicate via JSON encoding. * <strong>IAPI</strong>: IAPI v2.9.6. = 2.9.5 - Mar 14 2019 = * 🌱 Auto convert default WordPress nonce to ESI to avoid expiration. * 🌱 <strong>API</strong>: Ability to easily convert custom nonce to ESI by registering `LiteSpeed_Cache_API::nonce_action`. * <strong>OPTM</strong>: Tweaked redundant attr `data-no-optimize` in func `_analyse_links` to `data-ignore-optimize` to offer the API to bypass optimization but still move src to top of source code. * <strong>API</strong>: Renamed default nonce ESI ID from `lscwp_nonce_esi` to `nonce`. * <strong>API</strong>: Added WebP generation & validation hook API. (@alim #wp-stateless) * <strong>API</strong>: Added hook to bypass vary commenter check. (#wpdiscuz) * <strong>Doc</strong>: Clarified Cache Mobile description. (@JohnnyNguyen) * <strong>Doc</strong>: Replaced incorrect link in description. (@JohnnyNguyen) * <strong>3rd</strong>: Improved wpDiscuz compatibility. * 🐞<strong>3rd</strong>: Fixed Divi Theme Builder comment compatibility on non-builder pages. (#410919) * <strong>3rd</strong>: Added YITH ESI adjustment. = 2.9.4.1 - Feb 28 2019 = * 🔥🐞<strong>Tag</strong>: Fixed issue where unnecessary warning potentially displayed after upgrade process when object cache is enabled. = 2.9.4 - Feb 27 2019 = * 🐞<strong>REST</strong>: New REST class with better WP5 Gutenberg and internal REST call support when ESI is embedded. * <strong>ESI</strong>: ESI block ID is now in plain text in ESI URL parameters. * 🐞<strong>ESI</strong>: Fixed a redundant ESI 301 redirect when comma is in ESI URL. * <strong>ESI</strong>: REST call can now parse shortcodes in ESI. * <strong>API</strong>: Changed ESI `parse_esi_param()` function to private and `load_esi_block` function to non-static. * <strong>API</strong>: Added `litespeed_is_json` hook for buffer JSON conversion. * <strong>GUI</strong>: Prepended plugin name to new version notification banner. * <strong>3rd</strong>: WPML multi domains can now be handled in optimization without CDN tricks. = 2.9.3 - Feb 20 2019 = * <strong>ESI</strong>: ESI shortcodes can now be saved in Gutenberg editor. * <strong>ESI</strong>: ESI now honors the parent page JSON data type to avoid breaking REST calls (LSWS 5.3.6+). * <strong>ESI</strong>: Added is_json parameter support for admin_bar. * <strong>ESI</strong>: Simplified comment form code. * <strong>3rd</strong>: Better page builder plugin compatibility within AJAX calls. * <strong>3rd</strong>: Compatibility with FacetWP (LSWS 5.3.6+). * <strong>3rd</strong>: Compatibility with Beaver Builder. * <strong>Debug</strong>: Added ESI buffer content to log. * <strong>Tag</strong>: Only append blog ID to cache tags when site is part of a network. * <strong>IAPI</strong>: Optimized database query for pulling images. * <strong>GUI</strong>: Added more plugin version checking for better feature compatibility. * <strong>GUI</strong>: Ability to bypass non-critical banners with the file .litespeed_no_banner. * <strong>Media</strong>: Background image WebP replacement now supports quotes around src. = 2.9.2 - Feb 5 2019 = * <strong>API</strong>: Add a hook `litespeed_esi_shortcode-*` for ESI shortcodes. * <strong>3rd</strong>: WooCommerce can purge products now when variation stock is changed. * 🐞🕷️: Forced HTTP1.1 for crawler due to a CURL HTTP2 bug. = 2.9.1 - Jan 25 2019 = * <strong>Compatibility</strong>: Fixed fatal error for PHP 5.3. * <strong>Compatibility</strong>: Fixed PHP warning in htmlspecialchars when building URLs. (@souljahn2) * <strong>Media</strong>: Excluded invalid image src from lazyload. (@andrew55) * <strong>Optm</strong>: Improved URL compatibility when detecting closest cloud server. * <strong>ESI</strong>: Supported JSON format comment format in ESI with `is_json` parameter. * <strong>API</strong>: Added filters to CCSS/CSS/JS content. (@lhoucine) * <strong>3rd</strong>: Improved comment compatibility with Elegant Divi Builder. * <strong>IAPI</strong>: New Europe Image Optimization server (EU5). <strong>Please whitelist the new [IAPI IP List](https://wp.api.litespeedtech.com/ips).</strong> * <strong>GUI</strong>: No longer show banners when `Disable All` in `Debug` is ON. (@rabbitwordpress) * <strong>GUI</strong>: Fixed button style for RTL languages. * <strong>GUI</strong>: Removed unnecessary translation in report. * <strong>GUI</strong>: Updated readme wiki links. * <strong>GUI</strong>: Fixed pie styles in image optimization page. = 2.9 - Dec 31 2018 = * 🌱<strong>Media</strong>: Lazy Load Image Classname Excludes. (@thinkmedia) * 🌱: New EU/AS cloud servers for faster image optimization handling. * 🌱: New EU/AS cloud servers for faster CCSS generation. * 🌱: New EU/AS cloud servers for faster responsive placeholder generation. * 🌱<strong>Conf</strong>: Ability to set single options via link. * 🌱<strong>Cache</strong>: Ability to add custom TTLs to Force Cache URIs. * <strong>Purge</strong>: Added post type to Purge tags. * <strong>Purge</strong>: Redefined CCSS page types. * <strong>Core</strong>: Using Exception for .htaccess R/W. * <strong>IAPI</strong>: <strong>New cloud servers added. Please whitelist the new [IAPI IP List](https://wp.api.litespeedtech.com/ips).</strong> * <strong>Optm</strong>: Trim BOM when detecting if the page is HTML. * <strong>GUI</strong>: Added PageSpeed Score comparison into promotion banner. * <strong>GUI</strong>: Refactored promotion banner logic. * <strong>GUI</strong>: Removed page optimized comment when ESI Silence is requested. * <strong>GUI</strong>: WHM transient changed to option instead of transient when storing. * <strong>GUI</strong>: Appending more descriptions to CDN filetype setting. * <strong>IAPI</strong>: Removed duplicate messages. * <strong>IAPI</strong>: Removed taken_failed/client_pull(duplicated) status. * <strong>Debug</strong>: Environment report no longer generates hash for validation. * <strong>3rd</strong>: Non-cacheable pages no longer punch ESI holes for Divi compatibility. * 🐞<strong>Network</strong>: Added slashes for mobile rules when activating plugin. * 🐞<strong>CCSS</strong>: Eliminated a PHP notice when appending CCSS. = 2.8.1 - Dec 5 2018 = * 🐞🕷️: Fixed an activation warning related to cookie crawler. (@kacper3355 @rastel72) * 🐞<strong>Media</strong>: Replace safely by checking if pulled images is empty or not first. (@Monarobase) * <strong>3rd</strong>: Shortcode ESI compatibility with Elementor. = 2.8 - Nov 30 2018 = * 🌱: ESI shortcodes. * 🌱: Mobile crawler. * 🌱: Cookie crawler. * <strong>API</strong>: Can now add `_litespeed_rm_qs=0` to bypass Remove Query Strings. * <strong>Optm</strong>: Removed error log when minify JS failed. * 🐞<strong>Core</strong>: Fixed a bug that caused network activation PHP warning. * <strong>Media</strong>: Removed canvas checking for WebP to support TOR. (@odeskumair) * <strong>Media</strong>: Eliminated potential image placeholder PHP warning. * <strong>3rd</strong>: Bypassed Google recaptcha from Remove Query Strings for better compatibility. * <strong>IAPI</strong>: Showed destroy timeout details. * <strong>Debug</strong>: Moved Google Fonts log to advanced level. * <strong>GUI</strong>: Replaced all Learn More links for functions. * <strong>GUI</strong>: Cosmetic updates including Emoji. * 🕷️: Removed duplicated data in sitemap and blacklist. = 2.7.3 - Nov 26 2018 = * <strong>Optm</strong>: Improved page render speed with Web Font Loader JS library for Load Google Fonts Asynchronously. * <strong>Optm</strong>: Directly used JS library files in plugin folder instead of short links `/min/`. * <strong>Optm</strong>: Handled exceptions in JS optimization when meeting badly formatted JS. * <strong>3rd</strong>: Added Adobe Lightroom support for NextGen Gallery. * <strong>3rd</strong>: Improved Postman app support for POST JSON requests. * <strong>IAPI</strong>: <strong>US3 server IP changed to 68.183.60.185</strong>. = 2.7.2 - Nov 19 2018 = * 🌱: Auto Upgrade feature. * <strong>CDN</strong>: Bypass CDN for cron to avoid WP jQuery deregister warning. = 2.7.1 - Nov 15 2018 = * 🌱<strong>CLI</strong>: Ability to set CDN mapping by `set_option litespeed-cache-cdn_mapping[url][0] https://url`. * 🌱<strong>CDN</strong>: Ability to customize default CDN mapping data in default.ini. * 🌱<strong>API</strong>: Default.ini now supports both text-area items and on/off options. * <strong>Vary</strong>: Refactored Vary and related API. * <strong>Vary</strong>: New hook to manipulate vary cookies value. * <strong>Core</strong>: Activation now can generate Object Cache file. * <strong>Core</strong>: Unified Object Cache/rewrite rules generation process across activation/import/reset/CLI. * <strong>Core</strong>: Always hook activation to make activation available through the front end. * 🐞<strong>IAPI</strong>: Fixed a bug where environment report gave incorrect image optimization data. * 🐞<strong>OLS</strong>: Fixed a bug where login cookie kept showing a warning on OpenLiteSpeed. * 🐞<strong>Core</strong>: Fixed a bug where Import/Activation/CLI was missing CDN mapping settings. * <strong>API</strong>: <strong>Filters `litespeed_cache_media_lazy_img_excludes/litespeed_optm_js_defer_exc` passed-in parameter is changed from string to array.</strong> = 2.7 - Nov 2 2018 = * 🌱: Separate Purge log for better debugging. * <strong>3rd</strong>: Now fully compatible with WPML. * <strong>IAPI</strong>: Sped up Image Optimization workflow. * <strong>GUI</strong>: Current IP now shows in Debug settings. * <strong>GUI</strong>: Space separated placeholder queue list for better look. * <strong>IAPI</strong>: <strong>EU3 server IP changed to 165.227.131.98</strong>. = 2.6.4.1 - Oct 25 2018 = * 🔥🐞<strong>Media</strong>: Fixed a bug where the wrong table was used in the Image Optimization process. * <strong>IAPI</strong>: IAPI v2.6.4.1. = 2.6.4 - Oct 24 2018 = * 🌱: Ability to create custom default config options per hosting company. * 🌱: Ability to generate mobile Critical CSS. * 🐞<strong>Media</strong>: Fixed a bug where Network sites could incorrectly override optimized images. * 🐞<strong>CDN</strong>: Fixed a bug where image URLs containing backslashes were matched. * <strong>Cache</strong>: Added default Mobile UA config setting. * <strong>GUI</strong>: Fixed unknown shortcut characters for non-English languages Setting tabs. = 2.6.3 - Oct 18 2018 = * 🌱: Ability to Reset All Options. * 🌱<strong>CLI</strong>: Added new `lscache-admin reset_options` command. * <strong>GUI</strong>: Added shortcuts for more of the Settings tabs. * <strong>Media</strong>: Updated Lazy Load JS library to the most recent version. * There is no longer any need to explicitly Save Settings upon Import. * Remove Query String now will remove *all* query strings in JS/CSS static files. * <strong>IAPI</strong>: Added summary info to debug log. = 2.6.2 - Oct 11 2018 = * <strong>Setting</strong>: Automatically correct invalid numeric values in configuration settings upon submit. * 🐞<strong>Media</strong>: Fixed the issue where iframe lazy load was broken by latest Chrome release. (@ofmarconi) * 🐞: Fixed an issue with Multisite where subsites failed to purge when only primary site has WooCommerce . (@kierancalv) = 2.6.1 - Oct 4 2018 = * 🌱: Ability to generate separate Critical CSS Cache for Post Types & URIs. * <strong>API</strong>: Filter `litespeed_frontend_htaccess` for frontend htaccess path. * <strong>Media</strong>: Removed responsive placeholder generation history to save space. = 2.6.0.1 - Sep 24 2018 = * 🔥🐞: Fixed an issue in responsive placeholder generation where redundant history data was being saved and using a lot of space. = 2.6 - Sep 22 2018 = * <strong>Vary</strong>: Moved `litespeed_cache_api_vary` hook outside of OLS condition for .htaccess generation. * <strong>CDN</strong>: Trim spaces in original URL of CDN setting. * <strong>API</strong>: New filter `litespeed_option_` to change all options dynamically. * <strong>API</strong>: New `LiteSpeed_Cache_API::force_option()` to change all options dynamically. * <strong>API</strong>: New `LiteSpeed_Cache_API::vary()` to set default vary directly for easier compaitiblity with WPML WooCommerce Multilingual. * <strong>API</strong>: New `LiteSpeed_Cache_API::nonce()` to safely and easily allow caching of wp-nonce. * <strong>API</strong>: New `LiteSpeed_Cache_API::hook_vary_add()` to add new vary. * <strong>Optm</strong>: Changed HTML/JS/CSS optimization options assignment position from constructor to `finalize()`. * <strong>Doc</strong>: Added nonce to FAQ and mentioned nonce in 3rd Party Compatibility section. * <strong>GUI</strong>: Moved inline minify to under html minify due to the dependency. * <strong>3rd</strong>: Cached Aelia CurrencySwitcher by default. * 🐞: Fixed issue where enabling remote JQuery caused missing jquery-migrate library error. = 2.5.1 - Sep 11 2018 = * 🌱 Responsive placeholder. (@szmigieldesign) * Changed CSS::ccss_realpath function scope to private. * 🐞 Detected JS filetype before optimizing to avoid PHP source conflict. (@closte #50) = 2.5 - Sep 6 2018 = * [IMPROVEMENT] <strong>CLI</strong> can now execute Remove Original Image Backups. (@Shon) * [UPDATE] Fixed issue where WP-PostViews documentation contained extra slashes. (#545638) * [UPDATE] Check LITESPEED_SERVER_TYPE for more accurate LSCache Disabled messaging. * [IAPI] Fixed a bug where optimize/fetch error notification was not being received. (@LucasRolff) = 2.4.4 - Aug 31 2018 = * [NEW] <strong>CLI</strong> can now support image optimization. (@Shon) * [IMPROVEMENT] <strong>GUI</strong> Cron/CLI will not create admin message anymore. * [UPDATE] <strong>Media</strong> Fixed a PHP notice that appeared when pulling optimized images. * [UPDATE] Fixed a PHP notice when detecting origin of ajax call. (@iosoft) * [DEBUG] Debug log can now log referer URL. * [DEBUG] Changes to options will now be logged. = 2.4.3 - Aug 27 2018 = * [NEW] <strong>Media</strong> Ability to inline image lazyload JS library. (@Music47ell) * [IMPROVEMENT] <strong>Media</strong> Deleting images will now clear related optimization file & info too. * [IMPROVEMENT] <strong>Media</strong> Non-image postfix data will now be bypassed before sending image optimization request. * [BUGFIX] <strong>CDN</strong> CDN URL will no longer be replaced during admin ajax call. (@pankaj) * [BUGFIX] <strong>CLI</strong> WPCLI can now save options without incorrectly clearing textarea items. (@Shon) * [GUI] Moved Settings above Manage on the main menu. = 2.4.2 - Aug 21 2018 = * [IMPROVEMENT] <strong>Media</strong> Sped up Image Optimization process by replacing IAPI server pull communication. * [IMPROVEMENT] <strong>Media</strong> Ability to delete optimized WebP/original image by item in Media Library. (@redgoodapple) * [IMPROVEMENT] <strong>CSS Optimize</strong> Generate new optimized CSS name based on purge timestamp. Allows CSS cache to be cleared for visitors. (@bradbrownmagic) * [IMPROVEMENT] <strong>API</strong> added litespeed_img_optm_options_per_image. (@gintsg) * [UPDATE] Stopped showing "No Image Found" message when all images have finished optimization. (@knutsp) * [UPDATE] Improved a PHP warning when saving settings. (@sergialarconrecio) * [UPDATE] Changed backend adminbar icon default behavior from Purge All to Purge LSCache. * [UPDATE] Clearing CCSS cache will clear unfinished queue too. * [UPDATE] Added "$" exact match when adding URL by frontend adminbar dropdown menu, to avoid affecting any sub-URLs. * [UPDATE] Fixed IAPI error message showing array bug. (@thiomas) * [UPDATE] Debug Disable All will do a Purge All. * [UPDATE] <strong>Critical CSS server IP changed to 142.93.3.57</strong>. * [GUI] Showed plugin update link for IAPI version message. * [GUI] Bypassed null IAPI response message. * [GUI] Grouped related settings with indent. * [IAPI] Added 503 handler for IAPI response. * [IAPI] IAPI v2.4.2. * [IAPI] <strong>Center Server IP Changed from 34.198.229.186 to 142.93.112.87</strong>. = 2.4.1 - Jul 19 2018 = * [NEW FEATURE] <strong>Media</strong> Auto Level Up. Auto refill credit. * [NEW FEATURE] <strong>Media</strong> Auto delete original backups after pulled. (@borisov87 @JMCA2) * [NEW FEATURE] <strong>Media</strong> Auto request image optimization. (@ericsondr) * [IMPROVEMENT] <strong>Media</strong> Fetch 404 error will notify client as other errors. * [IMPROVEMENT] <strong>Media</strong> Support WebP for PageSpeed Insights. (@LucasRolff) * [BUGFIX] <strong>CLI</strong> Fixed the issue where CLI import/export caused certain textarea settings to be lost. (#767519) * [BUGFIX] <strong>CSS Optimize</strong> Fixed the issue that duplicated optimized CSS and caused rapid expansion of CSS cache folder. * [GUI] <strong>Media</strong> Refactored operation workflow and interface. * [UPDATE] <strong>Media</strong> Set timeout seconds to avoid pulling timeout. (@Jose) * [UPDATE] <strong>CDN</strong>Fixed the notice when no path is in URL. (@sabitkamera) * [UPDATE] <strong>Media</strong> Auto correct credits when pulling. * [UPDATE] <strong>GUI</strong> Removed redundant double quote in gui.cls. (@DaveyJake) * [IAPI] IAPI v2.4.1. * [IAPI] Allow new error status notification and success message from IAPI. = 2.4 - Jul 2 2018 = * [NEW FEATURE] <strong>Media</strong> Added lossless optimization. * [NEW FEATURE] <strong>Media</strong> Added Request Original Images ON/OFF. * [NEW FEATURE] <strong>Media</strong> Added Request WebP ON/OFF. (@JMCA2) * [IMPROVEMENT] <strong>Media</strong> Improved optimization tools to archive maximum compression and score. * [IMPROVEMENT] <strong>Media</strong> Improved speed of image pull. * [IMPROVEMENT] <strong>Media</strong> Automatically recover credit after pulled. * [REFACTOR] <strong>Config</strong> Separated configure const class. * [BUGFIX] <strong>Report</strong> Report can be sent successfully with emoji now. (@music47ell) * [IAPI] New Europe Image Optimization server (EU3/EU4). * [IAPI] New America Image Optimization server (US3/US4/US5/US6). * [IAPI] New Asian Image Optimization server (AS3). * [IAPI] Refactored optimization process. * [IAPI] Increased credit limit. * [IAPI] Removed request interval limit. * [IAPI] IAPI v2.4. * <strong>We strongly recommended that you re-optimize your image library to get a better compression result</strong>. = 2.3.1 - Jun 18 2018 = * [IMPROVEMENT] New setting to disable Generate Critical CSS. (@cybmeta) * [IMPROVEMENT] Added filter to can_cdn/can_optm check. (@Jacob) * [UPDATE] *Critical CSS* Added 404 css. Limit cron interval. * [UPDATE] AJAX will not bypass CDN anymore by default. (@Jacob) * [GUI] Show Disable All Features warning if it is on in Debug tab. = 2.3 - Jun 13 2018 = * [NEW FEATURE] Automatically generate critical CSS. (@joeee @ivan_ivanov @3dseo) * [BUGFIX] "Mark this page as..." from dropdown menu will not reset settings anymore. (@cbratschi) = 2.2.7 - Jun 4 2018 = * [IMPROVEMENT] Improved redirection for manual image pull to avoid too many redirections warning. * [IAPI] Increased credit limit. * [BUGFIX] Fixed 503 error when enabling log filters in Debug tab. (#525206) * [UPDATE] Improve compatibility when using sitemap url on servers with allow_url_open off. * [UPDATE] Removed Crawler HTTP2 option due to causing no-cache blacklist issue for certain environments. * [UPDATE] Privacy policy can be now translated. (@Josemi) * [UPDATE] IAPI Increased default img request max to 3000. = 2.2.6 - May 24 2018 = * [NEW FEATURE] Original image backups can be removed now. (@borisov87 @JMCA2) * [BUGFIX] Role Excludes in Tuning tab can save now. (@pako69) * [UPDATE] Added privacy policy support. = 2.2.5 - May 14 2018 = * [IAPI] <strong>Image Optimization</strong> New Asian Image Optimization server (AS2). * [INTEGRATION] Removed wpForo 3rd party file. (@massimod) = 2.2.4 - May 7 2018 = * [IMPROVEMENT] Improved compatibility with themes using the same js_min library. (#129093 @Darren) * [BUGFIX] Fixed a bug when checking image path for dynamic files. (@miladk) * [INTEGRATION] Compatibility with Universal Star Rating. (@miladk) = 2.2.3 - Apr 27 2018 = * [NEW FEATURE] WebP For Extra srcset setting in Media tab. (@vengen) * [REFACTOR] Removed redundant LS consts. * [REFACTOR] Refactored adv_cache generation flow. * [BUGFIX] Fixed issue where inline JS minify exception caused a blank page. (@oomskaap @kenb1978) * [UPDATE] Changed HTTP/2 Crawl default value to OFF. * [UPDATE] Added img.data-src to default WebP replacement value for WooCommerce WebP support. * [UPDATE] Detached crawler from LSCache LITESPEED_ON status. * [API] Improved ESI API to honor the cache control in ESI wrapper. * [API] Added LITESPEED_PURGE_SILENT const to bypass the notification when purging * [INTEGRATION] Fixed issue with nonce expiration when using ESI API. (#923505 @Dan) * [INTEGRATION] Improved compatibility with Ninja Forms by bypassing non-javascript JS from inline JS minify. * [INTEGRATION] Added a hook for plugins that change the CSS/JS path e.g. Hide My WordPress. = 2.2.2 - Apr 16 2018 = * [NEW FEATURE] WebP Attribute To Replace setting in Media tab. (@vengen) * [IMPROVEMENT] Generate adv_cache file automatically when it is lost. * [IMPROVEMENT] Improved compatibility with ajax login. (@veganostomy) * [UPDATE] Added object cache lib check in case user downgrades LSCWP to non-object-cache versions. * [UPDATE] Avoided infinite loop when users enter invalid hook values in Purge All Hooks settings. * [UPDATE] Updated log format in media&cdn class. * [UPDATE] Added more items to Report. = 2.2.1 - Apr 10 2018 = * [NEW FEATURE] Included Directories setting in CDN tab. (@Dave) * [NEW FEATURE] Purge All Hooks setting in Advanced tab. * [UPDATE] Added background-image WebP replacement support. (@vengen) * [UPDATE] Show recommended values for textarea items in settings. * [UPDATE] Moved CSS/JS optimizer log to Advanced level. * [INTEGRATION] Added WebP support for Avada Fusion Sliders. (@vengen) = 2.2.0.2 - Apr 3 2018 = * [HOTFIX] <strong>Object Cache</strong> Fixed the PHP warning caused by previous improvement to Object Cache. = 2.2.0.1 - Apr 3 2018 = * [HOTFIX] Object parameter will no longer cause warnings to be logged for Purge and Cache classes. (@kelltech @khrifat) * [UPDATE] Removed duplicated del_file func from Object Cache class. * [BUGFIX] `CLI` no longer shows 400 error upon successful result. = 2.2 - Apr 2 2018 = * [NEW FEATURE] <strong>Debug</strong> Disable All Features setting in Debug tab. (@monarobase) * [NEW FEATURE] <strong>Cache</strong> Force Cacheable URIs setting in Excludes tab. * [NEW FEATURE] <strong>Purge</strong> Purge all LSCache and other caches in one link. * [REFACTOR] <strong>Purge</strong> Refactored Purge class. * [BUGFIX] Query strings in DoNotCacheURI setting now works. * [BUGFIX] <strong>Cache</strong> Mobile cache compatibility with WebP vary. (@Shivam #987121) * [UPDATE] <strong>Purge</strong> Moved purge_all to Purge class from core class. * [API] Set cacheable/Set force cacheable. (@Jacob) = 2.1.2 - Mar 28 2018 = * [NEW FEATURE] <strong>Image Optimization</strong> Clean Up Unfinished Data feature. * [IAPI] IAPI v2.1.2. * [IMPROVEMENT] <strong>CSS/JS Minify</strong> Reduced loading time significantly by improving CSS/JS minify loading process. (@kokers) * [IMPROVEMENT] <strong>CSS/JS Minify</strong> Cache empty JS Minify content. (@kokers) * [IMPROVEMENT] <strong>Cache</strong> Cache 301 redirect when scheme/host are same. * [BUGFIX] <strong>Media</strong> Lazy load now can support WebP. (@relle) * [UPDATE] <strong>CSS/JS Optimize</strong> Serve static files for CSS async & lazy load JS library. * [UPDATE] <strong>Report</strong> Appended Basic/Advanced View setting to Report. * [UPDATE] <strong>CSS/JS Minify</strong> Removed zero-width space from CSS/JS content. * [GUI] Added Purge CSS/JS Cache link in Admin. = 2.1.1.1 - Mar 21 2018 = * [BUGFIX] Fixed issue where activation failed to add rules to .htaccess. * [BUGFIX] Fixed issue where 304 header was blank on feed page refresh. = 2.1.1 - Mar 20 2018 = * [NEW FEATURE] <strong>Browser Cache</strong> Unlocked for non-LiteSpeed users. * [IMPROVEMENT] <strong>Image Optimization</strong> Fixed issue where images with bad postmeta value continued to show in not-yet-requested queue. = 2.1 - Mar 15 2018 = * [NEW FEATURE] <strong>Image Optimization</strong> Unlocked for non-LiteSpeed users. * [NEW FEATURE] <strong>Object Cache</strong> Unlocked for non-LiteSpeed users. * [NEW FEATURE] <strong>Crawler</strong> Unlocked for non-LiteSpeed users. * [NEW FEATURE] <strong>Database Cleaner and Optimizer</strong> Unlocked for non-LiteSpeed users. * [NEW FEATURE] <strong>Lazy Load Images</strong> Unlocked for non-LiteSpeed users. * [NEW FEATURE] <strong>CSS/JS/HTML Minify/Combine Optimize</strong> Unlocked for non-LiteSpeed users. * [IAPI] IAPI v2.0. * [IAPI] Increased max rows prefetch when client has additional credit. * [IMPROVEMENT] <strong>CDN</strong> Multiple domains may now be used. * [IMPROVEMENT] <strong>Report</strong> Added WP environment constants for better debugging. * [REFACTOR] Separated Cloudflare CDN class. * [BUGFIX] <strong>Image Optimization</strong> Fixed issue where certain MySQL version failed to create img_optm table. (@philippwidmer) * [BUGFIX] <strong>Image Optimization</strong> Fixed issue where callback validation failed when pulling and sending request simultaneously. * [GUI] Added Slack community banner. * [INTEGRATION] CDN compatibility with WPML multiple domains. (@egemensarica) = 2.0 - Mar 7 2018 = * [NEW FEATURE] <strong>Image Optimization</strong> Added level up guidance. * [REFACTOR] <strong>Image Optimization</strong> Refactored Image Optimization class. * [IAPI] <strong>Image Optimization</strong> New European Image Optimization server (EU2). * [IMPROVEMENT] <strong>Image Optimization</strong> Manual pull action continues pulling until complete. * [IMPROVEMENT] <strong>CDN</strong> Multiple CDNs can now be randomized for a single resource. * [IMPROVEMENT] <strong>Image Optimization</strong> Improved compatibility of long src images. * [IMPROVEMENT] <strong>Image Optimization</strong> Reduced runtime load. * [IMPROVEMENT] <strong>Image Optimization</strong> Avoid potential loss/reset of notified images status when pulling. * [IMPROVEMENT] <strong>Image Optimization</strong> Avoid duplicated optimization for multiple records in Media that have the same image source. * [IMPROVEMENT] <strong>Image Optimization</strong> Fixed issue where phantom images continued to show in not-yet-requested queue. * [BUGFIX] <strong>Core</strong> Improved compatibility when upgrading outside of WP Admin. (@jikatal @TylorB) * [BUGFIX] <strong>Crawler</strong> Improved HTTP/2 compatibility to avoid erroneous blacklisting. * [BUGFIX] <strong>Crawler</strong> Changing Delay setting will use server variable for min value validation if set. * [UPDATE] <strong>Crawler</strong> Added HTTP/2 protocol switch in the Crawler settings. * [UPDATE] Removed unnecessary translation strings. * [GUI] Display translated role group name string instead of English values. (@Richard Hordern) * [GUI] Added Join LiteSpeed Slack link. * [GUI] <strong>Import / Export</strong> Cosmetic changes to Import Settings file field. * [INTEGRATION] Improved compatibility with WPML Media for Image Optimization. (@szmigieldesign) = 1.9.1.1 - February 20 2018 = * [Hotfix] Removed empty crawler when no role simulation is set. = 1.9.1 - February 20 2018 = * [NEW FEATURE] Role Simulation crawler. * [NEW FEATURE] WebP multiple crawler. * [NEW FEATURE] HTTP/2 support for crawler. * [BUGFIX] Fixed a js bug with the auto complete mobile user agents field when cache mobile is turned on. * [BUGFIX] Fixed a constant undefined warning after activation. * [GUI] Sitemap generation settings are no longer hidden when using a custom sitemap. = 1.9 - February 12 2018 = * [NEW FEATURE] Inline CSS/JS Minify. * [IMPROVEMENT] Removed Composer vendor to thin the plugin folder. * [UPDATE] Tweaked H2 to H1 in Admin headings for accessibility. (@steverep) * [GUI] Added Mobile User Agents to basic view. * [GUI] Moved Object Cache & Browser Cache from Cache tab to Advanced tab. * [GUI] Moved LSCache Purge All from Adminbar to dropdown menu. = 1.8.3 - February 2 2018 = * [NEW FEATURE] Crawler server variable limitation support. * [IMPROVEMENT] Added Store Transients option to fix transients missing issue when Cache Wp-Admin setting is OFF. * [IMPROVEMENT] Tweaked ARIA support. (@steverep) * [IMPROVEMENT] Used strpos instead of strncmp for performance. (@Zach E) * [BUGFIX] Transient cache can now be removed when the Cache Wp-Admin setting is ON in Object Cache. * [BUGFIX] Network sites can now save Advanced settings. * [BUGFIX] Media list now shows in network sites. * [BUGFIX] Show Crawler Status button is working again. * [UPDATE] Fixed a couple of potential PHP notices in the Network cache tab and when no vary group is set. * [GUI] Added Learn More link to all setting pages. = 1.8.2 - January 29 2018 = * [NEW FEATURE] Instant Click in the Advanced tab. * [NEW FEATURE] Import/Export settings. * [NEW FEATURE] Opcode Cache support. * [NEW FEATURE] Basic/Advanced setting view. * [IMPROVEMENT] Added ARIA support in widget settings. * [BUGFIX] Multiple WordPress instances with same Object Cache address will no longer see shared data. * [BUGFIX] WebP Replacement may now be set at the Network level. * [BUGFIX] Object Cache file can now be removed at the Network level uninstall. = 1.8.1 - January 22 2018 = * [NEW FEATURE] Object Cache now supports Redis. * [IMPROVEMENT] Memcached Object Cache now supports authorization. * [IMPROVEMENT] A 500 error will no longer be encountered when turning on Object Cache without the proper PHP extension installed. * [BUGFIX] Object Cache settings can now be saved at the Network level. * [BUGFIX] Mu-plugin now supports Network setting. * [BUGFIX] Fixed admin bar showing inaccurate Edit Page link. * [UPDATE] Removed warning information when no Memcached server is available. = 1.8 - January 17 2018 = * [NEW FEATURE] Object Cache. * [REFACTOR] Refactored Log class. * [REFACTOR] Refactored LSCWP basic const initialization. * [BUGFIX] Fixed Cloudflare domain search breaking when saving more than 50 domains under a single account. * [UPDATE] Log filter settings are now their own item in the wp-option table. = 1.7.2 - January 5 2018 = * [NEW FEATURE] Cloudflare API support. * [IMPROVEMENT] IAPI key can now be reset to avoid issues when domain is changed. * [BUGFIX] Fixed JS optimizer breaking certain plugins JS. * [UPDATE] Added cdn settings to environment report. * [GUI] Added more shortcuts to backend adminbar. * [INTEGRATION] WooCommerce visitors are now served from public cache when cart is empty. = 1.7.1.1 - December 29 2017 = * [BUGFIX] Fixed an extra trailing underscore issue when saving multiple lines with DNS Prefetch. * [UPDATE] Cleaned up unused dependency vendor files. = 1.7.1 - December 28 2017 = * [NEW FEATURE] Added DNS Prefetch setting on the Optimize page. * [NEW FEATURE] Added Combined File Max Size setting on the Tuning page. * [IMPROVEMENT] Improved JS/CSS minify to achieve higher page scores. * [IMPROVEMENT] Optimized JS/CSS files will not be served from private cache for OLS or with ESI off. * [UPDATE] Fixed a potential warning for new installations on the Settings page. * [UPDATE] Fixed an issue with guest users occasionally receiving PHP warnings. * [BUGFIX] Fixed a bug with the Improve HTTPS Compatibility setting failing to save. * Thanks to all of our users for your encouragement and support! Happy New Year! * PS: Lookout 2018, we're back! = 1.7 - December 22 2017 = * [NEW FEATURE] Drop Query Strings setting in the Cache tab. * [NEW FEATURE] Multiple CDN Mapping in the CDN tab. * [IMPROVEMENT] Improve HTTP/HTTPS Compatibility setting in the Advanced tab. * [IMPROVEMENT] Keep JS/CSS original position in HTML when excluded in setting. * [IAPI] Reset client level credit after Image Optimization data is destroyed. * [REFACTOR] Refactored build_input/textarea functions in admin_display class. * [REFACTOR] Refactored CDN class. * [GUI] Added a notice to Image Optimization and Crawler to warn when cache is disabled. * [GUI] Improved image optimization indicator styles in Media Library List. = 1.6.7 - December 15 2017 = * [IAPI] Added ability to scan for new image thumbnail sizes and auto-resend image optimization requests. * [IAPI] Added ability to destroy all optimization data. * [IAPI] Updated IAPI to v1.6.7. * [INTEGRATION] Fixed certain 3rd party plugins calling REST without user nonce causing logged in users to be served as guest. = 1.6.6.1 - December 8 2017 = * [IAPI] Limit first-time submission to one image group for test-run purposes. * [BUGFIX] Fixed vary group generation issue associated with custom user role plugins. * [BUGFIX] Fixed WooCommerce issue where logged-in users were erroneously purged when ESI is off. * [BUGFIX] Fixed WooCommerce cache miss issue when ESI is off. = 1.6.6 - December 6 2017 = * [NEW FEATURE] Preserve EXIF in Media setting. * [NEW FEATURE] Clear log button in Debug Log Viewer. * [IAPI] Fixed notified images resetting to previous status when pulling. * [IAPI] Fixed HTTPS compatibility for image optimization initialization. * [IAPI] An error message is now displayed when image optimization request submission is bypassed due to a lack of credit. * [IAPI] IAPI v1.6.6. * [IMPROVEMENT] Support JS data-no-optimize attribute to bypass optimization. * [GUI] Added image group wiki link. * [INTEGRATION] Improved compatibility with Login With Ajax. * [INTEGRATION] Added function_exists check for WooCommerce to avoid 500 errors. = 1.6.5.1 - December 1 2017 = * [HOTFIX] Fixed warning message on Edit .htaccess page. = 1.6.5 - November 30 2017 = * [IAPI] Manually pull image optimization action button. * [IAPI] Automatic credit system for image optimization to bypass unfinished image optimization error. * [IAPI] Notify failed images from LiteSpeed's Image Server. * [IAPI] Reset/Clear failed images feature. * [IAPI] Redesigned report page. * [REFACTOR] Moved pull_img logic from admin_api to media. * [BUGFIX] Fixed a compatibility issue for clients who have allow_url_open setting off. * [BUGFIX] Fixed logged in users sometimes being served from guest cache. * [UPDATE] Environment report is no longer saved to a file. * [UPDATE] Removed crawler reset notification. * [GUI] Added more details on image optimization. * [GUI] Removed info page from admin menu. * [GUI] Moved environment report from network level to single site level. * [GUI] Crawler time added in a user friendly format. * [INTEGRATION] Improved compatibility with FacetWP json call. = 1.6.4 - November 22 2017 = * [NEW FEATURE] Send env reports privately with a new built-in report number referral system. * [IAPI] Increased request timeout to fix a cUrl 28 timeout issue. * [BUGFIX] Fixed a TTL max value validation bug. * [INTEGRATION] Improved Contact Form 7 REST call compatibility for logged in users. * Thanks for all your ratings. That encouraged us to be more diligent. Happy Thanksgiving. = 1.6.3 - November 17 2017 = * [NEW FEATURE] Only async Google Fonts setting. * [NEW FEATURE] Only create WebP images when optimizing setting. * [NEW FEATURE] Batch switch images to original/optimized versions in Image Optimization. * [NEW FEATURE] Browser Cache TTL setting. * [NEW FEATURE] Cache WooCommerce Cart setting. * [IMPROVEMENT] Moved optimized JS/CSS snippet in header html to after meta charset. * [IMPROVEMENT] Added a constant for better JS/CSS optimization compatibility for different dir WordPress installation. * [IAPI] Take over failed callback check instead of bypassing it. * [IAPI] Image optimization requests are now limited to 500 images per request. * [BUGFIX] Fixed a parsing failure bug not using attributes in html elements with dash. * [BUGFIX] Fixed a bug causing non-script code to move to the top of a page when not using combination. * [UPDATE] Added detailed logs for external link detection. * [UPDATE] Added new lines in footer comment to avoid Firefox crash when enabled HTML minify. * [API] `Purge private` / `Purge private all` / `Add private tag` functions. * [GUI] Redesigned image optimization operation links in Media Lib list. * [GUI] Tweaked wp-admin form save button position. * [GUI] Added "learn more" link for image optimization. = 1.6.2.1 - November 6 2017 = * [INTEGRATION] Improved compatibility with old WooCommerce versions to avoid unknown 500 errors. * [BUGFIX] Fixed WebP images sometimes being used in non-supported browsers. * [BUGFIX] Kept query strings for HTTP/2 push to avoid re-fetching pushed sources. * [BUGFIX] Excluded JS/CSS from HTTP/2 push when using CDN. * [GUI] Fixed a typo in Media list. * [GUI] Made more image optimization strings translatable. * [GUI] Updated Tuning description to include API documentation. = 1.6.2 - November 3 2017 = * [NEW FEATURE] Do Not Cache Roles. * [NEW FEATURE] Use WebP Images for supported browsers. * [NEW FEATURE] Disable Optimization Poll ON/OFF Switch in Media tab. * [NEW FEATURE] Revert image optimization per image in Media list. * [NEW FEATURE] Disable/Enable image WebP per image in Media list. * [IAPI] Limit optimized images fetching cron to a single process. * [IAPI] Updated IAPI to v1.6.2. * [IAPI] Fixed repeating image request issue by adding a failure status to local images. * [REFACTOR] Refactored login vary logic. = 1.6.1 - October 29 2017 = * [IAPI] Updated LiteSpeed Image Optimization Server API to v1.6.1. = 1.6 - October 27 2017 = * [NEW FEATURE] Image Optimization. * [NEW FEATURE] Role Excludes for Optimization. * [NEW FEATURE] Combined CSS/JS Priority. * [IMPROVEMENT] Bypass CDN for login/register page. * [UPDATE] Expanded ExpiresByType rules to include new font types. ( Thanks to JMCA2 ) * [UPDATE] Removed duplicated type param in admin action link. * [BUGFIX] Fixed CDN wrongly replacing img base64 and "fake" src in JS. * [BUGFIX] Fixed image lazy load replacing base64 src. * [BUGFIX] Fixed a typo in Optimize class exception. * [GUI] New Tuning tab in admin settings panel. * [REFACTOR] Simplified router by reducing actions and adding types. * [REFACTOR] Renamed `run()` to `finalize()` in buffer process. = 1.5 - October 17 2017 = * [NEW FEATURE] Exclude JQuery (to fix inline JS error when using JS Combine). * [NEW FEATURE] Load JQuery Remotely. * [NEW FEATURE] JS Deferred Excludes. * [NEW FEATURE] Lazy Load Images Excludes. * [NEW FEATURE] Lazy Load Image Placeholder. * [IMPROVEMENT] Improved Lazy Load size attribute for w3c validator. * [UPDATE] Added basic caching info and LSCWP version to HTML comment. * [UPDATE] Added debug log to HTML detection. * [BUGFIX] Fixed potential font CORS issue when using CDN. * [GUI] Added API docs to setting description. * [REFACTOR] Relocated all classes under includes with backwards compatibility. * [REFACTOR] Relocated admin templates. = 1.4 - October 11 2017 = * [NEW FEATURE] Lazy load images/iframes. * [NEW FEATURE] Clean CSS/JS optimizer data functionality in DB Optimizer panel. * [NEW FEATURE] Exclude certain URIs from optimizer. * [IMPROVEMENT] Improved optimizer HTML check compatibility to avoid conflicts with ESI functions. * [IMPROVEMENT] Added support for using ^ when matching the start of a path in matching settings. * [IMPROVEMENT] Added wildcard support in CDN original URL. * [IMPROVEMENT] Moved optimizer table initialization to admin setting panel with failure warning. * [UPDATE] Added a one-time welcome banner. * [UPDATE] Partly relocated class: 'api'. * [API] Added API wrapper for removing wrapped HTML output. * [INTEGRATION] Fixed WooCommerce conflict with optimizer. * [INTEGRATION] Private cache support for WooCommerce v3.2.0+. * [GUI] Added No Optimization menu to frontend. = 1.3.1.1 - October 6 2017 = * [BUGFIX] Improved optimizer table creating process in certain database charset to avoid css/js minify/combination failure. = 1.3.1 - October 5 2017 = * [NEW FEATURE] Remove WP Emoji Option. * [IMPROVEMENT] Separated optimizer data from wp_options to improve compatibility with backup plugins. * [IMPROVEMENT] Enhanced crawler cron hook to prevent de-scheduling in some cases. * [IMPROVEMENT] Enhanced Remove Query Strings to also remove Emoji query strings. * [IMPROVEMENT] Enhanced HTML detection when extra spaces are present at the beginning. * [UPDATE] Added private cache support for OLS. * [BUGFIX] Self-redirects are no longer cached. * [BUGFIX] Fixed css async lib warning when loading in HTTP/2 push. = 1.3 - October 1 2017 = * [NEW FEATURE] Added Browser Cache support. * [NEW FEATURE] Added Remove Query Strings support. * [NEW FEATURE] Added Remove Google Fonts support. * [NEW FEATURE] Added Load CSS Asynchronously support. * [NEW FEATURE] Added Load JS Deferred support. * [NEW FEATURE] Added Critical CSS Rules support. * [NEW FEATURE] Added Private Cached URIs support. * [NEW FEATURE] Added Do Not Cache Query Strings support. * [NEW FEATURE] Added frontend adminbar shortcuts ( Purge this page/Do Not Cache/Private cache ). * [IMPROVEMENT] Do Not Cache URIs now supports full URLs. * [IMPROVEMENT] Improved performance of Do Not Cache settings. * [IMPROVEMENT] Encrypted vary cookie. * [IMPROVEMENT] Enhanced HTML optimizer. * [IMPROVEMENT] Limited combined file size to avoid heavy memory usage. * [IMPROVEMENT] CDN supports custom upload folder for media files. * [API] Added purge single post API. * [API] Added version compare API. * [API] Enhanced ESI API for third party plugins. * [INTEGRATION] Compatibility with NextGEN Gallery v2.2.14. * [INTEGRATION] Compatibility with Caldera Forms v1.5.6.2+. * [BUGFIX] Fixed CDN&Minify compatibility with css url links. * [BUGFIX] Fixed .htaccess being regenerated despite there being no changes. * [BUGFIX] Fixed CDN path bug for subfolder WP instance. * [BUGFIX] Fixed crawler path bug for subfolder WP instance with different site url and home url. * [BUGFIX] Fixed a potential Optimizer generating redundant duplicated JS in HTML bug. * [GUI] Added a more easily accessed submit button in admin settings. * [GUI] Admin settings page cosmetic changes. * [GUI] Reorganized GUI css/img folder structure. * [REFACTOR] Refactored configuration init. * [REFACTOR] Refactored admin setting save. * [REFACTOR] Refactored .htaccess operator and rewrite rule generation. = 1.2.3.1 - September 20 2017 = * [UPDATE] Improved PHP5.3 compatibility. = 1.2.3 - September 20 2017 = * [NEW FEATURE] Added CDN support. * [IMPROVEMENT] Improved compatibility when upgrading by fixing a possible fatal error. * [IMPROVEMENT] Added support for custom wp-content paths. * [BUGFIX] Fixed non-primary network blogs not being able to minify. * [BUGFIX] Fixed HTML Minify preventing Facebook from being able to parse og tags. * [BUGFIX] Preview page is no longer cacheable. * [BUGFIX] Corrected log and crawler timezone to match set WP timezone. * [GUI] Revamp of plugin GUI. = 1.2.2 - September 15 2017 = * [NEW FEATURE] Added CSS/JS minification. * [NEW FEATURE] Added CSS/JS combining. * [NEW FEATURE] Added CSS/JS HTTP/2 server push. * [NEW FEATURE] Added HTML minification. * [NEW FEATURE] Added CSS/JS cache purge button in management. * [UPDATE] Improved debug log formatting. * [UPDATE] Fixed some description typos. = 1.2.1 - September 7 2017 = * [NEW FEATURE] Added Database Optimizer. * [NEW FEATURE] Added Tab switch shortcut. * [IMPROVEMENT] Added cache disabled check for management pages. * [IMPROVEMENT] Renamed .htaccess backup for security. * [BUGFIX] Fixed woocommerce default ESI setting bug. * [REFACTOR] Show ESI page for OLS with notice. * [REFACTOR] Management Purge GUI updated. = 1.2.0.1 - September 1 2017 = * [BUGFIX] Fixed a naming bug for network constant ON2. = 1.2.0 - September 1 2017 = * [NEW FEATURE] Added ESI support. * [NEW FEATURE] Added a private cache TTL setting. * [NEW FEATURE] Debug level can now be set to either 'Basic' or 'Advanced'. * [REFACTOR] Renamed const 'NOTSET' to 'ON2' in class config. = 1.1.6 - August 23 2017 = * [NEW FEATURE] Added option to privately cache logged-in users. * [NEW FEATURE] Added option to privately cache commenters. * [NEW FEATURE] Added option to cache requests made through WordPress REST API. * [BUGFIX] Fixed network 3rd-party full-page cache detection bug. * [GUI] New Cache and Purge menus in Settings. = 1.1.5.1 - August 16 2017 = * [IMPROVEMENT] Improved compatibility of frontend&backend .htaccess path detection when site url is different than installation path. * [UPDATE] Removed unused format string from header tags. * [BUGFIX] 'showheader' Admin Query String now works. * [REFACTOR] Cache tags will no longer output if not needed. = 1.1.5 - August 10 2017 = * [NEW FEATURE] Scheduled Purge URLs feature. * [NEW FEATURE] Added buffer callback to improve compatibility with some plugins that force buffer cleaning. * [NEW FEATURE] Hide purge_all admin bar quick link if cache is disabled. * [NEW FEATURE] Required htaccess rules are now displayed when .htaccess is not writable. * [NEW FEATURE] Debug log features: filter log support; heartbeat control; log file size limit; log viewer. * [IMPROVEMENT] Separate crawler access log. * [IMPROVEMENT] Lazy PURGE requests made after output are now queued and working. * [IMPROVEMENT] Improved readme.txt with keywords relating to our compatible plugins list. * [UPDATE] 'ExpiresDefault' conflict msg is now closeable and only appears in the .htaccess edit screen. * [UPDATE] Improved debug log formatting. * [INTEGRATION] Compatibility with MainWP plugin. * [BUGFIX] Fixed WooCommerce order not purging product stock quantity. * [BUGFIX] Fixed WooCommerce scheduled sale price not updating issue. * [REFACTOR] Combined cache_enable functions into a single function. = 1.1.4 - August 1 2017 = * [IMPROVEMENT] Unexpected rewrite rules will now show an error message. * [IMPROVEMENT] Added Cache Tag Prefix setting info in the Env Report and Info page. * [IMPROVEMENT] LSCWP setting link is now displayed in the plugin list. * [IMPROVEMENT] Improved performance when setting cache control. * [UPDATE] Added backward compatibility for v1.1.2.2 API calls. (used by 3rd-party plugins) * [BUGFIX] Fixed WPCLI purge tag/category never succeeding. = 1.1.3 - July 31 2017 = * [NEW FEATURE] New LiteSpeed_Cache_API class and documentation for 3rd party integration. * [NEW FEATURE] New API function litespeed_purge_single_post($post_id). * [NEW FEATURE] PHP CLI support for crawler. * [IMPROVEMENT] Set 'no cache' for same location 301 redirects. * [IMPROVEMENT] Improved LiteSpeed footer comment compatibility. * [UPDATE] Removed 'cache tag prefix' setting. * [BUGFIX] Fixed a bug involving CLI purge all. * [BUGFIX] Crawler now honors X-LiteSpeed-Cache-Control for the 'no-cache' header. * [BUGFIX] Cache/rewrite rules are now cleared when the plugin is uninstalled. * [BUGFIX] Prevent incorrect removal of the advanced-cache.php on deactivation if it was added by another plugin. * [BUGFIX] Fixed subfolder WP installations being unable to Purge By URL using a full URL path. * [REFACTOR] Reorganized existing code for an upcoming ESI release. = 1.1.2.2 - July 13 2017 = * [BUGFIX] Fixed blank page in Hebrew language post editor by removing unused font-awesome and jquery-ui css libraries. = 1.1.2.1 - July 5 2017 = * [UPDATE] Improved compatibility with WooCommerce v3.1.0. = 1.1.2 - June 20 2017 = * [BUGFIX] Fixed missing form close tag. * [UPDATE] Added a wiki link for enabling the crawler. * [UPDATE] Improved Site IP description. * [UPDATE] Added an introduction to the crawler on the Information page. * [REFACTOR] Added more detailed error messages for Site IP and Custom Sitemap settings. = 1.1.1.1 - June 15 2017 = * [BUGFIX] Hotfix for insufficient validation of site IP value in crawler settings. = 1.1.1 - June 15 2017 = * [NEW] As of LiteSpeed Web Server v.5.1.16, the crawler can now be enabled/disabled at the server level. * [NEW] Added the ability to provide a custom sitemap for crawling. * [NEW] Added ability to use site IP address directly in crawler settings. * [NEW] Crawler performance improved with the use of new custom user agent 'lsrunner'. * [NEW] "Purge By URLs" now supports full URL paths. * [NEW] Added thirdparty WP-PostRatings compatibility. * [BUGFIX] Cache is now cleared when changing post status from published to draft. * [BUGFIX] WHM activation message no longer continues to reappear after being dismissed. * [COSMETIC] Display recommended values for settings. = 1.1.0.1 - June 8 2017 = * [UPDATE] Improved default crawler interval setting. * [UPDATE] Tested up to WP 4.8. * [BUGFIX] Fixed compatibility with plugins that output json data. * [BUGFIX] Fixed tab switching bug. * [BUGFIX] Removed occasional duplicated messages on save. * [COSMETIC] Improved crawler tooltips and descriptions. = 1.1.0 - June 6 2017 = * [NEW] Added a crawler - this includes configuration options and a dedicated admin page. Uses wp-cron * [NEW] Added integration for WPLister * [NEW] Added integration for Avada * [UPDATE] General structure of the plugin revamped * [UPDATE] Improved look of admin pages * [BUGFIX] Fix any/all wp-content path retrieval issues * [BUGFIX] Use realpath to clear symbolic link when determining .htaccess paths * [BUGFIX] Fixed a bug where upgrading multiple plugins did not trigger a purge all * [BUGFIX] Fixed a bug where cli import_options did not actually update the options. * [REFACTOR] Most of the files in the code were split into more, smaller files = 1.0.15 - April 20 2017 = * [NEW] Added Purge Pages and Purge Recent Posts Widget pages options. * [NEW] Added wp-cli command for setting and getting options. * [NEW] Added an import/export options cli command. * [NEW] Added wpForo integration. * [NEW] Added Theme My Login integration. * [UPDATE] Purge adjacent posts when publish a new post. * [UPDATE] Change environment report file to .php and increase security. * [UPDATE] Added new purgeby option to wp-cli. * [UPDATE] Remove nag for multiple sites. * [UPDATE] Only inject LiteSpeed javascripts in LiteSpeed pages. * [REFACTOR] Properly check for zero in ttl settings. * [BUGFIX] Fixed the 404 issue that can be caused by some certain plugins when save the settings. * [BUGFIX] Fixed mu-plugin compatibility. * [BUGFIX] Fixed problem with creating zip backup. * [BUGFIX] Fixed conflict with jetpack. = 1.0.14.1 - January 31 2017 = * [UPDATE] Removed Freemius integration due to feedback. = 1.0.14 - January 30 2017 = * [NEW] Added error page caching. Currently supports 403, 404, 500s. * [NEW] Added a purge errors action. * [NEW] Added wp-cli integration. * [UPDATE] Added support for multiple varies. * [UPDATE] Reorganize the admin interface to be less cluttered. * [UPDATE] Add support for LiteSpeed Web ADC. * [UPDATE] Add Freemius integration. * [REFACTOR] Made some changes so that the rewrite rules are a little more consistent. * [BUGFIX] Check member type before adding purge all button. * [BUGFIX] Fixed a bug where activating/deactivating the plugin quickly caused the WP_CACHE error to show up. * [BUGFIX] Handle more characters in the rewrite parser. * [BUGFIX] Correctly purge posts when they are made public/private. = 1.0.13.1 - November 30 2016 = * [BUGFIX] Fixed a bug where a global was being used without checking existence first, causing unnecessary log entries. = 1.0.13 - November 28 2016 = * [NEW] Add an Empty Entire Cache button. * [NEW] Add stale logic to certain purge actions. * [NEW] Add option to use primary site settings for all subsites in a multisite environment. * [NEW] Add support for Aelia CurrencySwitcher * [UPDATE] Add logic to allow third party vary headers * [UPDATE] Handle password protected pages differently. * [BUGFIX] Fixed bug caused by saving settings. * [BUGFIX] FIxed bug when searching for advanced-cache.php = 1.0.12 - November 14 2016 = * [NEW] Added logic to generate environment reports. * [NEW] Created a notice that will be triggered when the WHM Plugin installs this plugin. This will notify users when the plugin is installed by their server admin. * [NEW] Added the option to cache 404 pages via 404 Page TTL setting. * [NEW] Reworked log system to be based on selection of yes or no instead of log level. * [NEW] Added support for Autoptimize. * [NEW] Added Better WP Minify integration. * [UPDATE] On plugin disable, clear .htaccess. * [UPDATE] Introduced URL tag. Changed Purge by URL to use this new tag. * [BUGFIX] Fixed a bug triggered when .htaccess files were empty. * [BUGFIX] Correctly determine when to clear files in multisite environments (wp-config, advanced-cache, etc.). * [BUGFIX] When disabling the cache, settings changed in the same save will now be saved. * [BUGFIX] Various bugs from setting changes and multisite fixed. * [BUGFIX] Fixed two bugs with the .htaccess path search. * [BUGFIX] Do not alter $_GET in add_quick_purge. This may cause issues for functionality occurring later in the same request. * [BUGFIX] Right to left radio settings were incorrectly displayed. The radio buttons themselves were the opposite direction of the associated text. = 1.0.11 - October 11 2016 = * [NEW] The plugin will now set cachelookup public on. * [NEW] New option - check advanced-cache.php. This enables users to have two caching plugins enabled at the same time as long as the other plugin is not used for caching purposes. For example, using another cache plugin for css/js minification. * [UPDATE] Rules added by the plugin will now be inserted into an LSCACHE START/END PLUGIN comment block. * [UPDATE] For woocommerce pages, if a user visits a non-cached page with a non-empty cart, do not cache the page. * [UPDATE] If woocommerce needs to display any notice, do not cache the page. * [UPDATE] Single site settings are now in both the litespeed cache submenu and the settings submenu. * [BUGFIX] Multisite network options were not updated on upgrade. This is now corrected. = 1.0.10 - September 16 2016 = * Added a check for LSCACHE_NO_CACHE definition. * Added a Purge All button to the admin bar. * Added logic to purge the cache when upgrading a plugin or theme. By default this is enabled on single site installations and disabled on multisite installations. * Added support for WooCommerce Versions < 2.5.0. * Added .htaccess backup rotation. Every 10 backups, an .htaccess archive will be created. If one already exists, it will be overwritten. * Moved some settings to the new Specific Pages tab to reduce clutter in the General tab. * The .htaccess editor is now disabled if DISALLOW_FILE_EDIT is set. * After saving the Cache Tag Prefix setting, all cache will be purged. = 1.0.9.1 - August 26 2016 = * Fixed a bug where an error displayed on the configuration screen despite not being an error. * Change logic to check .htaccess file less often. = 1.0.9 - August 25 2016 = * [NEW] Added functionality to cache and purge feeds. * [NEW] Added cache tag prefix setting to avoid conflicts when using LiteSpeed Cache for WordPress with LiteSpeed Cache for XenForo and LiteMage. * [NEW] Added hooks to allow third party plugins to create config options. * [NEW] Added WooCommerce config options. * The plugin now also checks for wp-config in the parent directory. * Improved WooCommerce support. * Changed .htaccess backup process. Will create a .htaccess_lscachebak_orig file if one does not exist. If it does already exist, creates a backup using the date and timestamp. * Fixed a bug where get_home_path() sometimes returned an invalid path. * Fixed a bug where if the .htaccess was removed from a WordPress subdirectory, it was not handled properly. = 1.0.8.1 - July 28 2016 = * Fixed a bug where check cacheable was sometimes not hit. * Fixed a bug where extra slashes in clear rules were stripped. = 1.0.8 - July 25 2016 = * Added purge all on update check to purge by post id logic. * Added uninstall logic. * Added configuration for caching favicons. * Added configuration for caching the login page. * Added configuration for caching php resources (scripts/stylesheets accessed as .php). * Set login cookie if user is logged in and it isn’t set. * Improved NextGenGallery support to include new actions. * Now displays a notice on the network admin if WP_CACHE is not set. * Fixed a few php syntax issues. * Fixed a bug where purge by pid didn’t work. * Fixed a bug where the Network Admin settings were shown when the plugin was active in a subsite, but not network active. * Fixed a bug where the Advanced Cache check would sometimes not work. = 1.0.7.1 - May 26 2016 = * Fixed a bug where enabling purge all in the auto purge on update settings page did not purge the correct blogs. * Fixed a bug reported by user wpc on our forums where enabling purge all in the auto purge on update settings page caused nothing to be cached. = 1.0.7 - May 24 2016 = * Added login cookie configuration to the Advanced Settings page. * Added support for WPTouch plugin. * Added support for WP-Polls plugin. * Added Like Dislike Counter third party integration. * Added support for Admin IP Query String Actions. * Added confirmation pop up for purge all. * Refactor: LiteSpeed_Cache_Admin is now split into LiteSpeed_Cache_Admin, LiteSpeed_Cache_Admin_Display, and LiteSpeed_Cache_Admin_Rules * Refactor: Rename functions to accurately represent their functionality * Fixed a bug that sometimes caused a “no valid header” error message. = 1.0.6 - May 5 2016 = * Fixed a bug reported by Knut Sparhell that prevented dashboard widgets from being opened or closed. * Fixed a bug reported by Knut Sparhell that caused problems with https support for admin pages. = 1.0.5 - April 26 2016 = * [BETA] Added NextGen Gallery plugin support. * Added third party plugin integration. * Improved cache tag system. * Improved formatting for admin settings pages. * Converted bbPress to use the new third party integration system. * Converted WooCommerce to use the new third party integration system. * If .htaccess is not writable, disable separate mobile view and do not cache cookies/user agents. * Cache is now automatically purged when disabled. * Fixed a bug where .htaccess was not checked properly when adding common rules. * Fixed a bug where multisite setups would be completely purged when one site requested a purge all. = 1.0.4 - April 7 2016 = * Added logic to cache commenters. * Added htaccess backup to the install script. * Added an htaccess editor in the wp-admin dashboard. * Added do not cache user agents. * Added do not cache cookies. * Created new LiteSpeed Cache Settings submenu entries. * Implemented Separate Mobile View. * Modified WP_CACHE not defined message to only show up for users who can manage options. * Moved enabled all/disable all from network management to network settings. * Fixed a bug where WP_CACHE was not defined on activation if it was commented out. = 1.0.3 - March 23 2016 = * Added a Purge Front Page button to the LiteSpeed Cache Management page. * Added a Default Front Page TTL option to the general settings. * Added ability to define web application specific cookie names through rewrite rules to handle logged-in cookie conflicts when using multiple web applications. <strong>[Requires LSWS 5.0.15+]</strong> * Improved WooCommerce handling. * Fixed a bug where activating lscwp sets the “enable cache” radio button to enabled, but the cache was not enabled by default. * Refactored code to make it cleaner. * Updated readme.txt. = 1.0.2 - March 11 2016 = * Added a "Use Network Admin Setting" option for "Enable LiteSpeed Cache". For single sites, this choice will default to enabled. * Added enable/disable all buttons for network admin. This controls the setting of all managed sites with "Use Network Admin Setting" selected for "Enable LiteSpeed Cache". * Exclude by Category/Tag are now text areas to avoid slow load times on the LiteSpeed Cache Settings page for sites with a large number of categories/tags. * Added a new line to advanced-cache.php to allow identification as a LiteSpeed Cache file. * Activation/Deactivation are now better handled in multi-site environments. * Enable LiteSpeed Cache setting is now a radio button selection instead of a single checkbox. * Can now add '$' to the end of a URL in Exclude URI to perform an exact match. * The _lscache_vary cookie will now be deleted upon logout. * Fixed a bug in multi-site setups that would cause a "function already defined" error. = 1.0.1 - March 8 2016 = * Added Do Not Cache by URI, by Category, and by Tag. URI is a prefix/string equals match. * Added a help tab for plugin compatibilities. * Created logic for other plugins to purge a single post if updated. * Fixed a bug where woocommerce pages that display the cart were cached. * Fixed a bug where admin menus in multi-site setups were not correctly displayed. * Fixed a bug where logged in users were served public cached pages. * Fixed a compatibility bug with bbPress. If there is a new forum/topic/reply, the parent pages will now be purged as well. * Fixed a bug that didn't allow cron job to update scheduled posts. = 1.0.0 - January 20 2016 = * Initial Release. composer.lock 0000644 00000014237 15162130316 0007252 0 ustar 00 { "_readme": [ "This file locks the dependencies of your project to a known state", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "21330dd959c1642a4c7dbc91aa5effef", "packages": [], "packages-dev": [ { "name": "phpcompatibility/php-compatibility", "version": "9.3.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", "shasum": "" }, "require": { "php": ">=5.3", "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" }, "conflict": { "squizlabs/php_codesniffer": "2.6.2" }, "require-dev": { "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ "LGPL-3.0-or-later" ], "authors": [ { "name": "Wim Godden", "homepage": "https://github.com/wimg", "role": "lead" }, { "name": "Juliette Reinders Folmer", "homepage": "https://github.com/jrfnl", "role": "lead" }, { "name": "Contributors", "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" } ], "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", "keywords": [ "compatibility", "phpcs", "standards" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibility" }, "time": "2019-12-27T09:44:58+00:00" }, { "name": "squizlabs/php_codesniffer", "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", "shasum": "" }, "require": { "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", "php": ">=5.4.0" }, "require-dev": { "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ "bin/phpcbf", "bin/phpcs" ], "type": "library", "extra": { "branch-alias": { "dev-master": "3.x-dev" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Greg Sherwood", "role": "Former lead" }, { "name": "Juliette Reinders Folmer", "role": "Current lead" }, { "name": "Contributors", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", "standards", "static analysis" ], "support": { "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, "funding": [ { "url": "https://github.com/PHPCSStandards", "type": "github" }, { "url": "https://github.com/jrfnl", "type": "github" }, { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" } ], "time": "2024-07-21T23:26:44+00:00" } ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": [], "platform-dev": [], "plugin-api-version": "2.6.0" } security.md 0000644 00000000562 15162130317 0006737 0 ustar 00 # Security Policy ## Reporting Security Bugs We take security seriously. Please report potential vulnerabilities found in the LiteSpeed Cache plugin's source code via email to `support@litespeedtech.com` or open a ticket from your LiteSpeed Client Area. Please see [Reporting Vulnerabilities](https://www.litespeedtech.com/report-security-bugs) for more information. lib/object-cache.php 0000644 00000003401 15162130322 0010323 0 ustar 00 <?php /** * Plugin Name: LiteSpeed Cache - Object Cache (Drop-in) * Plugin URI: https://www.litespeedtech.com/products/cache-plugins/wordpress-acceleration * Description: High-performance page caching and site optimization from LiteSpeed. * Author: LiteSpeed Technologies * Author URI: https://www.litespeedtech.com */ defined('WPINC') || exit; /** * LiteSpeed Object Cache * * @since 1.8 */ !defined('LSCWP_OBJECT_CACHE') && define('LSCWP_OBJECT_CACHE', true); // Initialize const `LSCWP_DIR` and locate LSCWP plugin folder $lscwp_dir = (defined('WP_PLUGIN_DIR') ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins') . '/litespeed-cache/'; // Use plugin as higher priority than MU plugin if (!file_exists($lscwp_dir . 'litespeed-cache.php')) { // Check if is mu plugin or not $lscwp_dir = (defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : WP_CONTENT_DIR . '/mu-plugins') . '/litespeed-cache/'; if (!file_exists($lscwp_dir . 'litespeed-cache.php')) { $lscwp_dir = ''; } } $data_file = WP_CONTENT_DIR . '/.litespeed_conf.dat'; $lib_file = $lscwp_dir . 'src/object.lib.php'; // Can't find LSCWP location, terminate object cache process if (!$lscwp_dir || !file_exists($data_file) || (!file_exists($lib_file))) { if (!is_admin()) { // Bypass object cache for frontend require_once ABSPATH . WPINC . '/cache.php'; } else { $err = 'Can NOT find LSCWP path for object cache initialization in ' . __FILE__; error_log($err); add_action(is_network_admin() ? 'network_admin_notices' : 'admin_notices', function () use (&$err) { echo $err; }); } } else { if (!LSCWP_OBJECT_CACHE) { // Disable cache wp_using_ext_object_cache(false); } // Init object cache & LSCWP else if (file_exists($lib_file)) { require_once $lib_file; } } lib/html-min.cls.php 0000644 00000017334 15162130324 0010335 0 ustar 00 <?php /** * Compress HTML * * This is a heavy regex-based removal of whitespace, unnecessary comments and * tokens. IE conditional comments are preserved. There are also options to have * STYLE and SCRIPT blocks compressed by callback functions. * * A test suite is available. * * @package Minify * @author Stephen Clay <steve@mrclay.org> */ namespace LiteSpeed\Lib ; defined( 'WPINC' ) || exit ; class HTML_MIN { /** * @var string */ protected $_html = ''; /** * @var boolean */ protected $_jsCleanComments = true; protected $_skipComments = array(); /** * "Minify" an HTML page * * @param string $html * * @param array $options * * 'cssMinifier' : (optional) callback function to process content of STYLE * elements. * * 'jsMinifier' : (optional) callback function to process content of SCRIPT * elements. Note: the type attribute is ignored. * * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If * unset, minify will sniff for an XHTML doctype. * * @return string */ public static function minify($html, $options = array()) { $min = new self($html, $options); return $min->process(); } /** * Create a minifier object * * @param string $html * * @param array $options * * 'cssMinifier' : (optional) callback function to process content of STYLE * elements. * * 'jsMinifier' : (optional) callback function to process content of SCRIPT * elements. Note: the type attribute is ignored. * * 'jsCleanComments' : (optional) whether to remove HTML comments beginning and end of script block * * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If * unset, minify will sniff for an XHTML doctype. */ public function __construct($html, $options = array()) { $this->_html = str_replace("\r\n", "\n", trim($html)); if (isset($options['xhtml'])) { $this->_isXhtml = (bool)$options['xhtml']; } if (isset($options['cssMinifier'])) { $this->_cssMinifier = $options['cssMinifier']; } if (isset($options['jsMinifier'])) { $this->_jsMinifier = $options['jsMinifier']; } if (isset($options['jsCleanComments'])) { $this->_jsCleanComments = (bool)$options['jsCleanComments']; } if (isset($options['skipComments'])) { $this->_skipComments = $options['skipComments']; } } /** * Minify the markeup given in the constructor * * @return string */ public function process() { if ($this->_isXhtml === null) { $this->_isXhtml = (false !== strpos($this->_html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML')); } $this->_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); $this->_placeholders = array(); // replace SCRIPTs (and minify) with placeholders $this->_html = preg_replace_callback( '/(\\s*)<script(\\b[^>]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' ,array($this, '_removeScriptCB') ,$this->_html); // replace STYLEs (and minify) with placeholders $this->_html = preg_replace_callback( '/\\s*<style(\\b[^>]*>)([\\s\\S]*?)<\\/style>\\s*/i' ,array($this, '_removeStyleCB') ,$this->_html); // remove HTML comments (not containing IE conditional comments). $this->_html = preg_replace_callback( '/<!--([\\s\\S]*?)-->/' ,array($this, '_commentCB') ,$this->_html); // replace PREs with placeholders $this->_html = preg_replace_callback('/\\s*<pre(\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i' ,array($this, '_removePreCB') ,$this->_html); // replace TEXTAREAs with placeholders $this->_html = preg_replace_callback( '/\\s*<textarea(\\b[^>]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' ,array($this, '_removeTextareaCB') ,$this->_html); // trim each line. // @todo take into account attribute values that span multiple lines. $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); // remove ws around block/undisplayed elements $this->_html = preg_replace('/\\s+(<\\/?(?:area|article|aside|base(?:font)?|blockquote|body' .'|canvas|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|figcaption|figure|footer|form' .'|frame(?:set)?|h[1-6]|head|header|hgroup|hr|html|legend|li|link|main|map|menu|meta|nav' .'|ol|opt(?:group|ion)|output|p|param|section|t(?:able|body|head|d|h||r|foot|itle)' .'|ul|video)\\b[^>]*>)/i', '$1', $this->_html); // remove ws outside of all elements $this->_html = preg_replace( '/>(\\s(?:\\s*))?([^<]+)(\\s(?:\s*))?</' ,'>$1$2$3<' ,$this->_html); // use newlines before 1st attribute in open tags (to limit line lengths) // $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html); // fill placeholders $this->_html = str_replace( array_keys($this->_placeholders) ,array_values($this->_placeholders) ,$this->_html ); // issue 229: multi-pass to catch scripts that didn't get replaced in textareas $this->_html = str_replace( array_keys($this->_placeholders) ,array_values($this->_placeholders) ,$this->_html ); return $this->_html; } /** * From LSCWP 6.2: Changed the function to test for special comments that will be skipped. See: https://github.com/litespeedtech/lscache_wp/pull/622 */ protected function _commentCB($m) { // If is IE conditional comment return it. if(0 === strpos($m[1], '[') || false !== strpos($m[1], '<![')) return $m[0]; // Check if comment text is present in Page Optimization -> HTML Settings -> HTML Keep comments if(count($this->_skipComments) > 0){ foreach ($this->_skipComments as $comment) { if ($comment && strpos($m[1], $comment) !== false) { return $m[0]; } } } // Comment can be removed. return ''; } protected function _reservePlace($content) { $placeholder = '%' . $this->_replacementHash . count($this->_placeholders) . '%'; $this->_placeholders[$placeholder] = $content; return $placeholder; } protected $_isXhtml = null; protected $_replacementHash = null; protected $_placeholders = array(); protected $_cssMinifier = null; protected $_jsMinifier = null; protected function _removePreCB($m) { return $this->_reservePlace("<pre{$m[1]}"); } protected function _removeTextareaCB($m) { return $this->_reservePlace("<textarea{$m[1]}"); } protected function _removeStyleCB($m) { $openStyle = "<style{$m[1]}"; $css = $m[2]; // remove HTML comments $css = preg_replace('/(?:^\\s*<!--|-->\\s*$)/', '', $css); // remove CDATA section markers $css = $this->_removeCdata($css); // minify $minifier = $this->_cssMinifier ? $this->_cssMinifier : 'trim'; $css = call_user_func($minifier, $css); return $this->_reservePlace($this->_needsCdata($css) ? "{$openStyle}/*<![CDATA[*/{$css}/*]]>*/</style>" : "{$openStyle}{$css}</style>" ); } protected function _removeScriptCB($m) { $openScript = "<script{$m[2]}"; $js = $m[3]; // whitespace surrounding? preserve at least one space $ws1 = ($m[1] === '') ? '' : ' '; $ws2 = ($m[4] === '') ? '' : ' '; // remove HTML comments (and ending "//" if present) if ($this->_jsCleanComments) { $js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js); } // remove CDATA section markers $js = $this->_removeCdata($js); // minify /** * Added 2nd param by LiteSpeed * * @since 2.2.3 */ if ( $this->_jsMinifier ) { $js = call_user_func( $this->_jsMinifier, $js, trim( $m[ 2 ] ) ) ; } else { $js = trim( $js ) ; } return $this->_reservePlace($this->_needsCdata($js) ? "{$ws1}{$openScript}/*<![CDATA[*/{$js}/*]]>*/</script>{$ws2}" : "{$ws1}{$openScript}{$js}</script>{$ws2}" ); } protected function _removeCdata($str) { return (false !== strpos($str, '<![CDATA[')) ? str_replace(array('<![CDATA[', ']]>'), '', $str) : $str; } protected function _needsCdata($str) { return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); } } lib/guest.cls.php 0000644 00000010673 15162130327 0007741 0 ustar 00 <?php namespace LiteSpeed\Lib; /** * Update guest vary * * @since 4.1 */ class Guest { const CONF_FILE = '.litespeed_conf.dat'; const HASH = 'hash'; // Not set-able const O_CACHE_LOGIN_COOKIE = 'cache-login_cookie'; const O_DEBUG = 'debug'; const O_DEBUG_IPS = 'debug-ips'; const O_UTIL_NO_HTTPS_VARY = 'util-no_https_vary'; const O_GUEST_UAS = 'guest_uas'; const O_GUEST_IPS = 'guest_ips'; private static $_ip; private static $_vary_name = '_lscache_vary'; // this default vary cookie is used for logged in status check private $_conf = false; /** * Constructor * * @since 4.1 */ public function __construct() { !defined('LSCWP_CONTENT_FOLDER') && define('LSCWP_CONTENT_FOLDER', dirname(dirname(dirname(__DIR__)))); // Load config $this->_conf = file_get_contents(LSCWP_CONTENT_FOLDER . '/' . self::CONF_FILE); if ($this->_conf) { $this->_conf = json_decode($this->_conf, true); } if (!empty($this->_conf[self::O_CACHE_LOGIN_COOKIE])) { self::$_vary_name = $this->_conf[self::O_CACHE_LOGIN_COOKIE]; } } /** * Update Guest vary * * @since 4.0 */ public function update_guest_vary() { // This process must not be cached /** * @reference https://wordpress.org/support/topic/soft-404-from-google-search-on-litespeed-cache-guest-vary-php/#post-16838583 */ header('X-Robots-Tag: noindex'); header('X-LiteSpeed-Cache-Control: no-cache'); if ($this->always_guest()) { echo '[]'; exit; } // If contains vary already, don't reload to avoid infinite loop when parent page having browser cache if ($this->_conf && self::has_vary()) { echo '[]'; exit; } // Send vary cookie $vary = 'guest_mode:1'; if ($this->_conf && empty($this->_conf[self::O_DEBUG])) { $vary = md5($this->_conf[self::HASH] . $vary); } $expire = time() + 2 * 86400; $is_ssl = !empty($this->_conf[self::O_UTIL_NO_HTTPS_VARY]) ? false : $this->is_ssl(); setcookie(self::$_vary_name, $vary, $expire, '/', false, $is_ssl, true); // return json echo json_encode(array('reload' => 'yes')); exit; } /** * WP's is_ssl() func * * @since 4.1 */ private function is_ssl() { if (isset($_SERVER['HTTPS'])) { if ('on' === strtolower($_SERVER['HTTPS'])) { return true; } if ('1' == $_SERVER['HTTPS']) { return true; } } elseif (isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT'])) { return true; } return false; } /** * Check if default vary has a value * * @since 1.1.3 * @access public */ public static function has_vary() { if (empty($_COOKIE[self::$_vary_name])) { return false; } return $_COOKIE[self::$_vary_name]; } /** * Detect if is a guest visitor or not * * @since 4.0 */ public function always_guest() { if (empty($_SERVER['HTTP_USER_AGENT'])) { return false; } if ($this->_conf[self::O_GUEST_UAS]) { $quoted_uas = array(); foreach ($this->_conf[self::O_GUEST_UAS] as $v) { $quoted_uas[] = preg_quote($v, '#'); } $match = preg_match('#' . implode('|', $quoted_uas) . '#i', $_SERVER['HTTP_USER_AGENT']); if ($match) { return true; } } if ($this->ip_access($this->_conf[self::O_GUEST_IPS])) { return true; } return false; } /** * Check if the ip is in the range * * @since 1.1.0 * @access public */ public function ip_access($ip_list) { if (!$ip_list) { return false; } if (!isset(self::$_ip)) { self::$_ip = self::get_ip(); } // $uip = explode('.', $_ip); // if(empty($uip) || count($uip) != 4) Return false; // foreach($ip_list as $key => $ip) $ip_list[$key] = explode('.', trim($ip)); // foreach($ip_list as $key => $ip) { // if(count($ip) != 4) continue; // for($i = 0; $i <= 3; $i++) if($ip[$i] == '*') $ip_list[$key][$i] = $uip[$i]; // } return in_array(self::$_ip, $ip_list); } /** * Get client ip * * @since 1.1.0 * @since 1.6.5 changed to public * @access public * @return string */ public static function get_ip() { $_ip = ''; if (function_exists('apache_request_headers')) { $apache_headers = apache_request_headers(); $_ip = !empty($apache_headers['True-Client-IP']) ? $apache_headers['True-Client-IP'] : false; if (!$_ip) { $_ip = !empty($apache_headers['X-Forwarded-For']) ? $apache_headers['X-Forwarded-For'] : false; $_ip = explode(',', $_ip); $_ip = $_ip[0]; } } if (!$_ip) { $_ip = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false; } return $_ip; } } lib/urirewriter.cls.php 0000644 00000024146 15162130331 0011170 0 ustar 00 <?php /** * Rewrite file-relative URIs as root-relative in CSS files * * @package Minify * @author Stephen Clay <steve@mrclay.org> */ namespace LiteSpeed\Lib; defined( 'WPINC' ) || exit ; class UriRewriter { /** * rewrite() and rewriteRelative() append debugging information here * * @var string */ public static $debugText = ''; /** * In CSS content, rewrite file relative URIs as root relative * * @param string $css * * @param string $currentDir The directory of the current CSS file. * * @param string $docRoot The document root of the web site in which * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']). * * @param array $symlinks (default = array()) If the CSS file is stored in * a symlink-ed directory, provide an array of link paths to * target paths, where the link paths are within the document root. Because * paths need to be normalized for this to work, use "//" to substitute * the doc root in the link paths (the array keys). E.g.: * <code> * array('//symlink' => '/real/target/path') // unix * array('//static' => 'D:\\staticStorage') // Windows * </code> * * @return string */ public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array()) { self::$_docRoot = self::_realpath( $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT'] ); self::$_currentDir = self::_realpath($currentDir); self::$_symlinks = array(); // normalize symlinks in order to map to link foreach ($symlinks as $link => $target) { $link = ($link === '//') ? self::$_docRoot : str_replace('//', self::$_docRoot . '/', $link); $link = strtr($link, '/', DIRECTORY_SEPARATOR); self::$_symlinks[$link] = self::_realpath($target); } self::$debugText .= "docRoot : " . self::$_docRoot . "\n" . "currentDir : " . self::$_currentDir . "\n"; if (self::$_symlinks) { self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n"; } self::$debugText .= "\n"; $css = self::_trimUrls($css); $css = self::_owlifySvgPaths($css); // rewrite $pattern = '/@import\\s+([\'"])(.*?)[\'"]/'; $css = preg_replace_callback($pattern, __CLASS__ . '::_processUriCB', $css); $pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/'; $css = preg_replace_callback($pattern, __CLASS__ . '::_processUriCB', $css); $css = self::_unOwlify($css); return $css; } /** * In CSS content, prepend a path to relative URIs * * @param string $css * * @param string $path The path to prepend. * * @return string */ public static function prepend($css, $path) { self::$_prependPath = $path; $css = self::_trimUrls($css); $css = self::_owlifySvgPaths($css); // append $pattern = '/@import\\s+([\'"])(.*?)[\'"]/'; $css = preg_replace_callback($pattern, __CLASS__ . '::_processUriCB', $css); $pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/'; $css = preg_replace_callback($pattern, __CLASS__ . '::_processUriCB', $css); $css = self::_unOwlify($css); self::$_prependPath = null; return $css; } /** * Get a root relative URI from a file relative URI * * <code> * UriRewriter::rewriteRelative( * '../img/hello.gif' * , '/home/user/www/css' // path of CSS file * , '/home/user/www' // doc root * ); * // returns '/img/hello.gif' * * // example where static files are stored in a symlinked directory * UriRewriter::rewriteRelative( * 'hello.gif' * , '/var/staticFiles/theme' * , '/home/user/www' * , array('/home/user/www/static' => '/var/staticFiles') * ); * // returns '/static/theme/hello.gif' * </code> * * @param string $uri file relative URI * * @param string $realCurrentDir realpath of the current file's directory. * * @param string $realDocRoot realpath of the site document root. * * @param array $symlinks (default = array()) If the file is stored in * a symlink-ed directory, provide an array of link paths to * real target paths, where the link paths "appear" to be within the document * root. E.g.: * <code> * array('/home/foo/www/not/real/path' => '/real/target/path') // unix * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows * </code> * * @return string */ public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array()) { // prepend path with current dir separator (OS-independent) $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR); $path .= DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR); self::$debugText .= "file-relative URI : {$uri}\n" . "path prepended : {$path}\n"; // "unresolve" a symlink back to doc root foreach ($symlinks as $link => $target) { if (0 === strpos($path, $target)) { // replace $target with $link $path = $link . substr($path, strlen($target)); self::$debugText .= "symlink unresolved : {$path}\n"; break; } } // strip doc root $path = substr($path, strlen($realDocRoot)); self::$debugText .= "docroot stripped : {$path}\n"; // fix to root-relative URI $uri = strtr($path, '/\\', '//'); $uri = self::removeDots($uri); self::$debugText .= "traversals removed : {$uri}\n\n"; return $uri; } /** * Remove instances of "./" and "../" where possible from a root-relative URI * * @param string $uri * * @return string */ public static function removeDots($uri) { $uri = str_replace('/./', '/', $uri); // inspired by patch from Oleg Cherniy do { $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); } while ($changed); return $uri; } /** * Get realpath with any trailing slash removed. If realpath() fails, * just remove the trailing slash. * * @param string $path * * @return mixed path with no trailing slash */ protected static function _realpath($path) { $realPath = realpath($path); if ($realPath !== false) { $path = $realPath; } return rtrim($path, '/\\'); } /** * Directory of this stylesheet * * @var string */ private static $_currentDir = ''; /** * DOC_ROOT * * @var string */ private static $_docRoot = ''; /** * directory replacements to map symlink targets back to their * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' * * @var array */ private static $_symlinks = array(); /** * Path to prepend * * @var string */ private static $_prependPath = null; /** * @param string $css * * @return string */ private static function _trimUrls($css) { $pattern = '/ url\\( # url( \\s* ([^\\)]+?) # 1 = URI (assuming does not contain ")") \\s* \\) # ) /x'; return preg_replace($pattern, 'url($1)', $css); } /** * @param array $m * * @return string */ private static function _processUriCB($m) { // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' $isImport = ($m[0][0] === '@'); // determine URI and the quote character (if any) if ($isImport) { $quoteChar = $m[1]; $uri = $m[2]; } else { // $m[1] is either quoted or not $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') ? $m[1][0] : ''; $uri = ($quoteChar === '') ? $m[1] : substr($m[1], 1, strlen($m[1]) - 2); } if ($uri === '') { return $m[0]; } // if not anchor id, not root/scheme relative, and not starts with scheme if (!preg_match('~^(#|/|[a-z]+\:)~', $uri)) { // URI is file-relative: rewrite depending on options if (self::$_prependPath === null) { $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); } else { $uri = self::$_prependPath . $uri; if ($uri[0] === '/') { $root = ''; $rootRelative = $uri; $uri = $root . self::removeDots($rootRelative); } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { $root = $m[1]; $rootRelative = substr($uri, strlen($root)); $uri = $root . self::removeDots($rootRelative); } } } if ($isImport) { return "@import {$quoteChar}{$uri}{$quoteChar}"; } else { return "url({$quoteChar}{$uri}{$quoteChar})"; } } /** * Mungs some inline SVG URL declarations so they won't be touched * * @link https://github.com/mrclay/minify/issues/517 * @see _unOwlify * * @param string $css * @return string */ private static function _owlifySvgPaths($css) { $pattern = '~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)url(\(\s*#\w+\s*\))~'; return preg_replace($pattern, '$1owl$2', $css); } /** * Undo work of _owlify * * @see _owlifySvgPaths * * @param string $css * @return string */ private static function _unOwlify($css) { $pattern = '~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)owl~'; return preg_replace($pattern, '$1url', $css); } } lib/php-compatibility.func.php 0000644 00000012373 15162130332 0012415 0 ustar 00 <?php /** * LiteSpeed PHP compatibility functions for lower PHP version * * @since 1.1.3 * @package LiteSpeed_Cache * @subpackage LiteSpeed_Cache/lib * @author LiteSpeed Technologies <info@litespeedtech.com> */ defined( 'WPINC' ) || exit ; /** * http_build_url() compatibility * */ if ( ! function_exists('http_build_url') ) { if ( ! defined( 'HTTP_URL_REPLACE' ) ) define('HTTP_URL_REPLACE', 1); // Replace every part of the first URL when there's one of the second URL if ( ! defined( 'HTTP_URL_JOIN_PATH' ) ) define('HTTP_URL_JOIN_PATH', 2); // Join relative paths if ( ! defined( 'HTTP_URL_JOIN_QUERY' ) ) define('HTTP_URL_JOIN_QUERY', 4); // Join query strings if ( ! defined( 'HTTP_URL_STRIP_USER' ) ) define('HTTP_URL_STRIP_USER', 8); // Strip any user authentication information if ( ! defined( 'HTTP_URL_STRIP_PASS' ) ) define('HTTP_URL_STRIP_PASS', 16); // Strip any password authentication information if ( ! defined( 'HTTP_URL_STRIP_AUTH' ) ) define('HTTP_URL_STRIP_AUTH', 32); // Strip any authentication information if ( ! defined( 'HTTP_URL_STRIP_PORT' ) ) define('HTTP_URL_STRIP_PORT', 64); // Strip explicit port numbers if ( ! defined( 'HTTP_URL_STRIP_PATH' ) ) define('HTTP_URL_STRIP_PATH', 128); // Strip complete path if ( ! defined( 'HTTP_URL_STRIP_QUERY' ) ) define('HTTP_URL_STRIP_QUERY', 256); // Strip query string if ( ! defined( 'HTTP_URL_STRIP_FRAGMENT' ) ) define('HTTP_URL_STRIP_FRAGMENT', 512); // Strip any fragments (#identifier) if ( ! defined( 'HTTP_URL_STRIP_ALL' ) ) define('HTTP_URL_STRIP_ALL', 1024); // Strip anything but scheme and host // Build an URL // The parts of the second URL will be merged into the first according to the flags argument. // // @param mixed (Part(s) of) an URL in form of a string or associative array like parse_url() returns // @param mixed Same as the first argument // @param int A bitmask of binary or'ed HTTP_URL constants (Optional)HTTP_URL_REPLACE is the default // @param array If set, it will be filled with the parts of the composed url like parse_url() would return function http_build_url($url, $parts = array(), $flags = HTTP_URL_REPLACE, &$new_url = false) { $keys = array('user','pass','port','path','query','fragment'); // HTTP_URL_STRIP_ALL becomes all the HTTP_URL_STRIP_Xs if ( $flags & HTTP_URL_STRIP_ALL ) { $flags |= HTTP_URL_STRIP_USER; $flags |= HTTP_URL_STRIP_PASS; $flags |= HTTP_URL_STRIP_PORT; $flags |= HTTP_URL_STRIP_PATH; $flags |= HTTP_URL_STRIP_QUERY; $flags |= HTTP_URL_STRIP_FRAGMENT; } // HTTP_URL_STRIP_AUTH becomes HTTP_URL_STRIP_USER and HTTP_URL_STRIP_PASS else if ( $flags & HTTP_URL_STRIP_AUTH ) { $flags |= HTTP_URL_STRIP_USER; $flags |= HTTP_URL_STRIP_PASS; } // Parse the original URL // - Suggestion by Sayed Ahad Abbas // In case you send a parse_url array as input $parse_url = !is_array($url) ? parse_url($url) : $url; // Scheme and Host are always replaced if ( isset($parts['scheme']) ) { $parse_url['scheme'] = $parts['scheme']; } if ( isset($parts['host']) ) { $parse_url['host'] = $parts['host']; } // (If applicable) Replace the original URL with it's new parts if ( $flags & HTTP_URL_REPLACE ) { foreach ($keys as $key) { if ( isset($parts[$key]) ) { $parse_url[$key] = $parts[$key]; } } } else { // Join the original URL path with the new path if (isset($parts['path']) && ($flags & HTTP_URL_JOIN_PATH)) { if ( isset($parse_url['path']) ) { $parse_url['path'] = rtrim(str_replace(basename($parse_url['path']), '', $parse_url['path']), '/') . '/' . ltrim($parts['path'], '/'); } else { $parse_url['path'] = $parts['path']; } } // Join the original query string with the new query string if ( isset($parts['query']) && ($flags & HTTP_URL_JOIN_QUERY) ) { if ( isset($parse_url['query']) ) { $parse_url['query'] .= '&' . $parts['query']; } else { $parse_url['query'] = $parts['query']; } } } // Strips all the applicable sections of the URL // Note: Scheme and Host are never stripped foreach ($keys as $key) { if ( $flags & (int)constant('HTTP_URL_STRIP_' . strtoupper($key)) ) { unset($parse_url[$key]); } } $new_url = $parse_url; return (isset($parse_url['scheme']) ? $parse_url['scheme'] . '://' : '') .(isset($parse_url['user']) ? $parse_url['user'] . (isset($parse_url['pass']) ? ':' . $parse_url['pass'] : '') .'@' : '') .(isset($parse_url['host']) ? $parse_url['host'] : '') .(isset($parse_url['port']) ? ':' . $parse_url['port'] : '') .(isset($parse_url['path']) ? $parse_url['path'] : '') .(isset($parse_url['query']) ? '?' . $parse_url['query'] : '') .(isset($parse_url['fragment']) ? '#' . $parse_url['fragment'] : '') ; } } if ( ! function_exists( 'array_key_first' ) ) { function array_key_first( array $arr ) { foreach( $arr as $k => $unused ) { return $k ; } return NULL ; } } if ( ! function_exists( 'array_column' ) ) { function array_column( $array, $column_name ) { return array_map( function( $element ) use( $column_name ) { return $element[ $column_name ]; }, $array ); } } lib/css_js_min/minify/minify.cls.php 0000644 00000041636 15162130340 0013525 0 ustar 00 <?php /** * modified PHP implementation of Matthias Mullie's Abstract minifier class. * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace LiteSpeed\Lib\CSS_JS_MIN\Minify; use LiteSpeed\Lib\CSS_JS_MIN\Minify\Exception\IOException; defined( 'WPINC' ) || exit ; abstract class Minify { /** * The data to be minified. * * @var string[] */ protected $data = array(); /** * Array of patterns to match. * * @var string[] */ protected $patterns = array(); /** * This array will hold content of strings and regular expressions that have * been extracted from the JS source code, so we can reliably match "code", * without having to worry about potential "code-like" characters inside. * * @internal * * @var string[] */ public $extracted = array(); /** * Init the minify class - optionally, code may be passed along already. */ public function __construct(/* $data = null, ... */) { // it's possible to add the source through the constructor as well ;) if (func_num_args()) { call_user_func_array(array($this, 'add'), func_get_args()); } } /** * Add a file or straight-up code to be minified. * * @param string|string[] $data * * @return static */ public function add($data /* $data = null, ... */) { // bogus "usage" of parameter $data: scrutinizer warns this variable is // not used (we're using func_get_args instead to support overloading), // but it still needs to be defined because it makes no sense to have // this function without argument :) $args = array($data) + func_get_args(); // this method can be overloaded foreach ($args as $data) { if (is_array($data)) { call_user_func_array(array($this, 'add'), $data); continue; } // redefine var $data = (string) $data; // load data $value = $this->load($data); $key = ($data != $value) ? $data : count($this->data); // replace CR linefeeds etc. // @see https://github.com/matthiasmullie/minify/pull/139 $value = str_replace(array("\r\n", "\r"), "\n", $value); // store data $this->data[$key] = $value; } return $this; } /** * Add a file to be minified. * * @param string|string[] $data * * @return static * * @throws IOException */ public function addFile($data /* $data = null, ... */) { // bogus "usage" of parameter $data: scrutinizer warns this variable is // not used (we're using func_get_args instead to support overloading), // but it still needs to be defined because it makes no sense to have // this function without argument :) $args = array($data) + func_get_args(); // this method can be overloaded foreach ($args as $path) { if (is_array($path)) { call_user_func_array(array($this, 'addFile'), $path); continue; } // redefine var $path = (string) $path; // check if we can read the file if (!$this->canImportFile($path)) { throw new IOException('The file "' . $path . '" could not be opened for reading. Check if PHP has enough permissions.'); } $this->add($path); } return $this; } /** * Minify the data & (optionally) saves it to a file. * * @param string[optional] $path Path to write the data to * * @return string The minified data */ public function minify($path = null) { $content = $this->execute($path); // save to path if ($path !== null) { $this->save($content, $path); } return $content; } /** * Minify & gzip the data & (optionally) saves it to a file. * * @param string[optional] $path Path to write the data to * @param int[optional] $level Compression level, from 0 to 9 * * @return string The minified & gzipped data */ public function gzip($path = null, $level = 9) { $content = $this->execute($path); $content = gzencode($content, $level, FORCE_GZIP); // save to path if ($path !== null) { $this->save($content, $path); } return $content; } /** * Minify the data. * * @param string[optional] $path Path to write the data to * * @return string The minified data */ abstract public function execute($path = null); /** * Load data. * * @param string $data Either a path to a file or the content itself * * @return string */ protected function load($data) { // check if the data is a file if ($this->canImportFile($data)) { $data = file_get_contents($data); // strip BOM, if any if (substr($data, 0, 3) == "\xef\xbb\xbf") { $data = substr($data, 3); } } return $data; } /** * Save to file. * * @param string $content The minified data * @param string $path The path to save the minified data to * * @throws IOException */ protected function save($content, $path) { $handler = $this->openFileForWriting($path); $this->writeToFile($handler, $content); @fclose($handler); } /** * Register a pattern to execute against the source content. * * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work. * If you need that functionality, use a callback instead. * * @param string $pattern PCRE pattern * @param string|callable $replacement Replacement value for matched pattern */ protected function registerPattern($pattern, $replacement = '') { // study the pattern, we'll execute it more than once $pattern .= 'S'; $this->patterns[] = array($pattern, $replacement); } /** * Both JS and CSS use the same form of multi-line comment, so putting the common code here. */ protected function stripMultilineComments() { // First extract comments we want to keep, so they can be restored later // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier) { $count = count($minifier->extracted); $placeholder = '/*' . $count . '*/'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; }; $this->registerPattern('/ # optional newline \n? # start comment \/\* # comment content (?: # either starts with an ! ! | # or, after some number of characters which do not end the comment (?:(?!\*\/).)*? # there is either a @license or @preserve tag @(?:license|preserve) ) # then match to the end of the comment .*?\*\/\n? /ixs', $callback); // Then strip all other comments $this->registerPattern('/\/\*.*?\*\//s', ''); } /** * We can't "just" run some regular expressions against JavaScript: it's a * complex language. E.g. having an occurrence of // xyz would be a comment, * unless it's used within a string. Of you could have something that looks * like a 'string', but inside a comment. * The only way to accurately replace these pieces is to traverse the JS one * character at a time and try to find whatever starts first. * * @param string $content The content to replace patterns in * * @return string The (manipulated) content */ protected function replace($content) { $contentLength = strlen($content); $output = ''; $processedOffset = 0; $positions = array_fill(0, count($this->patterns), -1); $matches = array(); while ($processedOffset < $contentLength) { // find first match for all patterns foreach ($this->patterns as $i => $pattern) { list($pattern, $replacement) = $pattern; // we can safely ignore patterns for positions we've unset earlier, // because we know these won't show up anymore if (array_key_exists($i, $positions) == false) { continue; } // no need to re-run matches that are still in the part of the // content that hasn't been processed if ($positions[$i] >= $processedOffset) { continue; } $match = null; if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { $matches[$i] = $match; // we'll store the match position as well; that way, we // don't have to redo all preg_matches after changing only // the first (we'll still know where those others are) $positions[$i] = $match[0][1]; } else { // if the pattern couldn't be matched, there's no point in // executing it again in later runs on this same content; // ignore this one until we reach end of content unset($matches[$i], $positions[$i]); } } // no more matches to find: everything's been processed, break out if (!$matches) { // output the remaining content $output .= substr($content, $processedOffset); break; } // see which of the patterns actually found the first thing (we'll // only want to execute that one, since we're unsure if what the // other found was not inside what the first found) $matchOffset = min($positions); $firstPattern = array_search($matchOffset, $positions); $match = $matches[$firstPattern]; // execute the pattern that matches earliest in the content string list(, $replacement) = $this->patterns[$firstPattern]; // add the part of the input between $processedOffset and the first match; // that content wasn't matched by anything $output .= substr($content, $processedOffset, $matchOffset - $processedOffset); // add the replacement for the match $output .= $this->executeReplacement($replacement, $match); // advance $processedOffset past the match $processedOffset = $matchOffset + strlen($match[0][0]); } return $output; } /** * If $replacement is a callback, execute it, passing in the match data. * If it's a string, just pass it through. * * @param string|callable $replacement Replacement value * @param array $match Match data, in PREG_OFFSET_CAPTURE form * * @return string */ protected function executeReplacement($replacement, $match) { if (!is_callable($replacement)) { return $replacement; } // convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects foreach ($match as &$matchItem) { $matchItem = $matchItem[0]; } return $replacement($match); } /** * Strings are a pattern we need to match, in order to ignore potential * code-like content inside them, but we just want all of the string * content to remain untouched. * * This method will replace all string content with simple STRING# * placeholder text, so we've rid all strings from characters that may be * misinterpreted. Original string content will be saved in $this->extracted * and after doing all other minifying, we can restore the original content * via restoreStrings(). * * @param string[optional] $chars * @param string[optional] $placeholderPrefix */ protected function extractStrings($chars = '\'"', $placeholderPrefix = '') { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier, $placeholderPrefix) { // check the second index here, because the first always contains a quote if ($match[2] === '') { /* * Empty strings need no placeholder; they can't be confused for * anything else anyway. * But we still needed to match them, for the extraction routine * to skip over this particular string. */ return $match[0]; } $count = count($minifier->extracted); $placeholder = $match[1] . $placeholderPrefix . $count . $match[1]; $minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1]; return $placeholder; }; /* * The \\ messiness explained: * * Don't count ' or " as end-of-string if it's escaped (has backslash * in front of it) * * Unless... that backslash itself is escaped (another leading slash), * in which case it's no longer escaping the ' or " * * So there can be either no backslash, or an even number * * multiply all of that times 4, to account for the escaping that has * to be done to pass the backslash into the PHP string without it being * considered as escape-char (times 2) and to get it in the regex, * escaped (times 2) */ $this->registerPattern('/([' . $chars . '])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); } /** * This method will restore all extracted data (strings, regexes) that were * replaced with placeholder text in extract*(). The original content was * saved in $this->extracted. * * @param string $content * * @return string */ protected function restoreExtractedData($content) { if (!$this->extracted) { // nothing was extracted, nothing to restore return $content; } $content = strtr($content, $this->extracted); $this->extracted = array(); return $content; } /** * Check if the path is a regular file and can be read. * * @param string $path * * @return bool */ protected function canImportFile($path) { $parsed = parse_url($path); if ( // file is elsewhere isset($parsed['host']) // file responds to queries (may change, or need to bypass cache) || isset($parsed['query']) ) { return false; } try { return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); } // catch openbasedir exceptions which are not caught by @ on is_file() catch (\Exception $e) { return false; } } /** * Attempts to open file specified by $path for writing. * * @param string $path The path to the file * * @return resource Specifier for the target file * * @throws IOException */ protected function openFileForWriting($path) { if ($path === '' || ($handler = @fopen($path, 'w')) === false) { throw new IOException('The file "' . $path . '" could not be opened for writing. Check if PHP has enough permissions.'); } return $handler; } /** * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. * * @param resource $handler The resource to write to * @param string $content The content to write * @param string $path The path to the file (for exception printing only) * * @throws IOException */ protected function writeToFile($handler, $content, $path = '') { if ( !is_resource($handler) || ($result = @fwrite($handler, $content)) === false || ($result < strlen($content)) ) { throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.'); } } protected static function str_replace_first($search, $replace, $subject) { $pos = strpos($subject, $search); if ($pos !== false) { return substr_replace($subject, $replace, $pos, strlen($search)); } return $subject; } } lib/css_js_min/minify/exception.cls.php 0000644 00000000665 15162130341 0014226 0 ustar 00 <?php /** * exception.cls.php - modified PHP implementation of Matthias Mullie's Exceptions Classes. * @author Matthias Mullie <minify@mullie.eu> */ namespace LiteSpeed\Lib\CSS_JS_MIN\Minify\Exception; defined( 'WPINC' ) || exit ; abstract class Exception extends \Exception { } abstract class BasicException extends Exception { } class FileImportException extends BasicException { } class IOException extends BasicException { } lib/css_js_min/minify/data/js/operators.txt 0000644 00000000170 15162130345 0015036 0 ustar 00 + - * / % = += -= *= /= %= <<= >>= >>>= &= ^= |= & | ^ ~ << >> >>> == === != !== > < >= <= && || ! . [ ] ? : , ; ( ) { } lib/css_js_min/minify/data/js/keywords_reserved.txt 0000644 00000000636 15162130347 0016577 0 ustar 00 do if in for let new try var case else enum eval null this true void with break catch class const false super throw while yield delete export import public return static switch typeof default extends finally package private continue debugger function arguments interface protected implements instanceof abstract boolean byte char double final float goto int long native short synchronized throws transient volatile lib/css_js_min/minify/data/js/operators_after.txt 0000644 00000000162 15162130350 0016214 0 ustar 00 + - * / % = += -= *= /= %= <<= >>= >>>= &= ^= |= & | ^ << >> >>> == === != !== > < >= <= && || . [ ] ? : , ; ( ) } lib/css_js_min/minify/data/js/keywords_after.txt 0000644 00000000071 15162130350 0016044 0 ustar 00 in public extends private protected implements instanceof lib/css_js_min/minify/data/js/operators_before.txt 0000644 00000000163 15162130352 0016360 0 ustar 00 + - * / % = += -= *= /= %= <<= >>= >>>= &= ^= |= & | ^ ~ << >> >>> == === != !== > < >= <= && || ! . [ ? : , ; ( { lib/css_js_min/minify/data/js/keywords_before.txt 0000644 00000000247 15162130354 0016216 0 ustar 00 do in let new var case else enum void with class const yield delete export import public static typeof extends package private function protected implements instanceof lib/css_js_min/minify/css.cls.php 0000644 00000073667 15162130354 0013040 0 ustar 00 <?php /** * css.cls.php - modified PHP implementation of Matthias Mullie's CSS minifier * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace LiteSpeed\Lib\CSS_JS_MIN\Minify; use LiteSpeed\Lib\CSS_JS_MIN\Minify\Minify; use LiteSpeed\Lib\CSS_JS_MIN\Minify\Exception\FileImportException; use LiteSpeed\Lib\CSS_JS_MIN\PathConverter\Converter; use LiteSpeed\Lib\CSS_JS_MIN\PathConverter\ConverterInterface; defined( 'WPINC' ) || exit ; class CSS extends Minify { /** * @var int maximum import size in kB */ protected $maxImportSize = 5; /** * @var string[] valid import extensions */ protected $importExtensions = array( 'gif' => 'data:image/gif', 'png' => 'data:image/png', 'jpe' => 'data:image/jpeg', 'jpg' => 'data:image/jpeg', 'jpeg' => 'data:image/jpeg', 'svg' => 'data:image/svg+xml', 'woff' => 'data:application/x-font-woff', 'woff2' => 'data:application/x-font-woff2', 'avif' => 'data:image/avif', 'apng' => 'data:image/apng', 'webp' => 'data:image/webp', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'xbm' => 'image/x-xbitmap', ); /** * Set the maximum size if files to be imported. * * Files larger than this size (in kB) will not be imported into the CSS. * Importing files into the CSS as data-uri will save you some connections, * but we should only import relatively small decorative images so that our * CSS file doesn't get too bulky. * * @param int $size Size in kB */ public function setMaxImportSize($size) { $this->maxImportSize = $size; } /** * Set the type of extensions to be imported into the CSS (to save network * connections). * Keys of the array should be the file extensions & respective values * should be the data type. * * @param string[] $extensions Array of file extensions */ public function setImportExtensions(array $extensions) { $this->importExtensions = $extensions; } /** * Move any import statements to the top. * * @param string $content Nearly finished CSS content * * @return string */ public function moveImportsToTop($content) { if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { // remove from content foreach ($matches[0] as $import) { $content = str_replace($import, '', $content); } // add to top $content = implode(';', $matches[2]) . ';' . trim($content, ';'); } return $content; } /** * Combine CSS from import statements. * * \@import's will be loaded and their content merged into the original file, * to save HTTP requests. * * @param string $source The file to combine imports for * @param string $content The CSS content to combine imports for * @param string[] $parents Parent paths, for circular reference checks * * @return string * * @throws FileImportException */ protected function combineImports($source, $content, $parents) { $importRegexes = array( // @import url(xxx) '/ # import statement @import # whitespace \s+ # open url() url\( # (optional) open path enclosure (?P<quotes>["\']?) # fetch path (?P<path>.+?) # (optional) close path enclosure (?P=quotes) # close url() \) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P<media>[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', // @import 'xxx' '/ # import statement @import # whitespace \s+ # open path enclosure (?P<quotes>["\']) # fetch path (?P<path>.+?) # close path enclosure (?P=quotes) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P<media>[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', ); // find all relative imports in css $matches = array(); foreach ($importRegexes as $importRegex) { if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { $matches = array_merge($matches, $regexMatches); } } $search = array(); $replace = array(); // loop the matches foreach ($matches as $match) { // get the path for the file that will be imported $importPath = dirname($source) . '/' . $match['path']; // only replace the import with the content if we can grab the // content of the file if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { continue; } // check if current file was not imported previously in the same // import chain. if (in_array($importPath, $parents)) { throw new FileImportException('Failed to import file "' . $importPath . '": circular reference detected.'); } // grab referenced file & minify it (which may include importing // yet other @import statements recursively) $minifier = new self($importPath); $minifier->setMaxImportSize($this->maxImportSize); $minifier->setImportExtensions($this->importExtensions); $importContent = $minifier->execute($source, $parents); // check if this is only valid for certain media if (!empty($match['media'])) { $importContent = '@media ' . $match['media'] . '{' . $importContent . '}'; } // add to replacement array $search[] = $match[0]; $replace[] = $importContent; } // replace the import statements return str_replace($search, $replace, $content); } /** * Import files into the CSS, base64-ized. * * @url(image.jpg) images will be loaded and their content merged into the * original file, to save HTTP requests. * * @param string $source The file to import files for * @param string $content The CSS content to import files for * * @return string */ protected function importFiles($source, $content) { $regex = '/url\((["\']?)(.+?)\\1\)/i'; if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { $search = array(); $replace = array(); // loop the matches foreach ($matches as $match) { $extension = substr(strrchr($match[2], '.'), 1); if ($extension && !array_key_exists($extension, $this->importExtensions)) { continue; } // get the path for the file that will be imported $path = $match[2]; $path = dirname($source) . '/' . $path; // only replace the import with the content if we're able to get // the content of the file, and it's relatively small if ($this->canImportFile($path) && $this->canImportBySize($path)) { // grab content && base64-ize $importContent = $this->load($path); $importContent = base64_encode($importContent); // build replacement $search[] = $match[0]; $replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')'; } } // replace the import statements $content = str_replace($search, $replace, $content); } return $content; } /** * Minify the data. * Perform CSS optimizations. * * @param string[optional] $path Path to write the data to * @param string[] $parents Parent paths, for circular reference checks * * @return string The minified data */ public function execute($path = null, $parents = array()) { $content = ''; // loop CSS data (raw data and files) foreach ($this->data as $source => $css) { /* * Let's first take out strings & comments, since we can't just * remove whitespace anywhere. If whitespace occurs inside a string, * we should leave it alone. E.g.: * p { content: "a test" } */ $this->extractStrings(); $this->stripComments(); $this->extractMath(); $this->extractCustomProperties(); $css = $this->replace($css); $css = $this->stripWhitespace($css); $css = $this->convertLegacyColors($css); $css = $this->cleanupModernColors($css); $css = $this->shortenHEXColors($css); $css = $this->shortenZeroes($css); $css = $this->shortenFontWeights($css); $css = $this->stripEmptyTags($css); // restore the string we've extracted earlier $css = $this->restoreExtractedData($css); $source = is_int($source) ? '' : $source; $parents = $source ? array_merge($parents, array($source)) : $parents; $css = $this->combineImports($source, $css, $parents); $css = $this->importFiles($source, $css); /* * If we'll save to a new path, we'll have to fix the relative paths * to be relative no longer to the source file, but to the new path. * If we don't write to a file, fall back to same path so no * conversion happens (because we still want it to go through most * of the move code, which also addresses url() & @import syntax...) */ $converter = $this->getPathConverter($source, $path ?: $source); $css = $this->move($converter, $css); // combine css $content .= $css; } $content = $this->moveImportsToTop($content); return $content; } /** * Moving a css file should update all relative urls. * Relative references (e.g. ../images/image.gif) in a certain css file, * will have to be updated when a file is being saved at another location * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). * * @param ConverterInterface $converter Relative path converter * @param string $content The CSS content to update relative urls for * * @return string */ protected function move(ConverterInterface $converter, $content) { /* * Relative path references will usually be enclosed by url(). @import * is an exception, where url() is not necessary around the path (but is * allowed). * This *could* be 1 regular expression, where both regular expressions * in this array are on different sides of a |. But we're using named * patterns in both regexes, the same name on both regexes. This is only * possible with a (?J) modifier, but that only works after a fairly * recent PCRE version. That's why I'm doing 2 separate regular * expressions & combining the matches after executing of both. */ $relativeRegexes = array( // url(xxx) '/ # open url() url\( \s* # open path enclosure (?P<quotes>["\'])? # fetch path (?P<path>.+?) # close path enclosure (?(quotes)(?P=quotes)) \s* # close url() \) /ix', // @import "xxx" '/ # import statement @import # whitespace \s+ # we don\'t have to check for @import url(), because the # condition above will already catch these # open path enclosure (?P<quotes>["\']) # fetch path (?P<path>.+?) # close path enclosure (?P=quotes) /ix', ); // find all relative urls in css $matches = array(); foreach ($relativeRegexes as $relativeRegex) { if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { $matches = array_merge($matches, $regexMatches); } } $search = array(); $replace = array(); // loop all urls foreach ($matches as $match) { // determine if it's a url() or an @import match $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); $url = $match['path']; if ($this->canImportByPath($url)) { // attempting to interpret GET-params makes no sense, so let's discard them for awhile $params = strrchr($url, '?'); $url = $params ? substr($url, 0, -strlen($params)) : $url; // fix relative url $url = $converter->convert($url); // now that the path has been converted, re-apply GET-params $url .= $params; } /* * Urls with control characters above 0x7e should be quoted. * According to Mozilla's parser, whitespace is only allowed at the * end of unquoted urls. * Urls with `)` (as could happen with data: uris) should also be * quoted to avoid being confused for the url() closing parentheses. * And urls with a # have also been reported to cause issues. * Urls with quotes inside should also remain escaped. * * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 * @see https://github.com/matthiasmullie/minify/issues/193 */ $url = trim($url); if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { $url = $match['quotes'] . $url . $match['quotes']; } // build replacement $search[] = $match[0]; if ($type === 'url') { $replace[] = 'url(' . $url . ')'; } elseif ($type === 'import') { $replace[] = '@import "' . $url . '"'; } } // replace urls return str_replace($search, $replace, $content); } /** * Shorthand HEX color codes. * #FF0000FF -> #f00 -> red * #FF00FF00 -> transparent. * * @param string $content The CSS content to shorten the HEX color codes for * * @return string */ protected function shortenHexColors($content) { // shorten repeating patterns within HEX .. $content = preg_replace('/(?<=[: ])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3(?:([0-9a-f])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); // remove alpha channel if it's pointless .. $content = preg_replace('/(?<=[: ])#([0-9a-f]{6})ff(?=[; }])/i', '#$1', $content); $content = preg_replace('/(?<=[: ])#([0-9a-f]{3})f(?=[; }])/i', '#$1', $content); // replace `transparent` with shortcut .. $content = preg_replace('/(?<=[: ])#[0-9a-f]{6}00(?=[; }])/i', '#fff0', $content); $colors = array( // make these more readable '#00f' => 'blue', '#dc143c' => 'crimson', '#0ff' => 'cyan', '#8b0000' => 'darkred', '#696969' => 'dimgray', '#ff69b4' => 'hotpink', '#0f0' => 'lime', '#fdf5e6' => 'oldlace', '#87ceeb' => 'skyblue', '#d8bfd8' => 'thistle', // we can shorten some even more by replacing them with their color name '#f0ffff' => 'azure', '#f5f5dc' => 'beige', '#ffe4c4' => 'bisque', '#a52a2a' => 'brown', '#ff7f50' => 'coral', '#ffd700' => 'gold', '#808080' => 'gray', '#008000' => 'green', '#4b0082' => 'indigo', '#fffff0' => 'ivory', '#f0e68c' => 'khaki', '#faf0e6' => 'linen', '#800000' => 'maroon', '#000080' => 'navy', '#808000' => 'olive', '#ffa500' => 'orange', '#da70d6' => 'orchid', '#cd853f' => 'peru', '#ffc0cb' => 'pink', '#dda0dd' => 'plum', '#800080' => 'purple', '#f00' => 'red', '#fa8072' => 'salmon', '#a0522d' => 'sienna', '#c0c0c0' => 'silver', '#fffafa' => 'snow', '#d2b48c' => 'tan', '#008080' => 'teal', '#ff6347' => 'tomato', '#ee82ee' => 'violet', '#f5deb3' => 'wheat', // or the other way around 'black' => '#000', 'fuchsia' => '#f0f', 'magenta' => '#f0f', 'white' => '#fff', 'yellow' => '#ff0', // and also `transparent` 'transparent' => '#fff0', ); return preg_replace_callback( '/(?<=[: ])(' . implode('|', array_keys($colors)) . ')(?=[; }])/i', function ($match) use ($colors) { return $colors[strtolower($match[0])]; }, $content ); } /** * Convert RGB|HSL color codes. * rgb(255,0,0,.5) -> rgb(255 0 0 / .5). * rgb(255,0,0) -> #f00. * * @param string $content The CSS content to shorten the RGB color codes for * * @return string */ protected function convertLegacyColors($content) { /* https://drafts.csswg.org/css-color/#color-syntax-legacy https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl */ // convert legacy color syntax $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content); $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*\)/i', '$1($2 $3 $4)', $content); $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content); $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*\)/i', '$1($2 $3 $4)', $content); // convert `rgb` to `hex` $dec = '([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])'; return preg_replace_callback( "/rgb\($dec $dec $dec\)/i", function ($match) { return sprintf('#%02x%02x%02x', $match[1], $match[2], $match[3]); }, $content ); } /** * Cleanup RGB|HSL|HWB|LCH|LAB * rgb(255 0 0 / 1) -> rgb(255 0 0). * rgb(255 0 0 / 0) -> transparent. * * @param string $content The CSS content to cleanup HSL|HWB|LCH|LAB * * @return string */ protected function cleanupModernColors($content) { /* https://drafts.csswg.org/css-color/#color-syntax-modern https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab */ $tag = '(rgb|hsl|hwb|(?:(?:ok)?(?:lch|lab)))'; // remove alpha channel if it's pointless .. $content = preg_replace('/' . $tag . '\(\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\/\s+1(?:(?:\.\d?)*|00%)?\s*\)/i', '$1($2 $3 $4)', $content); // replace `transparent` with shortcut .. $content = preg_replace('/' . $tag . '\(\s*[^\s]+\s+[^\s]+\s+[^\s]+\s+\/\s+0(?:[\.0%]*)?\s*\)/i', '#fff0', $content); return $content; } /** * Shorten CSS font weights. * * @param string $content The CSS content to shorten the font weights for * * @return string */ protected function shortenFontWeights($content) { $weights = array( 'normal' => 400, 'bold' => 700, ); $callback = function ($match) use ($weights) { return $match[1] . $weights[$match[2]]; }; return preg_replace_callback('/(font-weight\s*:\s*)(' . implode('|', array_keys($weights)) . ')(?=[;}])/', $callback, $content); } /** * Shorthand 0 values to plain 0, instead of e.g. -0em. * * @param string $content The CSS content to shorten the zero values for * * @return string */ protected function shortenZeroes($content) { // we don't want to strip units in `calc()` expressions: // `5px - 0px` is valid, but `5px - 0` is not // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but // `10 * 0` is invalid // we've extracted calcs earlier, so we don't need to worry about this // reusable bits of code throughout these regexes: // before & after are used to make sure we don't match lose unintended // 0-like values (e.g. in #000, or in http://url/1.0) // units can be stripped from 0 values, or used to recognize non 0 // values (where wa may be able to strip a .0 suffix) $before = '(?<=[:(, ])'; $after = '(?=[ ,);}])'; $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; // strip units after zeroes (0px -> 0) // NOTE: it should be safe to remove all units for a 0 value, but in // practice, Webkit (especially Safari) seems to stumble over at least // 0%, potentially other units as well. Only stripping 'px' for now. // @see https://github.com/matthiasmullie/minify/issues/60 $content = preg_replace('/' . $before . '(-?0*(\.0+)?)(?<=0)px' . $after . '/', '\\1', $content); // strip 0-digits (.0 -> 0) $content = preg_replace('/' . $before . '\.0+' . $units . '?' . $after . '/', '0\\1', $content); // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px $content = preg_replace('/' . $before . '(-?[0-9]+\.[0-9]+)0+' . $units . '?' . $after . '/', '\\1\\2', $content); // strip trailing 0: 50.00 -> 50, 50.00px -> 50px $content = preg_replace('/' . $before . '(-?[0-9]+)\.0+' . $units . '?' . $after . '/', '\\1\\2', $content); // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 $content = preg_replace('/' . $before . '(-?)0+([0-9]*\.[0-9]+)' . $units . '?' . $after . '/', '\\1\\2\\3', $content); // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) $content = preg_replace('/' . $before . '-?0+' . $units . '?' . $after . '/', '0\\1', $content); // IE doesn't seem to understand a unitless flex-basis value (correct - // it goes against the spec), so let's add it in again (make it `%`, // which is only 1 char: 0%, 0px, 0 anything, it's all just the same) // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content); $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content); return $content; } /** * Strip empty tags from source code. * * @param string $content * * @return string */ protected function stripEmptyTags($content) { $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); return $content; } /** * Strip comments from source code. */ protected function stripComments() { $this->stripMultilineComments(); } /** * Strip whitespace. * * @param string $content The CSS content to strip the whitespace for * * @return string */ protected function stripWhitespace($content) { // remove leading & trailing whitespace $content = preg_replace('/^\s*/m', '', $content); $content = preg_replace('/\s*$/m', '', $content); // replace newlines with a single space $content = preg_replace('/\s+/', ' ', $content); // remove whitespace around meta characters // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); // whitespace around + and - can only be stripped inside some pseudo- // classes, like `:nth-child(3+2n)` // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or // selectors like `div.weird- p` $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); $content = preg_replace('/:(' . implode('|', $pseudos) . ')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); // remove semicolon/whitespace followed by closing bracket $content = str_replace(';}', '}', $content); return trim($content); } /** * Replace all occurrences of functions that may contain math, where * whitespace around operators needs to be preserved (e.g. calc, clamp). */ protected function extractMath() { $functions = array('calc', 'clamp', 'min', 'max'); $pattern = '/\b(' . implode('|', $functions) . ')(\(.+?)(?=$|;|})/m'; // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier, $pattern, &$callback) { $function = $match[1]; $length = strlen($match[2]); $expr = ''; $opened = 0; // the regular expression for extracting math has 1 significant problem: // it can't determine the correct closing parenthesis... // instead, it'll match a larger portion of code to where it's certain that // the calc() musts have ended, and we'll figure out which is the correct // closing parenthesis here, by counting how many have opened for ($i = 0; $i < $length; ++$i) { $char = $match[2][$i]; $expr .= $char; if ($char === '(') { ++$opened; } elseif ($char === ')' && --$opened === 0) { break; } } // now that we've figured out where the calc() starts and ends, extract it $count = count($minifier->extracted); $placeholder = 'math(' . $count . ')'; $minifier->extracted[$placeholder] = $function . '(' . trim(substr($expr, 1, -1)) . ')'; // and since we've captured more code than required, we may have some leftover // calc() in here too - go recursive on the remaining but of code to go figure // that out and extract what is needed $rest = $minifier->str_replace_first($function . $expr, '', $match[0]); $rest = preg_replace_callback($pattern, $callback, $rest); return $placeholder . $rest; }; $this->registerPattern($pattern, $callback); } /** * Replace custom properties, whose values may be used in scenarios where * we wouldn't want them to be minified (e.g. inside calc). */ protected function extractCustomProperties() { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $this->registerPattern( '/(?<=^|[;}{])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m', function ($match) use ($minifier) { $placeholder = '--custom-' . count($minifier->extracted) . ':0'; $minifier->extracted[$placeholder] = $match[1] . ':' . trim($match[2]); return $placeholder; } ); } /** * Check if file is small enough to be imported. * * @param string $path The path to the file * * @return bool */ protected function canImportBySize($path) { return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; } /** * Check if file a file can be imported, going by the path. * * @param string $path * * @return bool */ protected function canImportByPath($path) { return preg_match('/^(data:|https?:|\\/)/', $path) === 0; } /** * Return a converter to update relative paths to be relative to the new * destination. * * @param string $source * @param string $target * * @return ConverterInterface */ protected function getPathConverter($source, $target) { return new Converter($source, $target); } } lib/css_js_min/minify/LICENSE 0000644 00000002043 15162130356 0011742 0 ustar 00 Copyright (c) 2012 Matthias Mullie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lib/css_js_min/minify/js.cls.php 0000644 00000116040 15162130360 0012640 0 ustar 00 <?php /** * js.cls.php - modified PHP implementation of Matthias Mullie's JavaScript minifier * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace LiteSpeed\Lib\CSS_JS_MIN\Minify; defined( 'WPINC' ) || exit ; class JS extends Minify { /** * Var-matching regex based on http://stackoverflow.com/a/9337047/802993. * * Note that regular expressions using that bit must have the PCRE_UTF8 * pattern modifier (/u) set. * * @internal * * @var string */ const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b'; /** * Full list of JavaScript reserved words. * Will be loaded from /data/js/keywords_reserved.txt. * * @see https://mathiasbynens.be/notes/reserved-keywords * * @var string[] */ protected $keywordsReserved = array(); /** * List of JavaScript reserved words that accept a <variable, value, ...> * after them. Some end of lines are not the end of a statement, like with * these keywords. * * E.g.: we shouldn't insert a ; after this else * else * console.log('this is quite fine') * * Will be loaded from /data/js/keywords_before.txt * * @var string[] */ protected $keywordsBefore = array(); /** * List of JavaScript reserved words that accept a <variable, value, ...> * before them. Some end of lines are not the end of a statement, like when * continued by one of these keywords on the newline. * * E.g.: we shouldn't insert a ; before this instanceof * variable * instanceof String * * Will be loaded from /data/js/keywords_after.txt * * @var string[] */ protected $keywordsAfter = array(); /** * List of all JavaScript operators. * * Will be loaded from /data/js/operators.txt * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators * * @var string[] */ protected $operators = array(); /** * List of JavaScript operators that accept a <variable, value, ...> after * them. Some end of lines are not the end of a statement, like with these * operators. * * Note: Most operators are fine, we've only removed ++ and --. * ++ & -- have to be joined with the value they're in-/decrementing. * * Will be loaded from /data/js/operators_before.txt * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators * * @var string[] */ protected $operatorsBefore = array(); /** * List of JavaScript operators that accept a <variable, value, ...> before * them. Some end of lines are not the end of a statement, like when * continued by one of these operators on the newline. * * Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~. * There can't be a newline separating ! or ~ and whatever it is negating. * ++ & -- have to be joined with the value they're in-/decrementing. * ) & ] are "special" in that they have lots or usecases. () for example * is used for function calls, for grouping, in if () and for (), ... * * Will be loaded from /data/js/operators_after.txt * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators * * @var string[] */ protected $operatorsAfter = array(); public function __construct() { call_user_func_array(array('\\LiteSpeed\\Lib\\CSS_JS_MIN\\Minify\\Minify', '__construct'), func_get_args()); $dataDir = __DIR__ . '/data/js/'; $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; $this->keywordsReserved = file($dataDir . 'keywords_reserved.txt', $options); $this->keywordsBefore = file($dataDir . 'keywords_before.txt', $options); $this->keywordsAfter = file($dataDir . 'keywords_after.txt', $options); $this->operators = file($dataDir . 'operators.txt', $options); $this->operatorsBefore = file($dataDir . 'operators_before.txt', $options); $this->operatorsAfter = file($dataDir . 'operators_after.txt', $options); } /** * Minify the data. * Perform JS optimizations. * * @param string[optional] $path Path to write the data to * * @return string The minified data */ public function execute($path = null) { $content = ''; /* * Let's first take out strings, comments and regular expressions. * All of these can contain JS code-like characters, and we should make * sure any further magic ignores anything inside of these. * * Consider this example, where we should not strip any whitespace: * var str = "a test"; * * Comments will be removed altogether, strings and regular expressions * will be replaced by placeholder text, which we'll restore later. */ $this->extractStrings('\'"`'); $this->stripComments(); $this->extractRegex(); // loop files foreach ($this->data as $source => $js) { // take out strings, comments & regex (for which we've registered // the regexes just a few lines earlier) $js = $this->replace($js); $js = $this->propertyNotation($js); $js = $this->shortenBools($js); $js = $this->stripWhitespace($js); // combine js: separating the scripts by a ; $content .= $js . ';'; } // clean up leftover `;`s from the combination of multiple scripts $content = ltrim($content, ';'); $content = (string) substr($content, 0, -1); /* * Earlier, we extracted strings & regular expressions and replaced them * with placeholder text. This will restore them. */ $content = $this->restoreExtractedData($content); return $content; } /** * Strip comments from source code. */ protected function stripComments() { $this->stripMultilineComments(); // single-line comments $this->registerPattern('/\/\/.*$/m', ''); } /** * JS can have /-delimited regular expressions, like: /ab+c/.match(string). * * The content inside the regex can contain characters that may be confused * for JS code: e.g. it could contain whitespace it needs to match & we * don't want to strip whitespace in there. * * The regex can be pretty simple: we don't have to care about comments, * (which also use slashes) because stripComments() will have stripped those * already. * * This method will replace all string content with simple REGEX# * placeholder text, so we've rid all regular expressions from characters * that may be misinterpreted. Original regex content will be saved in * $this->extracted and after doing all other minifying, we can restore the * original content via restoreRegex() */ protected function extractRegex() { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier) { $count = count($minifier->extracted); $placeholder = '"' . $count . '"'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; }; // match all chars except `/` and `\` // `\` is allowed though, along with whatever char follows (which is the // one being escaped) // this should allow all chars, except for an unescaped `/` (= the one // closing the regex) // then also ignore bare `/` inside `[]`, where they don't need to be // escaped: anything inside `[]` can be ignored safely $pattern = '\\/(?!\*)(?:[^\\[\\/\\\\\n\r]++|(?:\\\\.)++|(?:\\[(?:[^\\]\\\\\n\r]++|(?:\\\\.)++)++\\])++)++\\/[gimuy]*'; // a regular expression can only be followed by a few operators or some // of the RegExp methods (a `\` followed by a variable or value is // likely part of a division, not a regex) $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof'); $before = '(^|[=:,;\+\-\*\?\/\}\(\{\[&\|!]|' . implode('|', $keywords) . ')\s*'; $propertiesAndMethods = array( // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2 'constructor', 'flags', 'global', 'ignoreCase', 'multiline', 'source', 'sticky', 'unicode', // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Methods_2 'compile(', 'exec(', 'test(', 'toSource(', 'toString(', ); $delimiters = array_fill(0, count($propertiesAndMethods), '/'); $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters); $after = '(?=\s*([\.,;:\)\}&\|+]|\/\/|$|\.(' . implode('|', $propertiesAndMethods) . ')))'; $this->registerPattern('/' . $before . '\K' . $pattern . $after . '/', $callback); // regular expressions following a `)` are rather annoying to detect... // quite often, `/` after `)` is a division operator & if it happens to // be followed by another one (or a comment), it is likely to be // confused for a regular expression // however, it's perfectly possible for a regex to follow a `)`: after // a single-line `if()`, `while()`, ... statement, for example // since, when they occur like that, they're always the start of a // statement, there's only a limited amount of ways they can be useful: // by calling the regex methods directly // if a regex following `)` is not followed by `.<property or method>`, // it's quite likely not a regex $before = '\)\s*'; $after = '(?=\s*\.(' . implode('|', $propertiesAndMethods) . '))'; $this->registerPattern('/' . $before . '\K' . $pattern . $after . '/', $callback); // 1 more edge case: a regex can be followed by a lot more operators or // keywords if there's a newline (ASI) in between, where the operator // actually starts a new statement // (https://github.com/matthiasmullie/minify/issues/56) $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/'); $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/'); $after = '(?=\s*\n\s*(' . implode('|', $operators) . '))'; $this->registerPattern('/' . $pattern . $after . '/', $callback); } /** * Strip whitespace. * * We won't strip *all* whitespace, but as much as possible. The thing that * we'll preserve are newlines we're unsure about. * JavaScript doesn't require statements to be terminated with a semicolon. * It will automatically fix missing semicolons with ASI (automatic semi- * colon insertion) at the end of line causing errors (without semicolon.) * * Because it's sometimes hard to tell if a newline is part of a statement * that should be terminated or not, we'll just leave some of them alone. * * @param string $content The content to strip the whitespace for * * @return string */ protected function stripWhitespace($content) { // uniform line endings, make them all line feed $content = str_replace(array("\r\n", "\r"), "\n", $content); // collapse all non-line feed whitespace into a single space $content = preg_replace('/[^\S\n]+/', ' ', $content); // strip leading & trailing whitespace $content = str_replace(array(" \n", "\n "), "\n", $content); // collapse consecutive line feeds into just 1 $content = preg_replace('/\n+/', "\n", $content); $operatorsBefore = $this->getOperatorsForRegex($this->operatorsBefore, '/'); $operatorsAfter = $this->getOperatorsForRegex($this->operatorsAfter, '/'); $operators = $this->getOperatorsForRegex($this->operators, '/'); $keywordsBefore = $this->getKeywordsForRegex($this->keywordsBefore, '/'); $keywordsAfter = $this->getKeywordsForRegex($this->keywordsAfter, '/'); // strip whitespace that ends in (or next line begin with) an operator // that allows statements to be broken up over multiple lines unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']); $content = preg_replace( array( '/(' . implode('|', $operatorsBefore) . ')\s+/', '/\s+(' . implode('|', $operatorsAfter) . ')/', ), '\\1', $content ); // make sure + and - can't be mistaken for, or joined into ++ and -- $content = preg_replace( array( '/(?<![\+\-])\s*([\+\-])(?![\+\-])/', '/(?<![\+\-])([\+\-])\s*(?![\+\-])/', ), '\\1', $content ); // collapse whitespace around reserved words into single space $content = preg_replace('/(^|[;\}\s])\K(' . implode('|', $keywordsBefore) . ')\s+/', '\\2 ', $content); $content = preg_replace('/\s+(' . implode('|', $keywordsAfter) . ')(?=([;\{\s]|$))/', ' \\1', $content); /* * We didn't strip whitespace after a couple of operators because they * could be used in different contexts and we can't be sure it's ok to * strip the newlines. However, we can safely strip any non-line feed * whitespace that follows them. */ $operatorsDiffBefore = array_diff($operators, $operatorsBefore); $operatorsDiffAfter = array_diff($operators, $operatorsAfter); $content = preg_replace('/(' . implode('|', $operatorsDiffBefore) . ')[^\S\n]+/', '\\1', $content); $content = preg_replace('/[^\S\n]+(' . implode('|', $operatorsDiffAfter) . ')/', '\\1', $content); /* * Whitespace after `return` can be omitted in a few occasions * (such as when followed by a string or regex) * Same for whitespace in between `)` and `{`, or between `{` and some * keywords. */ $content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content); $content = preg_replace('/\)\s+\{/', '){', $content); $content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content); /* * Get rid of double semicolons, except where they can be used like: * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))". * I'll safeguard these double semicolons inside for-loops by * temporarily replacing them with an invalid condition: they won't have * a double semicolon and will be easy to spot to restore afterwards. */ $content = preg_replace('/\bfor\(([^;]*);;([^;]*)\)/', 'for(\\1;-;\\2)', $content); $content = preg_replace('/;+/', ';', $content); $content = preg_replace('/\bfor\(([^;]*);-;([^;]*)\)/', 'for(\\1;;\\2)', $content); /* * Next, we'll be removing all semicolons where ASI kicks in. * for-loops however, can have an empty body (ending in only a * semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);` * Here, nothing happens during the loop; it's just used to keep * increasing `i`. With that ; omitted, the next line would be expected * to be the for-loop's body... Same goes for while loops. * I'm going to double that semicolon (if any) so after the next line, * which strips semicolons here & there, we're still left with this one. * Note the special recursive construct in the three inner parts of the for: * (\{([^\{\}]*(?-2))*[^\{\}]*\})? - it is intended to match inline * functions bodies, e.g.: i<arr.map(function(e){return e}).length. * Also note that the construct is applied only once and multiplied * for each part of the for, otherwise it risks a catastrophic backtracking. * The limitation is that it will not allow closures in more than one * of the three parts for a specific for() case. * REGEX throwing catastrophic backtracking: $content = preg_replace('/(for\([^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*;[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*;[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*\));(\}|$)/s', '\\1;;\\8', $content); */ $content = preg_replace('/(for\((?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*);[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\4', $content); $content = preg_replace('/(for\([^;\{]*;(?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*);[^;\{]*\));(\}|$)/s', '\\1;;\\4', $content); $content = preg_replace('/(for\([^;\{]*;[^;\{]*;(?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*)\));(\}|$)/s', '\\1;;\\4', $content); $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); /* * Do the same for the if's that don't have a body but are followed by ;} */ $content = preg_replace('/(\bif\s*\([^{;]*\));\}/s', '\\1;;}', $content); /* * Below will also keep `;` after a `do{}while();` along with `while();` * While these could be stripped after do-while, detecting this * distinction is cumbersome, so I'll play it safe and make sure `;` * after any kind of `while` is kept. */ $content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); /* * We also can't strip empty else-statements. Even though they're * useless and probably shouldn't be in the code in the first place, we * shouldn't be stripping the `;` that follows it as it breaks the code. * We can just remove those useless else-statements completely. * * @see https://github.com/matthiasmullie/minify/issues/91 */ $content = preg_replace('/else;/s', '', $content); /* * We also don't really want to terminate statements followed by closing * curly braces (which we've ignored completely up until now) or end-of- * script: ASI will kick in here & we're all about minifying. * Semicolons at beginning of the file don't make any sense either. */ $content = preg_replace('/;(\}|$)/s', '\\1', $content); $content = ltrim($content, ';'); // get rid of remaining whitespace af beginning/end return trim($content); } /** * We'll strip whitespace around certain operators with regular expressions. * This will prepare the given array by escaping all characters. * * @param string[] $operators * @param string $delimiter * * @return string[] */ protected function getOperatorsForRegex(array $operators, $delimiter = '/') { // escape operators for use in regex $delimiters = array_fill(0, count($operators), $delimiter); $escaped = array_map('preg_quote', $operators, $delimiters); $operators = array_combine($operators, $escaped); // ignore + & - for now, they'll get special treatment unset($operators['+'], $operators['-']); // dot can not just immediately follow a number; it can be confused for // decimal point, or calling a method on it, e.g. 42 .toString() $operators['.'] = '(?<![0-9]\s)\.'; // don't confuse = with other assignment shortcuts (e.g. +=) $chars = preg_quote('+-*\=<>%&|', $delimiter); $operators['='] = '(?<![' . $chars . '])\='; return $operators; } /** * We'll strip whitespace around certain keywords with regular expressions. * This will prepare the given array by escaping all characters. * * @param string[] $keywords * @param string $delimiter * * @return string[] */ protected function getKeywordsForRegex(array $keywords, $delimiter = '/') { // escape keywords for use in regex $delimiter = array_fill(0, count($keywords), $delimiter); $escaped = array_map('preg_quote', $keywords, $delimiter); // add word boundaries array_walk($keywords, function ($value) { return '\b' . $value . '\b'; }); $keywords = array_combine($keywords, $escaped); return $keywords; } /** * Replaces all occurrences of array['key'] by array.key. * * @param string $content * * @return string */ protected function propertyNotation($content) { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $keywords = $this->keywordsReserved; $callback = function ($match) use ($minifier, $keywords) { $property = trim($minifier->extracted[$match[1]], '\'"'); /* * Check if the property is a reserved keyword. In this context (as * property of an object literal/array) it shouldn't matter, but IE8 * freaks out with "Expected identifier". */ if (in_array($property, $keywords)) { return $match[0]; } /* * See if the property is in a variable-like format (e.g. * array['key-here'] can't be replaced by array.key-here since '-' * is not a valid character there. */ if (!preg_match('/^' . $minifier::REGEX_VARIABLE . '$/u', $property)) { return $match[0]; } return '.' . $property; }; /* * Figure out if previous character is a variable name (of the array * we want to use property notation on) - this is to make sure * standalone ['value'] arrays aren't confused for keys-of-an-array. * We can (and only have to) check the last character, because PHP's * regex implementation doesn't allow unfixed-length look-behind * assertions. */ preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); $previousChar = $previousChar[1]; /* * Make sure word preceding the ['value'] is not a keyword, e.g. * return['x']. Because -again- PHP's regex implementation doesn't allow * unfixed-length look-behind assertions, I'm just going to do a lot of * separate look-behind assertions, one for each keyword. */ $keywords = $this->getKeywordsForRegex($keywords); $keywords = '(?<!' . implode(')(?<!', $keywords) . ')'; return preg_replace_callback('/(?<=' . $previousChar . '|\])' . $keywords . '\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content); } /** * Replaces true & false by !0 and !1. * * @param string $content * * @return string */ protected function shortenBools($content) { /* * 'true' or 'false' could be used as property names (which may be * followed by whitespace) - we must not replace those! * Since PHP doesn't allow variable-length (to account for the * whitespace) lookbehind assertions, I need to capture the leading * character and check if it's a `.` */ $callback = function ($match) { if (trim($match[1]) === '.') { return $match[0]; } return $match[1] . ($match[2] === 'true' ? '!0' : '!1'); }; $content = preg_replace_callback('/(^|.\s*)\b(true|false)\b(?!:)/', $callback, $content); // for(;;) is exactly the same as while(true), but shorter :) $content = preg_replace('/\bwhile\(!0\){/', 'for(;;){', $content); // now make sure we didn't turn any do ... while(true) into do ... for(;;) preg_match_all('/\bdo\b/', $content, $dos, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); // go backward to make sure positional offsets aren't altered when $content changes $dos = array_reverse($dos); foreach ($dos as $do) { $offsetDo = $do[0][1]; // find all `while` (now `for`) following `do`: one of those must be // associated with the `do` and be turned back into `while` preg_match_all('/\bfor\(;;\)/', $content, $whiles, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offsetDo); foreach ($whiles as $while) { $offsetWhile = $while[0][1]; $open = substr_count($content, '{', $offsetDo, $offsetWhile - $offsetDo); $close = substr_count($content, '}', $offsetDo, $offsetWhile - $offsetDo); if ($open === $close) { // only restore `while` if amount of `{` and `}` are the same; // otherwise, that `for` isn't associated with this `do` $content = substr_replace($content, 'while(!0)', $offsetWhile, strlen('for(;;)')); break; } } } return $content; } } lib/css_js_min/pathconverter/converter.cls.php 0000644 00000014040 15162130365 0015626 0 ustar 00 <?php /** * modified PHP implementation of Matthias Mullie's convert path class * Convert paths relative from 1 file to another. * * E.g. * ../../images/icon.jpg relative to /css/imports/icons.css * becomes * ../images/icon.jpg relative to /css/minified.css * * @author Matthias Mullie <pathconverter@mullie.eu> * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved * @license MIT License */ namespace LiteSpeed\Lib\CSS_JS_MIN\PathConverter; defined( 'WPINC' ) || exit ; interface ConverterInterface { /** * Convert file paths. * * @param string $path The path to be converted * * @return string The new path */ public function convert($path); } class Converter implements ConverterInterface { /** * @var string */ protected $from; /** * @var string */ protected $to; /** * @param string $from The original base path (directory, not file!) * @param string $to The new base path (directory, not file!) * @param string $root Root directory (defaults to `getcwd`) */ public function __construct($from, $to, $root = '') { $shared = $this->shared($from, $to); if ($shared === '') { // when both paths have nothing in common, one of them is probably // absolute while the other is relative $root = $root ?: getcwd(); $from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from); $to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to); // or traveling the tree via `..` // attempt to resolve path, or assume it's fine if it doesn't exist $from = @realpath($from) ?: $from; $to = @realpath($to) ?: $to; } $from = $this->dirname($from); $to = $this->dirname($to); $from = $this->normalize($from); $to = $this->normalize($to); $this->from = $from; $this->to = $to; } /** * Normalize path. * * @param string $path * * @return string */ protected function normalize($path) { // deal with different operating systems' directory structure $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/'); // remove leading current directory. if (substr($path, 0, 2) === './') { $path = substr($path, 2); } // remove references to current directory in the path. $path = str_replace('/./', '/', $path); /* * Example: * /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif * to * /home/forkcms/frontend/core/layout/images/img.gif */ do { $path = preg_replace('/[^\/]+(?<!\.\.)\/\.\.\//', '', $path, -1, $count); } while ($count); return $path; } /** * Figure out the shared path of 2 locations. * * Example: * /home/forkcms/frontend/core/layout/images/img.gif * and * /home/forkcms/frontend/cache/minified_css * share * /home/forkcms/frontend * * @param string $path1 * @param string $path2 * * @return string */ protected function shared($path1, $path2) { // $path could theoretically be empty (e.g. no path is given), in which // case it shouldn't expand to array(''), which would compare to one's // root / $path1 = $path1 ? explode('/', $path1) : array(); $path2 = $path2 ? explode('/', $path2) : array(); $shared = array(); // compare paths & strip identical ancestors foreach ($path1 as $i => $chunk) { if (isset($path2[$i]) && $path1[$i] == $path2[$i]) { $shared[] = $chunk; } else { break; } } return implode('/', $shared); } /** * Convert paths relative from 1 file to another. * * E.g. * ../images/img.gif relative to /home/forkcms/frontend/core/layout/css * should become: * ../../core/layout/images/img.gif relative to * /home/forkcms/frontend/cache/minified_css * * @param string $path The relative path that needs to be converted * * @return string The new relative path */ public function convert($path) { // quit early if conversion makes no sense if ($this->from === $this->to) { return $path; } $path = $this->normalize($path); // if we're not dealing with a relative path, just return absolute if (strpos($path, '/') === 0) { return $path; } // normalize paths $path = $this->normalize($this->from.'/'.$path); // strip shared ancestor paths $shared = $this->shared($path, $this->to); $path = mb_substr($path, mb_strlen($shared)); $to = mb_substr($this->to, mb_strlen($shared)); // add .. for every directory that needs to be traversed to new path $to = str_repeat('../', count(array_filter(explode('/', $to)))); return $to.ltrim($path, '/'); } /** * Attempt to get the directory name from a path. * * @param string $path * * @return string */ protected function dirname($path) { if (@is_file($path)) { return dirname($path); } if (@is_dir($path)) { return rtrim($path, '/'); } // no known file/dir, start making assumptions // ends in / = dir if (mb_substr($path, -1) === '/') { return rtrim($path, '/'); } // has a dot in the name, likely a file if (preg_match('/.*\..*$/', basename($path)) !== 0) { return dirname($path); } // you're on your own here! return $path; } } class NoConverter implements ConverterInterface { /** * {@inheritdoc} */ public function convert($path) { return $path; } } lib/css_js_min/pathconverter/LICENSE 0000644 00000002043 15162130367 0013335 0 ustar 00 Copyright (c) 2015 Matthias Mullie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qc-ping.txt 0000644 00000000120 15162130367 0006640 0 ustar 00 For QUIC.cloud connectivity ping test, please do not delete, generated by LSCWP cli/debug.cls.php 0000644 00000001044 15162130371 0007670 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Debug2; use LiteSpeed\Report; use WP_CLI; /** * Debug API CLI */ class Debug { private $__report; public function __construct() { Debug2::debug('CLI_Debug init'); $this->__report = Report::cls(); } /** * Send report * * ## OPTIONS * * ## EXAMPLES * * # Send env report to LiteSpeed * $ wp litespeed-debug send * */ public function send() { $num = $this->__report->post_env(); WP_CLI::success('Report Number = ' . $num); } } cli/online.cls.php 0000644 00000015500 15162130373 0010072 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Debug2; use LiteSpeed\Cloud; use WP_CLI; /** * QUIC.cloud API CLI */ class Online { private $__cloud; public function __construct() { Debug2::debug('CLI_Cloud init'); $this->__cloud = Cloud::cls(); } /** * Init domain on QUIC.cloud server (See https://quic.cloud/terms/) * * ## OPTIONS * * ## EXAMPLES * * # Activate domain on QUIC.cloud (! Require SERVER IP setting to be set first) * $ wp litespeed-online init * */ public function init() { $resp = $this->__cloud->init_qc_cli(); if (!empty($resp['qc_activated'])) { $main_domain = !empty($resp['main_domain']) ? $resp['main_domain'] : false; $this->__cloud->update_qc_activation($resp['qc_activated'], $main_domain); WP_CLI::success('Init successfully. Activated type: ' . $resp['qc_activated']); } else { WP_CLI::error('Init failed!'); } } /** * Init domain CDN service on QUIC.cloud server (See https://quic.cloud/terms/) * * ## OPTIONS * * ## EXAMPLES * * # Activate domain CDN on QUIC.cloud (support --format=json) * $ wp litespeed-online cdn_init --method=cname|ns * $ wp litespeed-online cdn_init --method=cname|ns --ssl-cert=xxx.pem --ssl-key=xxx * $ wp litespeed-online cdn_init --method=cfi --cf-token=xxxxxxxx * $ wp litespeed-online cdn_init --method=cfi --cf-token=xxxxxxxx --ssl-cert=xxx.pem --ssl-key=xxx * */ public function cdn_init($args, $assoc_args) { if (empty($assoc_args['method'])) { WP_CLI::error('Init CDN failed! Missing parameters `--method`.'); return; } if ((!empty($assoc_args['ssl-cert']) && empty($assoc_args['ssl-key'])) || (empty($assoc_args['ssl-cert']) && !empty($assoc_args['ssl-key']))) { WP_CLI::error('Init CDN failed! SSL cert must be present together w/ SSL key.'); return; } if ($assoc_args['method'] == 'cfi' && empty($assoc_args['cf-token'])) { WP_CLI::error('Init CDN failed! CFI must set `--cf-token`.'); return; } $cert = !empty($assoc_args['ssl-cert']) ? $assoc_args['ssl-cert'] : ''; $key = !empty($assoc_args['ssl-key']) ? $assoc_args['ssl-key'] : ''; $cf_token = !empty($assoc_args['cf-token']) ? $assoc_args['cf-token'] : ''; $resp = $this->__cloud->init_qc_cdn_cli($assoc_args['method'], $cert, $key, $cf_token); if (!empty($resp['qc_activated'])) { $main_domain = !empty($resp['main_domain']) ? $resp['main_domain'] : false; $this->__cloud->update_qc_activation($resp['qc_activated'], $main_domain, true); } if (!empty($assoc_args['format']) && $assoc_args['format'] == 'json') { WP_CLI::log(json_encode($resp)); return; } if (!empty($resp['qc_activated'])) { WP_CLI::success('Init QC CDN successfully. Activated type: ' . $resp['qc_activated']); } else { WP_CLI::error('Init QC CDN failed!'); } if (!empty($resp['cname'])) { WP_CLI::success('cname: ' . $resp['cname']); } if (!empty($resp['msgs'])) { WP_CLI::success('msgs: ' . var_export($resp['msgs'], true)); } } /** * Link user account by api key * * ## OPTIONS * * ## EXAMPLES * * # Link user account by api key * $ wp litespeed-online link --email=xxx@example.com --api-key=xxxx * */ public function link($args, $assoc_args) { if (empty($assoc_args['email']) || empty($assoc_args['api-key'])) { WP_CLI::error('Link to QUIC.cloud failed! Missing parameters `--email` or `--api-key`.'); return; } $resp = $this->__cloud->link_qc_cli($assoc_args['email'], $assoc_args['api-key']); if (!empty($resp['qc_activated'])) { $main_domain = !empty($resp['main_domain']) ? $resp['main_domain'] : false; $this->__cloud->update_qc_activation($resp['qc_activated'], $main_domain, true); WP_CLI::success('Link successfully!'); WP_CLI::log(json_encode($resp)); } else { WP_CLI::error('Link failed!'); } } /** * Sync usage data from QUIC.cloud * * ## OPTIONS * * ## EXAMPLES * * # Sync QUIC.cloud service usage info * $ wp litespeed-online sync * */ public function sync($args, $assoc_args) { $json = $this->__cloud->sync_usage(); if (!empty($assoc_args['format'])) { WP_CLI::print_value($json, $assoc_args); return; } WP_CLI::success('Sync successfully'); $list = array(); foreach (Cloud::$SERVICES as $v) { $list[] = array( 'key' => $v, 'used' => !empty($json['usage.' . $v]['used']) ? $json['usage.' . $v]['used'] : 0, 'quota' => !empty($json['usage.' . $v]['quota']) ? $json['usage.' . $v]['quota'] : 0, 'PayAsYouGo_Used' => !empty($json['usage.' . $v]['pag_used']) ? $json['usage.' . $v]['pag_used'] : 0, 'PayAsYouGo_Balance' => !empty($json['usage.' . $v]['pag_bal']) ? $json['usage.' . $v]['pag_bal'] : 0, ); } WP_CLI\Utils\format_items('table', $list, array('key', 'used', 'quota', 'PayAsYouGo_Used', 'PayAsYouGo_Balance')); } /** * Check QC account status * * ## OPTIONS * * ## EXAMPLES * * # Check QC account status * $ wp litespeed-online cdn_status * */ public function cdn_status($args, $assoc_args) { $resp = $this->__cloud->cdn_status_cli(); WP_CLI::log(json_encode($resp)); } /** * List all QUIC.cloud services * * ## OPTIONS * * ## EXAMPLES * * # List all services tag * $ wp litespeed-online services * */ public function services($args, $assoc_args) { if (!empty($assoc_args['format'])) { WP_CLI::print_value(Cloud::$SERVICES, $assoc_args); return; } $list = array(); foreach (Cloud::$SERVICES as $v) { $list[] = array( 'service' => $v, ); } WP_CLI\Utils\format_items('table', $list, array('service')); } /** * List all QUIC.cloud servers in use * * ## OPTIONS * * ## EXAMPLES * * # List all QUIC.cloud servers in use * $ wp litespeed-online nodes * */ public function nodes($args, $assoc_args) { $json = Cloud::get_summary(); $list = array(); $json_output = array(); foreach (Cloud::$SERVICES as $v) { $server = !empty($json['server.' . $v]) ? $json['server.' . $v] : ''; $list[] = array( 'service' => $v, 'server' => $server, ); $json_output[] = array($v => $server); } if (!empty($assoc_args['format'])) { WP_CLI::print_value($json_output, $assoc_args); return; } WP_CLI\Utils\format_items('table', $list, array('service', 'server')); } /** * Detect closest node server for current service * * ## OPTIONS * * ## EXAMPLES * * # Detect closest node for one service * $ wp litespeed-online ping img_optm * $ wp litespeed-online ping img_optm --force * */ public function ping($param, $assoc_args) { $svc = $param[0]; $force = !empty($assoc_args['force']); $json = $this->__cloud->detect_cloud($svc, $force); if ($json) { WP_CLI::success('Updated closest server.'); } WP_CLI::log('svc = ' . $svc); WP_CLI::log('node = ' . ($json ?: '-')); } } cli/purge.cls.php 0000644 00000015244 15162130375 0007737 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Core; use LiteSpeed\Router; use LiteSpeed\Admin_Display; use WP_CLI; /** * LiteSpeed Cache Purge Interface */ class Purge { /** * List all site domains and ids on the network. * * For use with the blog subcommand. * * ## EXAMPLES * * # List all the site domains and ids in a table. * $ wp litespeed-purge network_list */ public function network_list($args) { if (!is_multisite()) { WP_CLI::error('This is not a multisite installation!'); return; } $buf = WP_CLI::colorize("%CThe list of installs:%n\n"); if (version_compare($GLOBALS['wp_version'], '4.6', '<')) { $sites = wp_get_sites(); foreach ($sites as $site) { $buf .= WP_CLI::colorize('%Y' . $site['domain'] . $site['path'] . ':%n ID ' . $site['blog_id']) . "\n"; } } else { $sites = get_sites(); foreach ($sites as $site) { $buf .= WP_CLI::colorize('%Y' . $site->domain . $site->path . ':%n ID ' . $site->blog_id) . "\n"; } } WP_CLI::line($buf); } /** * Sends an ajax request to the site. Takes an action and the nonce string to perform. * * @since 1.0.14 */ private function _send_request($action, $extra = array()) { $data = array( Router::ACTION => $action, Router::NONCE => wp_create_nonce($action), ); if (!empty($extra)) { $data = array_merge($data, $extra); } $url = admin_url('admin-ajax.php'); WP_CLI::debug('URL is ' . $url); $out = WP_CLI\Utils\http_request('GET', $url, $data); return $out; } /** * Purges all cache entries for the blog (the entire network if multisite). * * ## EXAMPLES * * # Purge Everything associated with the WordPress install. * $ wp litespeed-purge all * */ public function all($args) { if (is_multisite()) { $action = Core::ACTION_QS_PURGE_EMPTYCACHE; } else { $action = Core::ACTION_QS_PURGE_ALL; } $purge_ret = $this->_send_request($action); if ($purge_ret->success) { WP_CLI::success(__('Purged All!', 'litespeed-cache')); } else { WP_CLI::error('Something went wrong! Got ' . $purge_ret->status_code); } } /** * Purges all cache entries for the blog. * * ## OPTIONS * * <blogid> * : The blog id to purge * * ## EXAMPLES * * # In a multisite install, purge only the shop.example.com cache (stored as blog id 2). * $ wp litespeed-purge blog 2 * */ public function blog($args) { if (!is_multisite()) { WP_CLI::error('Not a multisite installation.'); return; } $blogid = $args[0]; if (!is_numeric($blogid)) { $error = WP_CLI::colorize('%RError: invalid blog id entered.%n'); WP_CLI::line($error); $this->network_list($args); return; } $site = get_blog_details($blogid); if ($site === false) { $error = WP_CLI::colorize('%RError: invalid blog id entered.%n'); WP_CLI::line($error); $this->network_list($args); return; } switch_to_blog($blogid); $purge_ret = $this->_send_request(Core::ACTION_QS_PURGE_ALL); if ($purge_ret->success) { WP_CLI::success(__('Purged the blog!', 'litespeed-cache')); } else { WP_CLI::error('Something went wrong! Got ' . $purge_ret->status_code); } } /** * Purges all cache tags related to a url. * * ## OPTIONS * * <url> * : The url to purge. * * ## EXAMPLES * * # Purge the front page. * $ wp litespeed-purge url https://mysite.com/ * */ public function url($args) { $data = array( Router::ACTION => Core::ACTION_QS_PURGE, ); $url = $args[0]; $deconstructed = wp_parse_url($url); if (empty($deconstructed)) { WP_CLI::error('url passed in is invalid.'); return; } if (is_multisite()) { if (get_blog_id_from_url($deconstructed['host'], '/') === 0) { WP_CLI::error('Multisite url passed in is invalid.'); return; } } else { $deconstructed_site = wp_parse_url(get_home_url()); if ($deconstructed['host'] !== $deconstructed_site['host']) { WP_CLI::error('Single site url passed in is invalid.'); return; } } WP_CLI::debug('url is ' . $url); $purge_ret = WP_CLI\Utils\http_request('GET', $url, $data); if ($purge_ret->success) { WP_CLI::success(__('Purged the url!', 'litespeed-cache')); } else { WP_CLI::error('Something went wrong! Got ' . $purge_ret->status_code); } } /** * Helper function for purging by ids. * * @access private * @since 1.0.15 * @param array $args The id list to parse. * @param string $select The purge by kind * @param function(int $id) $callback The callback function to check the id. */ private function _purgeby($args, $select, $callback) { $filtered = array(); foreach ($args as $val) { if (!ctype_digit($val)) { WP_CLI::debug('[LSCACHE] Skip val, not a number. ' . $val); continue; } $term = $callback($val); if (!empty($term)) { WP_CLI::line($term->name); $filtered[] = in_array($callback, array('get_tag', 'get_category')) ? $term->name : $val; } else { WP_CLI::debug('[LSCACHE] Skip val, not a valid term. ' . $val); } } if (empty($filtered)) { WP_CLI::error('Arguments must be integer ids.'); return; } $str = implode(',', $filtered); $purge_titles = array( 0 => 'Category', 1 => 'Post ID', 2 => 'Tag', 3 => 'URL', ); WP_CLI::line('Will purge the following: [' . $purge_titles[$select] . '] ' . $str); $data = array( Admin_Display::PURGEBYOPT_SELECT => $select, Admin_Display::PURGEBYOPT_LIST => $str, ); $purge_ret = $this->_send_request(Core::ACTION_PURGE_BY, $data); if ($purge_ret->success) { WP_CLI::success(__('Purged!', 'litespeed-cache')); } else { WP_CLI::error('Something went wrong! Got ' . $purge_ret->status_code); } } /** * Purges cache tags for a WordPress tag * * ## OPTIONS * * <ids>... * : the Term IDs to purge. * * ## EXAMPLES * * # Purge the tag ids 1, 3, and 5 * $ wp litespeed-purge tag 1 3 5 * */ public function tag($args) { $this->_purgeby($args, Admin_Display::PURGEBY_TAG, 'get_tag'); } /** * Purges cache tags for a WordPress category * * ## OPTIONS * * <ids>... * : the Term IDs to purge. * * ## EXAMPLES * * # Purge the category ids 1, 3, and 5 * $ wp litespeed-purge category 1 3 5 * */ public function category($args) { $this->_purgeby($args, Admin_Display::PURGEBY_CAT, 'get_category'); } /** * Purges cache tags for a WordPress Post/Product * * @alias product * * ## OPTIONS * * <ids>... * : the Post IDs to purge. * * ## EXAMPLES * * # Purge the post ids 1, 3, and 5 * $ wp litespeed-purge post_id 1 3 5 * */ public function post_id($args) { $this->_purgeby($args, Admin_Display::PURGEBY_PID, 'get_post'); } } cli/image.cls.php 0000644 00000006454 15162130376 0007703 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Lang; use LiteSpeed\Debug2; use LiteSpeed\Img_Optm; use LiteSpeed\Utility; use WP_CLI; /** * Image Optm API CLI */ class Image { private $__img_optm; public function __construct() { Debug2::debug('CLI_Cloud init'); $this->__img_optm = Img_Optm::cls(); } /** * Batch toggle optimized images w/ original images * * ## OPTIONS * * ## EXAMPLES * * # Switch to original images * $ wp litespeed-image batch_switch orig * * # Switch to optimized images * $ wp litespeed-image batch_switch optm * */ public function batch_switch($param) { $type = $param[0]; $this->__img_optm->batch_switch($type); } /** * Send image optimization request to QUIC.cloud server * * ## OPTIONS * * ## EXAMPLES * * # Send image optimization request * $ wp litespeed-image push * */ public function push() { $this->__img_optm->new_req(); } /** * Pull optimized images from QUIC.cloud server * * ## OPTIONS * * ## EXAMPLES * * # Pull images back from cloud * $ wp litespeed-image pull * */ public function pull() { $this->__img_optm->pull(true); } /** * Show optimization status based on local data * * ## OPTIONS * * ## EXAMPLES * * # Show optimization status * $ wp litespeed-image s * */ public function s() { $this->status(); } /** * Show optimization status based on local data * * ## OPTIONS * * ## EXAMPLES * * # Show optimization status * $ wp litespeed-image status * */ public function status() { $summary = Img_Optm::get_summary(); $img_count = $this->__img_optm->img_count(); foreach (Lang::img_status() as $k => $v) { if (isset($img_count["img.$k"])) { $img_count["$v - images"] = $img_count["img.$k"]; unset($img_count["img.$k"]); } if (isset($img_count["group.$k"])) { $img_count["$v - groups"] = $img_count["group.$k"]; unset($img_count["group.$k"]); } } foreach (array('reduced', 'reduced_webp', 'reduced_avif') as $v) { if (!empty($summary[$v])) { $summary[$v] = Utility::real_size($summary[$v]); } } if (!empty($summary['last_requested'])) { $summary['last_requested'] = date('m/d/y H:i:s', $summary['last_requested']); } $list = array(); foreach ($summary as $k => $v) { $list[] = array('key' => $k, 'value' => $v); } $list2 = array(); foreach ($img_count as $k => $v) { if (!$v) { continue; } $list2[] = array('key' => $k, 'value' => $v); } WP_CLI\Utils\format_items('table', $list, array('key', 'value')); WP_CLI::line(WP_CLI::colorize('%CImages in database summary:%n')); WP_CLI\Utils\format_items('table', $list2, array('key', 'value')); } /** * Clean up unfinished image data from QUIC.cloud server * * ## OPTIONS * * ## EXAMPLES * * # Clean up unfinished requests * $ wp litespeed-image clean * */ public function clean() { $this->__img_optm->clean(); WP_CLI::line(WP_CLI::colorize('%CLatest status:%n')); $this->status(); } /** * Remove original image backups * * ## OPTIONS * * ## EXAMPLES * * # Remove original image backups * $ wp litespeed-image rm_bkup * */ public function rm_bkup() { $this->__img_optm->rm_bkup(); } } cli/option.cls.php 0000644 00000021155 15162130377 0010125 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Base; use LiteSpeed\Admin_Settings; use LiteSpeed\Utility; use WP_CLI; /** * LiteSpeed Cache option Interface */ class Option extends Base { /** * Set an individual LiteSpeed Cache option. * * ## OPTIONS * * <key> * : The option key to update. * * <newvalue> * : The new value to set the option to. * * ## EXAMPLES * * # Set to not cache the login page * $ wp litespeed-option set cache-priv false * $ wp litespeed-option set 'cdn-mapping[url][0]' https://cdn.EXAMPLE.com * $ wp litespeed-option set media-lqip_exc $'line1\nline2' * */ public function set($args, $assoc_args) { /** * Note: If the value is multiple dimensions like cdn-mapping, need to specially handle it both here and in `const.default.json` * * For CDN/Crawler mutlti dimension settings, if all children are empty in one line, will delete that line. To delete one line, just set all to empty. * E.g. to delete cdn-mapping[0], need to run below: * `set cdn-mapping[url][0] ''` * `set cdn-mapping[inc_img][0] ''` * `set cdn-mapping[inc_css][0] ''` * `set cdn-mapping[inc_js][0] ''` * `set cdn-mapping[filetype][0] ''` */ $key = $args[0]; $val = $args[1]; /** * For CDN mapping, allow: * `set 'cdn-mapping[url][0]' https://the1st_cdn_url` * `set 'cdn-mapping[inc_img][0]' true` * `set 'cdn-mapping[inc_img][0]' 1` * @since 2.7.1 * * For Crawler cookies: * `set 'crawler-cookies[name][0]' my_currency` * `set 'crawler-cookies[vals][0]' "USD\nTWD"` * * For multi lines setting: * `set media-lqip_exc $'img1.jpg\nimg2.jpg'` */ // Build raw data $raw_data = array( Admin_Settings::ENROLL => array($key), ); // Contains child set if (strpos($key, '[')) { parse_str($key . '=' . $val, $key2); $raw_data = array_merge($raw_data, $key2); } else { $raw_data[$key] = $val; } $this->cls('Admin_Settings')->save($raw_data); WP_CLI::line("$key:"); $this->get($args, $assoc_args); } /** * Get the plugin options. * * ## OPTIONS * * ## EXAMPLES * * # Get all options * $ wp litespeed-option all * $ wp litespeed-option all --json * */ public function all($args, $assoc_args) { $options = $this->get_options(); if (!empty($assoc_args['format'])) { WP_CLI::print_value($options, $assoc_args); return; } $option_out = array(); $buf = WP_CLI::colorize('%CThe list of options:%n'); WP_CLI::line($buf); foreach ($options as $k => $v) { if ($k == self::O_CDN_MAPPING || $k == self::O_CRAWLER_COOKIES) { foreach ($v as $k2 => $v2) { // $k2 is numeric if (is_array($v2)) { foreach ($v2 as $k3 => $v3) { // $k3 = 'url/inc_img/name/vals' if (is_array($v3)) { $option_out[] = array('key' => '', 'value' => ''); foreach ($v3 as $k4 => $v4) { $option_out[] = array('key' => $k4 == 0 ? "{$k}[$k3][$k2]" : '', 'value' => $v4); } $option_out[] = array('key' => '', 'value' => ''); } else { $option_out[] = array('key' => "{$k}[$k3][$k2]", 'value' => $v3); } } } } continue; } elseif (is_array($v) && $v) { // $v = implode( PHP_EOL, $v ); $option_out[] = array('key' => '', 'value' => ''); foreach ($v as $k2 => $v2) { $option_out[] = array('key' => $k2 == 0 ? $k : '', 'value' => $v2); } $option_out[] = array('key' => '', 'value' => ''); continue; } if (array_key_exists($k, self::$_default_options) && is_bool(self::$_default_options[$k]) && !$v) { $v = 0; } if ($v === '' || $v === array()) { $v = "''"; } $option_out[] = array('key' => $k, 'value' => $v); } WP_CLI\Utils\format_items('table', $option_out, array('key', 'value')); } /** * Get the plugin options. * * ## OPTIONS * * ## EXAMPLES * * # Get one option * $ wp litespeed-option get cache-priv * $ wp litespeed-option get 'cdn-mapping[url][0]' * */ public function get($args, $assoc_args) { $id = $args[0]; $child = false; if (strpos($id, '[')) { parse_str($id, $id2); Utility::compatibility(); $id = array_key_first($id2); $child = array_key_first($id2[$id]); // `url` if (!$child) { WP_CLI::error('Wrong child key'); return; } $numeric = array_key_first($id2[$id][$child]); // `0` if ($numeric === null) { WP_CLI::error('Wrong 2nd level numeric key'); return; } } if (!isset(self::$_default_options[$id])) { WP_CLI::error('ID not exist [id] ' . $id); return; } $v = $this->conf($id); $default_v = self::$_default_options[$id]; /** * For CDN_mapping and crawler_cookies * Examples of option name: * cdn-mapping[url][0] * crawler-cookies[name][1] */ if ($id == self::O_CDN_MAPPING) { if (!in_array($child, array(self::CDN_MAPPING_URL, self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS, self::CDN_MAPPING_FILETYPE))) { WP_CLI::error('Wrong child key'); return; } } if ($id == self::O_CRAWLER_COOKIES) { if (!in_array($child, array(self::CRWL_COOKIE_NAME, self::CRWL_COOKIE_VALS))) { WP_CLI::error('Wrong child key'); return; } } if ($id == self::O_CDN_MAPPING || $id == self::O_CRAWLER_COOKIES) { if (!empty($v[$numeric][$child])) { $v = $v[$numeric][$child]; } else { if ($id == self::O_CDN_MAPPING) { if (in_array($child, array(self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS))) { $v = 0; } else { $v = "''"; } } else { $v = "''"; } } } if (is_array($v)) { $v = implode(PHP_EOL, $v); } if (!$v && $id != self::O_CDN_MAPPING && $id != self::O_CRAWLER_COOKIES) { // empty array for CDN/crawler has been handled if (is_bool($default_v)) { $v = 0; } elseif (!is_array($default_v)) { $v = "''"; } } WP_CLI::line($v); } /** * Export plugin options to a file. * * ## OPTIONS * * [--filename=<path>] * : The default path used is CURRENTDIR/lscache_wp_options_DATE-TIME.txt. * To select a different file, use this option. * * ## EXAMPLES * * # Export options to a file. * $ wp litespeed-option export * */ public function export($args, $assoc_args) { if (isset($assoc_args['filename'])) { $file = $assoc_args['filename']; } else { $file = getcwd() . '/litespeed_options_' . date('d_m_Y-His') . '.data'; } if (!is_writable(dirname($file))) { WP_CLI::error('Directory not writable.'); return; } $data = $this->cls('Import')->export(true); if (file_put_contents($file, $data) === false) { WP_CLI::error('Failed to create file.'); } else { WP_CLI::success('Created file ' . $file); } } /** * Import plugin options from a file. * * The file must be formatted as such: * option_key=option_value * One per line. * A Semicolon at the beginning of the line indicates a comment and will be skipped. * * ## OPTIONS * * <file> * : The file to import options from. * * ## EXAMPLES * * # Import options from CURRENTDIR/options.txt * $ wp litespeed-option import options.txt * */ public function import($args, $assoc_args) { $file = $args[0]; if (!file_exists($file) || !is_readable($file)) { WP_CLI::error('File does not exist or is not readable.'); } $res = $this->cls('Import')->import($file); if (!$res) { WP_CLI::error('Failed to parse serialized data from file.'); } WP_CLI::success('Options imported. [File] ' . $file); } /** * Import plugin options from a remote file. * * The file must be formatted as such: * option_key=option_value * One per line. * A Semicolon at the beginning of the line indicates a comment and will be skipped. * * ## OPTIONS * * <url> * : The URL to import options from. * * ## EXAMPLES * * # Import options from https://domain.com/options.txt * $ wp litespeed-option import_remote https://domain.com/options.txt * */ public function import_remote($args, $assoc_args) { $file = $args[0]; $tmp_file = download_url($file); if (is_wp_error($tmp_file)) { WP_CLI::error('Failed to download file.'); return; } $res = $this->cls('Import')->import($tmp_file); if (!$res) { WP_CLI::error('Failed to parse serialized data from file.'); } WP_CLI::success('Options imported. [File] ' . $file); } /** * Reset all options to default. * * ## EXAMPLES * * # Reset all options * $ wp litespeed-option reset * */ public function reset() { $this->cls('Import')->reset(); } } cli/crawler.cls.php 0000644 00000012225 15162130400 0010235 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Debug2; use LiteSpeed\Base; use LiteSpeed\Task; use LiteSpeed\Crawler as Crawler2; use WP_CLI; /** * Crawler */ class Crawler extends Base { private $__crawler; public function __construct() { Debug2::debug('CLI_Crawler init'); $this->__crawler = Crawler2::cls(); } /** * List all crawler * * ## OPTIONS * * ## EXAMPLES * * # List all crawlers * $ wp litespeed-crawler l * */ public function l() { $this->list(); } /** * List all crawler * * ## OPTIONS * * ## EXAMPLES * * # List all crawlers * $ wp litespeed-crawler list * */ public function list() { $crawler_list = $this->__crawler->list_crawlers(); $summary = Crawler2::get_summary(); if ($summary['curr_crawler'] >= count($crawler_list)) { $summary['curr_crawler'] = 0; } $is_running = time() - $summary['is_running'] <= 900; $CRAWLER_RUN_INTERVAL = defined('LITESPEED_CRAWLER_RUN_INTERVAL') ? LITESPEED_CRAWLER_RUN_INTERVAL : 600; // Specify time in seconds for the time between each run interval if ($CRAWLER_RUN_INTERVAL > 0) { $recurrence = ''; $hours = (int) floor($CRAWLER_RUN_INTERVAL / 3600); if ($hours) { if ($hours > 1) { $recurrence .= sprintf(__('%d hours', 'litespeed-cache'), $hours); } else { $recurrence .= sprintf(__('%d hour', 'litespeed-cache'), $hours); } } $minutes = (int) floor(($CRAWLER_RUN_INTERVAL % 3600) / 60); if ($minutes) { $recurrence .= ' '; if ($minutes > 1) { $recurrence .= sprintf(__('%d minutes', 'litespeed-cache'), $minutes); } else { $recurrence .= sprintf(__('%d minute', 'litespeed-cache'), $minutes); } } } $list = array(); foreach ($crawler_list as $i => $v) { $hit = !empty($summary['crawler_stats'][$i][Crawler2::STATUS_HIT]) ? $summary['crawler_stats'][$i][Crawler2::STATUS_HIT] : 0; $miss = !empty($summary['crawler_stats'][$i][Crawler2::STATUS_MISS]) ? $summary['crawler_stats'][$i][Crawler2::STATUS_MISS] : 0; $blacklisted = !empty($summary['crawler_stats'][$i][Crawler2::STATUS_BLACKLIST]) ? $summary['crawler_stats'][$i][Crawler2::STATUS_BLACKLIST] : 0; $blacklisted += !empty($summary['crawler_stats'][$i][Crawler2::STATUS_NOCACHE]) ? $summary['crawler_stats'][$i][Crawler2::STATUS_NOCACHE] : 0; if (isset($summary['crawler_stats'][$i][Crawler2::STATUS_WAIT])) { $waiting = $summary['crawler_stats'][$i][Crawler2::STATUS_WAIT] ?: 0; } else { $waiting = $summary['list_size'] - $hit - $miss - $blacklisted; } $analytics = 'Waiting: ' . $waiting; $analytics .= ' Hit: ' . $hit; $analytics .= ' Miss: ' . $miss; $analytics .= ' Blocked: ' . $blacklisted; $running = ''; if ($i == $summary['curr_crawler']) { $running = 'Pos: ' . ($summary['last_pos'] + 1); if ($is_running) { $running .= '(Running)'; } } $status = $this->__crawler->is_active($i) ? '✅' : '❌'; $list[] = array( 'ID' => $i + 1, 'Name' => wp_strip_all_tags($v['title']), 'Frequency' => $recurrence, 'Status' => $status, 'Analytics' => $analytics, 'Running' => $running, ); } WP_CLI\Utils\format_items('table', $list, array('ID', 'Name', 'Frequency', 'Status', 'Analytics', 'Running')); } /** * Enable one crawler * * ## OPTIONS * * ## EXAMPLES * * # Turn on 2nd crawler * $ wp litespeed-crawler enable 2 * */ public function enable($args) { $id = $args[0] - 1; if ($this->__crawler->is_active($id)) { WP_CLI::error('ID #' . $id . ' had been enabled'); return; } $this->__crawler->toggle_activeness($id); WP_CLI::success('Enabled crawler #' . $id); } /** * Disable one crawler * * ## OPTIONS * * ## EXAMPLES * * # Turn off 1st crawler * $ wp litespeed-crawler disable 1 * */ public function disable($args) { $id = $args[0] - 1; if (!$this->__crawler->is_active($id)) { WP_CLI::error('ID #' . $id . ' has been disabled'); return; } $this->__crawler->toggle_activeness($id); WP_CLI::success('Disabled crawler #' . $id); } /** * Run crawling * * ## OPTIONS * * ## EXAMPLES * * # Start crawling * $ wp litespeed-crawler r * */ public function r() { $this->run(); } /** * Run crawling * * ## OPTIONS * * ## EXAMPLES * * # Start crawling * $ wp litespeed-crawler run * */ public function run() { self::debug('⚠️⚠️⚠️ Forced take over lane (CLI)'); $this->__crawler->Release_lane(); Task::async_call('crawler'); $summary = Crawler2::get_summary(); WP_CLI::success('Start crawling. Current crawler #' . ($summary['curr_crawler'] + 1) . ' [position] ' . $summary['last_pos'] . ' [total] ' . $summary['list_size']); } /** * Reset position * * ## OPTIONS * * ## EXAMPLES * * # Reset crawler position * $ wp litespeed-crawler reset * */ public function reset() { $this->__crawler->reset_pos(); $summary = Crawler2::get_summary(); WP_CLI::success('Reset position. Current crawler #' . ($summary['curr_crawler'] + 1) . ' [position] ' . $summary['last_pos'] . ' [total] ' . $summary['list_size']); } } cli/presets.cls.php 0000644 00000002660 15162130402 0010267 0 ustar 00 <?php namespace LiteSpeed\CLI; defined('WPINC') || exit(); use LiteSpeed\Debug2; use LiteSpeed\Preset; use WP_CLI; /** * Presets CLI */ class Presets { private $__preset; public function __construct() { Debug2::debug('CLI_Presets init'); $this->__preset = Preset::cls(); } /** * Applies a standard preset's settings. * * ## OPTIONS * * ## EXAMPLES * * # Apply the preset called "basic" * $ wp litespeed-presets apply basic * */ public function apply($args) { $preset = $args[0]; if (!isset($preset)) { WP_CLI::error('Please specify a preset to apply.'); return; } return $this->__preset->apply($preset); } /** * Returns sorted backup names. * * ## OPTIONS * * ## EXAMPLES * * # Get all backups * $ wp litespeed-presets get_backups * */ public function get_backups() { $backups = $this->__preset->get_backups(); foreach ($backups as $backup) { WP_CLI::line($backup); } } /** * Restores settings from the backup file with the given timestamp, then deletes the file. * * ## OPTIONS * * ## EXAMPLES * * # Restore the backup with the timestamp 1667485245 * $ wp litespeed-presets restore 1667485245 * */ public function restore($args) { $timestamp = $args[0]; if (!isset($timestamp)) { WP_CLI::error('Please specify a timestamp to restore.'); return; } return $this->__preset->restore($timestamp); } } src/rest.cls.php 0000644 00000016715 15162130403 0007606 0 ustar 00 <?php /** * The REST related class. * * @since 2.9.4 */ namespace LiteSpeed; defined('WPINC') || exit(); class REST extends Root { const LOG_TAG = '☎️'; private $_internal_rest_status = false; /** * Confructor of ESI * * @since 2.9.4 */ public function __construct() { // Hook to internal REST call add_filter('rest_request_before_callbacks', array($this, 'set_internal_rest_on')); add_filter('rest_request_after_callbacks', array($this, 'set_internal_rest_off')); add_action('rest_api_init', array($this, 'rest_api_init')); } /** * Register REST hooks * * @since 3.0 * @access public */ public function rest_api_init() { // Activate or deactivate a specific crawler callback register_rest_route('litespeed/v1', '/toggle_crawler_state', array( 'methods' => 'POST', 'callback' => array($this, 'toggle_crawler_state'), 'permission_callback' => function () { return current_user_can('manage_network_options') || current_user_can('manage_options'); }, )); register_rest_route('litespeed/v1', '/tool/check_ip', array( 'methods' => 'GET', 'callback' => array($this, 'check_ip'), 'permission_callback' => function () { return current_user_can('manage_network_options') || current_user_can('manage_options'); }, )); // IP callback validate register_rest_route('litespeed/v3', '/ip_validate', array( 'methods' => 'POST', 'callback' => array($this, 'ip_validate'), 'permission_callback' => array($this, 'is_from_cloud'), )); ## 1.2. WP REST Dryrun Callback register_rest_route('litespeed/v3', '/wp_rest_echo', array( 'methods' => 'POST', 'callback' => array($this, 'wp_rest_echo'), 'permission_callback' => array($this, 'is_from_cloud'), )); register_rest_route('litespeed/v3', '/ping', array( 'methods' => 'POST', 'callback' => array($this, 'ping'), 'permission_callback' => array($this, 'is_from_cloud'), )); // CDN setup callback notification register_rest_route('litespeed/v3', '/cdn_status', array( 'methods' => 'POST', 'callback' => array($this, 'cdn_status'), 'permission_callback' => array($this, 'is_from_cloud'), )); // Image optm notify_img // Need validation register_rest_route('litespeed/v1', '/notify_img', array( 'methods' => 'POST', 'callback' => array($this, 'notify_img'), 'permission_callback' => array($this, 'is_from_cloud'), )); register_rest_route('litespeed/v1', '/notify_ccss', array( 'methods' => 'POST', 'callback' => array($this, 'notify_ccss'), 'permission_callback' => array($this, 'is_from_cloud'), )); register_rest_route('litespeed/v1', '/notify_ucss', array( 'methods' => 'POST', 'callback' => array($this, 'notify_ucss'), 'permission_callback' => array($this, 'is_from_cloud'), )); register_rest_route('litespeed/v1', '/notify_vpi', array( 'methods' => 'POST', 'callback' => array($this, 'notify_vpi'), 'permission_callback' => array($this, 'is_from_cloud'), )); register_rest_route('litespeed/v3', '/err_domains', array( 'methods' => 'POST', 'callback' => array($this, 'err_domains'), 'permission_callback' => array($this, 'is_from_cloud'), )); // Image optm check_img // Need validation register_rest_route('litespeed/v1', '/check_img', array( 'methods' => 'POST', 'callback' => array($this, 'check_img'), 'permission_callback' => array($this, 'is_from_cloud'), )); } /** * Call to freeze or melt the crawler clicked * * @since 4.3 */ public function toggle_crawler_state() { if (isset($_POST['crawler_id'])) { return $this->cls('Crawler')->toggle_activeness($_POST['crawler_id']) ? 1 : 0; } } /** * Check if the request is from cloud nodes * * @since 4.2 * @since 4.4.7 As there is always token/api key validation, ip validation is redundant */ public function is_from_cloud() { // return true; return $this->cls('Cloud')->is_from_cloud(); } /** * Ping pong * * @since 3.0.4 */ public function ping() { return $this->cls('Cloud')->ping(); } /** * Launch api call * * @since 3.0 */ public function check_ip() { return Tool::cls()->check_ip(); } /** * Launch api call * * @since 3.0 */ public function ip_validate() { return $this->cls('Cloud')->ip_validate(); } /** * Launch api call * * @since 3.0 */ public function wp_rest_echo() { return $this->cls('Cloud')->wp_rest_echo(); } /** * Endpoint for QC to notify plugin of CDN status update. * * @since 7.0 */ public function cdn_status() { return $this->cls('Cloud')->update_cdn_status(); } /** * Launch api call * * @since 3.0 */ public function notify_img() { return Img_Optm::cls()->notify_img(); } /** * @since 7.1 */ public function notify_ccss() { self::debug('notify_ccss'); return CSS::cls()->notify(); } /** * @since 5.2 */ public function notify_ucss() { self::debug('notify_ucss'); return UCSS::cls()->notify(); } /** * @since 4.7 */ public function notify_vpi() { self::debug('notify_vpi'); return VPI::cls()->notify(); } /** * @since 4.7 */ public function err_domains() { self::debug('err_domains'); return $this->cls('Cloud')->rest_err_domains(); } /** * Launch api call * * @since 3.0 */ public function check_img() { return Img_Optm::cls()->check_img(); } /** * Return error * * @since 5.7.0.1 */ public static function err($code) { return array('_res' => 'err', '_msg' => $code); } /** * Set internal REST tag to ON * * @since 2.9.4 * @access public */ public function set_internal_rest_on($not_used = null) { $this->_internal_rest_status = true; Debug2::debug2('[REST] ✅ Internal REST ON [filter] rest_request_before_callbacks'); return $not_used; } /** * Set internal REST tag to OFF * * @since 2.9.4 * @access public */ public function set_internal_rest_off($not_used = null) { $this->_internal_rest_status = false; Debug2::debug2('[REST] ❎ Internal REST OFF [filter] rest_request_after_callbacks'); return $not_used; } /** * Get internal REST tag * * @since 2.9.4 * @access public */ public function is_internal_rest() { return $this->_internal_rest_status; } /** * Check if an URL or current page is REST req or not * * @since 2.9.3 * @since 2.9.4 Moved here from Utility, dropped static * @access public */ public function is_rest($url = false) { // For WP 4.4.0- compatibility if (!function_exists('rest_get_url_prefix')) { return defined('REST_REQUEST') && REST_REQUEST; } $prefix = rest_get_url_prefix(); // Case #1: After WP_REST_Request initialisation if (defined('REST_REQUEST') && REST_REQUEST) { return true; } // Case #2: Support "plain" permalink settings if (isset($_GET['rest_route']) && strpos(trim($_GET['rest_route'], '\\/'), $prefix, 0) === 0) { return true; } if (!$url) { return false; } // Case #3: URL Path begins with wp-json/ (REST prefix) Safe for subfolder installation $rest_url = wp_parse_url(site_url($prefix)); $current_url = wp_parse_url($url); // Debug2::debug( '[Util] is_rest check [base] ', $rest_url ); // Debug2::debug( '[Util] is_rest check [curr] ', $current_url ); // Debug2::debug( '[Util] is_rest check [curr2] ', wp_parse_url( add_query_arg( array( ) ) ) ); if ($current_url !== false && !empty($current_url['path']) && $rest_url !== false && !empty($rest_url['path'])) { return strpos($current_url['path'], $rest_url['path']) === 0; } return false; } } src/optimize.cls.php 0000644 00000111727 15162130405 0010472 0 ustar 00 <?php /** * The optimize class. * * @since 1.2.2 */ namespace LiteSpeed; defined('WPINC') || exit(); class Optimize extends Base { const LIB_FILE_CSS_ASYNC = 'assets/js/css_async.min.js'; const LIB_FILE_WEBFONTLOADER = 'assets/js/webfontloader.min.js'; const LIB_FILE_JS_DELAY = 'assets/js/js_delay.min.js'; const ITEM_TIMESTAMP_PURGE_CSS = 'timestamp_purge_css'; private $content; private $content_ori; private $cfg_css_min; private $cfg_css_comb; private $cfg_js_min; private $cfg_js_comb; private $cfg_css_async; private $cfg_js_delay_inc = array(); private $cfg_js_defer; private $cfg_js_defer_exc = false; private $cfg_ggfonts_async; private $_conf_css_font_display; private $cfg_ggfonts_rm; private $dns_prefetch; private $dns_preconnect; private $_ggfonts_urls = array(); private $_ccss; private $_ucss = false; private $__optimizer; private $html_foot = ''; // The html info append to <body> private $html_head = ''; // The html info prepend to <body> private static $_var_i = 0; private $_var_preserve_js = array(); private $_request_url; /** * Constructor * @since 4.0 */ public function __construct() { Debug2::debug('[Optm] init'); $this->__optimizer = $this->cls('Optimizer'); } /** * Init optimizer * * @since 3.0 * @access protected */ public function init() { $this->cfg_css_async = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_ASYNC); if ($this->cfg_css_async) { if (!$this->cls('Cloud')->activated()) { Debug2::debug('[Optm] ❌ CCSS set to OFF due to QC not activated'); $this->cfg_css_async = false; } if ((defined('LITESPEED_GUEST_OPTM') || ($this->conf(self::O_OPTM_UCSS) && $this->conf(self::O_OPTM_CSS_COMB))) && $this->conf(self::O_OPTM_UCSS_INLINE)) { Debug2::debug('[Optm] ⚠️ CCSS set to OFF due to UCSS Inline'); $this->cfg_css_async = false; } } $this->cfg_js_defer = $this->conf(self::O_OPTM_JS_DEFER); if (defined('LITESPEED_GUEST_OPTM')) { $this->cfg_js_defer = 2; } if ($this->cfg_js_defer == 2) { add_filter( 'litespeed_optm_cssjs', function ($con, $file_type) { if ($file_type == 'js') { $con = str_replace('DOMContentLoaded', 'DOMContentLiteSpeedLoaded', $con); // $con = str_replace( 'addEventListener("load"', 'addEventListener("litespeedLoad"', $con ); } return $con; }, 20, 2 ); } // To remove emoji from WP if ($this->conf(self::O_OPTM_EMOJI_RM)) { $this->_emoji_rm(); } if ($this->conf(self::O_OPTM_QS_RM)) { add_filter('style_loader_src', array($this, 'remove_query_strings'), 999); add_filter('script_loader_src', array($this, 'remove_query_strings'), 999); } // GM JS exclude @since 4.1 if (defined('LITESPEED_GUEST_OPTM')) { $this->cfg_js_defer_exc = apply_filters('litespeed_optm_gm_js_exc', $this->conf(self::O_OPTM_GM_JS_EXC)); } else { /** * Exclude js from deferred setting * @since 1.5 */ if ($this->cfg_js_defer) { add_filter('litespeed_optm_js_defer_exc', array($this->cls('Data'), 'load_js_defer_exc')); $this->cfg_js_defer_exc = apply_filters('litespeed_optm_js_defer_exc', $this->conf(self::O_OPTM_JS_DEFER_EXC)); $this->cfg_js_delay_inc = apply_filters('litespeed_optm_js_delay_inc', $this->conf(self::O_OPTM_JS_DELAY_INC)); } } /** * Add vary filter for Role Excludes * @since 1.6 */ add_filter('litespeed_vary', array($this, 'vary_add_role_exclude')); /** * Prefetch DNS * @since 1.7.1 */ $this->_dns_prefetch_init(); /** * Preconnect * @since 5.6.1 */ $this->_dns_preconnect_init(); add_filter('litespeed_buffer_finalize', array($this, 'finalize'), 20); } /** * Exclude role from optimization filter * * @since 1.6 * @access public */ public function vary_add_role_exclude($vary) { if ($this->cls('Conf')->in_optm_exc_roles()) { $vary['role_exclude_optm'] = 1; } return $vary; } /** * Remove emoji from WP * * @since 1.4 * @since 2.9.8 Changed to private * @access private */ private function _emoji_rm() { remove_action('wp_head', 'print_emoji_detection_script', 7); remove_action('admin_print_scripts', 'print_emoji_detection_script'); remove_filter('the_content_feed', 'wp_staticize_emoji'); remove_filter('comment_text_rss', 'wp_staticize_emoji'); /** * Added for better result * @since 1.6.2.1 */ remove_action('wp_print_styles', 'print_emoji_styles'); remove_action('admin_print_styles', 'print_emoji_styles'); remove_filter('wp_mail', 'wp_staticize_emoji_for_email'); } /** * Delete file-based cache folder * * @since 2.1 * @access public */ public function rm_cache_folder($subsite_id = false) { if ($subsite_id) { file_exists(LITESPEED_STATIC_DIR . '/css/' . $subsite_id) && File::rrmdir(LITESPEED_STATIC_DIR . '/css/' . $subsite_id); file_exists(LITESPEED_STATIC_DIR . '/js/' . $subsite_id) && File::rrmdir(LITESPEED_STATIC_DIR . '/js/' . $subsite_id); return; } file_exists(LITESPEED_STATIC_DIR . '/css') && File::rrmdir(LITESPEED_STATIC_DIR . '/css'); file_exists(LITESPEED_STATIC_DIR . '/js') && File::rrmdir(LITESPEED_STATIC_DIR . '/js'); } /** * Remove QS * * @since 1.3 * @access public */ public function remove_query_strings($src) { if (strpos($src, '_litespeed_rm_qs=0') || strpos($src, '/recaptcha')) { return $src; } if (!Utility::is_internal_file($src)) { return $src; } if (strpos($src, '.js?') !== false || strpos($src, '.css?') !== false) { $src = preg_replace('/\?.*/', '', $src); } return $src; } /** * Run optimize process * NOTE: As this is after cache finalized, can NOT set any cache control anymore * * @since 1.2.2 * @access public * @return string The content that is after optimization */ public function finalize($content) { if (defined('LITESPEED_NO_PAGEOPTM')) { Debug2::debug2('[Optm] bypass: NO_PAGEOPTM const'); return $content; } if (!defined('LITESPEED_IS_HTML')) { Debug2::debug('[Optm] bypass: Not frontend HTML type'); return $content; } if (!defined('LITESPEED_GUEST_OPTM')) { if (!Control::is_cacheable()) { Debug2::debug('[Optm] bypass: Not cacheable'); return $content; } // Check if hit URI excludes add_filter('litespeed_optm_uri_exc', array($this->cls('Data'), 'load_optm_uri_exc')); $excludes = apply_filters('litespeed_optm_uri_exc', $this->conf(self::O_OPTM_EXC)); $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes); if ($result) { Debug2::debug('[Optm] bypass: hit URI Excludes setting: ' . $result); return $content; } } Debug2::debug('[Optm] start'); $this->content_ori = $this->content = $content; $this->_optimize(); return $this->content; } /** * Optimize css src * * @since 1.2.2 * @access private */ private function _optimize() { global $wp; $this->_request_url = get_permalink(); // Backup, in case get_permalink() fails. if (!$this->_request_url) { $this->_request_url = home_url($wp->request); } $this->cfg_css_min = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_MIN); $this->cfg_css_comb = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_COMB); $this->cfg_js_min = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_JS_MIN); $this->cfg_js_comb = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_JS_COMB); $this->cfg_ggfonts_rm = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_GGFONTS_RM); $this->cfg_ggfonts_async = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_OPTM_GGFONTS_ASYNC); // forced rm already $this->_conf_css_font_display = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_OPTM_CSS_FONT_DISPLAY); if (!$this->cls('Router')->can_optm()) { Debug2::debug('[Optm] bypass: admin/feed/preview'); return; } if ($this->cfg_css_async) { $this->_ccss = $this->cls('CSS')->prepare_ccss(); if (!$this->_ccss) { Debug2::debug('[Optm] ❌ CCSS set to OFF due to CCSS not generated yet'); $this->cfg_css_async = false; } elseif (strpos($this->_ccss, '<style id="litespeed-ccss" data-error') === 0) { Debug2::debug('[Optm] ❌ CCSS set to OFF due to CCSS failed to generate'); $this->cfg_css_async = false; } } do_action('litespeed_optm'); // Parse css from content $src_list = false; if ($this->cfg_css_min || $this->cfg_css_comb || $this->cfg_ggfonts_rm || $this->cfg_css_async || $this->cfg_ggfonts_async || $this->_conf_css_font_display) { add_filter('litespeed_optimize_css_excludes', array($this->cls('Data'), 'load_css_exc')); list($src_list, $html_list) = $this->_parse_css(); } // css optimizer if ($this->cfg_css_min || $this->cfg_css_comb) { if ($src_list) { // IF combine if ($this->cfg_css_comb) { // Check if has inline UCSS enabled or not if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_UCSS)) && $this->conf(self::O_OPTM_UCSS_INLINE)) { $filename = $this->cls('UCSS')->load($this->_request_url, true); if ($filename) { $filepath_prefix = $this->_build_filepath_prefix('ucss'); $this->_ucss = File::read(LITESPEED_STATIC_DIR . $filepath_prefix . $filename); // Drop all css $this->content = str_replace($html_list, '', $this->content); } } if (!$this->_ucss) { $url = $this->_build_hash_url($src_list); if ($url) { // Handle css async load if ($this->cfg_css_async) { $this->html_head .= '<link rel="preload" data-asynced="1" data-optimized="2" as="style" onload="this.onload=null;this.rel=\'stylesheet\'" href="' . Str::trim_quotes($url) . '" />'; // todo: How to use " in attr wrapper " } else { $this->html_head .= '<link data-optimized="2" rel="stylesheet" href="' . Str::trim_quotes($url) . '" />'; // use 2 as combined } // Move all css to top $this->content = str_replace($html_list, '', $this->content); } } } // Only minify elseif ($this->cfg_css_min) { // will handle async css load inside $this->_src_queue_handler($src_list, $html_list); } // Only HTTP2 push else { foreach ($src_list as $src_info) { if (!empty($src_info['inl'])) { continue; } } } } } // Handle css lazy load if not handled async loaded yet if ($this->cfg_css_async && !$this->cfg_css_min && !$this->cfg_css_comb) { // async html $html_list_async = $this->_async_css_list($html_list, $src_list); // Replace async css $this->content = str_replace($html_list, $html_list_async, $this->content); } // Parse js from buffer as needed $src_list = false; if ($this->cfg_js_min || $this->cfg_js_comb || $this->cfg_js_defer || $this->cfg_js_delay_inc) { add_filter('litespeed_optimize_js_excludes', array($this->cls('Data'), 'load_js_exc')); list($src_list, $html_list) = $this->_parse_js(); } // js optimizer if ($src_list) { // IF combine if ($this->cfg_js_comb) { $url = $this->_build_hash_url($src_list, 'js'); if ($url) { $this->html_foot .= $this->_build_js_tag($url); // Will move all JS to bottom combined one $this->content = str_replace($html_list, '', $this->content); } } // Only minify elseif ($this->cfg_js_min) { // Will handle js defer inside $this->_src_queue_handler($src_list, $html_list, 'js'); } // Only HTTP2 push and Defer else { foreach ($src_list as $k => $src_info) { // Inline JS if (!empty($src_info['inl'])) { if ($this->cfg_js_defer) { $attrs = !empty($src_info['attrs']) ? $src_info['attrs'] : ''; $deferred = $this->_js_inline_defer($src_info['src'], $attrs); if ($deferred) { $this->content = str_replace($html_list[$k], $deferred, $this->content); } } } // JS files else { if ($this->cfg_js_defer) { $deferred = $this->_js_defer($html_list[$k], $src_info['src']); if ($deferred) { $this->content = str_replace($html_list[$k], $deferred, $this->content); } } elseif ($this->cfg_js_delay_inc) { $deferred = $this->_js_delay($html_list[$k], $src_info['src']); if ($deferred) { $this->content = str_replace($html_list[$k], $deferred, $this->content); } } } } } } // Append JS inline var for preserved ESI // Shouldn't give any optm (defer/delay) @since 4.4 if ($this->_var_preserve_js) { $this->html_head .= '<script>var ' . implode(',', $this->_var_preserve_js) . ';</script>'; Debug2::debug2('[Optm] Inline JS defer vars', $this->_var_preserve_js); } // Append async compatibility lib to head if ($this->cfg_css_async) { // Inline css async lib if ($this->conf(self::O_OPTM_CSS_ASYNC_INLINE)) { $this->html_head .= $this->_build_js_inline(File::read(LSCWP_DIR . self::LIB_FILE_CSS_ASYNC), true); } else { $css_async_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_CSS_ASYNC; $this->html_head .= $this->_build_js_tag($css_async_lib_url, 'litespeed-css-async-lib'); // Don't exclude it from defer for now } } /** * Handle google fonts async * This will result in a JS snippet in head, so need to put it in the end to avoid being replaced by JS parser */ $this->_async_ggfonts(); /** * Font display optm * @since 3.0 */ $this->_font_optm(); // Inject JS Delay lib $this->_maybe_js_delay(); /** * HTML Lazyload */ if ($this->conf(self::O_OPTM_HTML_LAZY)) { $this->html_head = $this->cls('CSS')->prepare_html_lazy() . $this->html_head; } // Maybe prepend inline UCSS if ($this->_ucss) { $this->html_head = '<style id="litespeed-ucss">' . $this->_ucss . '</style>' . $this->html_head; } // Check if there is any critical css rules setting if ($this->cfg_css_async && $this->_ccss) { $this->html_head = $this->_ccss . $this->html_head; } // Replace html head part $this->html_head = apply_filters('litespeed_optm_html_head', $this->html_head); if ($this->html_head) { if (apply_filters('litespeed_optm_html_after_head', false)) { $this->content = str_replace('</head>', $this->html_head . '</head>', $this->content); } else { // Put header content to be after charset if (strpos($this->content, '<meta charset') !== false) { $this->content = preg_replace('#<meta charset([^>]*)>#isU', '<meta charset$1>' . $this->html_head, $this->content, 1); } else { $this->content = preg_replace('#<head([^>]*)>#isU', '<head$1>' . $this->html_head, $this->content, 1); } } } // Replace html foot part $this->html_foot = apply_filters('litespeed_optm_html_foot', $this->html_foot); if ($this->html_foot) { $this->content = str_replace('</body>', $this->html_foot . '</body>', $this->content); } // Drop noscript if enabled if ($this->conf(self::O_OPTM_NOSCRIPT_RM)) { // $this->content = preg_replace( '#<noscript>.*</noscript>#isU', '', $this->content ); } // HTML minify if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_HTML_MIN)) { $this->content = $this->__optimizer->html_min($this->content); } } /** * Build a full JS tag * * @since 4.0 */ private function _build_js_tag($src) { if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) { return '<script data-optimized="1" type="litespeed/javascript" data-src="' . Str::trim_quotes($src) . '"></script>'; } if ($this->cfg_js_defer) { return '<script data-optimized="1" src="' . Str::trim_quotes($src) . '" defer></script>'; } return '<script data-optimized="1" src="' . Str::trim_quotes($src) . '"></script>'; } /** * Build a full inline JS snippet * * @since 4.0 */ private function _build_js_inline($script, $minified = false) { if ($this->cfg_js_defer) { $deferred = $this->_js_inline_defer($script, false, $minified); if ($deferred) { return $deferred; } } return '<script>' . $script . '</script>'; } /** * Load JS delay lib * * @since 4.0 */ private function _maybe_js_delay() { if ($this->cfg_js_defer !== 2 && !$this->cfg_js_delay_inc) { return; } $this->html_foot .= '<script>' . File::read(LSCWP_DIR . self::LIB_FILE_JS_DELAY) . '</script>'; } /** * Google font async * * @since 2.7.3 * @access private */ private function _async_ggfonts() { if (!$this->cfg_ggfonts_async || !$this->_ggfonts_urls) { return; } Debug2::debug2('[Optm] google fonts async found: ', $this->_ggfonts_urls); $html = '<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />'; /** * Append fonts * * Could be multiple fonts * * <link rel='stylesheet' href='//fonts.googleapis.com/css?family=Open+Sans%3A400%2C600%2C700%2C800%2C300&ver=4.9.8' type='text/css' media='all' /> * <link rel='stylesheet' href='//fonts.googleapis.com/css?family=PT+Sans%3A400%2C700%7CPT+Sans+Narrow%3A400%7CMontserrat%3A600&subset=latin&ver=4.9.8' type='text/css' media='all' /> * -> family: PT Sans:400,700|PT Sans Narrow:400|Montserrat:600 * <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,300italic,400italic,600,700,900&subset=latin%2Clatin-ext' /> */ $script = 'WebFontConfig={google:{families:['; $families = array(); foreach ($this->_ggfonts_urls as $v) { $qs = wp_specialchars_decode($v); $qs = urldecode($qs); $qs = parse_url($qs, PHP_URL_QUERY); parse_str($qs, $qs); if (empty($qs['family'])) { Debug2::debug('[Optm] ERR ggfonts failed to find family: ' . $v); continue; } $subset = empty($qs['subset']) ? '' : ':' . $qs['subset']; foreach (array_filter(explode('|', $qs['family'])) as $v2) { $families[] = Str::trim_quotes($v2 . $subset); } } $script .= '"' . implode('","', $families) . ($this->_conf_css_font_display ? '&display=swap' : '') . '"'; $script .= ']}};'; // if webfontloader lib was loaded before WebFontConfig variable, call WebFont.load $script .= 'if ( typeof WebFont === "object" && typeof WebFont.load === "function" ) { WebFont.load( WebFontConfig ); }'; $html .= $this->_build_js_inline($script); // https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js $webfont_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_WEBFONTLOADER; // default async, if js defer set use defer $html .= $this->_build_js_tag($webfont_lib_url); // Put this in the very beginning for preconnect $this->html_head = $html . $this->html_head; } /** * Font optm * * @since 3.0 * @access private */ private function _font_optm() { if (!$this->_conf_css_font_display || !$this->_ggfonts_urls) { return; } Debug2::debug2('[Optm] google fonts optm ', $this->_ggfonts_urls); foreach ($this->_ggfonts_urls as $v) { if (strpos($v, 'display=')) { continue; } $this->html_head = str_replace($v, $v . '&display=swap', $this->html_head); $this->html_foot = str_replace($v, $v . '&display=swap', $this->html_foot); $this->content = str_replace($v, $v . '&display=swap', $this->content); } } /** * Prefetch DNS * * @since 1.7.1 * @access private */ private function _dns_prefetch_init() { // Widely enable link DNS prefetch if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_DNS_PREFETCH_CTRL)) { @header('X-DNS-Prefetch-Control: on'); } $this->dns_prefetch = $this->conf(self::O_OPTM_DNS_PREFETCH); if (!$this->dns_prefetch) { return; } if (function_exists('wp_resource_hints')) { add_filter('wp_resource_hints', array($this, 'dns_prefetch_filter'), 10, 2); } else { add_action('litespeed_optm', array($this, 'dns_prefetch_output')); } } /** * Preconnect init * * @since 5.6.1 */ private function _dns_preconnect_init() { $this->dns_preconnect = $this->conf(self::O_OPTM_DNS_PRECONNECT); if ($this->dns_preconnect) { add_action('litespeed_optm', array($this, 'dns_preconnect_output')); } } /** * Prefetch DNS hook for WP * * @since 1.7.1 * @access public */ public function dns_prefetch_filter($urls, $relation_type) { if ($relation_type !== 'dns-prefetch') { return $urls; } foreach ($this->dns_prefetch as $v) { if ($v) { $urls[] = $v; } } return $urls; } /** * Prefetch DNS * * @since 1.7.1 * @access public */ public function dns_prefetch_output() { foreach ($this->dns_prefetch as $v) { if ($v) { $this->html_head .= '<link rel="dns-prefetch" href="' . Str::trim_quotes($v) . '" />'; } } } /** * Preconnect * * @since 5.6.1 * @access public */ public function dns_preconnect_output() { foreach ($this->dns_preconnect as $v) { if ($v) { $this->html_head .= '<link rel="preconnect" href="' . Str::trim_quotes($v) . '" />'; } } } /** * Run minify with src queue list * * @since 1.2.2 * @access private */ private function _src_queue_handler($src_list, $html_list, $file_type = 'css') { $html_list_ori = $html_list; $can_webp = $this->cls('Media')->webp_support(); $tag = $file_type == 'css' ? 'link' : 'script'; foreach ($src_list as $key => $src_info) { // Minify inline CSS/JS if (!empty($src_info['inl'])) { if ($file_type == 'css') { $code = Optimizer::minify_css($src_info['src']); $can_webp && ($code = $this->cls('Media')->replace_background_webp($code)); $snippet = str_replace($src_info['src'], $code, $html_list[$key]); } else { // Inline defer JS if ($this->cfg_js_defer) { $attrs = !empty($src_info['attrs']) ? $src_info['attrs'] : ''; $snippet = $this->_js_inline_defer($src_info['src'], $attrs) ?: $html_list[$key]; } else { $code = Optimizer::minify_js($src_info['src']); $snippet = str_replace($src_info['src'], $code, $html_list[$key]); } } } // CSS/JS files else { $url = $this->_build_single_hash_url($src_info['src'], $file_type); if ($url) { $snippet = str_replace($src_info['src'], $url, $html_list[$key]); } // Handle css async load if ($file_type == 'css' && $this->cfg_css_async) { $snippet = $this->_async_css($snippet); } // Handle js defer if ($file_type === 'js' && $this->cfg_js_defer) { $snippet = $this->_js_defer($snippet, $src_info['src']) ?: $snippet; } } $snippet = str_replace("<$tag ", '<' . $tag . ' data-optimized="1" ', $snippet); $html_list[$key] = $snippet; } $this->content = str_replace($html_list_ori, $html_list, $this->content); } /** * Build a single URL mapped filename (This will not save in DB) * @since 4.0 */ private function _build_single_hash_url($src, $file_type = 'css') { $content = $this->__optimizer->load_file($src, $file_type); $is_min = $this->__optimizer->is_min($src); $content = $this->__optimizer->optm_snippet($content, $file_type, !$is_min, $src); $filepath_prefix = $this->_build_filepath_prefix($file_type); // Save to file $filename = $filepath_prefix . md5($this->remove_query_strings($src)) . '.' . $file_type; $static_file = LITESPEED_STATIC_DIR . $filename; File::save($static_file, $content, true); // QS is required as $src may contains version info $qs_hash = substr(md5($src), -5); return LITESPEED_STATIC_URL . "$filename?ver=$qs_hash"; } /** * Generate full URL path with hash for a list of src * * @since 1.2.2 * @access private */ private function _build_hash_url($src_list, $file_type = 'css') { // $url_sensitive = $this->conf( self::O_OPTM_CSS_UNIQUE ) && $file_type == 'css'; // If need to keep unique CSS per URI // Replace preserved ESI (before generating hash) if ($file_type == 'js') { foreach ($src_list as $k => $v) { if (empty($v['inl'])) { continue; } $src_list[$k]['src'] = $this->_preserve_esi($v['src']); } } $minify = $file_type === 'css' ? $this->cfg_css_min : $this->cfg_js_min; $filename_info = $this->__optimizer->serve($this->_request_url, $file_type, $minify, $src_list); if (!$filename_info) { return false; // Failed to generate } list($filename, $type) = $filename_info; // Add cache tag in case later file deleted to avoid lscache served stale non-existed files @since 4.4.1 Tag::add(Tag::TYPE_MIN . '.' . $filename); $qs_hash = substr(md5(self::get_option(self::ITEM_TIMESTAMP_PURGE_CSS)), -5); // As filename is already related to filecon md5, no need QS anymore $filepath_prefix = $this->_build_filepath_prefix($type); return LITESPEED_STATIC_URL . $filepath_prefix . $filename . '?ver=' . $qs_hash; } /** * Parse js src * * @since 1.2.2 * @access private */ private function _parse_js() { $excludes = apply_filters('litespeed_optimize_js_excludes', $this->conf(self::O_OPTM_JS_EXC)); $combine_ext_inl = $this->conf(self::O_OPTM_JS_COMB_EXT_INL); if (!apply_filters('litespeed_optm_js_comb_ext_inl', true)) { Debug2::debug2('[Optm] js_comb_ext_inl bypassed via litespeed_optm_js_comb_ext_inl filter'); $combine_ext_inl = false; } $src_list = array(); $html_list = array(); // V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line $content = preg_replace('#<!--.*-->(?:\r\n?|\n?)#sU', '', $this->content); preg_match_all('#<script([^>]*)>(.*)</script>(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs = empty($match[1]) ? array() : Utility::parse_attr($match[1]); if (isset($attrs['data-optimized'])) { continue; } if (!empty($attrs['data-no-optimize'])) { continue; } if (!empty($attrs['data-cfasync']) && $attrs['data-cfasync'] === 'false') { continue; } if (!empty($attrs['type']) && $attrs['type'] != 'text/javascript') { continue; } // to avoid multiple replacement if (in_array($match[0], $html_list)) { continue; } $this_src_arr = array(); // JS files if (!empty($attrs['src'])) { // Exclude check $js_excluded = Utility::str_hit_array($attrs['src'], $excludes); $is_internal = Utility::is_internal_file($attrs['src']); $is_file = substr($attrs['src'], 0, 5) != 'data:'; $ext_excluded = !$combine_ext_inl && !$is_internal; if ($js_excluded || $ext_excluded || !$is_file) { // Maybe defer if ($this->cfg_js_defer) { $deferred = $this->_js_defer($match[0], $attrs['src']); if ($deferred) { $this->content = str_replace($match[0], $deferred, $this->content); } } Debug2::debug2('[Optm] _parse_js bypassed due to ' . ($js_excluded ? 'js files excluded [hit] ' . $js_excluded : 'external js')); continue; } if (strpos($attrs['src'], '/localres/') !== false) { continue; } if (strpos($attrs['src'], 'instant_click') !== false) { continue; } $this_src_arr['src'] = $attrs['src']; } // Inline JS elseif (!empty($match[2])) { // Debug2::debug( '🌹🌹🌹 ' . $match[2] . '🌹' ); // Exclude check $js_excluded = Utility::str_hit_array($match[2], $excludes); if ($js_excluded || !$combine_ext_inl) { // Maybe defer if ($this->cfg_js_defer) { $deferred = $this->_js_inline_defer($match[2], $match[1]); if ($deferred) { $this->content = str_replace($match[0], $deferred, $this->content); } } Debug2::debug2('[Optm] _parse_js bypassed due to ' . ($js_excluded ? 'js excluded [hit] ' . $js_excluded : 'inline js')); continue; } $this_src_arr['inl'] = true; $this_src_arr['src'] = $match[2]; if ($match[1]) { $this_src_arr['attrs'] = $match[1]; } } else { // Compatibility to those who changed src to data-src already Debug2::debug2('[Optm] No JS src or inline JS content'); continue; } $src_list[] = $this_src_arr; $html_list[] = $match[0]; } return array($src_list, $html_list); } /** * Inline JS defer * * @since 3.0 * @access private */ private function _js_inline_defer($con, $attrs = false, $minified = false) { if (strpos($attrs, 'data-no-defer') !== false) { Debug2::debug2('[Optm] bypass: attr api data-no-defer'); return false; } $hit = Utility::str_hit_array($con, $this->cfg_js_defer_exc); if ($hit) { Debug2::debug2('[Optm] inline js defer excluded [setting] ' . $hit); return false; } $con = trim($con); // Minify JS first if (!$minified) { // && $this->cfg_js_defer !== 2 $con = Optimizer::minify_js($con); } if (!$con) { return false; } // Check if the content contains ESI nonce or not $con = $this->_preserve_esi($con); if ($this->cfg_js_defer === 2) { // Drop type attribute from $attrs if (strpos($attrs, ' type=') !== false) { $attrs = preg_replace('# type=([\'"])([^\1]+)\1#isU', '', $attrs); } // Replace DOMContentLoaded $con = str_replace('DOMContentLoaded', 'DOMContentLiteSpeedLoaded', $con); return '<script' . $attrs . ' type="litespeed/javascript">' . $con . '</script>'; // return '<script' . $attrs . ' type="litespeed/javascript" src="data:text/javascript;base64,' . base64_encode( $con ) . '"></script>'; // return '<script' . $attrs . ' type="litespeed/javascript">' . $con . '</script>'; } return '<script' . $attrs . ' src="data:text/javascript;base64,' . base64_encode($con) . '" defer></script>'; } /** * Replace ESI to JS inline var (mainly used to avoid nonce timeout) * * @since 3.5.1 */ private function _preserve_esi($con) { $esi_placeholder_list = $this->cls('ESI')->contain_preserve_esi($con); if (!$esi_placeholder_list) { return $con; } foreach ($esi_placeholder_list as $esi_placeholder) { $js_var = '__litespeed_var_' . self::$_var_i++ . '__'; $con = str_replace($esi_placeholder, $js_var, $con); $this->_var_preserve_js[] = $js_var . '=' . $esi_placeholder; } return $con; } /** * Parse css src and remove to-be-removed css * * @since 1.2.2 * @access private * @return array All the src & related raw html list */ private function _parse_css() { $excludes = apply_filters('litespeed_optimize_css_excludes', $this->conf(self::O_OPTM_CSS_EXC)); $ucss_file_exc_inline = apply_filters('litespeed_optimize_ucss_file_exc_inline', $this->conf(self::O_OPTM_UCSS_FILE_EXC_INLINE)); $combine_ext_inl = $this->conf(self::O_OPTM_CSS_COMB_EXT_INL); if (!apply_filters('litespeed_optm_css_comb_ext_inl', true)) { Debug2::debug2('[Optm] css_comb_ext_inl bypassed via litespeed_optm_css_comb_ext_inl filter'); $combine_ext_inl = false; } $css_to_be_removed = apply_filters('litespeed_optm_css_to_be_removed', array()); $src_list = array(); $html_list = array(); // $dom = new \PHPHtmlParser\Dom; // $dom->load( $content );return $val; // $items = $dom->find( 'link' ); // V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line $content = preg_replace( array('#<!--.*-->(?:\r\n?|\n?)#sU', '#<script([^>]*)>.*</script>(?:\r\n?|\n?)#isU', '#<noscript([^>]*)>.*</noscript>(?:\r\n?|\n?)#isU'), '', $this->content ); preg_match_all('#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { // to avoid multiple replacement if (in_array($match[0], $html_list)) { continue; } if ($exclude = Utility::str_hit_array($match[0], $excludes)) { Debug2::debug2('[Optm] _parse_css bypassed exclude ' . $exclude); continue; } $this_src_arr = array(); if (strpos($match[0], '<link') === 0) { $attrs = Utility::parse_attr($match[1]); if (empty($attrs['rel']) || $attrs['rel'] !== 'stylesheet') { continue; } if (empty($attrs['href'])) { continue; } // Check if need to remove this css if (Utility::str_hit_array($attrs['href'], $css_to_be_removed)) { Debug2::debug('[Optm] rm css snippet ' . $attrs['href']); // Delete this css snippet from orig html $this->content = str_replace($match[0], '', $this->content); continue; } // Check if need to inline this css file if ($this->conf(self::O_OPTM_UCSS) && Utility::str_hit_array($attrs['href'], $ucss_file_exc_inline)) { Debug2::debug('[Optm] ucss_file_exc_inline hit ' . $attrs['href']); // Replace this css to inline from orig html $inline_script = '<style>' . $this->__optimizer->load_file($attrs['href']) . '</style>'; $this->content = str_replace($match[0], $inline_script, $this->content); continue; } // Check Google fonts hit if (strpos($attrs['href'], 'fonts.googleapis.com') !== false) { /** * For async gg fonts, will add webfont into head, hence remove it from buffer and store the matches to use later * @since 2.7.3 * @since 3.0 For font display optm, need to parse google fonts URL too */ if (!in_array($attrs['href'], $this->_ggfonts_urls)) { $this->_ggfonts_urls[] = $attrs['href']; } if ($this->cfg_ggfonts_rm || $this->cfg_ggfonts_async) { Debug2::debug('[Optm] rm css snippet [Google fonts] ' . $attrs['href']); $this->content = str_replace($match[0], '', $this->content); continue; } } if (isset($attrs['data-optimized'])) { // $this_src_arr[ 'exc' ] = true; continue; } elseif (!empty($attrs['data-no-optimize'])) { // $this_src_arr[ 'exc' ] = true; continue; } $is_internal = Utility::is_internal_file($attrs['href']); $ext_excluded = !$combine_ext_inl && !$is_internal; if ($ext_excluded) { Debug2::debug2('[Optm] Bypassed due to external link'); // Maybe defer if ($this->cfg_css_async) { $snippet = $this->_async_css($match[0]); if ($snippet != $match[0]) { $this->content = str_replace($match[0], $snippet, $this->content); } } continue; } if (!empty($attrs['media']) && $attrs['media'] !== 'all') { $this_src_arr['media'] = $attrs['media']; } $this_src_arr['src'] = $attrs['href']; } else { // Inline style if (!$combine_ext_inl) { Debug2::debug2('[Optm] Bypassed due to inline'); continue; } $attrs = Utility::parse_attr($match[2]); if (!empty($attrs['data-no-optimize'])) { continue; } if (!empty($attrs['media']) && $attrs['media'] !== 'all') { $this_src_arr['media'] = $attrs['media']; } $this_src_arr['inl'] = true; $this_src_arr['src'] = $match[3]; } $src_list[] = $this_src_arr; $html_list[] = $match[0]; } return array($src_list, $html_list); } /** * Replace css to async loaded css * * @since 1.3 * @access private */ private function _async_css_list($html_list, $src_list) { foreach ($html_list as $k => $ori) { if (!empty($src_list[$k]['inl'])) { continue; } $html_list[$k] = $this->_async_css($ori); } return $html_list; } /** * Async CSS snippet * @since 3.5 */ private function _async_css($ori) { if (strpos($ori, 'data-asynced') !== false) { Debug2::debug2('[Optm] bypass: attr data-asynced exist'); return $ori; } if (strpos($ori, 'data-no-async') !== false) { Debug2::debug2('[Optm] bypass: attr api data-no-async'); return $ori; } // async replacement $v = str_replace('stylesheet', 'preload', $ori); $v = str_replace('<link', '<link data-asynced="1" as="style" onload="this.onload=null;this.rel=\'stylesheet\'" ', $v); // Append to noscript content if (!defined('LITESPEED_GUEST_OPTM') && !$this->conf(self::O_OPTM_NOSCRIPT_RM)) { $v .= '<noscript>' . preg_replace('/ id=\'[\w-]+\' /U', ' ', $ori) . '</noscript>'; } return $v; } /** * Defer JS snippet * * @since 3.5 */ private function _js_defer($ori, $src) { if (strpos($ori, ' async') !== false) { $ori = preg_replace('# async(?:=([\'"])(?:[^\1]+)\1)?#isU', '', $ori); } if (strpos($ori, 'defer') !== false) { return false; } if (strpos($ori, 'data-deferred') !== false) { Debug2::debug2('[Optm] bypass: attr data-deferred exist'); return false; } if (strpos($ori, 'data-no-defer') !== false) { Debug2::debug2('[Optm] bypass: attr api data-no-defer'); return false; } /** * Exclude JS from setting * @since 1.5 */ if (Utility::str_hit_array($src, $this->cfg_js_defer_exc)) { Debug2::debug('[Optm] js defer exclude ' . $src); return false; } if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) { if (strpos($ori, ' type=') !== false) { $ori = preg_replace('# type=([\'"])([^\1]+)\1#isU', '', $ori); } return str_replace(' src=', ' type="litespeed/javascript" data-src=', $ori); } return str_replace('></script>', ' defer data-deferred="1"></script>', $ori); } /** * Delay JS for included setting * * @since 5.6 */ private function _js_delay($ori, $src) { if (strpos($ori, ' async') !== false) { $ori = str_replace(' async', '', $ori); } if (strpos($ori, 'defer') !== false) { return false; } if (strpos($ori, 'data-deferred') !== false) { Debug2::debug2('[Optm] bypass: attr data-deferred exist'); return false; } if (strpos($ori, 'data-no-defer') !== false) { Debug2::debug2('[Optm] bypass: attr api data-no-defer'); return false; } if (!Utility::str_hit_array($src, $this->cfg_js_delay_inc)) { return; } if (strpos($ori, ' type=') !== false) { $ori = preg_replace('# type=([\'"])([^\1]+)\1#isU', '', $ori); } return str_replace(' src=', ' type="litespeed/javascript" data-src=', $ori); } } src/media.cls.php 0000644 00000101321 15162130406 0007677 0 ustar 00 <?php /** * The class to operate media data. * * @since 1.4 * @since 1.5 Moved into /inc * @package Core * @subpackage Core/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Media extends Root { const LOG_TAG = '📺'; const LIB_FILE_IMG_LAZYLOAD = 'assets/js/lazyload.min.js'; private $content; private $_wp_upload_dir; private $_vpi_preload_list = array(); private $_format = ''; private $_sys_format = ''; /** * Init * * @since 1.4 */ public function __construct() { Debug2::debug2('[Media] init'); $this->_wp_upload_dir = wp_upload_dir(); if ($this->conf(Base::O_IMG_OPTM_WEBP)) { $this->_sys_format = 'webp'; $this->_format = 'webp'; if ($this->conf(Base::O_IMG_OPTM_WEBP) == 2) { $this->_sys_format = 'avif'; $this->_format = 'avif'; } if (!$this->_browser_support_next_gen()) { $this->_format = ''; } } } /** * Init optm features * * @since 3.0 * @access public */ public function init() { if (is_admin()) { return; } // Due to ajax call doesn't send correct accept header, have to limit webp to HTML only if ($this->webp_support()) { // Hook to srcset if (function_exists('wp_calculate_image_srcset')) { add_filter('wp_calculate_image_srcset', array($this, 'webp_srcset'), 988); } // Hook to mime icon // add_filter( 'wp_get_attachment_image_src', array( $this, 'webp_attach_img_src' ), 988 );// todo: need to check why not // add_filter( 'wp_get_attachment_url', array( $this, 'webp_url' ), 988 ); // disabled to avoid wp-admin display } if ($this->conf(Base::O_MEDIA_LAZY) && !$this->cls('Metabox')->setting('litespeed_no_image_lazy')) { self::debug('Suppress default WP lazyload'); add_filter('wp_lazy_loading_enabled', '__return_false'); } /** * Replace gravatar * @since 3.0 */ $this->cls('Avatar'); add_filter('litespeed_buffer_finalize', array($this, 'finalize'), 4); add_filter('litespeed_optm_html_head', array($this, 'finalize_head')); } /** * Add featured image to head */ public function finalize_head($content) { global $wp_query; // <link rel="preload" as="image" href="xx"> if ($this->_vpi_preload_list) { foreach ($this->_vpi_preload_list as $v) { $content .= '<link rel="preload" as="image" href="' . Str::trim_quotes($v) . '">'; } } // $featured_image_url = get_the_post_thumbnail_url(); // if ($featured_image_url) { // self::debug('Append featured image to head: ' . $featured_image_url); // if ($this->webp_support()) { // $featured_image_url = $this->replace_webp($featured_image_url) ?: $featured_image_url; // } // } // } return $content; } /** * Adjust WP default JPG quality * * @since 3.0 * @access public */ public function adjust_jpg_quality($quality) { $v = $this->conf(Base::O_IMG_OPTM_JPG_QUALITY); if ($v) { return $v; } return $quality; } /** * Register admin menu * * @since 1.6.3 * @access public */ public function after_admin_init() { /** * JPG quality control * @since 3.0 */ add_filter('jpeg_quality', array($this, 'adjust_jpg_quality')); add_filter('manage_media_columns', array($this, 'media_row_title')); add_filter('manage_media_custom_column', array($this, 'media_row_actions'), 10, 2); add_action('litespeed_media_row', array($this, 'media_row_con')); // Hook to attachment delete action add_action('delete_attachment', __CLASS__ . '::delete_attachment'); } /** * Media delete action hook * * @since 2.4.3 * @access public */ public static function delete_attachment($post_id) { // if (!Data::cls()->tb_exist('img_optm')) { // return; // } self::debug('delete_attachment [pid] ' . $post_id); Img_Optm::cls()->reset_row($post_id); } /** * Return media file info if exists * * This is for remote attachment plugins * * @since 2.9.8 * @access public */ public function info($short_file_path, $post_id) { $short_file_path = wp_normalize_path($short_file_path); $basedir = $this->_wp_upload_dir['basedir'] . '/'; if (strpos($short_file_path, $basedir) === 0) { $short_file_path = substr($short_file_path, strlen($basedir)); } $real_file = $basedir . $short_file_path; if (file_exists($real_file)) { return array( 'url' => $this->_wp_upload_dir['baseurl'] . '/' . $short_file_path, 'md5' => md5_file($real_file), 'size' => filesize($real_file), ); } /** * WP Stateless compatibility #143 https://github.com/litespeedtech/lscache_wp/issues/143 * @since 2.9.8 * @return array( 'url', 'md5', 'size' ) */ $info = apply_filters('litespeed_media_info', array(), $short_file_path, $post_id); if (!empty($info['url']) && !empty($info['md5']) && !empty($info['size'])) { return $info; } return false; } /** * Delete media file * * @since 2.9.8 * @access public */ public function del($short_file_path, $post_id) { $real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path; if (file_exists($real_file)) { unlink($real_file); self::debug('deleted ' . $real_file); } do_action('litespeed_media_del', $short_file_path, $post_id); } /** * Rename media file * * @since 2.9.8 * @access public */ public function rename($short_file_path, $short_file_path_new, $post_id) { // self::debug('renaming ' . $short_file_path . ' -> ' . $short_file_path_new); $real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path; $real_file_new = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path_new; if (file_exists($real_file)) { rename($real_file, $real_file_new); self::debug('renamed ' . $real_file . ' to ' . $real_file_new); } do_action('litespeed_media_rename', $short_file_path, $short_file_path_new, $post_id); } /** * Media Admin Menu -> Image Optimization Column Title * * @since 1.6.3 * @access public */ public function media_row_title($posts_columns) { $posts_columns['imgoptm'] = __('LiteSpeed Optimization', 'litespeed-cache'); return $posts_columns; } /** * Media Admin Menu -> Image Optimization Column * * @since 1.6.2 * @access public */ public function media_row_actions($column_name, $post_id) { if ($column_name !== 'imgoptm') { return; } do_action('litespeed_media_row', $post_id); } /** * Display image optm info * * @since 3.0 */ public function media_row_con($post_id) { $att_info = wp_get_attachment_metadata($post_id); if (empty($att_info['file'])) { return; } $short_path = $att_info['file']; $size_meta = get_post_meta($post_id, Img_Optm::DB_SIZE, true); echo '<p>'; // Original image info if ($size_meta && !empty($size_meta['ori_saved'])) { $percent = ceil(($size_meta['ori_saved'] * 100) / $size_meta['ori_total']); $extension = pathinfo($short_path, PATHINFO_EXTENSION); $bk_file = substr($short_path, 0, -strlen($extension)) . 'bk.' . $extension; $bk_optm_file = substr($short_path, 0, -strlen($extension)) . 'bk.optm.' . $extension; $link = Utility::build_url(Router::ACTION_IMG_OPTM, 'orig' . $post_id); $desc = false; $cls = ''; if ($this->info($bk_file, $post_id)) { $curr_status = __('(optm)', 'litespeed-cache'); $desc = __('Currently using optimized version of file.', 'litespeed-cache') . ' ' . __('Click to switch to original (unoptimized) version.', 'litespeed-cache'); } elseif ($this->info($bk_optm_file, $post_id)) { $cls .= ' litespeed-warning'; $curr_status = __('(non-optm)', 'litespeed-cache'); $desc = __('Currently using original (unoptimized) version of file.', 'litespeed-cache') . ' ' . __('Click to switch to optimized version.', 'litespeed-cache'); } echo GUI::pie_tiny( $percent, 24, sprintf(__('Original file reduced by %1$s (%2$s)', 'litespeed-cache'), $percent . '%', Utility::real_size($size_meta['ori_saved'])), 'left' ); echo sprintf(__('Orig saved %s', 'litespeed-cache'), $percent . '%'); if ($desc) { echo sprintf( ' <a href="%1$s" class="litespeed-media-href %2$s" data-balloon-pos="left" data-balloon-break aria-label="%3$s">%4$s</a>', $link, $cls, $desc, $curr_status ); } else { echo sprintf( ' <span class="litespeed-desc" data-balloon-pos="left" data-balloon-break aria-label="%1$s">%2$s</span>', __('Using optimized version of file. ', 'litespeed-cache') . ' ' . __('No backup of original file exists.', 'litespeed-cache'), __('(optm)', 'litespeed-cache') ); } } elseif ($size_meta && $size_meta['ori_saved'] === 0) { echo GUI::pie_tiny(0, 24, __('Congratulation! Your file was already optimized', 'litespeed-cache'), 'left'); echo sprintf(__('Orig %s', 'litespeed-cache'), '<span class="litespeed-desc">' . __('(no savings)', 'litespeed-cache') . '</span>'); } else { echo __('Orig', 'litespeed-cache') . '<span class="litespeed-left10">—</span>'; } echo '</p>'; echo '<p>'; // WebP/AVIF info if ($size_meta && $this->webp_support(true) && !empty($size_meta[$this->_sys_format . '_saved'])) { $is_avif = 'avif' === $this->_sys_format; $size_meta_saved = $size_meta[$this->_sys_format . '_saved']; $size_meta_total = $size_meta[$this->_sys_format . '_total']; $percent = ceil(($size_meta_saved * 100) / $size_meta_total); $link = Utility::build_url(Router::ACTION_IMG_OPTM, $this->_sys_format . $post_id); $desc = false; $cls = ''; if ($this->info($short_path . '.' . $this->_sys_format, $post_id)) { $curr_status = __('(optm)', 'litespeed-cache'); $desc = $is_avif ? __('Currently using optimized version of AVIF file.', 'litespeed-cache') : __('Currently using optimized version of WebP file.', 'litespeed-cache'); $desc .= ' ' . __('Click to switch to original (unoptimized) version.', 'litespeed-cache'); } elseif ($this->info($short_path . '.optm.' . $this->_sys_format, $post_id)) { $cls .= ' litespeed-warning'; $curr_status = __('(non-optm)', 'litespeed-cache'); $desc = $is_avif ? __('Currently using original (unoptimized) version of AVIF file.', 'litespeed-cache') : __('Currently using original (unoptimized) version of WebP file.', 'litespeed-cache'); $desc .= ' ' . __('Click to switch to optimized version.', 'litespeed-cache'); } echo GUI::pie_tiny( $percent, 24, sprintf( $is_avif ? __('AVIF file reduced by %1$s (%2$s)', 'litespeed-cache') : __('WebP file reduced by %1$s (%2$s)', 'litespeed-cache'), $percent . '%', Utility::real_size($size_meta_saved) ), 'left' ); echo sprintf($is_avif ? __('AVIF saved %s', 'litespeed-cache') : __('WebP saved %s', 'litespeed-cache'), $percent . '%'); if ($desc) { echo sprintf( ' <a href="%1$s" class="litespeed-media-href %2$s" data-balloon-pos="left" data-balloon-break aria-label="%3$s">%4$s</a>', $link, $cls, $desc, $curr_status ); } else { echo sprintf( ' <span class="litespeed-desc" data-balloon-pos="left" data-balloon-break aria-label="%1$s %2$s">%3$s</span>', __('Using optimized version of file. ', 'litespeed-cache'), $is_avif ? __('No backup of unoptimized AVIF file exists.', 'litespeed-cache') : __('No backup of unoptimized WebP file exists.', 'litespeed-cache'), __('(optm)', 'litespeed-cache') ); } } else { echo $this->next_gen_image_title() . '<span class="litespeed-left10">—</span>'; } echo '</p>'; // Delete row btn if ($size_meta) { echo sprintf( '<div class="row-actions"><span class="delete"><a href="%1$s" class="">%2$s</a></span></div>', Utility::build_url(Router::ACTION_IMG_OPTM, Img_Optm::TYPE_RESET_ROW, false, null, array('id' => $post_id)), __('Restore from backup', 'litespeed-cache') ); echo '</div>'; } } /** * Get wp size info * * NOTE: this is not used because it has to be after admin_init * * @since 1.6.2 * @return array $sizes Data for all currently-registered image sizes. */ public function get_image_sizes() { global $_wp_additional_image_sizes; $sizes = array(); foreach (get_intermediate_image_sizes() as $_size) { if (in_array($_size, array('thumbnail', 'medium', 'medium_large', 'large'))) { $sizes[$_size]['width'] = get_option($_size . '_size_w'); $sizes[$_size]['height'] = get_option($_size . '_size_h'); $sizes[$_size]['crop'] = (bool) get_option($_size . '_crop'); } elseif (isset($_wp_additional_image_sizes[$_size])) { $sizes[$_size] = array( 'width' => $_wp_additional_image_sizes[$_size]['width'], 'height' => $_wp_additional_image_sizes[$_size]['height'], 'crop' => $_wp_additional_image_sizes[$_size]['crop'], ); } } return $sizes; } /** * Exclude role from optimization filter * * @since 1.6.2 * @access public */ public function webp_support($sys_level = false) { if ($sys_level) { return $this->_sys_format; } return $this->_format; // User level next gen support } private function _browser_support_next_gen() { if (!empty($_SERVER['HTTP_ACCEPT'])) { if (strpos($_SERVER['HTTP_ACCEPT'], 'image/' . $this->_sys_format) !== false) { return true; } } if (!empty($_SERVER['HTTP_USER_AGENT'])) { $user_agents = array('chrome-lighthouse', 'googlebot', 'page speed'); foreach ($user_agents as $user_agent) { if (stripos($_SERVER['HTTP_USER_AGENT'], $user_agent) !== false) { return true; } } if (preg_match('/iPhone OS (\d+)_/i', $_SERVER['HTTP_USER_AGENT'], $matches)) { if ($matches[1] >= 14) { return true; } } if (preg_match('/Firefox\/(\d+)/i', $_SERVER['HTTP_USER_AGENT'], $matches)) { if ($matches[1] >= 65) { return true; } } } return false; } /** * Get next gen image title * * @since 7.0 */ public function next_gen_image_title() { $next_gen_img = 'WebP'; if ($this->conf(Base::O_IMG_OPTM_WEBP) == 2) { $next_gen_img = 'AVIF'; } return $next_gen_img; } /** * Run lazy load process * NOTE: As this is after cache finalized, can NOT set any cache control anymore * * Only do for main page. Do NOT do for esi or dynamic content. * * @since 1.4 * @access public * @return string The buffer */ public function finalize($content) { if (defined('LITESPEED_NO_LAZY')) { Debug2::debug2('[Media] bypass: NO_LAZY const'); return $content; } if (!defined('LITESPEED_IS_HTML')) { Debug2::debug2('[Media] bypass: Not frontend HTML type'); return $content; } if (!Control::is_cacheable()) { self::debug('bypass: Not cacheable'); return $content; } self::debug('finalize'); $this->content = $content; $this->_finalize(); return $this->content; } /** * Run lazyload replacement for images in buffer * * @since 1.4 * @access private */ private function _finalize() { /** * Use webp for optimized images * @since 1.6.2 */ if ($this->webp_support()) { $this->content = $this->_replace_buffer_img_webp($this->content); } /** * Check if URI is excluded * @since 3.0 */ $excludes = $this->conf(Base::O_MEDIA_LAZY_URI_EXC); if (!defined('LITESPEED_GUEST_OPTM')) { $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes); if ($result) { self::debug('bypass lazyload: hit URI Excludes setting: ' . $result); return; } } $cfg_lazy = (defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_MEDIA_LAZY)) && !$this->cls('Metabox')->setting('litespeed_no_image_lazy'); $cfg_iframe_lazy = defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_MEDIA_IFRAME_LAZY); $cfg_js_delay = defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_OPTM_JS_DEFER) == 2; $cfg_trim_noscript = defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_OPTM_NOSCRIPT_RM); $cfg_vpi = defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_MEDIA_VPI); // Preload VPI if ($cfg_vpi) { $this->_parse_img_for_preload(); } if ($cfg_lazy) { if ($cfg_vpi) { add_filter('litespeed_media_lazy_img_excludes', array($this->cls('Metabox'), 'lazy_img_excludes')); } list($src_list, $html_list, $placeholder_list) = $this->_parse_img(); $html_list_ori = $html_list; } else { self::debug('lazyload disabled'); } // image lazy load if ($cfg_lazy) { $__placeholder = Placeholder::cls(); foreach ($html_list as $k => $v) { $size = $placeholder_list[$k]; $src = $src_list[$k]; $html_list[$k] = $__placeholder->replace($v, $src, $size); } } if ($cfg_lazy) { $this->content = str_replace($html_list_ori, $html_list, $this->content); } // iframe lazy load if ($cfg_iframe_lazy) { $html_list = $this->_parse_iframe(); $html_list_ori = $html_list; foreach ($html_list as $k => $v) { $snippet = $cfg_trim_noscript ? '' : '<noscript>' . $v . '</noscript>'; if ($cfg_js_delay) { $v = str_replace(' src=', ' data-litespeed-src=', $v); } else { $v = str_replace(' src=', ' data-src=', $v); } $v = str_replace('<iframe ', '<iframe data-lazyloaded="1" src="about:blank" ', $v); $snippet = $v . $snippet; $html_list[$k] = $snippet; } $this->content = str_replace($html_list_ori, $html_list, $this->content); } // Include lazyload lib js and init lazyload if ($cfg_lazy || $cfg_iframe_lazy) { $lazy_lib = '<script data-no-optimize="1">' . File::read(LSCWP_DIR . self::LIB_FILE_IMG_LAZYLOAD) . '</script>'; $this->content = str_replace('</body>', $lazy_lib . '</body>', $this->content); } } /** * Parse img src for VPI preload only * Note: Didn't reuse the _parse_img() bcoz it contains parent cls replacement and other logic which is not needed for preload * * @since 6.2 */ private function _parse_img_for_preload() { // Load VPI setting $is_mobile = $this->_separate_mobile(); $vpi_files = $this->cls('Metabox')->setting($is_mobile ? 'litespeed_vpi_list_mobile' : 'litespeed_vpi_list'); if ($vpi_files) { $vpi_files = Utility::sanitize_lines($vpi_files, 'basename'); } if (!$vpi_files) { return; } if (!$this->content) { return; } $content = preg_replace(array('#<!--.*-->#sU', '#<noscript([^>]*)>.*</noscript>#isU'), '', $this->content); if (!$content) { return; } preg_match_all('#<img\s+([^>]+)/?>#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs = Utility::parse_attr($match[1]); if (empty($attrs['src'])) { continue; } if (strpos($attrs['src'], 'base64') !== false || substr($attrs['src'], 0, 5) === 'data:') { Debug2::debug2('[Media] lazyload bypassed base64 img'); continue; } if (strpos($attrs['src'], '{') !== false) { Debug2::debug2('[Media] image src has {} ' . $attrs['src']); continue; } // If the src contains VPI filename, then preload it if (!Utility::str_hit_array($attrs['src'], $vpi_files)) { continue; } Debug2::debug2('[Media] VPI preload found and matched: ' . $attrs['src']); $this->_vpi_preload_list[] = $attrs['src']; } } /** * Parse img src * * @since 1.4 * @access private * @return array All the src & related raw html list */ private function _parse_img() { /** * Exclude list * @since 1.5 * @since 2.7.1 Changed to array */ $excludes = apply_filters('litespeed_media_lazy_img_excludes', $this->conf(Base::O_MEDIA_LAZY_EXC)); $cls_excludes = apply_filters('litespeed_media_lazy_img_cls_excludes', $this->conf(Base::O_MEDIA_LAZY_CLS_EXC)); $cls_excludes[] = 'skip-lazy'; // https://core.trac.wordpress.org/ticket/44427 $src_list = array(); $html_list = array(); $placeholder_list = array(); $content = preg_replace( array( '#<!--.*-->#sU', '#<noscript([^>]*)>.*</noscript>#isU', '#<script([^>]*)>.*</script>#isU', // Added to remove warning of file not found when image size detection is turned ON. ), '', $this->content ); /** * Exclude parent classes * @since 3.0 */ $parent_cls_exc = apply_filters('litespeed_media_lazy_img_parent_cls_excludes', $this->conf(Base::O_MEDIA_LAZY_PARENT_CLS_EXC)); if ($parent_cls_exc) { Debug2::debug2('[Media] Lazyload Class excludes', $parent_cls_exc); foreach ($parent_cls_exc as $v) { $content = preg_replace('#<(\w+) [^>]*class=(\'|")[^\'"]*' . preg_quote($v, '#') . '[^\'"]*\2[^>]*>.*</\1>#sU', '', $content); } } preg_match_all('#<img\s+([^>]+)/?>#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs = Utility::parse_attr($match[1]); if (empty($attrs['src'])) { continue; } /** * Add src validation to bypass base64 img src * @since 1.6 */ if (strpos($attrs['src'], 'base64') !== false || substr($attrs['src'], 0, 5) === 'data:') { Debug2::debug2('[Media] lazyload bypassed base64 img'); continue; } Debug2::debug2('[Media] lazyload found: ' . $attrs['src']); if ( !empty($attrs['data-no-lazy']) || !empty($attrs['data-skip-lazy']) || !empty($attrs['data-lazyloaded']) || !empty($attrs['data-src']) || !empty($attrs['data-srcset']) ) { Debug2::debug2('[Media] bypassed'); continue; } if (!empty($attrs['class']) && ($hit = Utility::str_hit_array($attrs['class'], $cls_excludes))) { Debug2::debug2('[Media] lazyload image cls excludes [hit] ' . $hit); continue; } /** * Exclude from lazyload by setting * @since 1.5 */ if ($excludes && Utility::str_hit_array($attrs['src'], $excludes)) { Debug2::debug2('[Media] lazyload image exclude ' . $attrs['src']); continue; } /** * Excldues invalid image src from buddypress avatar crop * @see https://wordpress.org/support/topic/lazy-load-breaking-buddypress-upload-avatar-feature * @since 3.0 */ if (strpos($attrs['src'], '{') !== false) { Debug2::debug2('[Media] image src has {} ' . $attrs['src']); continue; } // to avoid multiple replacement if (in_array($match[0], $html_list)) { continue; } // Add missing dimensions if (defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_MEDIA_ADD_MISSING_SIZES)) { if (!apply_filters('litespeed_media_add_missing_sizes', true)) { Debug2::debug2('[Media] add_missing_sizes bypassed via litespeed_media_add_missing_sizes filter'); } elseif (empty($attrs['width']) || $attrs['width'] == 'auto' || empty($attrs['height']) || $attrs['height'] == 'auto') { self::debug('⚠️ Missing sizes for image [src] ' . $attrs['src']); $dimensions = $this->_detect_dimensions($attrs['src']); if ($dimensions) { $ori_width = $dimensions[0]; $ori_height = $dimensions[1]; // Calculate height based on width if (!empty($attrs['width']) && $attrs['width'] != 'auto') { $ori_height = intval(($ori_height * $attrs['width']) / $ori_width); } elseif (!empty($attrs['height']) && $attrs['height'] != 'auto') { $ori_width = intval(($ori_width * $attrs['height']) / $ori_height); } $attrs['width'] = $ori_width; $attrs['height'] = $ori_height; $new_html = preg_replace('#\s+(width|height)=(["\'])[^\2]*?\2#', '', $match[0]); $new_html = preg_replace( '#<img\s+#i', '<img width="' . Str::trim_quotes($attrs['width']) . '" height="' . Str::trim_quotes($attrs['height']) . '" ', $new_html ); self::debug('Add missing sizes ' . $attrs['width'] . 'x' . $attrs['height'] . ' to ' . $attrs['src']); $this->content = str_replace($match[0], $new_html, $this->content); $match[0] = $new_html; } } } $placeholder = false; if (!empty($attrs['width']) && $attrs['width'] != 'auto' && !empty($attrs['height']) && $attrs['height'] != 'auto') { $placeholder = intval($attrs['width']) . 'x' . intval($attrs['height']); } $src_list[] = $attrs['src']; $html_list[] = $match[0]; $placeholder_list[] = $placeholder; } return array($src_list, $html_list, $placeholder_list); } /** * Detect the original sizes * * @since 4.0 */ private function _detect_dimensions($src) { if ($pathinfo = Utility::is_internal_file($src)) { $src = $pathinfo[0]; } elseif (apply_filters('litespeed_media_ignore_remote_missing_sizes', false)) { return false; } if (substr($src, 0, 2) == '//') { $src = 'https:' . $src; } try { $sizes = getimagesize($src); } catch (\Exception $e) { return false; } if (!empty($sizes[0]) && !empty($sizes[1])) { return $sizes; } return false; } /** * Parse iframe src * * @since 1.4 * @access private * @return array All the src & related raw html list */ private function _parse_iframe() { $cls_excludes = apply_filters('litespeed_media_iframe_lazy_cls_excludes', $this->conf(Base::O_MEDIA_IFRAME_LAZY_CLS_EXC)); $cls_excludes[] = 'skip-lazy'; // https://core.trac.wordpress.org/ticket/44427 $html_list = array(); $content = preg_replace('#<!--.*-->#sU', '', $this->content); /** * Exclude parent classes * @since 3.0 */ $parent_cls_exc = apply_filters('litespeed_media_iframe_lazy_parent_cls_excludes', $this->conf(Base::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC)); if ($parent_cls_exc) { Debug2::debug2('[Media] Iframe Lazyload Class excludes', $parent_cls_exc); foreach ($parent_cls_exc as $v) { $content = preg_replace('#<(\w+) [^>]*class=(\'|")[^\'"]*' . preg_quote($v, '#') . '[^\'"]*\2[^>]*>.*</\1>#sU', '', $content); } } preg_match_all('#<iframe \s*([^>]+)></iframe>#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs = Utility::parse_attr($match[1]); if (empty($attrs['src'])) { continue; } Debug2::debug2('[Media] found iframe: ' . $attrs['src']); if (!empty($attrs['data-no-lazy']) || !empty($attrs['data-skip-lazy']) || !empty($attrs['data-lazyloaded']) || !empty($attrs['data-src'])) { Debug2::debug2('[Media] bypassed'); continue; } if (!empty($attrs['class']) && ($hit = Utility::str_hit_array($attrs['class'], $cls_excludes))) { Debug2::debug2('[Media] iframe lazyload cls excludes [hit] ' . $hit); continue; } if (apply_filters('litespeed_iframe_lazyload_exc', false, $attrs['src'])) { Debug2::debug2('[Media] bypassed by filter'); continue; } // to avoid multiple replacement if (in_array($match[0], $html_list)) { continue; } $html_list[] = $match[0]; } return $html_list; } /** * Replace image src to webp * * @since 1.6.2 * @access private */ private function _replace_buffer_img_webp($content) { /** * Added custom element & attribute support * @since 2.2.2 */ $webp_ele_to_check = $this->conf(Base::O_IMG_OPTM_WEBP_ATTR); foreach ($webp_ele_to_check as $v) { if (!$v || strpos($v, '.') === false) { Debug2::debug2('[Media] buffer_webp no . attribute ' . $v); continue; } Debug2::debug2('[Media] buffer_webp attribute ' . $v); $v = explode('.', $v); $attr = preg_quote($v[1], '#'); if ($v[0]) { $pattern = '#<' . preg_quote($v[0], '#') . '([^>]+)' . $attr . '=([\'"])(.+)\2#iU'; } else { $pattern = '# ' . $attr . '=([\'"])(.+)\1#iU'; } preg_match_all($pattern, $content, $matches); foreach ($matches[$v[0] ? 3 : 2] as $k2 => $url) { // Check if is a DATA-URI if (strpos($url, 'data:image') !== false) { continue; } if (!($url2 = $this->replace_webp($url))) { continue; } if ($v[0]) { $html_snippet = sprintf('<' . $v[0] . '%1$s' . $v[1] . '=%2$s', $matches[1][$k2], $matches[2][$k2] . $url2 . $matches[2][$k2]); } else { $html_snippet = sprintf(' ' . $v[1] . '=%1$s', $matches[1][$k2] . $url2 . $matches[1][$k2]); } $content = str_replace($matches[0][$k2], $html_snippet, $content); } } // parse srcset // todo: should apply this to cdn too if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP_REPLACE_SRCSET)) && $this->webp_support()) { $content = Utility::srcset_replace($content, array($this, 'replace_webp')); } // Replace background-image if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP)) && $this->webp_support()) { $content = $this->replace_background_webp($content); } return $content; } /** * Replace background image * * @since 4.0 */ public function replace_background_webp($content) { Debug2::debug2('[Media] Start replacing background WebP/AVIF.'); // Handle Elementors data-settings json encode background-images $content = $this->replace_urls_in_json($content); // preg_match_all( '#background-image:(\s*)url\((.*)\)#iU', $content, $matches ); preg_match_all('#url\(([^)]+)\)#iU', $content, $matches); foreach ($matches[1] as $k => $url) { // Check if is a DATA-URI if (strpos($url, 'data:image') !== false) { continue; } /** * Support quotes in src `background-image: url('src')` * @since 2.9.3 */ $url = trim($url, '\'"'); // Fix Elementors Slideshow unusual background images like style="background-image: url("https://xxxx.png");" if (strpos($url, '"') === 0 && substr($url, -6) == '"') { $url = substr($url, 6, -6); } if (!($url2 = $this->replace_webp($url))) { continue; } // $html_snippet = sprintf( 'background-image:%1$surl(%2$s)', $matches[ 1 ][ $k ], $url2 ); $html_snippet = str_replace($url, $url2, $matches[0][$k]); $content = str_replace($matches[0][$k], $html_snippet, $content); } return $content; } /** * Replace images in json data settings attributes * * @since 6.2 */ public function replace_urls_in_json($content) { $pattern = '/data-settings="(.*?)"/i'; $parent_class = $this; preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { // Check if the string contains HTML entities $isEncoded = preg_match('/"|<|>|&|'/', $match[1]); // Decode HTML entities in the JSON string $jsonString = html_entity_decode($match[1]); $jsonData = \json_decode($jsonString, true); if (json_last_error() === JSON_ERROR_NONE) { $did_webp_replace = false; array_walk_recursive($jsonData, function (&$item, $key) use (&$did_webp_replace, $parent_class) { if ($key == 'url') { $item_image = $parent_class->replace_webp($item); if ($item_image) { $item = $item_image; $did_webp_replace = true; } } }); if ($did_webp_replace) { // Re-encode the modified array back to a JSON string $newJsonString = \json_encode($jsonData); // Re-encode the JSON string to HTML entities only if it was originally encoded if ($isEncoded) { $newJsonString = htmlspecialchars($newJsonString, ENT_QUOTES | 0); // ENT_HTML401 is for PHPv5.4+ } // Replace the old JSON string in the content with the new, modified JSON string $content = str_replace($match[1], $newJsonString, $content); } } } return $content; } /** * Replace internal image src to webp or avif * * @since 1.6.2 * @access public */ public function replace_webp($url) { if (!$this->webp_support()) { self::debug2('No next generation format chosen in setting, bypassed'); return false; } Debug2::debug2('[Media] ' . $this->_sys_format . ' replacing: ' . substr($url, 0, 200)); if (substr($url, -5) === '.' . $this->_sys_format) { Debug2::debug2('[Media] already ' . $this->_sys_format); return false; } /** * WebP API hook * NOTE: As $url may contain query strings, WebP check will need to parse_url before appending .webp * @since 2.9.5 * @see #751737 - API docs for WebP generation */ if (apply_filters('litespeed_media_check_ori', Utility::is_internal_file($url), $url)) { // check if has webp file if (apply_filters('litespeed_media_check_webp', Utility::is_internal_file($url, $this->_sys_format), $url)) { $url .= '.' . $this->_sys_format; } else { Debug2::debug2('[Media] -no WebP or AVIF file, bypassed'); return false; } } else { Debug2::debug2('[Media] -no file, bypassed'); return false; } Debug2::debug2('[Media] - replaced to: ' . $url); return $url; } /** * Hook to wp_get_attachment_image_src * * @since 1.6.2 * @access public * @param array $img The URL of the attachment image src, the width, the height * @return array */ public function webp_attach_img_src($img) { Debug2::debug2('[Media] changing attach src: ' . $img[0]); if ($img && ($url = $this->replace_webp($img[0]))) { $img[0] = $url; } return $img; } /** * Try to replace img url * * @since 1.6.2 * @access public * @param string $url * @return string */ public function webp_url($url) { if ($url && ($url2 = $this->replace_webp($url))) { $url = $url2; } return $url; } /** * Hook to replace WP responsive images * * @since 1.6.2 * @access public * @param array $srcs * @return array */ public function webp_srcset($srcs) { if ($srcs) { foreach ($srcs as $w => $data) { if (!($url = $this->replace_webp($data['url']))) { continue; } $srcs[$w]['url'] = $url; } } return $srcs; } } src/doc.cls.php 0000644 00000011353 15162130407 0007373 0 ustar 00 <?php /** * The Doc class. * * @since 2.2.7 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Doc { // protected static $_instance; /** * Show option is actually ON by GM * * @since 5.5 * @access public */ public static function maybe_on_by_gm($id) { if (apply_filters('litespeed_conf', $id)) { return; } if (!apply_filters('litespeed_conf', Base::O_GUEST)) { return; } if (!apply_filters('litespeed_conf', Base::O_GUEST_OPTM)) { return; } echo '<font class="litespeed-warning">'; echo '⚠️ ' . sprintf( __('This setting is %1$s for certain qualifying requests due to %2$s!', 'litespeed-cache'), '<code>' . __('ON', 'litespeed-cache') . '</code>', Lang::title(Base::O_GUEST_OPTM) ); self::learn_more('https://docs.litespeedtech.com/lscache/lscwp/general/#guest-optimization'); echo '</font>'; } /** * Changes affect crawler list warning * * @since 4.3 * @access public */ public static function crawler_affected() { echo '<font class="litespeed-primary">'; echo '⚠️ ' . __('This setting will regenerate crawler list and clear the disabled list!', 'litespeed-cache'); echo '</font>'; } /** * Privacy policy * * @since 2.2.7 * @access public */ public static function privacy_policy() { return __( 'This site utilizes caching in order to facilitate a faster response time and better user experience. Caching potentially stores a duplicate copy of every web page that is on display on this site. All cache files are temporary, and are never accessed by any third party, except as necessary to obtain technical support from the cache plugin vendor. Cache files expire on a schedule set by the site administrator, but may easily be purged by the admin before their natural expiration, if necessary. We may use QUIC.cloud services to process & cache your data temporarily.', 'litespeed-cache' ) . sprintf( __('Please see %s for more details.', 'litespeed-cache'), '<a href="https://quic.cloud/privacy-policy/" target="_blank">https://quic.cloud/privacy-policy/</a>' ); } /** * Learn more link * * @since 2.4.2 * @access public */ public static function learn_more($url, $title = false, $self = false, $class = false, $return = false) { if (!$class) { $class = 'litespeed-learn-more'; } if (!$title) { $title = __('Learn More', 'litespeed-cache'); } $self = $self ? '' : "target='_blank'"; $txt = " <a href='$url' $self class='$class'>$title</a>"; if ($return) { return $txt; } echo $txt; } /** * One per line * * @since 3.0 * @access public */ public static function one_per_line($return = false) { $str = __('One per line.', 'litespeed-cache'); if ($return) { return $str; } echo $str; } /** * One per line * * @since 3.4 * @access public */ public static function full_or_partial_url($string_only = false) { if ($string_only) { echo __('Both full and partial strings can be used.', 'litespeed-cache'); } else { echo __('Both full URLs and partial strings can be used.', 'litespeed-cache'); } } /** * Notice to edit .htaccess * * @since 3.0 * @access public */ public static function notice_htaccess() { echo '<font class="litespeed-primary">'; echo '⚠️ ' . __('This setting will edit the .htaccess file.', 'litespeed-cache'); echo ' <a href="https://docs.litespeedtech.com/lscache/lscwp/toolbox/#edit-htaccess-tab" target="_blank" class="litespeed-learn-more">' . __('Learn More', 'litespeed-cache') . '</a>'; echo '</font>'; } /** * Notice for whitelist IPs * * @since 3.0 * @access public */ public static function notice_ips() { echo '<div class="litespeed-primary">'; echo '⚠️ ' . sprintf(__('For online services to work correctly, you must allowlist all %s server IPs.', 'litespeed-cache'), 'QUIC.cloud') . '<br/>'; echo ' ' . __('Before generating key, please verify all IPs on this list are allowlisted', 'litespeed-cache') . ': '; echo '<a href="' . Cloud::CLOUD_IPS . '" target="_blank">' . __('Current Online Server IPs', 'litespeed-cache') . '</a>'; echo '</div>'; } /** * Gentle reminder that web services run asynchronously * * @since 5.3.1 * @access public */ public static function queue_issues($return = false) { $str = '<div class="litespeed-desc">' . __('The queue is processed asynchronously. It may take time.', 'litespeed-cache') . self::learn_more('https://docs.litespeedtech.com/lscache/lscwp/troubleshoot/#quiccloud-queue-issues', false, false, false, true) . '</div>'; if ($return) { return $str; } echo $str; } } src/import.preset.cls.php 0000644 00000012670 15162130410 0011436 0 ustar 00 <?php /** * The preset class. * * @since 5.3.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class Preset extends Import { protected $_summary; const MAX_BACKUPS = 10; const TYPE_APPLY = 'apply'; const TYPE_RESTORE = 'restore'; const STANDARD_DIR = LSCWP_DIR . 'data/preset'; const BACKUP_DIR = LITESPEED_STATIC_DIR . '/auto-backup'; /** * Returns sorted backup names * * @since 5.3.0 * @access public */ public static function get_backups() { self::init_filesystem(); global $wp_filesystem; $backups = array_map( function ($path) { return self::basename($path['name']); }, $wp_filesystem->dirlist(self::BACKUP_DIR) ?: array() ); rsort($backups); return $backups; } /** * Removes extra backup files * * @since 5.3.0 * @access public */ public static function prune_backups() { $backups = self::get_backups(); global $wp_filesystem; foreach (array_slice($backups, self::MAX_BACKUPS) as $backup) { $path = self::get_backup($backup); $wp_filesystem->delete($path); Debug2::debug('[Preset] Deleted old backup from ' . $backup); } } /** * Returns a settings file's extensionless basename given its filesystem path * * @since 5.3.0 * @access public */ public static function basename($path) { return basename($path, '.data'); } /** * Returns a standard preset's path given its extensionless basename * * @since 5.3.0 * @access public */ public static function get_standard($name) { return path_join(self::STANDARD_DIR, $name . '.data'); } /** * Returns a backup's path given its extensionless basename * * @since 5.3.0 * @access public */ public static function get_backup($name) { return path_join(self::BACKUP_DIR, $name . '.data'); } /** * Initializes the global $wp_filesystem object and clears stat cache * * @since 5.3.0 */ static function init_filesystem() { require_once ABSPATH . '/wp-admin/includes/file.php'; \WP_Filesystem(); clearstatcache(); } /** * Init * * @since 5.3.0 */ public function __construct() { Debug2::debug('[Preset] Init'); $this->_summary = self::get_summary(); } /** * Applies a standard preset's settings given its extensionless basename * * @since 5.3.0 * @access public */ public function apply($preset) { $this->make_backup($preset); $path = self::get_standard($preset); $result = $this->import_file($path) ? $preset : 'error'; $this->log($result); } /** * Restores settings from the backup file with the given timestamp, then deletes the file * * @since 5.3.0 * @access public */ public function restore($timestamp) { $backups = array(); foreach (self::get_backups() as $backup) { if (preg_match('/^backup-' . $timestamp . '(-|$)/', $backup) === 1) { $backups[] = $backup; } } if (empty($backups)) { $this->log('error'); return; } $backup = $backups[0]; $path = self::get_backup($backup); if (!$this->import_file($path)) { $this->log('error'); return; } self::init_filesystem(); global $wp_filesystem; $wp_filesystem->delete($path); Debug2::debug('[Preset] Deleted most recent backup from ' . $backup); $this->log('backup'); } /** * Saves current settings as a backup file, then prunes extra backup files * * @since 5.3.0 * @access public */ public function make_backup($preset) { $backup = 'backup-' . time() . '-before-' . $preset; $data = $this->export(true); $path = self::get_backup($backup); File::save($path, $data, true); Debug2::debug('[Preset] Backup saved to ' . $backup); self::prune_backups(); } /** * Tries to import from a given settings file * * @since 5.3.0 */ function import_file($path) { $debug = function ($result, $name) { $action = $result ? 'Applied' : 'Failed to apply'; Debug2::debug('[Preset] ' . $action . ' settings from ' . $name); return $result; }; $name = self::basename($path); $contents = file_get_contents($path); if (false === $contents) { Debug2::debug('[Preset] ❌ Failed to get file contents'); return $debug(false, $name); } $parsed = array(); try { // Check if the data is v4+ if (strpos($contents, '["_version",') === 0) { $contents = explode("\n", $contents); foreach ($contents as $line) { $line = trim($line); if (empty($line)) { continue; } list($key, $value) = \json_decode($line, true); $parsed[$key] = $value; } } else { $parsed = \json_decode(base64_decode($contents), true); } } catch (\Exception $ex) { Debug2::debug('[Preset] ❌ Failed to parse serialized data'); return $debug(false, $name); } if (empty($parsed)) { Debug2::debug('[Preset] ❌ Nothing to apply'); return $debug(false, $name); } $this->cls('Conf')->update_confs($parsed); return $debug(true, $name); } /** * Updates the log * * @since 5.3.0 */ function log($preset) { $this->_summary['preset'] = $preset; $this->_summary['preset_timestamp'] = time(); self::save_summary(); } /** * Handles all request actions from main cls * * @since 5.3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_APPLY: $this->apply(!empty($_GET['preset']) ? $_GET['preset'] : false); break; case self::TYPE_RESTORE: $this->restore(!empty($_GET['timestamp']) ? $_GET['timestamp'] : false); break; default: break; } Admin::redirect(); } } src/api.cls.php 0000644 00000026116 15162130412 0007376 0 ustar 00 <?php /** * The plugin API class. * * @since 1.1.3 * @since 1.4 Moved into /inc * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class API extends Base { const VERSION = Core::VER; const TYPE_FEED = Tag::TYPE_FEED; const TYPE_FRONTPAGE = Tag::TYPE_FRONTPAGE; const TYPE_HOME = Tag::TYPE_HOME; const TYPE_PAGES = Tag::TYPE_PAGES; const TYPE_PAGES_WITH_RECENT_POSTS = Tag::TYPE_PAGES_WITH_RECENT_POSTS; const TYPE_HTTP = Tag::TYPE_HTTP; const TYPE_ARCHIVE_POSTTYPE = Tag::TYPE_ARCHIVE_POSTTYPE; const TYPE_ARCHIVE_TERM = Tag::TYPE_ARCHIVE_TERM; const TYPE_AUTHOR = Tag::TYPE_AUTHOR; const TYPE_ARCHIVE_DATE = Tag::TYPE_ARCHIVE_DATE; const TYPE_BLOG = Tag::TYPE_BLOG; const TYPE_LOGIN = Tag::TYPE_LOGIN; const TYPE_URL = Tag::TYPE_URL; const TYPE_ESI = Tag::TYPE_ESI; const PARAM_NAME = ESI::PARAM_NAME; const WIDGET_O_ESIENABLE = ESI::WIDGET_O_ESIENABLE; const WIDGET_O_TTL = ESI::WIDGET_O_TTL; /** * Instance * * @since 3.0 */ public function __construct() { } /** * Define hooks to be used in other plugins. * * The benefit to use hooks other than functions is no need to detach if LSCWP enabled and function existed or not anymore * * @since 3.0 */ public function init() { /** * Init */ // Action `litespeed_init` // @previous API::hook_init( $hook ) /** * Conf */ add_filter('litespeed_conf', array($this, 'conf')); // @previous API::config($id) // Action `litespeed_conf_append` // @previous API::conf_append( $name, $default ) add_action('litespeed_conf_multi_switch', __NAMESPACE__ . '\Base::set_multi_switch', 10, 2); // Action ``litespeed_conf_force` // @previous API::force_option( $k, $v ) /** * Cache Control Hooks */ // Action `litespeed_control_finalize` // @previous API::hook_control($tags) && action `litespeed_api_control` add_action('litespeed_control_set_private', __NAMESPACE__ . '\Control::set_private'); // @previous API::set_cache_private() add_action('litespeed_control_set_nocache', __NAMESPACE__ . '\Control::set_nocache'); // @previous API::set_nocache( $reason = false ) add_action('litespeed_control_set_cacheable', array($this, 'set_cacheable')); // Might needed if not call hook `wp` // @previous API::set_cacheable( $reason ) add_action('litespeed_control_force_cacheable', __NAMESPACE__ . '\Control::force_cacheable'); // Set cache status to force cacheable ( Will ignore most kinds of non-cacheable conditions ) // @previous API::set_force_cacheable( $reason ) add_action('litespeed_control_force_public', __NAMESPACE__ . '\Control::set_public_forced'); // Set cache to force public cache if cacheable ( Will ignore most kinds of non-cacheable conditions ) // @previous API::set_force_public( $reason ) add_filter('litespeed_control_cacheable', __NAMESPACE__ . '\Control::is_cacheable', 3); // Note: Read-Only. Directly append to this filter won't work. Call actions above to set cacheable or not // @previous API::not_cacheable() add_action('litespeed_control_set_ttl', __NAMESPACE__ . '\Control::set_custom_ttl', 10, 2); // @previous API::set_ttl( $val ) add_filter('litespeed_control_ttl', array($this, 'get_ttl'), 3); // @previous API::get_ttl() /** * Tag Hooks */ // Action `litespeed_tag_finalize` // @previous API::hook_tag( $hook ) add_action('litespeed_tag', __NAMESPACE__ . '\Tag::add'); // Shorter alias of `litespeed_tag_add` add_action('litespeed_tag_post', __NAMESPACE__ . '\Tag::add_post'); // Shorter alias of `litespeed_tag_add_post` add_action('litespeed_tag_widget', __NAMESPACE__ . '\Tag::add_widget'); // Shorter alias of `litespeed_tag_add_widget` add_action('litespeed_tag_private', __NAMESPACE__ . '\Tag::add_private'); // Shorter alias of `litespeed_tag_add_private` add_action('litespeed_tag_private_esi', __NAMESPACE__ . '\Tag::add_private_esi'); // Shorter alias of `litespeed_tag_add_private_esi` add_action('litespeed_tag_add', __NAMESPACE__ . '\Tag::add'); // @previous API::tag_add( $tag ) add_action('litespeed_tag_add_post', __NAMESPACE__ . '\Tag::add_post'); add_action('litespeed_tag_add_widget', __NAMESPACE__ . '\Tag::add_widget'); add_action('litespeed_tag_add_private', __NAMESPACE__ . '\Tag::add_private'); // @previous API::tag_add_private( $tags ) add_action('litespeed_tag_add_private_esi', __NAMESPACE__ . '\Tag::add_private_esi'); /** * Purge Hooks */ // Action `litespeed_purge_finalize` // @previous API::hook_purge($tags) add_action('litespeed_purge', __NAMESPACE__ . '\Purge::add'); // @previous API::purge($tags) add_action('litespeed_purge_all', __NAMESPACE__ . '\Purge::purge_all'); add_action('litespeed_purge_post', array($this, 'purge_post')); // @previous API::purge_post( $pid ) add_action('litespeed_purge_posttype', __NAMESPACE__ . '\Purge::purge_posttype'); add_action('litespeed_purge_url', array($this, 'purge_url')); add_action('litespeed_purge_widget', __NAMESPACE__ . '\Purge::purge_widget'); add_action('litespeed_purge_esi', __NAMESPACE__ . '\Purge::purge_esi'); add_action('litespeed_purge_private', __NAMESPACE__ . '\Purge::add_private'); // @previous API::purge_private( $tags ) add_action('litespeed_purge_private_esi', __NAMESPACE__ . '\Purge::add_private_esi'); add_action('litespeed_purge_private_all', __NAMESPACE__ . '\Purge::add_private_all'); // @previous API::purge_private_all() // Action `litespeed_api_purge_post` // Triggered when purge a post // @previous API::hook_purge_post($hook) // Action `litespeed_purged_all` // Triggered after purged all. add_action('litespeed_purge_all_object', __NAMESPACE__ . '\Purge::purge_all_object'); add_action('litespeed_purge_ucss', __NAMESPACE__ . '\Purge::purge_ucss'); /** * ESI */ // Action `litespeed_nonce` // @previous API::nonce_action( $action ) & API::nonce( $action = -1, $defence_for_html_filter = true ) // NOTE: only available after `init` hook add_filter('litespeed_esi_status', array($this, 'esi_enabled')); // Get ESI enable status // @previous API::esi_enabled() add_filter('litespeed_esi_url', array($this, 'sub_esi_block'), 10, 8); // Generate ESI block url // @previous API::esi_url( $block_id, $wrapper, $params = array(), $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_val = false ) // Filter `litespeed_widget_default_options` // Hook widget default settings value. Currently used in Woo 3rd // @previous API::hook_widget_default_options( $hook ) // Filter `litespeed_esi_params` // @previous API::hook_esi_param( $hook ) // Action `litespeed_tpl_normal` // @previous API::hook_tpl_not_esi($hook) && Action `litespeed_is_not_esi_template` // Action `litespeed_esi_load-$block` // @usage add_action( 'litespeed_esi_load-' . $block, $hook ) // @previous API::hook_tpl_esi($block, $hook) add_action('litespeed_esi_combine', __NAMESPACE__ . '\ESI::combine'); /** * Vary * * To modify default vary, There are two ways: Action `litespeed_vary_append` or Filter `litespeed_vary` */ add_action('litespeed_vary_ajax_force', __NAMESPACE__ . '\Vary::can_ajax_vary'); // API::force_vary() -> Action `litespeed_vary_ajax_force` // Force finalize vary even if its in an AJAX call // Filter `litespeed_vary_curr_cookies` to generate current in use vary, which will be used for response vary header. // Filter `litespeed_vary_cookies` to register the final vary cookies, which will be written to rewrite rule. (litespeed_vary_curr_cookies are always equal to or less than litespeed_vary_cookies) // Filter `litespeed_vary` // Previous API::hook_vary_finalize( $hook ) add_action('litespeed_vary_no', __NAMESPACE__ . '\Control::set_no_vary'); // API::set_cache_no_vary() -> Action `litespeed_vary_no` // Set cache status to no vary // add_filter( 'litespeed_is_mobile', __NAMESPACE__ . '\Control::is_mobile' ); // API::set_mobile() -> Filter `litespeed_is_mobile` /** * Cloud */ add_filter('litespeed_is_from_cloud', array($this, 'is_from_cloud')); // Check if current request is from QC (usually its to check REST access) // @see https://wordpress.org/support/topic/image-optimization-not-working-3/ /** * Media */ add_action('litespeed_media_reset', __NAMESPACE__ . '\Media::delete_attachment'); // Reset one media row /** * GUI */ // API::clean_wrapper_begin( $counter = false ) -> Filter `litespeed_clean_wrapper_begin` // Start a to-be-removed html wrapper add_filter('litespeed_clean_wrapper_begin', __NAMESPACE__ . '\GUI::clean_wrapper_begin'); // API::clean_wrapper_end( $counter = false ) -> Filter `litespeed_clean_wrapper_end` // End a to-be-removed html wrapper add_filter('litespeed_clean_wrapper_end', __NAMESPACE__ . '\GUI::clean_wrapper_end'); /** * Mist */ add_action('litespeed_debug', __NAMESPACE__ . '\Debug2::debug', 10, 2); // API::debug()-> Action `litespeed_debug` add_action('litespeed_debug2', __NAMESPACE__ . '\Debug2::debug2', 10, 2); // API::debug2()-> Action `litespeed_debug2` add_action('litespeed_disable_all', array($this, '_disable_all')); // API::disable_all( $reason ) -> Action `litespeed_disable_all` add_action('litspeed_after_admin_init', array($this, '_after_admin_init')); } /** * API for admin related * * @since 3.0 * @access public */ public function _after_admin_init() { /** * GUI */ add_action('litespeed_setting_enroll', array($this->cls('Admin_Display'), 'enroll'), 10, 4); // API::enroll( $id ) // Register a field in setting form to save add_action('litespeed_build_switch', array($this->cls('Admin_Display'), 'build_switch')); // API::build_switch( $id ) // Build a switch div html snippet // API::hook_setting_content( $hook, $priority = 10, $args = 1 ) -> Action `litespeed_settings_content` // API::hook_setting_tab( $hook, $priority = 10, $args = 1 ) -> Action `litespeed_settings_tab` } /** * Disable All (Note: Not for direct call, always use Hooks) * * @since 2.9.7.2 * @access public */ public function _disable_all($reason) { do_action('litespeed_debug', '[API] Disabled_all due to ' . $reason); !defined('LITESPEED_DISABLE_ALL') && define('LITESPEED_DISABLE_ALL', true); } /** * @since 3.0 */ public static function vary_append_commenter() { Vary::cls()->append_commenter(); } /** * Check if is from Cloud * * @since 4.2 */ public function is_from_cloud() { return $this->cls('Cloud')->is_from_cloud(); } public function purge_post($pid) { $this->cls('Purge')->purge_post($pid); } public function purge_url($url) { $this->cls('Purge')->purge_url($url); } public function set_cacheable($reason = false) { $this->cls('Control')->set_cacheable($reason); } public function esi_enabled() { return $this->cls('Router')->esi_enabled(); } public function get_ttl() { return $this->cls('Control')->get_ttl(); } public function sub_esi_block( $block_id, $wrapper, $params = array(), $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_param = array() ) { return $this->cls('ESI')->sub_esi_block($block_id, $wrapper, $params, $control, $silence, $preserved, $svar, $inline_param); } } src/health.cls.php 0000644 00000005622 15162130413 0010072 0 ustar 00 <?php /** * The page health * * * @since 3.0 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Health extends Base { const TYPE_SPEED = 'speed'; const TYPE_SCORE = 'score'; protected $_summary; /** * Init * * @since 3.0 */ public function __construct() { $this->_summary = self::get_summary(); } /** * Test latest speed * * @since 3.0 */ private function _ping($type) { $data = array('action' => $type); $json = Cloud::post(Cloud::SVC_HEALTH, $data, 600); if (empty($json['data']['before']) || empty($json['data']['after'])) { Debug2::debug('[Health] ❌ no data'); return false; } $this->_summary[$type . '.before'] = $json['data']['before']; $this->_summary[$type . '.after'] = $json['data']['after']; self::save_summary(); Debug2::debug('[Health] saved result'); } /** * Generate scores * * @since 3.0 */ public function scores() { $speed_before = $speed_after = $speed_improved = 0; if (!empty($this->_summary['speed.before']) && !empty($this->_summary['speed.after'])) { // Format loading time $speed_before = $this->_summary['speed.before'] / 1000; if ($speed_before < 0.01) { $speed_before = 0.01; } $speed_before = number_format($speed_before, 2); $speed_after = $this->_summary['speed.after'] / 1000; if ($speed_after < 0.01) { $speed_after = number_format($speed_after, 3); } else { $speed_after = number_format($speed_after, 2); } $speed_improved = (($this->_summary['speed.before'] - $this->_summary['speed.after']) * 100) / $this->_summary['speed.before']; if ($speed_improved > 99) { $speed_improved = number_format($speed_improved, 2); } else { $speed_improved = number_format($speed_improved); } } $score_before = $score_after = $score_improved = 0; if (!empty($this->_summary['score.before']) && !empty($this->_summary['score.after'])) { $score_before = $this->_summary['score.before']; $score_after = $this->_summary['score.after']; // Format Score $score_improved = (($score_after - $score_before) * 100) / $score_after; if ($score_improved > 99) { $score_improved = number_format($score_improved, 2); } else { $score_improved = number_format($score_improved); } } return array( 'speed_before' => $speed_before, 'speed_after' => $speed_after, 'speed_improved' => $speed_improved, 'score_before' => $score_before, 'score_after' => $score_after, 'score_improved' => $score_improved, ); } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_SPEED: case self::TYPE_SCORE: $this->_ping($type); break; default: break; } Admin::redirect(); } } src/metabox.cls.php 0000644 00000010322 15162130414 0010256 0 ustar 00 <?php /** * The class to operate post editor metabox settings * * @since 4.7 * @package Core * @subpackage Core/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Metabox extends Root { const LOG_TAG = '📦'; const POST_NONCE_ACTION = 'post_nonce_action'; private $_postmeta_settings; /** * Get the setting list * @since 4.7 */ public function __construct() { // Append meta box $this->_postmeta_settings = array( 'litespeed_no_cache' => __('Disable Cache', 'litespeed-cache'), 'litespeed_no_image_lazy' => __('Disable Image Lazyload', 'litespeed-cache'), 'litespeed_no_vpi' => __('Disable VPI', 'litespeed-cache'), 'litespeed_vpi_list' => __('Viewport Images', 'litespeed-cache'), 'litespeed_vpi_list_mobile' => __('Viewport Images', 'litespeed-cache') . ' - ' . __('Mobile', 'litespeed-cache'), ); } /** * Register post edit settings * @since 4.7 */ public function register_settings() { add_action('add_meta_boxes', array($this, 'add_meta_boxes')); add_action('save_post', array($this, 'save_meta_box_settings'), 15, 2); add_action('attachment_updated', array($this, 'save_meta_box_settings'), 15, 2); } /** * Register meta box * @since 4.7 */ public function add_meta_boxes($post_type) { if (apply_filters('litespeed_bypass_metabox', false, $post_type)) { return; } $post_type_obj = get_post_type_object($post_type); if (!empty($post_type_obj) && !$post_type_obj->public) { self::debug('post type public=false, bypass add_meta_boxes'); return; } add_meta_box('litespeed_meta_boxes', __('LiteSpeed Options', 'litespeed-cache'), array($this, 'meta_box_options'), $post_type, 'side', 'core'); } /** * Show meta box content * @since 4.7 */ public function meta_box_options() { require_once LSCWP_DIR . 'tpl/inc/metabox.php'; } /** * Save settings * @since 4.7 */ public function save_meta_box_settings($post_id, $post) { global $pagenow; self::debug('Maybe save post2 [post_id] ' . $post_id); if ($pagenow != 'post.php' || !$post || !is_object($post)) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (!$this->cls('Router')->verify_nonce(self::POST_NONCE_ACTION)) { return; } self::debug('Saving post [post_id] ' . $post_id); foreach ($this->_postmeta_settings as $k => $v) { $val = isset($_POST[$k]) ? $_POST[$k] : false; $this->save($post_id, $k, $val); } } /** * Load setting per post * @since 4.7 */ public function setting($conf, $post_id = false) { // Check if has metabox non-cacheable setting or not if (!$post_id) { $home_id = get_option('page_for_posts'); if (is_singular()) { $post_id = get_the_ID(); } elseif ($home_id > 0 && is_home()) { $post_id = $home_id; } } if ($post_id && ($val = get_post_meta($post_id, $conf, true))) { return $val; } return null; } /** * Save a metabox value * @since 4.7 */ public function save($post_id, $name, $val, $is_append = false) { if (strpos($name, 'litespeed_vpi_list') !== false) { $val = Utility::sanitize_lines($val, 'basename,drop_webp'); } // Load existing data if has set if ($is_append) { $existing_data = $this->setting($name, $post_id); if ($existing_data) { $existing_data = Utility::sanitize_lines($existing_data, 'basename'); $val = array_unique(array_merge($val, $existing_data)); } } if ($val) { update_post_meta($post_id, $name, $val); } else { delete_post_meta($post_id, $name); } } /** * Load exclude images per post * @since 4.7 */ public function lazy_img_excludes($list) { $is_mobile = $this->_separate_mobile(); $excludes = $this->setting($is_mobile ? 'litespeed_vpi_list_mobile' : 'litespeed_vpi_list'); if ($excludes !== null) { $excludes = Utility::sanitize_lines($excludes, 'basename'); if ($excludes) { // Check if contains `data:` (invalid result, need to clear existing result) or not if (Utility::str_hit_array('data:', $excludes)) { $this->cls('VPI')->add_to_queue(); } else { return array_merge($list, $excludes); } } return $list; } $this->cls('VPI')->add_to_queue(); return $list; } } src/admin-settings.cls.php 0000644 00000024032 15162130416 0011552 0 ustar 00 <?php /** * The admin settings handler of the plugin. * * * @since 1.1.0 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Admin_Settings extends Base { const ENROLL = '_settings-enroll'; /** * Save settings * * Both $_POST and CLI can use this way * * Import will directly call conf.cls * * @since 3.0 * @access public */ public function save($raw_data) { Debug2::debug('[Settings] saving'); if (empty($raw_data[self::ENROLL])) { exit('No fields'); } $raw_data = Admin::cleanup_text($raw_data); // Convert data to config format $the_matrix = array(); foreach (array_unique($raw_data[self::ENROLL]) as $id) { $child = false; // Drop array format if (strpos($id, '[') !== false) { if (strpos($id, self::O_CDN_MAPPING) === 0 || strpos($id, self::O_CRAWLER_COOKIES) === 0) { // CDN child | Cookie Crawler settings $child = substr($id, strpos($id, '[') + 1, strpos($id, ']') - strpos($id, '[') - 1); $id = substr($id, 0, strpos($id, '[')); // Drop ending []; Compatible with xx[0] way from CLI } else { $id = substr($id, 0, strpos($id, '[')); // Drop ending [] } } if (!array_key_exists($id, self::$_default_options)) { continue; } // Validate $child if ($id == self::O_CDN_MAPPING) { if (!in_array($child, array(self::CDN_MAPPING_URL, self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS, self::CDN_MAPPING_FILETYPE))) { continue; } } if ($id == self::O_CRAWLER_COOKIES) { if (!in_array($child, array(self::CRWL_COOKIE_NAME, self::CRWL_COOKIE_VALS))) { continue; } } $data = false; if ($child) { $data = !empty($raw_data[$id][$child]) ? $raw_data[$id][$child] : false; // []=xxx or [0]=xxx } else { $data = !empty($raw_data[$id]) ? $raw_data[$id] : false; } /** * Sanitize the value */ if ($id == self::O_CDN_MAPPING || $id == self::O_CRAWLER_COOKIES) { // Use existing in queue data if existed (Only available when $child != false) $data2 = array_key_exists($id, $the_matrix) ? $the_matrix[$id] : (defined('WP_CLI') && WP_CLI ? $this->conf($id) : array()); } switch ($id) { case self::O_CRAWLER_ROLES: // Don't allow Editor/admin to be used in crawler role simulator $data = Utility::sanitize_lines($data); if ($data) { foreach ($data as $k => $v) { if (user_can($v, 'edit_posts')) { $msg = sprintf( __('The user with id %s has editor access, which is not allowed for the role simulator.', 'litespeed-cache'), '<code>' . $v . '</code>' ); Admin_Display::error($msg); unset($data[$k]); } } } break; case self::O_CDN_MAPPING: /** * CDN setting * * Raw data format: * cdn-mapping[url][] = 'xxx' * cdn-mapping[url][2] = 'xxx2' * cdn-mapping[inc_js][] = 1 * * Final format: * cdn-mapping[ 0 ][ url ] = 'xxx' * cdn-mapping[ 2 ][ url ] = 'xxx2' */ if ($data) { foreach ($data as $k => $v) { if ($child == self::CDN_MAPPING_FILETYPE) { $v = Utility::sanitize_lines($v); } if ($child == self::CDN_MAPPING_URL) { # If not a valid URL, turn off CDN if (strpos($v, 'https://') !== 0) { self::debug('❌ CDN mapping set to OFF due to invalid URL'); $the_matrix[self::O_CDN] = false; } $v = trailingslashit($v); } if (in_array($child, array(self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS))) { // Because these can't be auto detected in `config->update()`, need to format here $v = $v === 'false' ? 0 : (bool) $v; } if (empty($data2[$k])) { $data2[$k] = array(); } $data2[$k][$child] = $v; } } $data = $data2; break; case self::O_CRAWLER_COOKIES: /** * Cookie Crawler setting * Raw Format: * crawler-cookies[name][] = xxx * crawler-cookies[name][2] = xxx2 * crawler-cookies[vals][] = xxx * * todo: need to allow null for values * * Final format: * crawler-cookie[ 0 ][ name ] = 'xxx' * crawler-cookie[ 0 ][ vals ] = 'xxx' * crawler-cookie[ 2 ][ name ] = 'xxx2' * * empty line for `vals` use literal `_null` */ if ($data) { foreach ($data as $k => $v) { if ($child == self::CRWL_COOKIE_VALS) { $v = Utility::sanitize_lines($v); } if (empty($data2[$k])) { $data2[$k] = array(); } $data2[$k][$child] = $v; } } $data = $data2; break; case self::O_CACHE_EXC_CAT: // Cache exclude cat $data2 = array(); $data = Utility::sanitize_lines($data); foreach ($data as $v) { $cat_id = get_cat_ID($v); if (!$cat_id) { continue; } $data2[] = $cat_id; } $data = $data2; break; case self::O_CACHE_EXC_TAG: // Cache exclude tag $data2 = array(); $data = Utility::sanitize_lines($data); foreach ($data as $v) { $term = get_term_by('name', $v, 'post_tag'); if (!$term) { // todo: can show the error in admin error msg continue; } $data2[] = $term->term_id; } $data = $data2; break; default: break; } $the_matrix[$id] = $data; } // Special handler for CDN/Crawler 2d list to drop empty rows foreach ($the_matrix as $id => $data) { /** * cdn-mapping[ 0 ][ url ] = 'xxx' * cdn-mapping[ 2 ][ url ] = 'xxx2' * * crawler-cookie[ 0 ][ name ] = 'xxx' * crawler-cookie[ 0 ][ vals ] = 'xxx' * crawler-cookie[ 2 ][ name ] = 'xxx2' */ if ($id == self::O_CDN_MAPPING || $id == self::O_CRAWLER_COOKIES) { // Drop this line if all children elements are empty foreach ($data as $k => $v) { foreach ($v as $v2) { if ($v2) { continue 2; } } // If hit here, means all empty unset($the_matrix[$id][$k]); } } // Don't allow repeated cookie name if ($id == self::O_CRAWLER_COOKIES) { $existed = array(); foreach ($the_matrix[$id] as $k => $v) { if (!$v[self::CRWL_COOKIE_NAME] || in_array($v[self::CRWL_COOKIE_NAME], $existed)) { // Filter repeated or empty name unset($the_matrix[$id][$k]); continue; } $existed[] = $v[self::CRWL_COOKIE_NAME]; } } // CDN mapping allow URL values repeated // if ( $id == self::O_CDN_MAPPING ) {} // tmp fix the 3rd part woo update hook issue when enabling vary cookie if ($id == 'wc_cart_vary') { if ($data) { add_filter('litespeed_vary_cookies', function ($list) { $list[] = 'woocommerce_cart_hash'; return array_unique($list); }); } else { add_filter('litespeed_vary_cookies', function ($list) { if (in_array('woocommerce_cart_hash', $list)) { unset($list[array_search('woocommerce_cart_hash', $list)]); } return array_unique($list); }); } } } // id validation will be inside $this->cls('Conf')->update_confs($the_matrix); $msg = __('Options saved.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Parses any changes made by the network admin on the network settings. * * @since 3.0 * @access public */ public function network_save($raw_data) { Debug2::debug('[Settings] network saving'); if (empty($raw_data[self::ENROLL])) { exit('No fields'); } $raw_data = Admin::cleanup_text($raw_data); foreach (array_unique($raw_data[self::ENROLL]) as $id) { // Append current field to setting save if (!array_key_exists($id, self::$_default_site_options)) { continue; } $data = !empty($raw_data[$id]) ? $raw_data[$id] : false; // id validation will be inside $this->cls('Conf')->network_update($id, $data); } // Update related files Activation::cls()->update_files(); $msg = __('Options saved.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Hooked to the wp_redirect filter. * This will only hook if there was a problem when saving the widget. * * @since 1.1.3 * @access public * @param string $location The location string. * @return string the updated location string. */ public static function widget_save_err($location) { return str_replace('?message=0', '?error=0', $location); } /** * Hooked to the widget_update_callback filter. * Validate the LiteSpeed Cache settings on edit widget save. * * @since 1.1.3 * @access public * @param array $instance The new settings. * @param array $new_instance * @param array $old_instance The original settings. * @param WP_Widget $widget The widget * @return mixed Updated settings on success, false on error. */ public static function validate_widget_save($instance, $new_instance, $old_instance, $widget) { if (empty($new_instance)) { return $instance; } if (!isset($new_instance[ESI::WIDGET_O_ESIENABLE]) || !isset($new_instance[ESI::WIDGET_O_TTL])) { return $instance; } $esi = intval($new_instance[ESI::WIDGET_O_ESIENABLE]) % 3; $ttl = (int) $new_instance[ESI::WIDGET_O_TTL]; if ($ttl != 0 && $ttl < 30) { add_filter('wp_redirect', __CLASS__ . '::widget_save_err'); return false; // invalid ttl. } if (empty($instance[Conf::OPTION_NAME])) { // todo: to be removed $instance[Conf::OPTION_NAME] = array(); } $instance[Conf::OPTION_NAME][ESI::WIDGET_O_ESIENABLE] = $esi; $instance[Conf::OPTION_NAME][ESI::WIDGET_O_TTL] = $ttl; $current = !empty($old_instance[Conf::OPTION_NAME]) ? $old_instance[Conf::OPTION_NAME] : false; if (!strpos($_SERVER['HTTP_REFERER'], '/wp-admin/customize.php')) { if (!$current || $esi != $current[ESI::WIDGET_O_ESIENABLE]) { Purge::purge_all('Widget ESI_enable changed'); } elseif ($ttl != 0 && $ttl != $current[ESI::WIDGET_O_TTL]) { Purge::add(Tag::TYPE_WIDGET . $widget->id); } Purge::purge_all('Widget saved'); } return $instance; } } src/error.cls.php 0000644 00000015620 15162130417 0007761 0 ustar 00 <?php /** * The error class. * * @since 3.0 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Error { private static $CODE_SET = array( 'HTA_LOGIN_COOKIE_INVALID' => 4300, // .htaccess did not find. 'HTA_DNF' => 4500, // .htaccess did not find. 'HTA_BK' => 9010, // backup 'HTA_R' => 9041, // read htaccess 'HTA_W' => 9042, // write 'HTA_GET' => 9030, // failed to get ); /** * Throw an error with msg * * @since 3.0 */ public static function t($code, $args = null) { throw new \Exception(self::msg($code, $args)); } /** * Translate an error to description * * @since 3.0 */ public static function msg($code, $args = null) { switch ($code) { case 'disabled_all': $msg = sprintf(__('The setting %s is currently enabled.', 'litespeed-cache'), '<strong>' . Lang::title(Base::O_DEBUG_DISABLE_ALL) . '</strong>') . Doc::learn_more( is_network_admin() ? network_admin_url('admin.php?page=litespeed-toolbox') : admin_url('admin.php?page=litespeed-toolbox'), __('Click here to change.', 'litespeed-cache'), true, false, true ); break; case 'qc_setup_required': $msg = sprintf(__('You will need to finish %s setup to use the online services.', 'litespeed-cache'), '<strong>QUIC.cloud</strong>') . Doc::learn_more(admin_url('admin.php?page=litespeed-general'), __('Click here to set.', 'litespeed-cache'), true, false, true); break; case 'out_of_daily_quota': $msg = __('You have used all of your daily quota for today.', 'litespeed-cache'); $msg .= ' ' . Doc::learn_more( 'https://docs.quic.cloud/billing/services/#daily-limits-on-free-quota-usage', __('Learn more or purchase additional quota.', 'litespeed-cache'), false, false, true ); break; case 'out_of_quota': $msg = __('You have used all of your quota left for current service this month.', 'litespeed-cache'); $msg .= ' ' . Doc::learn_more( 'https://docs.quic.cloud/billing/services/#daily-limits-on-free-quota-usage', __('Learn more or purchase additional quota.', 'litespeed-cache'), false, false, true ); break; case 'too_many_requested': $msg = __('You have too many requested images, please try again in a few minutes.', 'litespeed-cache'); break; case 'too_many_notified': $msg = __('You have images waiting to be pulled. Please wait for the automatic pull to complete, or pull them down manually now.', 'litespeed-cache'); break; case 'empty_list': $msg = __('The image list is empty.', 'litespeed-cache'); break; case 'lack_of_param': $msg = __('Not enough parameters. Please check if the domain key is set correctly', 'litespeed-cache'); break; case 'unfinished_queue': $msg = __('There is proceeding queue not pulled yet.', 'litespeed-cache'); break; case strpos($code, 'unfinished_queue ') === 0: $msg = sprintf( __('There is proceeding queue not pulled yet. Queue info: %s.', 'litespeed-cache'), '<code>' . substr($code, strlen('unfinished_queue ')) . '</code>' ); break; case 'err_alias': $msg = __('The site is not a valid alias on QUIC.cloud.', 'litespeed-cache'); break; case 'site_not_registered': $msg = __('The site is not registered on QUIC.cloud.', 'litespeed-cache'); break; case 'err_key': $msg = __('The domain key is not correct. Please try to sync your domain key again.', 'litespeed-cache'); break; case 'heavy_load': $msg = __('The current server is under heavy load.', 'litespeed-cache'); break; case 'redetect_node': $msg = __('Online node needs to be redetected.', 'litespeed-cache'); break; case 'err_overdraw': $msg = __('Credits are not enough to proceed the current request.', 'litespeed-cache'); break; case 'W': $msg = __('%s file not writable.', 'litespeed-cache'); break; case 'HTA_DNF': if (!is_array($args)) { $args = array('<code>' . $args . '</code>'); } $args[] = '.htaccess'; $msg = __('Could not find %1$s in %2$s.', 'litespeed-cache'); break; case 'HTA_LOGIN_COOKIE_INVALID': $msg = sprintf(__('Invalid login cookie. Please check the %s file.', 'litespeed-cache'), '.htaccess'); break; case 'HTA_BK': $msg = sprintf(__('Failed to back up %s file, aborted changes.', 'litespeed-cache'), '.htaccess'); break; case 'HTA_R': $msg = sprintf(__('%s file not readable.', 'litespeed-cache'), '.htaccess'); break; case 'HTA_W': $msg = sprintf(__('%s file not writable.', 'litespeed-cache'), '.htaccess'); break; case 'HTA_GET': $msg = sprintf(__('Failed to get %s file contents.', 'litespeed-cache'), '.htaccess'); break; case 'failed_tb_creation': $msg = __('Failed to create table %s! SQL: %s.', 'litespeed-cache'); break; case 'crawler_disabled': $msg = __('Crawler disabled by the server admin.', 'litespeed-cache'); break; case 'try_later': // QC error code $msg = __('Previous request too recent. Please try again later.', 'litespeed-cache'); break; case strpos($code, 'try_later ') === 0: $msg = sprintf( __('Previous request too recent. Please try again after %s.', 'litespeed-cache'), '<code>' . Utility::readable_time(substr($code, strlen('try_later ')), 3600, true) . '</code>' ); break; case 'waiting_for_approval': $msg = __('Your application is waiting for approval.', 'litespeed-cache'); break; case 'callback_fail_hash': $msg = __('The callback validation to your domain failed due to hash mismatch.', 'litespeed-cache'); break; case 'callback_fail': $msg = __('The callback validation to your domain failed. Please make sure there is no firewall blocking our servers.', 'litespeed-cache'); break; case substr($code, 0, 14) === 'callback_fail ': $msg = __('The callback validation to your domain failed. Please make sure there is no firewall blocking our servers. Response code: ', 'litespeed-cache') . substr($code, 14); break; case 'forbidden': $msg = __('Your domain has been forbidden from using our services due to a previous policy violation.', 'litespeed-cache'); break; case 'err_dns_active': $msg = __( 'You cannot remove this DNS zone, because it is still in use. Please update the domain\'s nameservers, then try to delete this zone again, otherwise your site will become inaccessible.', 'litespeed-cache' ); break; default: $msg = __('Unknown error', 'litespeed-cache') . ': ' . $code; break; } if ($args !== null) { $msg = is_array($args) ? vsprintf($msg, $args) : sprintf($msg, $args); } if (isset(self::$CODE_SET[$code])) { $msg = 'ERROR ' . self::$CODE_SET[$code] . ': ' . $msg; } return $msg; } } src/tool.cls.php 0000644 00000006653 15162130420 0007605 0 ustar 00 <?php /** * The tools * * @since 3.0 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Tool extends Root { const LOG_TAG = '[Tool]'; /** * Get public IP * * @since 3.0 * @access public */ public function check_ip() { self::debug('✅ check_ip'); $response = wp_safe_remote_get('https://cyberpanel.sh/?ip', array( 'headers' => array( 'User-Agent' => 'curl/8.7.1', ), )); if (is_wp_error($response)) { return __('Failed to detect IP', 'litespeed-cache'); } $ip = trim($response['body']); self::debug('result [ip] ' . $ip); if (Utility::valid_ipv4($ip)) { return $ip; } return __('Failed to detect IP', 'litespeed-cache'); } /** * Heartbeat Control * * NOTE: since WP4.9, there could be a core bug that sometimes the hook is not working. * * @since 3.0 * @access public */ public function heartbeat() { add_action('wp_enqueue_scripts', array($this, 'heartbeat_frontend')); add_action('admin_enqueue_scripts', array($this, 'heartbeat_backend')); add_filter('heartbeat_settings', array($this, 'heartbeat_settings')); } /** * Heartbeat Control frontend control * * @since 3.0 * @access public */ public function heartbeat_frontend() { if (!$this->conf(Base::O_MISC_HEARTBEAT_FRONT)) { return; } if (!$this->conf(Base::O_MISC_HEARTBEAT_FRONT_TTL)) { wp_deregister_script('heartbeat'); Debug2::debug('[Tool] Deregistered frontend heartbeat'); } } /** * Heartbeat Control backend control * * @since 3.0 * @access public */ public function heartbeat_backend() { if ($this->_is_editor()) { if (!$this->conf(Base::O_MISC_HEARTBEAT_EDITOR)) { return; } if (!$this->conf(Base::O_MISC_HEARTBEAT_EDITOR_TTL)) { wp_deregister_script('heartbeat'); Debug2::debug('[Tool] Deregistered editor heartbeat'); } } else { if (!$this->conf(Base::O_MISC_HEARTBEAT_BACK)) { return; } if (!$this->conf(Base::O_MISC_HEARTBEAT_BACK_TTL)) { wp_deregister_script('heartbeat'); Debug2::debug('[Tool] Deregistered backend heartbeat'); } } } /** * Heartbeat Control settings * * @since 3.0 * @access public */ public function heartbeat_settings($settings) { // Check editor first to make frontend editor valid too if ($this->_is_editor()) { if ($this->conf(Base::O_MISC_HEARTBEAT_EDITOR)) { $settings['interval'] = $this->conf(Base::O_MISC_HEARTBEAT_EDITOR_TTL); Debug2::debug('[Tool] Heartbeat interval set to ' . $this->conf(Base::O_MISC_HEARTBEAT_EDITOR_TTL)); } } elseif (!is_admin()) { if ($this->conf(Base::O_MISC_HEARTBEAT_FRONT)) { $settings['interval'] = $this->conf(Base::O_MISC_HEARTBEAT_FRONT_TTL); Debug2::debug('[Tool] Heartbeat interval set to ' . $this->conf(Base::O_MISC_HEARTBEAT_FRONT_TTL)); } } else { if ($this->conf(Base::O_MISC_HEARTBEAT_BACK)) { $settings['interval'] = $this->conf(Base::O_MISC_HEARTBEAT_BACK_TTL); Debug2::debug('[Tool] Heartbeat interval set to ' . $this->conf(Base::O_MISC_HEARTBEAT_BACK_TTL)); } } return $settings; } /** * If is in editor * * @since 3.0 * @access public */ private function _is_editor() { $res = is_admin() && Utility::str_hit_array($_SERVER['REQUEST_URI'], array('post.php', 'post-new.php')); return apply_filters('litespeed_is_editor', $res); } } src/img-optm.cls.php 0000644 00000200144 15162130421 0010351 0 ustar 00 <?php /** * The class to optimize image. * * @since 2.0 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; use WpOrg\Requests\Autoload; use WpOrg\Requests\Requests; defined('WPINC') || exit(); class Img_Optm extends Base { const LOG_TAG = '🗜️'; const CLOUD_ACTION_NEW_REQ = 'new_req'; const CLOUD_ACTION_TAKEN = 'taken'; const CLOUD_ACTION_REQUEST_DESTROY = 'imgoptm_destroy'; const CLOUD_ACTION_CLEAN = 'clean'; const TYPE_NEW_REQ = 'new_req'; const TYPE_RESCAN = 'rescan'; const TYPE_DESTROY = 'destroy'; const TYPE_RESET_COUNTER = 'reset_counter'; const TYPE_CLEAN = 'clean'; const TYPE_PULL = 'pull'; const TYPE_BATCH_SWITCH_ORI = 'batch_switch_ori'; const TYPE_BATCH_SWITCH_OPTM = 'batch_switch_optm'; const TYPE_CALC_BKUP = 'calc_bkup'; const TYPE_RESET_ROW = 'reset_row'; const TYPE_RM_BKUP = 'rm_bkup'; const STATUS_NEW = 0; // 'new'; const STATUS_RAW = 1; // 'raw'; const STATUS_REQUESTED = 3; // 'requested'; const STATUS_NOTIFIED = 6; // 'notified'; const STATUS_DUPLICATED = 8; // 'duplicated'; const STATUS_PULLED = 9; // 'pulled'; const STATUS_FAILED = -1; //'failed'; const STATUS_MISS = -3; // 'miss'; const STATUS_ERR_FETCH = -5; // 'err_fetch'; const STATUS_ERR_404 = -6; // 'err_404'; const STATUS_ERR_OPTM = -7; // 'err_optm'; const STATUS_XMETA = -8; // 'xmeta'; const STATUS_ERR = -9; // 'err'; const DB_SIZE = 'litespeed-optimize-size'; const DB_SET = 'litespeed-optimize-set'; const DB_NEED_PULL = 'need_pull'; private $wp_upload_dir; private $tmp_pid; private $tmp_type; private $tmp_path; private $_img_in_queue = array(); private $_existed_src_list = array(); private $_pids_set = array(); private $_thumbnail_set = ''; private $_table_img_optm; private $_table_img_optming; private $_cron_ran = false; private $__media; private $__data; protected $_summary; private $_format = ''; /** * Init * * @since 2.0 */ public function __construct() { Debug2::debug2('[ImgOptm] init'); $this->wp_upload_dir = wp_upload_dir(); $this->__media = $this->cls('Media'); $this->__data = $this->cls('Data'); $this->_table_img_optm = $this->__data->tb('img_optm'); $this->_table_img_optming = $this->__data->tb('img_optming'); $this->_summary = self::get_summary(); if (empty($this->_summary['next_post_id'])) { $this->_summary['next_post_id'] = 0; } if ($this->conf(Base::O_IMG_OPTM_WEBP)) { $this->_format = 'webp'; if ($this->conf(Base::O_IMG_OPTM_WEBP) == 2) { $this->_format = 'avif'; } } } /** * Gather images auto when update attachment meta * This is to optimize new uploaded images first. Stored in img_optm table. * Later normal process will auto remove these records when trying to optimize these images again * * @since 4.0 */ public function wp_update_attachment_metadata($meta_value, $post_id) { global $wpdb; self::debug2('🖌️ Auto update attachment meta [id] ' . $post_id); if (empty($meta_value['file'])) { return; } // Load gathered images if (!$this->_existed_src_list) { // To aavoid extra query when recalling this function self::debug('SELECT src from img_optm table'); if ($this->__data->tb_exist('img_optm')) { $q = "SELECT src FROM `$this->_table_img_optm` WHERE post_id = %d"; $list = $wpdb->get_results($wpdb->prepare($q, $post_id)); foreach ($list as $v) { $this->_existed_src_list[] = $post_id . '.' . $v->src; } } if ($this->__data->tb_exist('img_optming')) { $q = "SELECT src FROM `$this->_table_img_optming` WHERE post_id = %d"; $list = $wpdb->get_results($wpdb->prepare($q, $post_id)); foreach ($list as $v) { $this->_existed_src_list[] = $post_id . '.' . $v->src; } } else { $this->__data->tb_create('img_optming'); } } // Prepare images $this->tmp_pid = $post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_append_img_queue($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_append_img_queue'), $meta_value['sizes']); } if (!$this->_img_in_queue) { self::debug('auto update attachment meta 2 bypass: empty _img_in_queue'); return; } // Save to DB $this->_save_raw(); // $this->_send_request(); } /** * Auto send optm request * * @since 2.4.1 * @access public */ public static function cron_auto_request() { if (!defined('DOING_CRON')) { return false; } $instance = self::cls(); $instance->new_req(); } /** * Calculate wet run allowance * * @since 3.0 */ public function wet_limit() { $wet_limit = 1; if (!empty($this->_summary['img_taken'])) { $wet_limit = pow($this->_summary['img_taken'], 2); } if ($wet_limit == 1 && !empty($this->_summary['img_status.' . self::STATUS_ERR_OPTM])) { $wet_limit = pow($this->_summary['img_status.' . self::STATUS_ERR_OPTM], 2); } if ($wet_limit < Cloud::IMG_OPTM_DEFAULT_GROUP) { return $wet_limit; } // No limit return false; } /** * Push raw img to image optm server * * @since 1.6 * @access public */ public function new_req() { global $wpdb; // check if is running if (!empty($this->_summary['is_running']) && time() - $this->_summary['is_running'] < apply_filters('litespeed_imgoptm_new_req_interval', 3600)) { self::debug('The previous req was in 3600s.'); return; } $this->_summary['is_running'] = time(); self::save_summary(); // Check if has credit to push $err = false; $allowance = Cloud::cls()->allowance(Cloud::SVC_IMG_OPTM, $err); $wet_limit = $this->wet_limit(); self::debug("allowance_max $allowance wet_limit $wet_limit"); if ($wet_limit && $wet_limit < $allowance) { $allowance = $wet_limit; } if (!$allowance) { self::debug('❌ No credit'); Admin_Display::error(Error::msg($err)); $this->_finished_running(); return; } self::debug('preparing images to push'); $this->__data->tb_create('img_optming'); $q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d"; $q = $wpdb->prepare($q, array(self::STATUS_REQUESTED)); $total_requested = $wpdb->get_var($q); $max_requested = $allowance * 1; if ($total_requested > $max_requested) { self::debug('❌ Too many queued images (' . $total_requested . ' > ' . $max_requested . ')'); Admin_Display::error(Error::msg('too_many_requested')); $this->_finished_running(); return; } $allowance -= $total_requested; if ($allowance < 1) { self::debug('❌ Too many requested images ' . $total_requested); Admin_Display::error(Error::msg('too_many_requested')); $this->_finished_running(); return; } // Limit maximum number of items waiting to be pulled $q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d"; $q = $wpdb->prepare($q, array(self::STATUS_NOTIFIED)); $total_notified = $wpdb->get_var($q); if ($total_notified > 0) { self::debug('❌ Too many notified images (' . $total_notified . ')'); Admin_Display::error(Error::msg('too_many_notified')); $this->_finished_running(); return; } $q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status IN (%d, %d)"; $q = $wpdb->prepare($q, array(self::STATUS_NEW, self::STATUS_RAW)); $total_new = $wpdb->get_var($q); // $allowance -= $total_new; // May need to get more images $list = array(); $more = $allowance - $total_new; if ($more > 0) { $q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.ID>%d AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d "; $q = $wpdb->prepare($q, array($this->_summary['next_post_id'], $more)); $list = $wpdb->get_results($q); foreach ($list as $v) { if (!$v->post_id) { continue; } $this->_summary['next_post_id'] = $v->post_id; $meta_value = $this->_parse_wp_meta_value($v); if (!$meta_value) { continue; } $meta_value['file'] = wp_normalize_path($meta_value['file']); $basedir = $this->wp_upload_dir['basedir'] . '/'; if (strpos($meta_value['file'], $basedir) === 0) { $meta_value['file'] = substr($meta_value['file'], strlen($basedir)); } $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_append_img_queue($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_append_img_queue'), $meta_value['sizes']); } } self::save_summary(); $num_a = count($this->_img_in_queue); self::debug('Images found: ' . $num_a); $this->_filter_duplicated_src(); self::debug('Images after duplicated: ' . count($this->_img_in_queue)); $this->_filter_invalid_src(); self::debug('Images after invalid: ' . count($this->_img_in_queue)); // Check w/ legacy imgoptm table, bypass finished images $this->_filter_legacy_src(); $num_b = count($this->_img_in_queue); if ($num_b != $num_a) { self::debug('Images after filtered duplicated/invalid/legacy src: ' . $num_b); } // Save to DB $this->_save_raw(); } // Push to Cloud server $accepted_imgs = $this->_send_request($allowance); $this->_finished_running(); if (!$accepted_imgs) { return; } $placeholder1 = Admin_Display::print_plural($accepted_imgs[0], 'image'); $placeholder2 = Admin_Display::print_plural($accepted_imgs[1], 'image'); $msg = sprintf(__('Pushed %1$s to Cloud server, accepted %2$s.', 'litespeed-cache'), $placeholder1, $placeholder2); Admin_Display::success($msg); } /** * Set running to done */ private function _finished_running() { $this->_summary['is_running'] = 0; self::save_summary(); } /** * Add a new img to queue which will be pushed to request * * @since 1.6 * @access private */ private function _append_img_queue($meta_value, $is_ori_file = false) { if (empty($meta_value['file']) || empty($meta_value['width']) || empty($meta_value['height'])) { self::debug2('bypass image due to lack of file/w/h: pid ' . $this->tmp_pid, $meta_value); return; } $short_file_path = $meta_value['file']; if (!$is_ori_file) { $short_file_path = $this->tmp_path . $short_file_path; } // Check if src is gathered already or not if (in_array($this->tmp_pid . '.' . $short_file_path, $this->_existed_src_list)) { // Debug2::debug2( '[Img_Optm] bypass image due to gathered: pid ' . $this->tmp_pid . ' ' . $short_file_path ); return; } else { // Append handled images $this->_existed_src_list[] = $this->tmp_pid . '.' . $short_file_path; } // check file exists or not $_img_info = $this->__media->info($short_file_path, $this->tmp_pid); $extension = pathinfo($short_file_path, PATHINFO_EXTENSION); if (!$_img_info || !in_array($extension, array('jpg', 'jpeg', 'png', 'gif'))) { self::debug2('bypass image due to file not exist: pid ' . $this->tmp_pid . ' ' . $short_file_path); return; } // Check if optimized file exists or not $target_needed = false; if ($this->_format) { $target_file_path = $short_file_path . '.' . $this->_format; if (!$this->__media->info($target_file_path, $this->tmp_pid)) { $target_needed = true; } } if ($this->conf(self::O_IMG_OPTM_ORI)) { $target_file_path = substr($short_file_path, 0, -strlen($extension)) . 'bk.' . $extension; if (!$this->__media->info($target_file_path, $this->tmp_pid)) { $target_needed = true; } } if (!$target_needed) { self::debug2('bypass image due to optimized file exists: pid ' . $this->tmp_pid . ' ' . $short_file_path); return; } // Debug2::debug2( '[Img_Optm] adding image: pid ' . $this->tmp_pid ); $this->_img_in_queue[] = array( 'pid' => $this->tmp_pid, 'md5' => $_img_info['md5'], 'url' => $_img_info['url'], 'src' => $short_file_path, // not needed in LiteSpeed IAPI, just leave for local storage after post 'mime_type' => !empty($meta_value['mime-type']) ? $meta_value['mime-type'] : '', ); } /** * Save gathered image raw data * * @since 3.0 */ private function _save_raw() { if (empty($this->_img_in_queue)) { return; } $data = array(); $pid_list = array(); foreach ($this->_img_in_queue as $k => $v) { $_img_info = $this->__media->info($v['src'], $v['pid']); // attachment doesn't exist, delete the record if (empty($_img_info['url']) || empty($_img_info['md5'])) { unset($this->_img_in_queue[$k]); continue; } $pid_list[] = (int) $v['pid']; $data[] = $v['pid']; $data[] = self::STATUS_RAW; $data[] = $v['src']; } global $wpdb; $fields = 'post_id, optm_status, src'; $q = "INSERT INTO `$this->_table_img_optming` ( $fields ) VALUES "; // Add placeholder $q .= Utility::chunk_placeholder($data, $fields); // Store data $wpdb->query($wpdb->prepare($q, $data)); $count = count($this->_img_in_queue); self::debug('Added raw images [total] ' . $count); $this->_img_in_queue = array(); // Save thumbnail groups for future rescan index $this->_gen_thumbnail_set(); $pid_list = array_unique($pid_list); self::debug('pid list to append to postmeta', $pid_list); $pid_list = array_diff($pid_list, $this->_pids_set); $this->_pids_set = array_merge($this->_pids_set, $pid_list); $existed_meta = $wpdb->get_results("SELECT * FROM `$wpdb->postmeta` WHERE post_id IN ('" . implode("','", $pid_list) . "') AND meta_key='" . self::DB_SET . "'"); $existed_pid = array(); if ($existed_meta) { foreach ($existed_meta as $v) { $existed_pid[] = $v->post_id; } self::debug('pid list to update postmeta', $existed_pid); $wpdb->query( $wpdb->prepare("UPDATE `$wpdb->postmeta` SET meta_value=%s WHERE post_id IN ('" . implode("','", $existed_pid) . "') AND meta_key=%s", array( $this->_thumbnail_set, self::DB_SET, )) ); } # Add new meta $new_pids = $existed_pid ? array_diff($pid_list, $existed_pid) : $pid_list; if ($new_pids) { self::debug('pid list to update postmeta', $new_pids); foreach ($new_pids as $v) { self::debug('New group set info [pid] ' . $v); $q = "INSERT INTO `$wpdb->postmeta` (post_id, meta_key, meta_value) VALUES (%d, %s, %s)"; $wpdb->query($wpdb->prepare($q, array($v, self::DB_SET, $this->_thumbnail_set))); } } } /** * Generate thumbnail sets of current image group * * @since 5.4 */ private function _gen_thumbnail_set() { if ($this->_thumbnail_set) { return; } $set = array(); foreach (Media::cls()->get_image_sizes() as $size) { $curr_size = $size['width'] . 'x' . $size['height']; if (in_array($curr_size, $set)) { continue; } $set[] = $curr_size; } $this->_thumbnail_set = implode(PHP_EOL, $set); } /** * Filter duplicated src in work table and $this->_img_in_queue, then mark them as duplicated * * @since 2.0 * @access private */ private function _filter_duplicated_src() { global $wpdb; $srcpath_list = array(); $list = $wpdb->get_results("SELECT src FROM `$this->_table_img_optming`"); foreach ($list as $v) { $srcpath_list[] = $v->src; } foreach ($this->_img_in_queue as $k => $v) { if (in_array($v['src'], $srcpath_list)) { unset($this->_img_in_queue[$k]); continue; } $srcpath_list[] = $v['src']; } } /** * Filter legacy finished ones * * @since 5.4 */ private function _filter_legacy_src() { global $wpdb; if (!$this->__data->tb_exist('img_optm')) { return; } if (!$this->_img_in_queue) { return; } $finished_ids = array(); Utility::compatibility(); $post_ids = array_unique(array_column($this->_img_in_queue, 'pid')); $list = $wpdb->get_results("SELECT post_id FROM `$this->_table_img_optm` WHERE post_id in (" . implode(',', $post_ids) . ') GROUP BY post_id'); foreach ($list as $v) { $finished_ids[] = $v->post_id; } foreach ($this->_img_in_queue as $k => $v) { if (in_array($v['pid'], $finished_ids)) { self::debug('Legacy image optimized [pid] ' . $v['pid']); unset($this->_img_in_queue[$k]); continue; } } // Drop all existing legacy records $wpdb->query("DELETE FROM `$this->_table_img_optm` WHERE post_id in (" . implode(',', $post_ids) . ')'); } /** * Filter the invalid src before sending * * @since 3.0.8.3 * @access private */ private function _filter_invalid_src() { $img_in_queue_invalid = array(); foreach ($this->_img_in_queue as $k => $v) { if ($v['src']) { $extension = pathinfo($v['src'], PATHINFO_EXTENSION); } if (!$v['src'] || empty($extension) || !in_array($extension, array('jpg', 'jpeg', 'png', 'gif'))) { $img_in_queue_invalid[] = $v['id']; unset($this->_img_in_queue[$k]); continue; } } if (!$img_in_queue_invalid) { return; } $count = count($img_in_queue_invalid); $msg = sprintf(__('Cleared %1$s invalid images.', 'litespeed-cache'), $count); Admin_Display::success($msg); self::debug('Found invalid src [total] ' . $count); } /** * Push img request to Cloud server * * @since 1.6.7 * @access private */ private function _send_request($allowance) { global $wpdb; $q = "SELECT id, src, post_id FROM `$this->_table_img_optming` WHERE optm_status=%d LIMIT %d"; $q = $wpdb->prepare($q, array(self::STATUS_RAW, $allowance)); $_img_in_queue = $wpdb->get_results($q); if (!$_img_in_queue) { return; } self::debug('Load img in queue [total] ' . count($_img_in_queue)); $list = array(); foreach ($_img_in_queue as $v) { $_img_info = $this->__media->info($v->src, $v->post_id); # If record is invalid, remove from img_optming table if (empty($_img_info['url']) || empty($_img_info['md5'])) { $wpdb->query($wpdb->prepare("DELETE FROM `$this->_table_img_optming` WHERE id=%d", $v->id)); continue; } $img = array( 'id' => $v->id, 'url' => $_img_info['url'], 'md5' => $_img_info['md5'], ); // Build the needed image types for request as we now support soft reset counter if ($this->_format) { $target_file_path = $v->src . '.' . $this->_format; if ($this->__media->info($target_file_path, $v->post_id)) { $img['optm_' . $this->_format] = 0; } } if ($this->conf(self::O_IMG_OPTM_ORI)) { $extension = pathinfo($v->src, PATHINFO_EXTENSION); $target_file_path = substr($v->src, 0, -strlen($extension)) . 'bk.' . $extension; if ($this->__media->info($target_file_path, $v->post_id)) { $img['optm_ori'] = 0; } } $list[] = $img; } if (!$list) { $msg = __('No valid image found in the current request.', 'litespeed-cache'); Admin_Display::error($msg); return; } $data = array( 'action' => self::CLOUD_ACTION_NEW_REQ, 'list' => \json_encode($list), 'optm_ori' => $this->conf(self::O_IMG_OPTM_ORI) ? 1 : 0, 'optm_lossless' => $this->conf(self::O_IMG_OPTM_LOSSLESS) ? 1 : 0, 'keep_exif' => $this->conf(self::O_IMG_OPTM_EXIF) ? 1 : 0, ); if ($this->_format) { $data['optm_' . $this->_format] = 1; } // Push to Cloud server $json = Cloud::post(Cloud::SVC_IMG_OPTM, $data); if (!$json) { return; } // Check data format if (empty($json['ids'])) { self::debug('Failed to parse response data from Cloud server ', $json); $msg = __('No valid image found by Cloud server in the current request.', 'litespeed-cache'); Admin_Display::error($msg); return; } self::debug('Returned data from Cloud server count: ' . count($json['ids'])); $ids = implode(',', array_map('intval', $json['ids'])); // Update img table $q = "UPDATE `$this->_table_img_optming` SET optm_status = '" . self::STATUS_REQUESTED . "' WHERE id IN ( $ids )"; $wpdb->query($q); $this->_summary['last_requested'] = time(); self::save_summary(); return array(count($list), count($json['ids'])); } /** * Cloud server notify Client img status changed * * @access public */ public function notify_img() { // Interval validation to avoid hacking domain_key if (!empty($this->_summary['notify_ts_err']) && time() - $this->_summary['notify_ts_err'] < 3) { return Cloud::err('too_often'); } $post_data = \json_decode(file_get_contents('php://input'), true); if (is_null($post_data)) { $post_data = $_POST; } global $wpdb; $notified_data = $post_data['data']; if (empty($notified_data) || !is_array($notified_data)) { self::debug('❌ notify exit: no notified data'); return Cloud::err('no notified data'); } if (empty($post_data['server']) || (substr($post_data['server'], -11) !== '.quic.cloud' && substr($post_data['server'], -15) !== '.quicserver.com')) { self::debug('notify exit: no/wrong server'); return Cloud::err('no/wrong server'); } if (empty($post_data['status'])) { self::debug('notify missing status'); return Cloud::err('no status'); } $status = $post_data['status']; self::debug('notified status=' . $status); $last_log_pid = 0; if (empty($this->_summary['reduced'])) { $this->_summary['reduced'] = 0; } if ($status == self::STATUS_NOTIFIED) { // Notified data format: [ img_optm_id => [ id=>, src_size=>, ori=>, ori_md5=>, ori_reduced=>, webp=>, webp_md5=>, webp_reduced=> ] ] $q = "SELECT a.*, b.meta_id as b_meta_id, b.meta_value AS b_optm_info FROM `$this->_table_img_optming` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.post_id AND b.meta_key = %s WHERE a.id IN ( " . implode(',', array_fill(0, count($notified_data), '%d')) . ' )'; $list = $wpdb->get_results($wpdb->prepare($q, array_merge(array(self::DB_SIZE), array_keys($notified_data)))); $ls_optm_size_row_exists_postids = array(); foreach ($list as $v) { $json = $notified_data[$v->id]; // self::debug('Notified data for [id] ' . $v->id, $json); $server = !empty($json['server']) ? $json['server'] : $post_data['server']; $server_info = array( 'server' => $server, ); // Save server side ID to send taken notification after pulled $server_info['id'] = $json['id']; if (!empty($json['file_id'])) { $server_info['file_id'] = $json['file_id']; } // Optm info array $postmeta_info = array( 'ori_total' => 0, 'ori_saved' => 0, 'webp_total' => 0, 'webp_saved' => 0, 'avif_total' => 0, 'avif_saved' => 0, ); // Init postmeta_info for the first one if (!empty($v->b_meta_id)) { foreach (maybe_unserialize($v->b_optm_info) as $k2 => $v2) { $postmeta_info[$k2] += $v2; } } if (!empty($json['ori'])) { $server_info['ori_md5'] = $json['ori_md5']; $server_info['ori'] = $json['ori']; // Append meta info $postmeta_info['ori_total'] += $json['src_size']; $postmeta_info['ori_saved'] += $json['ori_reduced']; // optimized image size info in img_optm tb will be updated when pull $this->_summary['reduced'] += $json['ori_reduced']; } if (!empty($json['webp'])) { $server_info['webp_md5'] = $json['webp_md5']; $server_info['webp'] = $json['webp']; // Append meta info $postmeta_info['webp_total'] += $json['src_size']; $postmeta_info['webp_saved'] += $json['webp_reduced']; $this->_summary['reduced'] += $json['webp_reduced']; } if (!empty($json['avif'])) { $server_info['avif_md5'] = $json['avif_md5']; $server_info['avif'] = $json['avif']; // Append meta info $postmeta_info['avif_total'] += $json['src_size']; $postmeta_info['avif_saved'] += $json['avif_reduced']; $this->_summary['reduced'] += $json['avif_reduced']; } // Update status and data in working table $q = "UPDATE `$this->_table_img_optming` SET optm_status = %d, server_info = %s WHERE id = %d "; $wpdb->query($wpdb->prepare($q, array($status, \json_encode($server_info), $v->id))); // Update postmeta for optm summary $postmeta_info = serialize($postmeta_info); if (empty($v->b_meta_id) && !in_array($v->post_id, $ls_optm_size_row_exists_postids)) { self::debug('New size info [pid] ' . $v->post_id); $q = "INSERT INTO `$wpdb->postmeta` ( post_id, meta_key, meta_value ) VALUES ( %d, %s, %s )"; $wpdb->query($wpdb->prepare($q, array($v->post_id, self::DB_SIZE, $postmeta_info))); $ls_optm_size_row_exists_postids[] = $v->post_id; } else { $q = "UPDATE `$wpdb->postmeta` SET meta_value = %s WHERE meta_id = %d "; $wpdb->query($wpdb->prepare($q, array($postmeta_info, $v->b_meta_id))); } // write log $pid_log = $last_log_pid == $v->post_id ? '.' : $v->post_id; self::debug('notify_img [status] ' . $status . " \t\t[pid] " . $pid_log . " \t\t[id] " . $v->id); $last_log_pid = $v->post_id; } self::save_summary(); // Mark need_pull tag for cron self::update_option(self::DB_NEED_PULL, self::STATUS_NOTIFIED); } else { // Other errors will directly remove the working records // Delete from working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id IN ( " . implode(',', array_fill(0, count($notified_data), '%d')) . ' ) '; $wpdb->query($wpdb->prepare($q, $notified_data)); } return Cloud::ok(array('count' => count($notified_data))); } /** * Cron start async req * * @since 5.5 */ public static function start_async_cron() { Task::async_call('imgoptm'); } /** * Manually start async req * * @since 5.5 */ public static function start_async() { Task::async_call('imgoptm_force'); $msg = __('Started async image optimization request', 'litespeed-cache'); Admin_Display::success($msg); } /** * Ajax req handler * * @since 5.5 */ public static function async_handler($force = false) { self::debug('------------async-------------start_async_handler'); $tag = self::get_option(self::DB_NEED_PULL); if (!$tag || $tag != self::STATUS_NOTIFIED) { self::debug('❌ no need pull [tag] ' . $tag); return; } if (defined('LITESPEED_IMG_OPTM_PULL_CRON') && !LITESPEED_IMG_OPTM_PULL_CRON) { self::debug('Cron disabled [define] LITESPEED_IMG_OPTM_PULL_CRON'); return; } self::cls()->pull($force); } /** * Calculate pull threads * * @since 5.8 * @access private */ private function _calc_pull_threads() { global $wpdb; if (defined('LITESPEED_IMG_OPTM_PULL_THREADS')) { return LITESPEED_IMG_OPTM_PULL_THREADS; } // Tune number of images per request based on number of images waiting and cloud packages $imgs_per_req = 1; // base 1, ramp up to ~50 max // Ramp up the request rate based on how many images are waiting $c = "SELECT count(id) FROM `$this->_table_img_optming` WHERE optm_status = %d"; $_c = $wpdb->prepare($c, array(self::STATUS_NOTIFIED)); $images_waiting = $wpdb->get_var($_c); if ($images_waiting && $images_waiting > 0) { $imgs_per_req = ceil($images_waiting / 1000); //ie. download 5/request if 5000 images are waiting } // Cap the request rate at 50 images per request $imgs_per_req = min(50, $imgs_per_req); self::debug('Pulling images at rate: ' . $imgs_per_req . ' Images per request.'); return $imgs_per_req; } /** * Pull optimized img * * @since 1.6 * @access public */ public function pull($manual = false) { global $wpdb; $timeoutLimit = ini_get('max_execution_time'); $endts = time() + $timeoutLimit; self::debug('' . ($manual ? 'Manually' : 'Cron') . ' pull started [timeout: ' . $timeoutLimit . 's]'); if ($this->cron_running()) { self::debug('Pull cron is running'); $msg = __('Pull Cron is running', 'litespeed-cache'); Admin_Display::note($msg); return; } $this->_summary['last_pulled'] = time(); $this->_summary['last_pulled_by_cron'] = !$manual; self::save_summary(); $imgs_per_req = $this->_calc_pull_threads(); $q = "SELECT * FROM `$this->_table_img_optming` WHERE optm_status = %d ORDER BY id LIMIT %d"; $_q = $wpdb->prepare($q, array(self::STATUS_NOTIFIED, $imgs_per_req)); $rm_ori_bkup = $this->conf(self::O_IMG_OPTM_RM_BKUP); $total_pulled_ori = 0; $total_pulled_webp = 0; $total_pulled_avif = 0; $server_list = array(); try { while ($img_rows = $wpdb->get_results($_q)) { self::debug('timeout left: ' . ($endts - time()) . 's'); if (function_exists('set_time_limit')) { $endts += 600; self::debug('Endtime extended to ' . date('Ymd H:i:s', $endts)); set_time_limit(600); // This will be no more important as we use noabort now } // Disabled as we use noabort // if ($endts - time() < 10) { // self::debug("🚨 End loop due to timeout limit reached " . $timeoutLimit . "s"); // break; // } /** * Update cron timestamp to avoid duplicated running * @since 1.6.2 */ $this->_update_cron_running(); // Run requests in parallel $requests = array(); // store each request URL for Requests::request_multiple() $imgs_by_req = array(); // store original request data so that we can reference it in the response $req_counter = 0; foreach ($img_rows as $row_img) { // request original image $server_info = \json_decode($row_img->server_info, true); if (!empty($server_info['ori'])) { $image_url = $server_info['server'] . '/' . $server_info['ori']; self::debug('Queueing pull: ' . $image_url); $requests[$req_counter] = array( 'url' => $image_url, 'type' => 'GET', ); $imgs_by_req[$req_counter++] = array( 'type' => 'ori', 'data' => $row_img, ); } // request webp image $webp_size = 0; if (!empty($server_info['webp'])) { $image_url = $server_info['server'] . '/' . $server_info['webp']; self::debug('Queueing pull WebP: ' . $image_url); $requests[$req_counter] = array( 'url' => $image_url, 'type' => 'GET', ); $imgs_by_req[$req_counter++] = array( 'type' => 'webp', 'data' => $row_img, ); } // request avif image $avif_size = 0; if (!empty($server_info['avif'])) { $image_url = $server_info['server'] . '/' . $server_info['avif']; self::debug('Queueing pull AVIF: ' . $image_url); $requests[$req_counter] = array( 'url' => $image_url, 'type' => 'GET', ); $imgs_by_req[$req_counter++] = array( 'type' => 'avif', 'data' => $row_img, ); } } self::debug('Loaded images count: ' . $req_counter); $complete_action = function ($response, $req_count) use ($imgs_by_req, $rm_ori_bkup, &$total_pulled_ori, &$total_pulled_webp, &$total_pulled_avif, &$server_list) { global $wpdb; $row_data = isset($imgs_by_req[$req_count]) ? $imgs_by_req[$req_count] : false; if (false === $row_data) { self::debug('❌ failed to pull image: Request not found in lookup variable.'); return; } $row_type = isset($row_data['type']) ? $row_data['type'] : 'ori'; $row_img = $row_data['data']; $local_file = $this->wp_upload_dir['basedir'] . '/' . $row_img->src; $server_info = \json_decode($row_img->server_info, true); if (empty($response->success)) { if (!empty($response->status_code) && 404 == $response->status_code) { $this->_step_back_image($row_img->id); $msg = __('Some optimized image file(s) has expired and was cleared.', 'litespeed-cache'); Admin_Display::error($msg); return; } else { // handle error $image_url = $server_info['server'] . '/' . $server_info[$row_type]; self::debug( '❌ failed to pull image (' . $row_type . '): ' . (!empty($response->status_code) ? $response->status_code : '') . ' [Local: ' . $row_img->src . '] / [remote: ' . $image_url . ']' ); throw new \Exception('Failed to pull image ' . (!empty($response->status_code) ? $response->status_code : '') . ' [url] ' . $image_url); return; } } // Handle wp_remote_get 404 as its success=true if (!empty($response->status_code)) { if ($response->status_code == 404) { $this->_step_back_image($row_img->id); $msg = __('Some optimized image file(s) has expired and was cleared.', 'litespeed-cache'); Admin_Display::error($msg); return; } // Note: if there is other error status code found in future, handle here } if ('webp' === $row_type) { file_put_contents($local_file . '.webp', $response->body); if (!file_exists($local_file . '.webp') || !filesize($local_file . '.webp') || md5_file($local_file . '.webp') !== $server_info['webp_md5']) { self::debug('❌ Failed to pull optimized webp img: file md5 mismatch, server md5: ' . $server_info['webp_md5']); // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; $wpdb->query($wpdb->prepare($q, $row_img->id)); $msg = __('Pulled WebP image md5 does not match the notified WebP image md5.', 'litespeed-cache'); Admin_Display::error($msg); return; } self::debug('Pulled optimized img WebP: ' . $local_file . '.webp'); $webp_size = filesize($local_file . '.webp'); /** * API for WebP * @since 2.9.5 * @since 3.0 $row_img less elements (see above one) * @see #751737 - API docs for WEBP generation */ do_action('litespeed_img_pull_webp', $row_img, $local_file . '.webp'); $total_pulled_webp++; } elseif ('avif' === $row_type) { file_put_contents($local_file . '.avif', $response->body); if (!file_exists($local_file . '.avif') || !filesize($local_file . '.avif') || md5_file($local_file . '.avif') !== $server_info['avif_md5']) { self::debug('❌ Failed to pull optimized avif img: file md5 mismatch, server md5: ' . $server_info['avif_md5']); // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; $wpdb->query($wpdb->prepare($q, $row_img->id)); $msg = __('Pulled AVIF image md5 does not match the notified AVIF image md5.', 'litespeed-cache'); Admin_Display::error($msg); return; } self::debug('Pulled optimized img AVIF: ' . $local_file . '.avif'); $avif_size = filesize($local_file . '.avif'); /** * API for AVIF * @since 7.0 */ do_action('litespeed_img_pull_avif', $row_img, $local_file . '.avif'); $total_pulled_avif++; } else { // "ori" image type file_put_contents($local_file . '.tmp', $response->body); if (!file_exists($local_file . '.tmp') || !filesize($local_file . '.tmp') || md5_file($local_file . '.tmp') !== $server_info['ori_md5']) { self::debug( '❌ Failed to pull optimized img: file md5 mismatch [url] ' . $server_info['server'] . '/' . $server_info['ori'] . ' [server_md5] ' . $server_info['ori_md5'] ); // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; $wpdb->query($wpdb->prepare($q, $row_img->id)); $msg = __('One or more pulled images does not match with the notified image md5', 'litespeed-cache'); Admin_Display::error($msg); return; } // Backup ori img if (!$rm_ori_bkup) { $extension = pathinfo($local_file, PATHINFO_EXTENSION); $bk_file = substr($local_file, 0, -strlen($extension)) . 'bk.' . $extension; file_exists($local_file) && rename($local_file, $bk_file); } // Replace ori img rename($local_file . '.tmp', $local_file); self::debug('Pulled optimized img: ' . $local_file); /** * API Hook * @since 2.9.5 * @since 3.0 $row_img has less elements now. Most useful ones are `post_id`/`src` */ do_action('litespeed_img_pull_ori', $row_img, $local_file); self::debug2('Remove _table_img_optming record [id] ' . $row_img->id); } // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; $wpdb->query($wpdb->prepare($q, $row_img->id)); // Save server_list to notify taken if (empty($server_list[$server_info['server']])) { $server_list[$server_info['server']] = array(); } $server_info_id = !empty($server_info['file_id']) ? $server_info['file_id'] : $server_info['id']; $server_list[$server_info['server']][] = $server_info_id; $total_pulled_ori++; }; $force_wp_remote_get = defined('LITESPEED_FORCE_WP_REMOTE_GET') && LITESPEED_FORCE_WP_REMOTE_GET; if (!$force_wp_remote_get && class_exists('\WpOrg\Requests\Requests') && class_exists('\WpOrg\Requests\Autoload') && version_compare(PHP_VERSION, '5.6.0', '>=')) { // Make sure Requests can load internal classes. Autoload::register(); // Run pull requests in parallel Requests::request_multiple($requests, array( 'timeout' => 60, 'connect_timeout' => 60, 'complete' => $complete_action, )); } else { foreach ($requests as $cnt => $req) { $wp_response = wp_safe_remote_get($req['url'], array('timeout' => 60)); $request_response = array( 'success' => false, 'status_code' => 0, 'body' => null, ); if (is_wp_error($wp_response)) { $error_message = $wp_response->get_error_message(); self::debug('❌ failed to pull image: ' . $error_message); } else { $request_response['success'] = true; $request_response['status_code'] = $wp_response['response']['code']; $request_response['body'] = $wp_response['body']; } self::debug('response code [code] ' . $wp_response['response']['code'] . ' [url] ' . $req['url']); $request_response = (object) $request_response; $complete_action($request_response, $cnt); } } self::debug('Current batch pull finished'); } } catch (\Exception $e) { Admin_Display::error('Image pull process failure: ' . $e->getMessage()); } // Notify IAPI images taken foreach ($server_list as $server => $img_list) { $data = array( 'action' => self::CLOUD_ACTION_TAKEN, 'list' => $img_list, 'server' => $server, ); // TODO: improve this so we do not call once per server, but just once and then filter on the server side Cloud::post(Cloud::SVC_IMG_OPTM, $data); } if (empty($this->_summary['img_taken'])) { $this->_summary['img_taken'] = 0; } $this->_summary['img_taken'] += $total_pulled_ori + $total_pulled_webp + $total_pulled_avif; self::save_summary(); // Manually running needs to roll back timestamp for next running if ($manual) { $this->_update_cron_running(true); } // $msg = sprintf(__('Pulled %d image(s)', 'litespeed-cache'), $total_pulled_ori + $total_pulled_webp); // Admin_Display::success($msg); // Check if there is still task in queue $q = "SELECT * FROM `$this->_table_img_optming` WHERE optm_status = %d LIMIT 1"; $to_be_continued = $wpdb->get_row($wpdb->prepare($q, self::STATUS_NOTIFIED)); if ($to_be_continued) { self::debug('Task in queue, to be continued...'); return; // return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_PULL); } // If all pulled, update tag to done self::debug('Marked pull status to all pulled'); self::update_option(self::DB_NEED_PULL, self::STATUS_PULLED); } /** * Push image back to previous status * * @since 3.0 * @access private */ private function _step_back_image($id) { global $wpdb; self::debug('Push image back to new status [id] ' . $id); // Reset the image to gathered status $q = "UPDATE `$this->_table_img_optming` SET optm_status = %d WHERE id = %d "; $wpdb->query($wpdb->prepare($q, array(self::STATUS_RAW, $id))); } /** * Parse wp's meta value * * @since 1.6.7 * @access private */ private function _parse_wp_meta_value($v) { if (empty($v)) { self::debug('bypassed parsing meta due to null value'); return false; } if (!$v->meta_value) { self::debug('bypassed parsing meta due to no meta_value: pid ' . $v->post_id); return false; } $meta_value = @maybe_unserialize($v->meta_value); if (!is_array($meta_value)) { self::debug('bypassed parsing meta due to meta_value not json: pid ' . $v->post_id); return false; } if (empty($meta_value['file'])) { self::debug('bypassed parsing meta due to no ori file: pid ' . $v->post_id); return false; } return $meta_value; } /** * Clean up all unfinished queue locally and to Cloud server * * @since 2.1.2 * @access public */ public function clean() { global $wpdb; // Reset img_optm table's queue if ($this->__data->tb_exist('img_optming')) { // Get min post id to mark $q = "SELECT MIN(post_id) FROM `$this->_table_img_optming`"; $min_pid = $wpdb->get_var($q) - 1; if ($this->_summary['next_post_id'] > $min_pid) { $this->_summary['next_post_id'] = $min_pid; self::save_summary(); } $q = "DELETE FROM `$this->_table_img_optming`"; $wpdb->query($q); } $msg = __('Cleaned up unfinished data successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Reset image counter * * @since 7.0 * @access private */ private function _reset_counter() { self::debug('reset image optm counter'); $this->_summary['next_post_id'] = 0; self::save_summary(); $this->clean(); $msg = __('Reset image optimization counter successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Destroy all optimized images * * @since 3.0 * @access private */ private function _destroy() { global $wpdb; self::debug('executing DESTROY process'); $offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0; /** * Limit images each time before redirection to fix Out of memory issue. #665465 * @since 2.9.8 */ // Start deleting files $limit = apply_filters('litespeed_imgoptm_destroy_max_rows', 500); $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; $q = $wpdb->prepare($img_q, array($offset * $limit, $limit)); $list = $wpdb->get_results($q); $i = 0; foreach ($list as $v) { if (!$v->post_id) { continue; } $meta_value = $this->_parse_wp_meta_value($v); if (!$meta_value) { continue; } $i++; $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_destroy_optm_file($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_destroy_optm_file'), $meta_value['sizes']); } } self::debug('batch switched images total: ' . $i); $offset++; $to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1))); if ($to_be_continued) { # Check if post_id is beyond next_post_id self::debug('[next_post_id] ' . $this->_summary['next_post_id'] . ' [cursor post id] ' . $to_be_continued->post_id); if ($to_be_continued->post_id <= $this->_summary['next_post_id']) { self::debug('redirecting to next'); return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_DESTROY); } self::debug('🎊 Finished destroying'); } // Delete postmeta info $q = "DELETE FROM `$wpdb->postmeta` WHERE meta_key = %s"; $wpdb->query($wpdb->prepare($q, self::DB_SIZE)); $wpdb->query($wpdb->prepare($q, self::DB_SET)); // Delete img_optm table $this->__data->tb_del('img_optm'); $this->__data->tb_del('img_optming'); // Clear options table summary info self::delete_option('_summary'); self::delete_option(self::DB_NEED_PULL); $msg = __('Destroy all optimization data successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Destroy optm file */ private function _destroy_optm_file($meta_value, $is_ori_file = false) { $short_file_path = $meta_value['file']; if (!$is_ori_file) { $short_file_path = $this->tmp_path . $short_file_path; } self::debug('deleting ' . $short_file_path); // del webp $this->__media->info($short_file_path . '.webp', $this->tmp_pid) && $this->__media->del($short_file_path . '.webp', $this->tmp_pid); $this->__media->info($short_file_path . '.optm.webp', $this->tmp_pid) && $this->__media->del($short_file_path . '.optm.webp', $this->tmp_pid); // del avif $this->__media->info($short_file_path . '.avif', $this->tmp_pid) && $this->__media->del($short_file_path . '.avif', $this->tmp_pid); $this->__media->info($short_file_path . '.optm.avif', $this->tmp_pid) && $this->__media->del($short_file_path . '.optm.avif', $this->tmp_pid); $extension = pathinfo($short_file_path, PATHINFO_EXTENSION); $local_filename = substr($short_file_path, 0, -strlen($extension) - 1); $bk_file = $local_filename . '.bk.' . $extension; $bk_optm_file = $local_filename . '.bk.optm.' . $extension; // del optimized ori if ($this->__media->info($bk_file, $this->tmp_pid)) { self::debug('deleting optim ori'); $this->__media->del($short_file_path, $this->tmp_pid); $this->__media->rename($bk_file, $short_file_path, $this->tmp_pid); } $this->__media->info($bk_optm_file, $this->tmp_pid) && $this->__media->del($bk_optm_file, $this->tmp_pid); } /** * Rescan to find new generated images * * @since 1.6.7 * @access private */ private function _rescan() { global $wpdb; exit('tobedone'); $offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0; $limit = 500; self::debug('rescan images'); // Get images $q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a, `$wpdb->postmeta` b WHERE a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') AND a.ID = b.post_id AND b.meta_key = '_wp_attachment_metadata' ORDER BY a.ID LIMIT %d, %d "; $list = $wpdb->get_results($wpdb->prepare($q, $offset * $limit, $limit + 1)); // last one is the seed for next batch if (!$list) { $msg = __('Rescanned successfully.', 'litespeed-cache'); Admin_Display::success($msg); self::debug('rescan bypass: no gathered image found'); return; } if (count($list) == $limit + 1) { $to_be_continued = true; array_pop($list); // last one is the seed for next round, discard here. } else { $to_be_continued = false; } // Prepare post_ids to inquery gathered images $pid_set = array(); $scanned_list = array(); foreach ($list as $v) { $meta_value = $this->_parse_wp_meta_value($v); if (!$meta_value) { continue; } $scanned_list[] = array( 'pid' => $v->post_id, 'meta' => $meta_value, ); $pid_set[] = $v->post_id; } // Build gathered images $q = "SELECT src, post_id FROM `$this->_table_img_optm` WHERE post_id IN (" . implode(',', array_fill(0, count($pid_set), '%d')) . ')'; $list = $wpdb->get_results($wpdb->prepare($q, $pid_set)); foreach ($list as $v) { $this->_existed_src_list[] = $v->post_id . '.' . $v->src; } // Find new images foreach ($scanned_list as $v) { $meta_value = $v['meta']; // Parse all child src and put them into $this->_img_in_queue, missing ones to $this->_img_in_queue_missed $this->tmp_pid = $v['pid']; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_append_img_queue($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_append_img_queue'), $meta_value['sizes']); } } self::debug('rescanned [img] ' . count($this->_img_in_queue)); $count = count($this->_img_in_queue); if ($count > 0) { // Save to DB $this->_save_raw(); } if ($to_be_continued) { return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_RESCAN); } $msg = $count ? sprintf(__('Rescanned %d images successfully.', 'litespeed-cache'), $count) : __('Rescanned successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Calculate bkup original images storage * * @since 2.2.6 * @access private */ private function _calc_bkup() { global $wpdb; $offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0; $limit = 500; if (!$offset) { $this->_summary['bk_summary'] = array( 'date' => time(), 'count' => 0, 'sum' => 0, ); } $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; $q = $wpdb->prepare($img_q, array($offset * $limit, $limit)); $list = $wpdb->get_results($q); foreach ($list as $v) { if (!$v->post_id) { continue; } $meta_value = $this->_parse_wp_meta_value($v); if (!$meta_value) { continue; } $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_get_bk_size($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_get_bk_size'), $meta_value['sizes']); } } $this->_summary['bk_summary']['date'] = time(); self::save_summary(); self::debug('_calc_bkup total: ' . $this->_summary['bk_summary']['count'] . ' [size] ' . $this->_summary['bk_summary']['sum']); $offset++; $to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1))); if ($to_be_continued) { return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_CALC_BKUP); } $msg = __('Calculated backups successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Calculate single size */ private function _get_bk_size($meta_value, $is_ori_file = false) { $short_file_path = $meta_value['file']; if (!$is_ori_file) { $short_file_path = $this->tmp_path . $short_file_path; } $extension = pathinfo($short_file_path, PATHINFO_EXTENSION); $local_filename = substr($short_file_path, 0, -strlen($extension) - 1); $bk_file = $local_filename . '.bk.' . $extension; $img_info = $this->__media->info($bk_file, $this->tmp_pid); if (!$img_info) { return; } $this->_summary['bk_summary']['count']++; $this->_summary['bk_summary']['sum'] += $img_info['size']; } /** * Delete bkup original images storage * * @since 2.5 * @access public */ public function rm_bkup() { global $wpdb; if (!$this->__data->tb_exist('img_optming')) { return; } $offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0; $limit = 500; if (empty($this->_summary['rmbk_summary'])) { $this->_summary['rmbk_summary'] = array( 'date' => time(), 'count' => 0, 'sum' => 0, ); } $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; $q = $wpdb->prepare($img_q, array($offset * $limit, $limit)); $list = $wpdb->get_results($q); foreach ($list as $v) { if (!$v->post_id) { continue; } $meta_value = $this->_parse_wp_meta_value($v); if (!$meta_value) { continue; } $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_del_bk_file($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_del_bk_file'), $meta_value['sizes']); } } $this->_summary['rmbk_summary']['date'] = time(); self::save_summary(); self::debug('rm_bkup total: ' . $this->_summary['rmbk_summary']['count'] . ' [size] ' . $this->_summary['rmbk_summary']['sum']); $offset++; $to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1))); if ($to_be_continued) { return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_RM_BKUP); } $msg = __('Removed backups successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Delete single file */ private function _del_bk_file($meta_value, $is_ori_file = false) { $short_file_path = $meta_value['file']; if (!$is_ori_file) { $short_file_path = $this->tmp_path . $short_file_path; } $extension = pathinfo($short_file_path, PATHINFO_EXTENSION); $local_filename = substr($short_file_path, 0, -strlen($extension) - 1); $bk_file = $local_filename . '.bk.' . $extension; $img_info = $this->__media->info($bk_file, $this->tmp_pid); if (!$img_info) { return; } $this->_summary['rmbk_summary']['count']++; $this->_summary['rmbk_summary']['sum'] += $img_info['size']; $this->__media->del($bk_file, $this->tmp_pid); } /** * Count images * * @since 1.6 * @access public */ public function img_count() { global $wpdb; $q = "SELECT count(*) FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') "; $groups_all = $wpdb->get_var($q); $groups_new = $wpdb->get_var($q . ' AND ID>' . (int) $this->_summary['next_post_id'] . ' ORDER BY ID'); $groups_done = $wpdb->get_var($q . ' AND ID<=' . (int) $this->_summary['next_post_id'] . ' ORDER BY ID'); $q = "SELECT b.post_id FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID DESC LIMIT 1 "; $max_id = $wpdb->get_var($q); $count_list = array( 'max_id' => $max_id, 'groups_all' => $groups_all, 'groups_new' => $groups_new, 'groups_done' => $groups_done, ); // images count from work table if ($this->__data->tb_exist('img_optming')) { $q = "SELECT COUNT(DISTINCT post_id),COUNT(*) FROM `$this->_table_img_optming` WHERE optm_status = %d"; $groups_to_check = array(self::STATUS_RAW, self::STATUS_REQUESTED, self::STATUS_NOTIFIED, self::STATUS_ERR_FETCH); foreach ($groups_to_check as $v) { $count_list['img.' . $v] = $count_list['group.' . $v] = 0; list($count_list['group.' . $v], $count_list['img.' . $v]) = $wpdb->get_row($wpdb->prepare($q, $v), ARRAY_N); } } return $count_list; } /** * Check if fetch cron is running * * @since 1.6.2 * @access public */ public function cron_running($bool_res = true) { $last_run = !empty($this->_summary['last_pull']) ? $this->_summary['last_pull'] : 0; $is_running = $last_run && time() - $last_run < 120; if ($bool_res) { return $is_running; } return array($last_run, $is_running); } /** * Update fetch cron timestamp tag * * @since 1.6.2 * @access private */ private function _update_cron_running($done = false) { $this->_summary['last_pull'] = time(); if ($done) { // Only update cron tag when its from the active running cron if ($this->_cron_ran) { // Rollback for next running $this->_summary['last_pull'] -= 120; } else { return; } } self::save_summary(); $this->_cron_ran = true; } /** * Batch switch images to ori/optm version * * @since 1.6.2 * @access public */ public function batch_switch($type) { global $wpdb; if (defined('LITESPEED_CLI') || defined('DOING_CRON')) { $offset = 0; while ($offset !== 'done') { Admin_Display::info("Starting switch to $type [offset] $offset"); $offset = $this->_batch_switch($type, $offset); } } else { $offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0; $newOffset = $this->_batch_switch($type, $offset); if ($newOffset !== 'done') { return Router::self_redirect(Router::ACTION_IMG_OPTM, $type); } } $msg = __('Switched images successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Switch images per offset */ private function _batch_switch($type, $offset) { global $wpdb; $limit = 500; $this->tmp_type = $type; $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; $q = $wpdb->prepare($img_q, array($offset * $limit, $limit)); $list = $wpdb->get_results($q); $i = 0; foreach ($list as $v) { if (!$v->post_id) { continue; } $meta_value = $this->_parse_wp_meta_value($v); if (!$meta_value) { continue; } $i++; $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_switch_bk_file($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_switch_bk_file'), $meta_value['sizes']); } } self::debug('batch switched images total: ' . $i . ' [type] ' . $type); $offset++; $to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1))); if ($to_be_continued) { return $offset; } return 'done'; } /** * Delete single file */ private function _switch_bk_file($meta_value, $is_ori_file = false) { $short_file_path = $meta_value['file']; if (!$is_ori_file) { $short_file_path = $this->tmp_path . $short_file_path; } $extension = pathinfo($short_file_path, PATHINFO_EXTENSION); $local_filename = substr($short_file_path, 0, -strlen($extension) - 1); $bk_file = $local_filename . '.bk.' . $extension; $bk_optm_file = $local_filename . '.bk.optm.' . $extension; // self::debug('_switch_bk_file ' . $bk_file . ' [type] ' . $this->tmp_type); // switch to ori if ($this->tmp_type === self::TYPE_BATCH_SWITCH_ORI || $this->tmp_type == 'orig') { // self::debug('switch to orig ' . $bk_file); if (!$this->__media->info($bk_file, $this->tmp_pid)) { return; } $this->__media->rename($local_filename . '.' . $extension, $bk_optm_file, $this->tmp_pid); $this->__media->rename($bk_file, $local_filename . '.' . $extension, $this->tmp_pid); } // switch to optm elseif ($this->tmp_type === self::TYPE_BATCH_SWITCH_OPTM || $this->tmp_type == 'optm') { // self::debug('switch to optm ' . $bk_file); if (!$this->__media->info($bk_optm_file, $this->tmp_pid)) { return; } $this->__media->rename($local_filename . '.' . $extension, $bk_file, $this->tmp_pid); $this->__media->rename($bk_optm_file, $local_filename . '.' . $extension, $this->tmp_pid); } } /** * Switch image between original one and optimized one * * @since 1.6.2 * @access private */ private function _switch_optm_file($type) { Admin_Display::success(__('Switched to optimized file successfully.', 'litespeed-cache')); return; global $wpdb; $pid = substr($type, 4); $switch_type = substr($type, 0, 4); $q = "SELECT src,post_id FROM `$this->_table_img_optm` WHERE post_id = %d AND optm_status = %d"; $list = $wpdb->get_results($wpdb->prepare($q, array($pid, self::STATUS_PULLED))); $msg = 'Unknown Msg'; foreach ($list as $v) { // to switch webp file if ($switch_type === 'webp') { if ($this->__media->info($v->src . '.webp', $v->post_id)) { $this->__media->rename($v->src . '.webp', $v->src . '.optm.webp', $v->post_id); self::debug('Disabled WebP: ' . $v->src); $msg = __('Disabled WebP file successfully.', 'litespeed-cache'); } elseif ($this->__media->info($v->src . '.optm.webp', $v->post_id)) { $this->__media->rename($v->src . '.optm.webp', $v->src . '.webp', $v->post_id); self::debug('Enable WebP: ' . $v->src); $msg = __('Enabled WebP file successfully.', 'litespeed-cache'); } } // to switch avif file elseif ($switch_type === 'avif') { if ($this->__media->info($v->src . '.avif', $v->post_id)) { $this->__media->rename($v->src . '.avif', $v->src . '.optm.avif', $v->post_id); self::debug('Disabled AVIF: ' . $v->src); $msg = __('Disabled AVIF file successfully.', 'litespeed-cache'); } elseif ($this->__media->info($v->src . '.optm.avif', $v->post_id)) { $this->__media->rename($v->src . '.optm.avif', $v->src . '.avif', $v->post_id); self::debug('Enable AVIF: ' . $v->src); $msg = __('Enabled AVIF file successfully.', 'litespeed-cache'); } } // to switch original file else { $extension = pathinfo($v->src, PATHINFO_EXTENSION); $local_filename = substr($v->src, 0, -strlen($extension) - 1); $bk_file = $local_filename . '.bk.' . $extension; $bk_optm_file = $local_filename . '.bk.optm.' . $extension; // revert ori back if ($this->__media->info($bk_file, $v->post_id)) { $this->__media->rename($v->src, $bk_optm_file, $v->post_id); $this->__media->rename($bk_file, $v->src, $v->post_id); self::debug('Restore original img: ' . $bk_file); $msg = __('Restored original file successfully.', 'litespeed-cache'); } elseif ($this->__media->info($bk_optm_file, $v->post_id)) { $this->__media->rename($v->src, $bk_file, $v->post_id); $this->__media->rename($bk_optm_file, $v->src, $v->post_id); self::debug('Switch to optm img: ' . $v->src); $msg = __('Switched to optimized file successfully.', 'litespeed-cache'); } } } Admin_Display::success($msg); } /** * Delete one optm data and recover original file * * @since 2.4.2 * @access public */ public function reset_row($post_id) { global $wpdb; if (!$post_id) { return; } // Gathered image don't have DB_SIZE info yet // $size_meta = get_post_meta( $post_id, self::DB_SIZE, true ); // if ( ! $size_meta ) { // return; // } self::debug('_reset_row [pid] ' . $post_id); # TODO: Load image sub files $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->postmeta` b WHERE b.post_id =%d AND b.meta_key = '_wp_attachment_metadata'"; $q = $wpdb->prepare($img_q, array($post_id)); $v = $wpdb->get_row($q); $meta_value = $this->_parse_wp_meta_value($v); if ($meta_value) { $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; $this->_destroy_optm_file($meta_value, true); if (!empty($meta_value['sizes'])) { array_map(array($this, '_destroy_optm_file'), $meta_value['sizes']); } } delete_post_meta($post_id, self::DB_SIZE); delete_post_meta($post_id, self::DB_SET); $msg = __('Reset the optimized data successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Show an image's optm status * * @since 1.6.5 * @access public */ public function check_img() { global $wpdb; $pid = $_POST['data']; self::debug('Check image [ID] ' . $pid); $data = array(); $data['img_count'] = $this->img_count(); $data['optm_summary'] = self::get_summary(); $data['_wp_attached_file'] = get_post_meta($pid, '_wp_attached_file', true); $data['_wp_attachment_metadata'] = get_post_meta($pid, '_wp_attachment_metadata', true); // Get img_optm data $q = "SELECT * FROM `$this->_table_img_optm` WHERE post_id = %d"; $list = $wpdb->get_results($wpdb->prepare($q, $pid)); $img_data = array(); if ($list) { foreach ($list as $v) { $img_data[] = array( 'id' => $v->id, 'optm_status' => $v->optm_status, 'src' => $v->src, 'srcpath_md5' => $v->srcpath_md5, 'src_md5' => $v->src_md5, 'server_info' => $v->server_info, ); } } $data['img_data'] = $img_data; return array('_res' => 'ok', 'data' => $data); } /** * Handle all request actions from main cls * * @since 2.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_RESET_ROW: $this->reset_row(!empty($_GET['id']) ? $_GET['id'] : false); break; case self::TYPE_CALC_BKUP: $this->_calc_bkup(); break; case self::TYPE_RM_BKUP: $this->rm_bkup(); break; case self::TYPE_NEW_REQ: $this->new_req(); break; case self::TYPE_RESCAN: $this->_rescan(); break; case self::TYPE_RESET_COUNTER: $this->_reset_counter(); break; case self::TYPE_DESTROY: $this->_destroy(); break; case self::TYPE_CLEAN: $this->clean(); break; case self::TYPE_PULL: self::start_async(); break; case self::TYPE_BATCH_SWITCH_ORI: case self::TYPE_BATCH_SWITCH_OPTM: $this->batch_switch($type); break; case substr($type, 0, 4) === 'avif': case substr($type, 0, 4) === 'webp': case substr($type, 0, 4) === 'orig': $this->_switch_optm_file($type); break; default: break; } Admin::redirect(); } } src/data_structure/img_optming.sql 0000644 00000000526 15162130422 0013415 0 ustar 00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `post_id` bigint(20) unsigned NOT NULL DEFAULT '0', `optm_status` tinyint(4) NOT NULL DEFAULT '0', `src` varchar(1000) NOT NULL DEFAULT '', `server_info` text NOT NULL, PRIMARY KEY (`id`), KEY `post_id` (`post_id`), KEY `optm_status` (`optm_status`), KEY `src` (`src`(191)) src/data_structure/avatar.sql 0000644 00000000404 15162130423 0012356 0 ustar 00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `url` varchar(1000) NOT NULL DEFAULT '', `md5` varchar(128) NOT NULL DEFAULT '', `dateline` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `md5` (`md5`), KEY `dateline` (`dateline`) src/data_structure/img_optm.sql 0000644 00000000632 15162130424 0012717 0 ustar 00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `post_id` bigint(20) unsigned NOT NULL DEFAULT '0', `optm_status` tinyint(4) NOT NULL DEFAULT '0', `src` text NOT NULL, `src_filesize` int(11) NOT NULL DEFAULT '0', `target_filesize` int(11) NOT NULL DEFAULT '0', `webp_filesize` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `post_id` (`post_id`), KEY `optm_status` (`optm_status`) src/data_structure/url.sql 0000644 00000000315 15162130424 0011704 0 ustar 00 `id` bigint(20) NOT NULL AUTO_INCREMENT, `url` varchar(500) NOT NULL, `cache_tags` varchar(1000) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `url` (`url`(191)), KEY `cache_tags` (`cache_tags`(191)) src/data_structure/crawler_blacklist.sql 0000644 00000000626 15162130425 0014577 0 ustar 00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `url` varchar(1000) NOT NULL DEFAULT '', `res` varchar(255) NOT NULL DEFAULT '' COMMENT '-=Not Blacklist, B=blacklist', `reason` text NOT NULL COMMENT 'Reason for blacklist, comma separated', `mtime` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`), KEY `url` (`url`(191)), KEY `res` (`res`) src/data_structure/crawler.sql 0000644 00000000632 15162130425 0012544 0 ustar 00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `url` varchar(1000) NOT NULL DEFAULT '', `res` varchar(255) NOT NULL DEFAULT '' COMMENT '-=not crawl, H=hit, M=miss, B=blacklist', `reason` text NOT NULL COMMENT 'response code, comma separated', `mtime` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`), KEY `url` (`url`(191)), KEY `res` (`res`) src/data_structure/url_file.sql 0000644 00000001210 15162130426 0012700 0 ustar 00 `id` bigint(20) NOT NULL AUTO_INCREMENT, `url_id` bigint(20) NOT NULL, `vary` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'md5 of final vary', `filename` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'md5 of file content', `type` tinyint(4) NOT NULL COMMENT 'css=1,js=2,ccss=3,ucss=4', `mobile` tinyint(4) NOT NULL COMMENT 'mobile=1', `webp` tinyint(4) NOT NULL COMMENT 'webp=1', `expired` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `filename` (`filename`), KEY `type` (`type`), KEY `url_id_2` (`url_id`,`vary`,`type`), KEY `filename_2` (`filename`,`expired`), KEY `url_id` (`url_id`,`expired`) src/data.cls.php 0000644 00000043164 15162130430 0007540 0 ustar 00 <?php /** * The class to store and manage litespeed db data. * * @since 1.3.1 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Data extends Root { const LOG_TAG = '🚀'; private $_db_updater = array( '3.5.0.3' => array('litespeed_update_3_5'), '4.0' => array('litespeed_update_4'), '4.1' => array('litespeed_update_4_1'), '4.3' => array('litespeed_update_4_3'), '4.4.4-b1' => array('litespeed_update_4_4_4'), '5.3-a5' => array('litespeed_update_5_3'), '7.0-b26' => array('litespeed_update_7'), '7.0.1-b1' => array('litespeed_update_7_0_1'), ); private $_db_site_updater = array( // Example // '2.0' => array( // 'litespeed_update_site_2_0', // ), ); private $_url_file_types = array( 'css' => 1, 'js' => 2, 'ccss' => 3, 'ucss' => 4, ); const TB_IMG_OPTM = 'litespeed_img_optm'; const TB_IMG_OPTMING = 'litespeed_img_optming'; // working table const TB_AVATAR = 'litespeed_avatar'; const TB_CRAWLER = 'litespeed_crawler'; const TB_CRAWLER_BLACKLIST = 'litespeed_crawler_blacklist'; const TB_URL = 'litespeed_url'; const TB_URL_FILE = 'litespeed_url_file'; /** * Init * * @since 1.3.1 */ public function __construct() { } /** * Correct table existence * * Call when activate -> update_confs() * Call when update_confs() * * @since 3.0 * @access public */ public function correct_tb_existence() { // Gravatar if ($this->conf(Base::O_DISCUSS_AVATAR_CACHE)) { $this->tb_create('avatar'); } // Crawler if ($this->conf(Base::O_CRAWLER)) { $this->tb_create('crawler'); $this->tb_create('crawler_blacklist'); } // URL mapping $this->tb_create('url'); $this->tb_create('url_file'); // Image optm is a bit different. Only trigger creation when sending requests. Drop when destroying. } /** * Upgrade conf to latest format version from previous versions * * NOTE: Only for v3.0+ * * @since 3.0 * @access public */ public function conf_upgrade($ver) { // Skip count check if `Use Primary Site Configurations` is on // Deprecated since v3.0 as network primary site didn't override the subsites conf yet // if ( ! is_main_site() && ! empty ( $this->_site_options[ self::NETWORK_O_USE_PRIMARY ] ) ) { // return; // } if ($this->_get_upgrade_lock()) { return; } $this->_set_upgrade_lock(true); require_once LSCWP_DIR . 'src/data.upgrade.func.php'; // Init log manually if ($this->conf(Base::O_DEBUG)) { $this->cls('Debug2')->init(); } foreach ($this->_db_updater as $k => $v) { if (version_compare($ver, $k, '<')) { // run each callback foreach ($v as $v2) { self::debug("Updating [ori_v] $ver \t[to] $k \t[func] $v2"); call_user_func($v2); } } } // Reload options $this->cls('Conf')->load_options(); $this->correct_tb_existence(); // Update related files $this->cls('Activation')->update_files(); // Update version to latest Conf::delete_option(Base::_VER); Conf::add_option(Base::_VER, Core::VER); self::debug('Updated version to ' . Core::VER); $this->_set_upgrade_lock(false); !defined('LSWCP_EMPTYCACHE') && define('LSWCP_EMPTYCACHE', true); // clear all sites caches Purge::purge_all(); return 'upgrade'; } /** * Upgrade site conf to latest format version from previous versions * * NOTE: Only for v3.0+ * * @since 3.0 * @access public */ public function conf_site_upgrade($ver) { if ($this->_get_upgrade_lock()) { return; } $this->_set_upgrade_lock(true); require_once LSCWP_DIR . 'src/data.upgrade.func.php'; foreach ($this->_db_site_updater as $k => $v) { if (version_compare($ver, $k, '<')) { // run each callback foreach ($v as $v2) { self::debug("Updating site [ori_v] $ver \t[to] $k \t[func] $v2"); call_user_func($v2); } } } // Reload options $this->cls('Conf')->load_site_options(); Conf::delete_site_option(Base::_VER); Conf::add_site_option(Base::_VER, Core::VER); self::debug('Updated site_version to ' . Core::VER); $this->_set_upgrade_lock(false); !defined('LSWCP_EMPTYCACHE') && define('LSWCP_EMPTYCACHE', true); // clear all sites caches Purge::purge_all(); } /** * Check if upgrade script is running or not * * @since 3.0.1 */ private function _get_upgrade_lock() { $is_upgrading = get_option('litespeed.data.upgrading'); if (!$is_upgrading) { $this->_set_upgrade_lock(false); // set option value to existed to avoid repeated db query next time } if ($is_upgrading && time() - $is_upgrading < 3600) { return $is_upgrading; } return false; } /** * Show the upgrading banner if upgrade script is running * * @since 3.0.1 */ public function check_upgrading_msg() { $is_upgrading = $this->_get_upgrade_lock(); if (!$is_upgrading) { return; } Admin_Display::info( sprintf( __('The database has been upgrading in the background since %s. This message will disappear once upgrade is complete.', 'litespeed-cache'), '<code>' . Utility::readable_time($is_upgrading) . '</code>' ) . ' [LiteSpeed]', true ); } /** * Set lock for upgrade process * * @since 3.0.1 */ private function _set_upgrade_lock($lock) { if (!$lock) { update_option('litespeed.data.upgrading', -1); } else { update_option('litespeed.data.upgrading', time()); } } /** * Upgrade the conf to v3.0 from previous v3.0- data * * NOTE: Only for v3.0- * * @since 3.0 * @access public */ public function try_upgrade_conf_3_0() { $previous_options = get_option('litespeed-cache-conf'); if (!$previous_options) { return 'new'; } $ver = $previous_options['version']; !defined('LSCWP_CUR_V') && define('LSCWP_CUR_V', $ver); // Init log manually if ($this->conf(Base::O_DEBUG)) { $this->cls('Debug2')->init(); } self::debug('Upgrading previous settings [from] ' . $ver . ' [to] v3.0'); if ($this->_get_upgrade_lock()) { return; } $this->_set_upgrade_lock(true); require_once LSCWP_DIR . 'src/data.upgrade.func.php'; // Here inside will update the version to v3.0 litespeed_update_3_0($ver); $this->_set_upgrade_lock(false); self::debug('Upgraded to v3.0'); // Upgrade from 3.0 to latest version $ver = '3.0'; if (Core::VER != $ver) { return $this->conf_upgrade($ver); } else { // Reload options $this->cls('Conf')->load_options(); $this->correct_tb_existence(); !defined('LSWCP_EMPTYCACHE') && define('LSWCP_EMPTYCACHE', true); // clear all sites caches Purge::purge_all(); return 'upgrade'; } } /** * Get the table name * * @since 3.0 * @access public */ public function tb($tb) { global $wpdb; switch ($tb) { case 'img_optm': return $wpdb->prefix . self::TB_IMG_OPTM; break; case 'img_optming': return $wpdb->prefix . self::TB_IMG_OPTMING; break; case 'avatar': return $wpdb->prefix . self::TB_AVATAR; break; case 'crawler': return $wpdb->prefix . self::TB_CRAWLER; break; case 'crawler_blacklist': return $wpdb->prefix . self::TB_CRAWLER_BLACKLIST; break; case 'url': return $wpdb->prefix . self::TB_URL; break; case 'url_file': return $wpdb->prefix . self::TB_URL_FILE; break; default: break; } } /** * Check if one table exists or not * * @since 3.0 * @access public */ public function tb_exist($tb) { global $wpdb; return $wpdb->get_var("SHOW TABLES LIKE '" . $this->tb($tb) . "'"); } /** * Get data structure of one table * * @since 2.0 * @access private */ private function _tb_structure($tb) { return File::read(LSCWP_DIR . 'src/data_structure/' . $tb . '.sql'); } /** * Create img optm table and sync data from wp_postmeta * * @since 3.0 * @access public */ public function tb_create($tb) { global $wpdb; self::debug2('[Data] Checking table ' . $tb); // Check if table exists first if ($this->tb_exist($tb)) { self::debug2('[Data] Existed'); return; } self::debug('Creating ' . $tb); $sql = sprintf( 'CREATE TABLE IF NOT EXISTS `%1$s` (' . $this->_tb_structure($tb) . ') %2$s;', $this->tb($tb), $wpdb->get_charset_collate() // 'DEFAULT CHARSET=utf8' ); $res = $wpdb->query($sql); if ($res !== true) { self::debug('Warning! Creating table failed!', $sql); Admin_Display::error(Error::msg('failed_tb_creation', array('<code>' . $tb . '</code>', '<code>' . $sql . '</code>'))); } } /** * Drop table * * @since 3.0 * @access public */ public function tb_del($tb) { global $wpdb; if (!$this->tb_exist($tb)) { return; } self::debug('Deleting table ' . $tb); $q = 'DROP TABLE IF EXISTS ' . $this->tb($tb); $wpdb->query($q); } /** * Drop generated tables * * @since 3.0 * @access public */ public function tables_del() { $this->tb_del('avatar'); $this->tb_del('crawler'); $this->tb_del('crawler_blacklist'); $this->tb_del('url'); $this->tb_del('url_file'); // Deleting img_optm only can be done when destroy all optm images } /** * Keep table but clear all data * * @since 4.0 */ public function table_truncate($tb) { global $wpdb; $q = 'TRUNCATE TABLE ' . $this->tb($tb); $wpdb->query($q); } /** * Clean certain type of url_file * * @since 4.0 */ public function url_file_clean($file_type) { global $wpdb; if (!$this->tb_exist('url_file')) { return; } $type = $this->_url_file_types[$file_type]; $q = 'DELETE FROM ' . $this->tb('url_file') . ' WHERE `type` = %d'; $wpdb->query($wpdb->prepare($q, $type)); // Added to cleanup url table. See issue: https://wordpress.org/support/topic/wp_litespeed_url-1-1-gb-in-db-huge-big/ $wpdb->query( 'DELETE d FROM `' . $this->tb('url') . '` AS d LEFT JOIN `' . $this->tb('url_file') . '` AS f ON d.`id` = f.`url_id` WHERE f.`url_id` IS NULL' ); } /** * Generate filename based on URL, if content md5 existed, reuse existing file. * @since 4.0 */ public function save_url($request_url, $vary, $file_type, $filecon_md5, $path, $mobile = false, $webp = false) { global $wpdb; if (strlen($vary) > 32) { $vary = md5($vary); } $type = $this->_url_file_types[$file_type]; $tb_url = $this->tb('url'); $tb_url_file = $this->tb('url_file'); $q = "SELECT * FROM `$tb_url` WHERE url=%s"; $url_row = $wpdb->get_row($wpdb->prepare($q, $request_url), ARRAY_A); if (!$url_row) { $q = "INSERT INTO `$tb_url` SET url=%s"; $wpdb->query($wpdb->prepare($q, $request_url)); $url_id = $wpdb->insert_id; } else { $url_id = $url_row['id']; } $q = "SELECT * FROM `$tb_url_file` WHERE url_id=%d AND vary=%s AND type=%d AND expired=0"; $file_row = $wpdb->get_row($wpdb->prepare($q, array($url_id, $vary, $type)), ARRAY_A); // Check if has previous file or not if ($file_row && $file_row['filename'] == $filecon_md5) { return; } // If the new $filecon_md5 is marked as expired by previous records, clear those records $q = "DELETE FROM `$tb_url_file` WHERE filename = %s AND expired > 0"; $wpdb->query($wpdb->prepare($q, $filecon_md5)); // Check if there is any other record used the same filename or not $q = "SELECT id FROM `$tb_url_file` WHERE filename = %s AND expired = 0 AND id != %d LIMIT 1"; if ($file_row && $wpdb->get_var($wpdb->prepare($q, array($file_row['filename'], $file_row['id'])))) { $q = "UPDATE `$tb_url_file` SET filename=%s WHERE id=%d"; $wpdb->query($wpdb->prepare($q, array($filecon_md5, $file_row['id']))); return; } // New record needed $q = "INSERT INTO `$tb_url_file` SET url_id=%d, vary=%s, filename=%s, type=%d, mobile=%d, webp=%d, expired=0"; $wpdb->query($wpdb->prepare($q, array($url_id, $vary, $filecon_md5, $type, $mobile ? 1 : 0, $webp ? 1 : 0))); // Mark existing rows as expired if ($file_row) { $q = "UPDATE `$tb_url_file` SET expired=%d WHERE id=%d"; $expired = time() + 86400 * apply_filters('litespeed_url_file_expired_days', 20); $wpdb->query($wpdb->prepare($q, array($expired, $file_row['id']))); // Also check if has other files expired already to be deleted $q = "SELECT * FROM `$tb_url_file` WHERE url_id = %d AND expired BETWEEN 1 AND %d"; $q = $wpdb->prepare($q, array($url_id, time())); $list = $wpdb->get_results($q, ARRAY_A); if ($list) { foreach ($list as $v) { $file_to_del = $path . '/' . $v['filename'] . '.' . ($file_type == 'js' ? 'js' : 'css'); if (file_exists($file_to_del)) { // Safe to delete self::debug('Delete expired unused file: ' . $file_to_del); // Clear related lscache first to avoid cache copy of same URL w/ diff QS // Purge::add( Tag::TYPE_MIN . '.' . $file_row[ 'filename' ] . '.' . $file_type ); unlink($file_to_del); } } $q = "DELETE FROM `$tb_url_file` WHERE url_id = %d AND expired BETWEEN 1 AND %d"; $wpdb->query($wpdb->prepare($q, array($url_id, time()))); } } // Purge this URL to avoid cache copy of same URL w/ diff QS // $this->cls( 'Purge' )->purge_url( Utility::make_relative( $request_url ) ?: '/', true, true ); } /** * Load CCSS related file * @since 4.0 */ public function load_url_file($request_url, $vary, $file_type) { global $wpdb; if (strlen($vary) > 32) { $vary = md5($vary); } $type = $this->_url_file_types[$file_type]; self::debug2('load url file: ' . $request_url); $tb_url = $this->tb('url'); $q = "SELECT * FROM `$tb_url` WHERE url=%s"; $url_row = $wpdb->get_row($wpdb->prepare($q, $request_url), ARRAY_A); if (!$url_row) { return false; } $url_id = $url_row['id']; $tb_url_file = $this->tb('url_file'); $q = "SELECT * FROM `$tb_url_file` WHERE url_id=%d AND vary=%s AND type=%d AND expired=0"; $file_row = $wpdb->get_row($wpdb->prepare($q, array($url_id, $vary, $type)), ARRAY_A); if (!$file_row) { return false; } return $file_row['filename']; } /** * Mark all entries of one URL to expired * @since 4.5 */ public function mark_as_expired($request_url, $auto_q = false) { global $wpdb; $tb_url = $this->tb('url'); self::debug('Try to mark as expired: ' . $request_url); $q = "SELECT * FROM `$tb_url` WHERE url=%s"; $url_row = $wpdb->get_row($wpdb->prepare($q, $request_url), ARRAY_A); if (!$url_row) { return; } self::debug('Mark url_id=' . $url_row['id'] . ' as expired'); $tb_url_file = $this->tb('url_file'); $existing_url_files = array(); if ($auto_q) { $q = "SELECT a.*, b.url FROM `$tb_url_file` a LEFT JOIN `$tb_url` b ON b.id=a.url_id WHERE a.url_id=%d AND a.type=4 AND a.expired=0"; $q = $wpdb->prepare($q, $url_row['id']); $existing_url_files = $wpdb->get_results($q, ARRAY_A); } $q = "UPDATE `$tb_url_file` SET expired=%d WHERE url_id=%d AND type=4 AND expired=0"; $expired = time() + 86400 * apply_filters('litespeed_url_file_expired_days', 20); $wpdb->query($wpdb->prepare($q, array($expired, $url_row['id']))); return $existing_url_files; } /** * Get list from `data/css_excludes.txt` * * @since 3.6 */ public function load_css_exc($list) { $data = $this->_load_per_line('css_excludes.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/ccss_whitelist.txt` * * @since 7.1 */ public function load_ccss_whitelist($list) { $data = $this->_load_per_line('ccss_whitelist.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/ucss_whitelist.txt` * * @since 4.0 */ public function load_ucss_whitelist($list) { $data = $this->_load_per_line('ucss_whitelist.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/js_excludes.txt` * * @since 3.5 */ public function load_js_exc($list) { $data = $this->_load_per_line('js_excludes.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/js_defer_excludes.txt` * * @since 3.6 */ public function load_js_defer_exc($list) { $data = $this->_load_per_line('js_defer_excludes.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/optm_uri_exc.txt` * * @since 5.4 */ public function load_optm_uri_exc($list) { $data = $this->_load_per_line('optm_uri_exc.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/esi.nonces.txt` * * @since 3.5 */ public function load_esi_nonces($list) { $data = $this->_load_per_line('esi.nonces.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Get list from `data/cache_nocacheable.txt` * * @since 6.3.0.1 */ public function load_cache_nocacheable($list) { $data = $this->_load_per_line('cache_nocacheable.txt'); if ($data) { $list = array_unique(array_filter(array_merge($list, $data))); } return $list; } /** * Load file per line * * Support two kinds of comments: * 1. `# this is comment` * 2. `##this is comment` * * @since 3.5 */ private function _load_per_line($file) { $data = File::read(LSCWP_DIR . 'data/' . $file); $data = explode(PHP_EOL, $data); $list = array(); foreach ($data as $v) { // Drop two kinds of comments if (strpos($v, '##') !== false) { $v = trim(substr($v, 0, strpos($v, '##'))); } if (strpos($v, '# ') !== false) { $v = trim(substr($v, 0, strpos($v, '# '))); } if (!$v) { continue; } $list[] = $v; } return $list; } } src/data.upgrade.func.php 0000644 00000056156 15162130432 0011347 0 ustar 00 <?php /** * Database upgrade funcs * * NOTE: whenever called this file, always call Data::get_upgrade_lock and Data::_set_upgrade_lock first. * * @since 3.0 */ defined('WPINC') || exit(); use LiteSpeed\Debug2; use LiteSpeed\Conf; use LiteSpeed\Admin_Display; use LiteSpeed\File; use LiteSpeed\Cloud; /** * Migrate v7.0- url_files URL from no trailing slash to trailing slash * @since 7.0.1 */ function litespeed_update_7_0_1() { global $wpdb; Debug2::debug('[Data] v7.0.1 upgrade started'); $tb_url = $wpdb->prefix . 'litespeed_url'; $tb_exists = $wpdb->get_var("SHOW TABLES LIKE '" . $tb_url . "'"); if (!$tb_exists) { Debug2::debug('[Data] Table `litespeed_url` not found, bypassed migration'); return; } $q = "SELECT * FROM `$tb_url` WHERE url LIKE 'https://%/'"; $q = $wpdb->prepare($q); $list = $wpdb->get_results($q, ARRAY_A); $existing_urls = array(); if ($list) { foreach ($list as $v) { $existing_urls[] = $v['url']; } } $q = "SELECT * FROM `$tb_url` WHERE url LIKE 'https://%'"; $q = $wpdb->prepare($q); $list = $wpdb->get_results($q, ARRAY_A); if (!$list) { return; } foreach ($list as $v) { if (substr($v['url'], -1) == '/') { continue; } $new_url = $v['url'] . '/'; if (in_array($new_url, $existing_urls)) { continue; } $q = "UPDATE `$tb_url` SET url = %s WHERE id = %d"; $q = $wpdb->prepare($q, $new_url, $v['id']); $wpdb->query($q); } } /** * Migrate from domain key to pk/sk for QC * @since 7.0 */ function litespeed_update_7() { Debug2::debug('[Data] v7 upgrade started'); $__cloud = Cloud::cls(); $domain_key = $__cloud->conf('api_key'); if (!$domain_key) { Debug2::debug('[Data] No domain key, bypassed migration'); return; } $new_prepared = $__cloud->init_qc_prepare(); if (!$new_prepared && $__cloud->activated()) { Debug2::debug('[Data] QC previously activated in v7, bypassed migration'); return; } $data = array( 'domain_key' => $domain_key, ); $resp = $__cloud->post(Cloud::SVC_D_V3UPGRADE, $data); if (!empty($resp['qc_activated'])) { if ($resp['qc_activated'] != 'deleted') { $cloud_summary_updates = array('qc_activated' => $resp['qc_activated']); if (!empty($resp['main_domain'])) { $cloud_summary_updates['main_domain'] = $resp['main_domain']; } Cloud::save_summary($cloud_summary_updates); Debug2::debug('[Data] Updated QC activated status to ' . $resp['qc_activated']); } } } /** * Append webp/mobile to url_file * @since 5.3 */ function litespeed_update_5_3() { global $wpdb; Debug2::debug('[Data] Upgrade url_file table'); $tb_exists = $wpdb->get_var('SHOW TABLES LIKE "' . $wpdb->prefix . 'litespeed_url_file"'); if ($tb_exists) { $q = 'ALTER TABLE `' . $wpdb->prefix . 'litespeed_url_file` ADD COLUMN `mobile` tinyint(4) NOT NULL COMMENT "mobile=1", ADD COLUMN `webp` tinyint(4) NOT NULL COMMENT "webp=1" '; $wpdb->query($q); } } /** * Add expired to url_file table * @since 4.4.4 */ function litespeed_update_4_4_4() { global $wpdb; Debug2::debug('[Data] Upgrade url_file table'); $tb_exists = $wpdb->get_var('SHOW TABLES LIKE "' . $wpdb->prefix . 'litespeed_url_file"'); if ($tb_exists) { $q = 'ALTER TABLE `' . $wpdb->prefix . 'litespeed_url_file` ADD COLUMN `expired` int(11) NOT NULL DEFAULT 0, ADD KEY `filename_2` (`filename`,`expired`), ADD KEY `url_id` (`url_id`,`expired`) '; $wpdb->query($q); } } /** * Drop cssjs table and rm cssjs folder * @since 4.3 */ function litespeed_update_4_3() { if (file_exists(LITESPEED_STATIC_DIR . '/ccsjs')) { File::rrmdir(LITESPEED_STATIC_DIR . '/ccsjs'); } } /** * Drop object cache data file * @since 4.1 */ function litespeed_update_4_1() { if (file_exists(WP_CONTENT_DIR . '/.object-cache.ini')) { unlink(WP_CONTENT_DIR . '/.object-cache.ini'); } } /** * Drop cssjs table and rm cssjs folder * @since 4.0 */ function litespeed_update_4() { global $wpdb; $tb = $wpdb->prefix . 'litespeed_cssjs'; $existed = $wpdb->get_var("SHOW TABLES LIKE '$tb'"); if (!$existed) { return; } $q = 'DROP TABLE IF EXISTS ' . $tb; $wpdb->query($q); if (file_exists(LITESPEED_STATIC_DIR . '/ccsjs')) { File::rrmdir(LITESPEED_STATIC_DIR . '/ccsjs'); } } /** * Append jQuery to JS optm exclude list for max compatibility * Turn off JS Combine and Defer * * @since 3.5.1 */ function litespeed_update_3_5() { $__conf = Conf::cls(); // Excludes jQuery foreach (array('optm-js_exc', 'optm-js_defer_exc') as $v) { $curr_setting = $__conf->conf($v); $curr_setting[] = 'jquery.js'; $curr_setting[] = 'jquery.min.js'; $__conf->update($v, $curr_setting); } // Turn off JS Combine and defer $show_msg = false; foreach (array('optm-js_comb', 'optm-js_defer', 'optm-js_inline_defer') as $v) { $curr_setting = $__conf->conf($v); if (!$curr_setting) { continue; } $show_msg = true; $__conf->update($v, false); } if ($show_msg) { $msg = sprintf( __( 'LiteSpeed Cache upgraded successfully. NOTE: Due to changes in this version, the settings %1$s and %2$s have been turned OFF. Please turn them back on manually and verify that your site layout is correct, and you have no JS errors.', 'litespeed-cache' ), '<code>' . __('JS Combine', 'litespeed-cache') . '</code>', '<code>' . __('JS Defer', 'litespeed-cache') . '</code>' ); $msg .= sprintf(' <a href="admin.php?page=litespeed-page_optm#settings_js">%s</a>.', __('Click here to settings', 'litespeed-cache')); Admin_Display::info($msg, false, true); } } /** * For version under v2.0 to v2.0+ * * @since 3.0 */ function litespeed_update_2_0($ver) { global $wpdb; // Table version only exists after all old data migrated // Last modified is v2.4.2 if (version_compare($ver, '2.4.2', '<')) { /** * Convert old data from postmeta to img_optm table * @since 2.0 */ // Migrate data from `wp_postmeta` to `wp_litespeed_img_optm` $mids_to_del = array(); $q = "SELECT * FROM $wpdb->postmeta WHERE meta_key = %s ORDER BY meta_id"; $meta_value_list = $wpdb->get_results($wpdb->prepare($q, 'litespeed-optimize-data')); if ($meta_value_list) { $max_k = count($meta_value_list) - 1; foreach ($meta_value_list as $k => $v) { $mids_to_del[] = $v->meta_id; // Delete from postmeta if (count($mids_to_del) > 100 || $k == $max_k) { $q = "DELETE FROM $wpdb->postmeta WHERE meta_id IN ( " . implode(',', array_fill(0, count($mids_to_del), '%s')) . ' ) '; $wpdb->query($wpdb->prepare($q, $mids_to_del)); $mids_to_del = array(); } } Debug2::debug('[Data] img_optm inserted records: ' . $k); } $q = "DELETE FROM $wpdb->postmeta WHERE meta_key = %s"; $rows = $wpdb->query($wpdb->prepare($q, 'litespeed-optimize-status')); Debug2::debug('[Data] img_optm delete optm_status records: ' . $rows); } /** * Add target_md5 field to table * @since 2.4.2 */ if (version_compare($ver, '2.4.2', '<') && version_compare($ver, '2.0', '>=')) { // NOTE: For new users, need to bypass this section $sql = sprintf('ALTER TABLE `%1$s` ADD `server_info` text NOT NULL, DROP COLUMN `server`', $wpdb->prefix . 'litespeed_img_optm'); $res = $wpdb->query($sql); if ($res !== true) { Debug2::debug('[Data] Warning: Alter table img_optm failed!', $sql); } else { Debug2::debug('[Data] Successfully upgraded table img_optm.'); } } // Delete img optm tb version delete_option($wpdb->prefix . 'litespeed_img_optm'); // Delete possible HTML optm data from wp_options delete_option('litespeed-cache-optimized'); // Delete HTML optm tb version delete_option($wpdb->prefix . 'litespeed_optimizer'); } /** * Move all options in litespeed-cache-conf from v3.0- to separate records * * @since 3.0 */ function litespeed_update_3_0($ver) { global $wpdb; // Upgrade v2.0- to v2.0 first if (version_compare($ver, '2.0', '<')) { litespeed_update_2_0($ver); } set_time_limit(86400); // conv items to litespeed.conf.* Debug2::debug('[Data] Conv items to litespeed.conf.*'); $data = array( 'litespeed-cache-exclude-cache-roles' => 'cache-exc_roles', 'litespeed-cache-drop_qs' => 'cache-drop_qs', 'litespeed-forced_cache_uri' => 'cache-force_uri', 'litespeed-cache_uri_priv' => 'cache-priv_uri', 'litespeed-excludes_uri' => 'cache-exc', 'litespeed-cache-vary-group' => 'cache-vary_group', 'litespeed-adv-purge_all_hooks' => 'purge-hook_all', 'litespeed-object_global_groups' => 'object-global_groups', 'litespeed-object_non_persistent_groups' => 'object-non_persistent_groups', 'litespeed-media-lazy-img-excludes' => 'media-lazy_exc', 'litespeed-media-lazy-img-cls-excludes' => 'media-lazy_cls_exc', 'litespeed-media-webp_attribute' => 'img_optm-webp_attr', 'litespeed-optm-css' => 'optm-ccss_con', 'litespeed-optm_excludes' => 'optm-exc', 'litespeed-optm-ccss-separate_posttype' => 'optm-ccss_sep_posttype', 'litespeed-optm-css-separate_uri' => 'optm-ccss_sep_uri', 'litespeed-optm-js-defer-excludes' => 'optm-js_defer_exc', 'litespeed-cache-dns_prefetch' => 'optm-dns_prefetch', 'litespeed-cache-exclude-optimization-roles' => 'optm-exc_roles', 'litespeed-log_ignore_filters' => 'debug-log_no_filters', // depreciated 'litespeed-log_ignore_part_filters' => 'debug-log_no_part_filters', // depreciated 'litespeed-cdn-ori_dir' => 'cdn-ori_dir', 'litespeed-cache-cdn_mapping' => 'cdn-mapping', 'litespeed-crawler-as-uids' => 'crawler-roles', 'litespeed-crawler-cookies' => 'crawler-cookies', ); foreach ($data as $k => $v) { $old_data = get_option($k); if ($old_data) { Debug2::debug("[Data] Convert $k"); // They must be an array if (!is_array($old_data) && $v != 'optm-ccss_con') { $old_data = explode("\n", $old_data); } if ($v == 'crawler-cookies') { $tmp = array(); $i = 0; foreach ($old_data as $k2 => $v2) { $tmp[$i]['name'] = $k2; $tmp[$i]['vals'] = explode("\n", $v2); $i++; } $old_data = $tmp; } add_option('litespeed.conf.' . $v, $old_data); } Debug2::debug("[Data] Delete $k"); delete_option($k); } // conv other items $data = array( 'litespeed-setting-mode' => 'litespeed.setting.mode', 'litespeed-media-need-pull' => 'litespeed.img_optm.need_pull', 'litespeed-env-ref' => 'litespeed.env.ref', 'litespeed-cache-cloudflare_status' => 'litespeed.cdn.cloudflare.status', ); foreach ($data as $k => $v) { $old_data = get_option($k); if ($old_data) { add_option($v, $old_data); } delete_option($k); } // Conv conf from litespeed-cache-conf child to litespeed.conf.* Debug2::debug('[Data] Conv conf from litespeed-cache-conf child to litespeed.conf.*'); $previous_options = get_option('litespeed-cache-conf'); $data = array( 'radio_select' => 'cache', 'hash' => 'hash', 'auto_upgrade' => 'auto_upgrade', 'news' => 'news', 'crawler_domain_ip' => 'server_ip', 'esi_enabled' => 'esi', 'esi_cached_admbar' => 'esi-cache_admbar', 'esi_cached_commform' => 'esi-cache_commform', 'heartbeat' => 'misc-heartbeat_front', 'cache_browser' => 'cache-browser', 'cache_browser_ttl' => 'cache-ttl_browser', 'instant_click' => 'util-instant_click', 'use_http_for_https_vary' => 'util-no_https_vary', 'purge_upgrade' => 'purge-upgrade', 'timed_urls' => 'purge-timed_urls', 'timed_urls_time' => 'purge-timed_urls_time', 'cache_priv' => 'cache-priv', 'cache_commenter' => 'cache-commenter', 'cache_rest' => 'cache-rest', 'cache_page_login' => 'cache-page_login', 'cache_favicon' => 'cache-favicon', 'cache_resources' => 'cache-resources', 'mobileview_enabled' => 'cache-mobile', 'mobileview_rules' => 'cache-mobile_rules', 'nocache_useragents' => 'cache-exc_useragents', 'nocache_cookies' => 'cache-exc_cookies', 'excludes_qs' => 'cache-exc_qs', 'excludes_cat' => 'cache-exc_cat', 'excludes_tag' => 'cache-exc_tag', 'public_ttl' => 'cache-ttl_pub', 'private_ttl' => 'cache-ttl_priv', 'front_page_ttl' => 'cache-ttl_frontpage', 'feed_ttl' => 'cache-ttl_feed', 'login_cookie' => 'cache-login_cookie', 'debug_disable_all' => 'debug-disable_all', 'debug' => 'debug', 'admin_ips' => 'debug-ips', 'debug_level' => 'debug-level', 'log_file_size' => 'debug-filesize', 'debug_cookie' => 'debug-cookie', 'collapse_qs' => 'debug-collapse_qs', // 'log_filters' => 'debug-log_filters', 'crawler_cron_active' => 'crawler', // 'crawler_include_posts' => 'crawler-inc_posts', // 'crawler_include_pages' => 'crawler-inc_pages', // 'crawler_include_cats' => 'crawler-inc_cats', // 'crawler_include_tags' => 'crawler-inc_tags', // 'crawler_excludes_cpt' => 'crawler-exc_cpt', // 'crawler_order_links' => 'crawler-order_links', 'crawler_usleep' => 'crawler-usleep', 'crawler_run_duration' => 'crawler-run_duration', 'crawler_run_interval' => 'crawler-run_interval', 'crawler_crawl_interval' => 'crawler-crawl_interval', 'crawler_threads' => 'crawler-threads', 'crawler_load_limit' => 'crawler-load_limit', 'crawler_custom_sitemap' => 'crawler-sitemap', 'cache_object' => 'object', 'cache_object_kind' => 'object-kind', 'cache_object_host' => 'object-host', 'cache_object_port' => 'object-port', 'cache_object_life' => 'object-life', 'cache_object_persistent' => 'object-persistent', 'cache_object_admin' => 'object-admin', 'cache_object_transients' => 'object-transients', 'cache_object_db_id' => 'object-db_id', 'cache_object_user' => 'object-user', 'cache_object_pswd' => 'object-psw', 'cdn' => 'cdn', 'cdn_ori' => 'cdn-ori', 'cdn_exclude' => 'cdn-exc', // 'cdn_remote_jquery' => 'cdn-remote_jq', 'cdn_quic' => 'cdn-quic', 'cdn_cloudflare' => 'cdn-cloudflare', 'cdn_cloudflare_email' => 'cdn-cloudflare_email', 'cdn_cloudflare_key' => 'cdn-cloudflare_key', 'cdn_cloudflare_name' => 'cdn-cloudflare_name', 'cdn_cloudflare_zone' => 'cdn-cloudflare_zone', 'media_img_lazy' => 'media-lazy', 'media_img_lazy_placeholder' => 'media-lazy_placeholder', 'media_placeholder_resp' => 'media-placeholder_resp', 'media_placeholder_resp_color' => 'media-placeholder_resp_color', 'media_placeholder_resp_async' => 'media-placeholder_resp_async', 'media_iframe_lazy' => 'media-iframe_lazy', // 'media_img_lazyjs_inline' => 'media-lazyjs_inline', 'media_optm_auto' => 'img_optm-auto', 'media_optm_cron' => 'img_optm-cron', 'media_optm_ori' => 'img_optm-ori', 'media_rm_ori_bkup' => 'img_optm-rm_bkup', // 'media_optm_webp' => 'img_optm-webp', 'media_webp_replace' => 'img_optm-webp', 'media_optm_lossless' => 'img_optm-lossless', 'media_optm_exif' => 'img_optm-exif', 'media_webp_replace_srcset' => 'img_optm-webp_replace_srcset', 'css_minify' => 'optm-css_min', // 'css_inline_minify' => 'optm-css_inline_min', 'css_combine' => 'optm-css_comb', // 'css_combined_priority' => 'optm-css_comb_priority', // 'css_http2' => 'optm-css_http2', 'css_exclude' => 'optm-css_exc', 'js_minify' => 'optm-js_min', // 'js_inline_minify' => 'optm-js_inline_min', 'js_combine' => 'optm-js_comb', // 'js_combined_priority' => 'optm-js_comb_priority', // 'js_http2' => 'optm-js_http2', 'js_exclude' => 'optm-js_exc', // 'optimize_ttl' => 'optm-ttl', 'html_minify' => 'optm-html_min', 'optm_qs_rm' => 'optm-qs_rm', 'optm_ggfonts_rm' => 'optm-ggfonts_rm', 'optm_css_async' => 'optm-css_async', // 'optm_ccss_gen' => 'optm-ccss_gen', // 'optm_ccss_async' => 'optm-ccss_async', 'optm_css_async_inline' => 'optm-css_async_inline', 'optm_js_defer' => 'optm-js_defer', 'optm_emoji_rm' => 'optm-emoji_rm', // 'optm_exclude_jquery' => 'optm-exc_jq', 'optm_ggfonts_async' => 'optm-ggfonts_async', // 'optm_max_size' => 'optm-max_size', // 'optm_rm_comment' => 'optm-rm_comment', ); foreach ($data as $k => $v) { if (!isset($previous_options[$k])) { continue; } // The following values must be array if (!is_array($previous_options[$k])) { if (in_array($v, array('cdn-ori', 'cache-exc_cat', 'cache-exc_tag'))) { $previous_options[$k] = explode(',', $previous_options[$k]); $previous_options[$k] = array_filter($previous_options[$k]); } elseif (in_array($v, array('cache-mobile_rules', 'cache-exc_useragents', 'cache-exc_cookies'))) { $previous_options[$k] = explode('|', str_replace('\\ ', ' ', $previous_options[$k])); $previous_options[$k] = array_filter($previous_options[$k]); } elseif ( in_array($v, array( 'purge-timed_urls', 'cache-exc_qs', 'debug-ips', // 'crawler-exc_cpt', 'cdn-exc', 'optm-css_exc', 'optm-js_exc', )) ) { $previous_options[$k] = explode("\n", $previous_options[$k]); $previous_options[$k] = array_filter($previous_options[$k]); } } // Special handler for heartbeat if ($v == 'misc-heartbeat_front') { if (!$previous_options[$k]) { add_option('litespeed.conf.misc-heartbeat_front', true); add_option('litespeed.conf.misc-heartbeat_back', true); add_option('litespeed.conf.misc-heartbeat_editor', true); add_option('litespeed.conf.misc-heartbeat_front_ttl', 0); add_option('litespeed.conf.misc-heartbeat_back_ttl', 0); add_option('litespeed.conf.misc-heartbeat_editor_ttl', 0); } continue; } add_option('litespeed.conf.' . $v, $previous_options[$k]); } // Conv purge_by_post $data = array( '-' => 'purge-post_all', 'F' => 'purge-post_f', 'H' => 'purge-post_h', 'PGS' => 'purge-post_p', 'PGSRP' => 'purge-post_pwrp', 'A' => 'purge-post_a', 'Y' => 'purge-post_y', 'M' => 'purge-post_m', 'D' => 'purge-post_d', 'T' => 'purge-post_t', 'PT' => 'purge-post_pt', ); if (isset($previous_options['purge_by_post'])) { $purge_by_post = explode('.', $previous_options['purge_by_post']); foreach ($data as $k => $v) { add_option('litespeed.conf.' . $v, in_array($k, $purge_by_post)); } } // Conv 404/403/500 TTL $ttl_status = array(); if (isset($previous_options['403_ttl'])) { $ttl_status[] = '403 ' . $previous_options['403_ttl']; } if (isset($previous_options['404_ttl'])) { $ttl_status[] = '404 ' . $previous_options['404_ttl']; } if (isset($previous_options['500_ttl'])) { $ttl_status[] = '500 ' . $previous_options['500_ttl']; } add_option('litespeed.conf.cache-ttl_status', $ttl_status); /** * Resave cdn cfg from lscfg to separate cfg when upgrade to v1.7 * * NOTE: this can be left here as `add_option` bcos it is after the item `litespeed-cache-cdn_mapping` is converted * * @since 1.7 */ if (isset($previous_options['cdn_url'])) { $cdn_mapping = array( 'url' => $previous_options['cdn_url'], 'inc_img' => $previous_options['cdn_inc_img'], 'inc_css' => $previous_options['cdn_inc_css'], 'inc_js' => $previous_options['cdn_inc_js'], 'filetype' => $previous_options['cdn_filetype'], ); add_option('litespeed.conf.cdn-mapping', array($cdn_mapping)); Debug2::debug('[Data] plugin_upgrade option adding CDN map'); } /** * Move Exclude settings to separate item * * NOTE: this can be left here as `add_option` bcos it is after the relevant items are converted * * @since 2.3 */ if (isset($previous_options['forced_cache_uri'])) { add_option('litespeed.conf.cache-force_uri', $previous_options['forced_cache_uri']); } if (isset($previous_options['cache_uri_priv'])) { add_option('litespeed.conf.cache-priv_uri', $previous_options['cache_uri_priv']); } if (isset($previous_options['optm_excludes'])) { add_option('litespeed.conf.optm-exc', $previous_options['optm_excludes']); } if (isset($previous_options['excludes_uri'])) { add_option('litespeed.conf.cache-exc', $previous_options['excludes_uri']); } // Backup stale conf Debug2::debug('[Data] Backup stale conf'); delete_option('litespeed-cache-conf'); add_option('litespeed-cache-conf.bk', $previous_options); // Upgrade site_options if is network if (is_multisite()) { $ver = get_site_option('litespeed.conf._version'); if (!$ver) { Debug2::debug('[Data] Conv multisite'); $previous_site_options = get_site_option('litespeed-cache-conf'); $data = array( 'network_enabled' => 'cache', 'use_primary_settings' => 'use_primary_settings', 'auto_upgrade' => 'auto_upgrade', 'purge_upgrade' => 'purge-upgrade', 'cache_favicon' => 'cache-favicon', 'cache_resources' => 'cache-resources', 'mobileview_enabled' => 'cache-mobile', 'mobileview_rules' => 'cache-mobile_rules', 'login_cookie' => 'cache-login_cookie', 'nocache_cookies' => 'cache-exc_cookies', 'nocache_useragents' => 'cache-exc_useragents', 'cache_object' => 'object', 'cache_object_kind' => 'object-kind', 'cache_object_host' => 'object-host', 'cache_object_port' => 'object-port', 'cache_object_life' => 'object-life', 'cache_object_persistent' => 'object-persistent', 'cache_object_admin' => 'object-admin', 'cache_object_transients' => 'object-transients', 'cache_object_db_id' => 'object-db_id', 'cache_object_user' => 'object-user', 'cache_object_pswd' => 'object-psw', 'cache_browser' => 'cache-browser', 'cache_browser_ttl' => 'cache-ttl_browser', 'media_webp_replace' => 'img_optm-webp', ); foreach ($data as $k => $v) { if (!isset($previous_site_options[$k])) { continue; } // The following values must be array if (!is_array($previous_site_options[$k])) { if (in_array($v, array('cache-mobile_rules', 'cache-exc_useragents', 'cache-exc_cookies'))) { $previous_site_options[$k] = explode('|', str_replace('\\ ', ' ', $previous_site_options[$k])); $previous_site_options[$k] = array_filter($previous_site_options[$k]); } } add_site_option('litespeed.conf.' . $v, $previous_site_options[$k]); } // These are already converted to single record in single site $data = array('object-global_groups', 'object-non_persistent_groups'); foreach ($data as $v) { $old_data = get_option($v); if ($old_data) { add_site_option('litespeed.conf.' . $v, $old_data); } } delete_site_option('litespeed-cache-conf'); add_site_option('litespeed.conf._version', '3.0'); } } // delete tables Debug2::debug('[Data] Drop litespeed_optimizer'); $q = 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'litespeed_optimizer'; $wpdb->query($q); // Update image optm table Debug2::debug('[Data] Upgrade img_optm table'); $tb_exists = $wpdb->get_var('SHOW TABLES LIKE "' . $wpdb->prefix . 'litespeed_img_optm"'); if ($tb_exists) { $status_mapping = array( 'requested' => 3, 'notified' => 6, 'pulled' => 9, 'failed' => -1, 'miss' => -3, 'err' => -9, 'err_fetch' => -5, 'err_optm' => -7, 'xmeta' => -8, ); foreach ($status_mapping as $k => $v) { $q = 'UPDATE `' . $wpdb->prefix . "litespeed_img_optm` SET optm_status='$v' WHERE optm_status='$k'"; $wpdb->query($q); } $q = 'ALTER TABLE `' . $wpdb->prefix . 'litespeed_img_optm` DROP INDEX `post_id_2`, DROP INDEX `root_id`, DROP INDEX `src_md5`, DROP INDEX `srcpath_md5`, DROP COLUMN `srcpath_md5`, DROP COLUMN `src_md5`, DROP COLUMN `root_id`, DROP COLUMN `target_saved`, DROP COLUMN `webp_saved`, DROP COLUMN `server_info`, MODIFY COLUMN `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, MODIFY COLUMN `optm_status` tinyint(4) NOT NULL DEFAULT 0, MODIFY COLUMN `src` text COLLATE utf8mb4_unicode_ci NOT NULL '; $wpdb->query($q); } delete_option('litespeed-recommended'); Debug2::debug('[Data] litespeed_update_3_0 done!'); add_option('litespeed.conf._version', '3.0'); } src/activation.cls.php 0000644 00000035636 15162130434 0011001 0 ustar 00 <?php /** * The plugin activation class. * * @since 1.1.0 * @since 1.5 Moved into /inc * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Activation extends Base { const TYPE_UPGRADE = 'upgrade'; const TYPE_INSTALL_3RD = 'install_3rd'; const TYPE_INSTALL_ZIP = 'install_zip'; const TYPE_DISMISS_RECOMMENDED = 'dismiss_recommended'; const NETWORK_TRANSIENT_COUNT = 'lscwp_network_count'; private static $_data_file; /** * Construct * * @since 4.1 */ public function __construct() { self::$_data_file = LSCWP_CONTENT_DIR . '/' . self::CONF_FILE; } /** * The activation hook callback. * * @since 1.0.0 * @access public */ public static function register_activation() { global $wp_version; $advanced_cache = LSCWP_CONTENT_DIR . '/advanced-cache.php'; if (version_compare($wp_version, '5.3', '<') && !file_exists($advanced_cache)) { $file_pointer = fopen($advanced_cache, 'w'); fwrite($file_pointer, "<?php\n\n// A compatibility placeholder for WordPress < v5.3\n// Created by LSCWP v6.1+"); fclose($file_pointer); } $count = 0; !defined('LSCWP_LOG_TAG') && define('LSCWP_LOG_TAG', 'Activate_' . get_current_blog_id()); /* Network file handler */ if (is_multisite()) { $count = self::get_network_count(); if ($count !== false) { $count = intval($count) + 1; set_site_transient(self::NETWORK_TRANSIENT_COUNT, $count, DAY_IN_SECONDS); } if (!is_network_admin()) { if ($count === 1) { // Only itself is activated, set .htaccess with only CacheLookUp try { Htaccess::cls()->insert_ls_wrapper(); } catch (\Exception $ex) { Admin_Display::error($ex->getMessage()); } } } } self::cls()->update_files(); if (defined('LSCWP_REF') && LSCWP_REF == 'whm') { GUI::update_option(GUI::WHM_MSG, GUI::WHM_MSG_VAL); } } /** * Uninstall plugin * @since 1.1.0 */ public static function uninstall_litespeed_cache() { Task::destroy(); // Delete options foreach (Conf::cls()->load_default_vals() as $k => $v) { Base::delete_option($k); } // Delete site options if (is_multisite()) { foreach (Conf::cls()->load_default_site_vals() as $k => $v) { Base::delete_site_option($k); } } // Delete avatar table Data::cls()->tables_del(); if (file_exists(LITESPEED_STATIC_DIR)) { File::rrmdir(LITESPEED_STATIC_DIR); } Cloud::version_check('uninstall'); // Files has been deleted when deactivated } /** * Get the blog ids for the network. Accepts function arguments. * * Will use wp_get_sites for WP versions less than 4.6 * * @since 1.0.12 * @access public * @return array The array of blog ids. */ public static function get_network_ids($args = array()) { global $wp_version; if (version_compare($wp_version, '4.6', '<')) { $blogs = wp_get_sites($args); if (!empty($blogs)) { foreach ($blogs as $key => $blog) { $blogs[$key] = $blog['blog_id']; } } } else { $args['fields'] = 'ids'; $blogs = get_sites($args); } return $blogs; } /** * Gets the count of active litespeed cache plugins on multisite. * * @since 1.0.12 * @access private */ private static function get_network_count() { $count = get_site_transient(self::NETWORK_TRANSIENT_COUNT); if ($count !== false) { return intval($count); } // need to update $default = array(); $count = 0; $sites = self::get_network_ids(array('deleted' => 0)); if (empty($sites)) { return false; } foreach ($sites as $site) { $bid = is_object($site) && property_exists($site, 'blog_id') ? $site->blog_id : $site; $plugins = get_blog_option($bid, 'active_plugins', $default); if (!empty($plugins) && in_array(LSCWP_BASENAME, $plugins, true)) { $count++; } } /** * In case this is called outside the admin page * @see https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network * @since 2.0 */ if (!function_exists('is_plugin_active_for_network')) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } if (is_plugin_active_for_network(LSCWP_BASENAME)) { $count++; } return $count; } /** * Is this deactivate call the last active installation on the multisite network? * * @since 1.0.12 * @access private */ private static function is_deactivate_last() { $count = self::get_network_count(); if ($count === false) { return false; } if ($count !== 1) { // Not deactivating the last one. $count--; set_site_transient(self::NETWORK_TRANSIENT_COUNT, $count, DAY_IN_SECONDS); return false; } delete_site_transient(self::NETWORK_TRANSIENT_COUNT); return true; } /** * The deactivation hook callback. * * Initializes all clean up functionalities. * * @since 1.0.0 * @access public */ public static function register_deactivation() { Task::destroy(); !defined('LSCWP_LOG_TAG') && define('LSCWP_LOG_TAG', 'Deactivate_' . get_current_blog_id()); Purge::purge_all(); if (is_multisite()) { if (!self::is_deactivate_last()) { if (is_network_admin()) { // Still other activated subsite left, set .htaccess with only CacheLookUp try { Htaccess::cls()->insert_ls_wrapper(); } catch (\Exception $ex) { Admin_Display::error($ex->getMessage()); } } return; } } /* 1) wp-config.php; */ try { self::cls()->_manage_wp_cache_const(false); } catch (\Exception $ex) { error_log('In wp-config.php: WP_CACHE could not be set to false during deactivation!'); Admin_Display::error($ex->getMessage()); } /* 2) adv-cache.php; Dropped in v3.0.4 */ /* 3) object-cache.php; */ Object_Cache::cls()->del_file(); /* 4) .htaccess; */ try { Htaccess::cls()->clear_rules(); } catch (\Exception $ex) { Admin_Display::error($ex->getMessage()); } /* 5) .litespeed_conf.dat; */ self::_del_conf_data_file(); // delete in case it's not deleted prior to deactivation. GUI::dismiss_whm(); } /** * Manage related files based on plugin latest conf * * NOTE: Only trigger this in backend admin access for efficiency concern * * Handle files: * 1) wp-config.php; * 2) adv-cache.php; * 3) object-cache.php; * 4) .htaccess; * 5) .litespeed_conf.dat; * * @since 3.0 * @access public */ public function update_files() { Debug2::debug('🗂️ [Activation] update_files'); // Update cache setting `_CACHE` $this->cls('Conf')->define_cache(); // Site options applied already $options = $this->get_options(); /* 1) wp-config.php; */ try { $this->_manage_wp_cache_const($options[self::_CACHE]); } catch (\Exception $ex) { // Add msg to admin page or CLI Admin_Display::error($ex->getMessage()); } /* 2) adv-cache.php; Dropped in v3.0.4 */ /* 3) object-cache.php; */ if ($options[self::O_OBJECT] && (!$options[self::O_DEBUG_DISABLE_ALL] || is_multisite())) { $this->cls('Object_Cache')->update_file($options); } else { $this->cls('Object_Cache')->del_file(); // Note: because it doesn't reconnect, which caused setting page OC option changes delayed, thus may meet Connect Test Failed issue (Next refresh will correct it). Not a big deal, will keep as is. } /* 4) .htaccess; */ try { $this->cls('Htaccess')->update($options); } catch (\Exception $ex) { Admin_Display::error($ex->getMessage()); } /* 5) .litespeed_conf.dat; */ if (($options[self::O_GUEST] || $options[self::O_OBJECT]) && (!$options[self::O_DEBUG_DISABLE_ALL] || is_multisite())) { $this->_update_conf_data_file($options); } } /** * Delete data conf file * * @since 4.1 */ private static function _del_conf_data_file() { if (file_exists(self::$_data_file)) { unlink(self::$_data_file); } } /** * Update data conf file for guest mode & object cache * * @since 4.1 */ private function _update_conf_data_file($options) { $ids = array(); if ($options[self::O_OBJECT]) { $this_ids = array( self::O_DEBUG, self::O_OBJECT_KIND, self::O_OBJECT_HOST, self::O_OBJECT_PORT, self::O_OBJECT_LIFE, self::O_OBJECT_USER, self::O_OBJECT_PSWD, self::O_OBJECT_DB_ID, self::O_OBJECT_PERSISTENT, self::O_OBJECT_ADMIN, self::O_OBJECT_TRANSIENTS, self::O_OBJECT_GLOBAL_GROUPS, self::O_OBJECT_NON_PERSISTENT_GROUPS, ); $ids = array_merge($ids, $this_ids); } if ($options[self::O_GUEST]) { $this_ids = array(self::HASH, self::O_CACHE_LOGIN_COOKIE, self::O_DEBUG_IPS, self::O_UTIL_NO_HTTPS_VARY, self::O_GUEST_UAS, self::O_GUEST_IPS); $ids = array_merge($ids, $this_ids); } $data = array(); foreach ($ids as $v) { $data[$v] = $options[$v]; } $data = \json_encode($data); $old_data = File::read(self::$_data_file); if ($old_data != $data) { defined('LSCWP_LOG') && Debug2::debug('[Activation] Updating .litespeed_conf.dat'); File::save(self::$_data_file, $data); } } /** * Update the WP_CACHE variable in the wp-config.php file. * * If enabling, check if the variable is defined, and if not, define it. * Vice versa for disabling. * * @since 1.0.0 * @since 3.0 Refactored * @access private */ private function _manage_wp_cache_const($enable) { if ($enable) { if (defined('WP_CACHE') && WP_CACHE) { return false; } } elseif (!defined('WP_CACHE') || (defined('WP_CACHE') && !WP_CACHE)) { return false; } if (apply_filters('litespeed_wpconfig_readonly', false)) { throw new \Exception('wp-config file is forbidden to modify due to API hook: litespeed_wpconfig_readonly'); } /** * Follow WP's logic to locate wp-config file * @see wp-load.php */ $conf_file = ABSPATH . 'wp-config.php'; if (!file_exists($conf_file)) { $conf_file = dirname(ABSPATH) . '/wp-config.php'; } $content = File::read($conf_file); if (!$content) { throw new \Exception('wp-config file content is empty: ' . $conf_file); } // Remove the line `define('WP_CACHE', true/false);` first if (defined('WP_CACHE')) { $content = preg_replace('/define\(\s*([\'"])WP_CACHE\1\s*,\s*\w+\s*\)\s*;/sU', '', $content); } // Insert const if ($enable) { $content = preg_replace('/^<\?php/', "<?php\ndefine( 'WP_CACHE', true );", $content); } $res = File::save($conf_file, $content, false, false, false); if ($res !== true) { throw new \Exception('wp-config.php operation failed when changing `WP_CACHE` const: ' . $res); } return true; } /** * Handle auto update * * @since 2.7.2 * @access public */ public function auto_update() { if (!$this->conf(Base::O_AUTO_UPGRADE)) { return; } add_filter('auto_update_plugin', array($this, 'auto_update_hook'), 10, 2); } /** * Auto upgrade hook * * @since 3.0 * @access public */ public function auto_update_hook($update, $item) { if (!empty($item->slug) && 'litespeed-cache' === $item->slug) { $auto_v = Cloud::version_check('auto_update_plugin'); if (!empty($auto_v['latest']) && !empty($item->new_version) && $auto_v['latest'] === $item->new_version) { return true; } } return $update; // Else, use the normal API response to decide whether to update or not } /** * Upgrade LSCWP * * @since 2.9 * @access public */ public function upgrade() { $plugin = Core::PLUGIN_FILE; /** * @see wp-admin/update.php */ include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/file.php'; include_once ABSPATH . 'wp-admin/includes/misc.php'; try { ob_start(); $skin = new \WP_Ajax_Upgrader_Skin(); $upgrader = new \Plugin_Upgrader($skin); $result = $upgrader->upgrade($plugin); if (!is_plugin_active($plugin)) { // todo: upgrade should reactivate the plugin again by WP. Need to check why disabled after upgraded. activate_plugin($plugin, '', is_multisite()); } ob_end_clean(); } catch (\Exception $e) { Admin_Display::error(__('Failed to upgrade.', 'litespeed-cache')); return; } if (is_wp_error($result)) { Admin_Display::error(__('Failed to upgrade.', 'litespeed-cache')); return; } Admin_Display::success(__('Upgraded successfully.', 'litespeed-cache')); } /** * Detect if the plugin is active or not * * @since 1.0 */ public function dash_notifier_is_plugin_active($plugin) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_path = $plugin . '/' . $plugin . '.php'; return is_plugin_active($plugin_path); } /** * Detect if the plugin is installed or not * * @since 1.0 */ public function dash_notifier_is_plugin_installed($plugin) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_path = $plugin . '/' . $plugin . '.php'; $valid = validate_plugin($plugin_path); return !is_wp_error($valid); } /** * Grab a plugin info from WordPress * * @since 1.0 */ public function dash_notifier_get_plugin_info($slug) { include_once ABSPATH . 'wp-admin/includes/plugin-install.php'; $result = plugins_api('plugin_information', array('slug' => $slug)); if (is_wp_error($result)) { return false; } return $result; } /** * Install the 3rd party plugin * * @since 1.0 */ public function dash_notifier_install_3rd() { !defined('SILENCE_INSTALL') && define('SILENCE_INSTALL', true); $slug = !empty($_GET['plugin']) ? $_GET['plugin'] : false; // Check if plugin is installed already if (!$slug || $this->dash_notifier_is_plugin_active($slug)) { return; } /** * @see wp-admin/update.php */ include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/file.php'; include_once ABSPATH . 'wp-admin/includes/misc.php'; $plugin_path = $slug . '/' . $slug . '.php'; if (!$this->dash_notifier_is_plugin_installed($slug)) { $plugin_info = $this->dash_notifier_get_plugin_info($slug); if (!$plugin_info) { return; } // Try to install plugin try { ob_start(); $skin = new \Automatic_Upgrader_Skin(); $upgrader = new \Plugin_Upgrader($skin); $result = $upgrader->install($plugin_info->download_link); ob_end_clean(); } catch (\Exception $e) { return; } } if (!is_plugin_active($plugin_path)) { activate_plugin($plugin_path); } } /** * Handle all request actions from main cls * * @since 2.9 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_UPGRADE: $this->upgrade(); break; case self::TYPE_INSTALL_3RD: $this->dash_notifier_install_3rd(); break; case self::TYPE_DISMISS_RECOMMENDED: Cloud::reload_summary(); Cloud::save_summary(array('news.new' => 0)); break; case self::TYPE_INSTALL_ZIP: Cloud::reload_summary(); $summary = Cloud::get_summary(); if (!empty($summary['news.zip'])) { Cloud::save_summary(array('news.new' => 0)); $this->cls('Debug2')->beta_test($summary['zip']); } break; default: break; } Admin::redirect(); } } src/cdn.cls.php 0000644 00000032274 15162130436 0007401 0 ustar 00 <?php /** * The CDN class. * * @since 1.2.3 * @since 1.5 Moved into /inc * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class CDN extends Root { const BYPASS = 'LITESPEED_BYPASS_CDN'; private $content; private $_cfg_cdn; private $_cfg_url_ori; private $_cfg_ori_dir; private $_cfg_cdn_mapping = array(); private $_cfg_cdn_exclude; private $cdn_mapping_hosts = array(); /** * Init * * @since 1.2.3 */ public function init() { Debug2::debug2('[CDN] init'); if (defined(self::BYPASS)) { Debug2::debug2('CDN bypass'); return; } if (!Router::can_cdn()) { if (!defined(self::BYPASS)) { define(self::BYPASS, true); } return; } $this->_cfg_cdn = $this->conf(Base::O_CDN); if (!$this->_cfg_cdn) { if (!defined(self::BYPASS)) { define(self::BYPASS, true); } return; } $this->_cfg_url_ori = $this->conf(Base::O_CDN_ORI); // Parse cdn mapping data to array( 'filetype' => 'url' ) $mapping_to_check = array(Base::CDN_MAPPING_INC_IMG, Base::CDN_MAPPING_INC_CSS, Base::CDN_MAPPING_INC_JS); foreach ($this->conf(Base::O_CDN_MAPPING) as $v) { if (!$v[Base::CDN_MAPPING_URL]) { continue; } $this_url = $v[Base::CDN_MAPPING_URL]; $this_host = parse_url($this_url, PHP_URL_HOST); // Check img/css/js foreach ($mapping_to_check as $to_check) { if ($v[$to_check]) { Debug2::debug2('[CDN] mapping ' . $to_check . ' -> ' . $this_url); // If filetype to url is one to many, make url be an array $this->_append_cdn_mapping($to_check, $this_url); if (!in_array($this_host, $this->cdn_mapping_hosts)) { $this->cdn_mapping_hosts[] = $this_host; } } } // Check file types if ($v[Base::CDN_MAPPING_FILETYPE]) { foreach ($v[Base::CDN_MAPPING_FILETYPE] as $v2) { $this->_cfg_cdn_mapping[Base::CDN_MAPPING_FILETYPE] = true; // If filetype to url is one to many, make url be an array $this->_append_cdn_mapping($v2, $this_url); if (!in_array($this_host, $this->cdn_mapping_hosts)) { $this->cdn_mapping_hosts[] = $this_host; } } Debug2::debug2('[CDN] mapping ' . implode(',', $v[Base::CDN_MAPPING_FILETYPE]) . ' -> ' . $this_url); } } if (!$this->_cfg_url_ori || !$this->_cfg_cdn_mapping) { if (!defined(self::BYPASS)) { define(self::BYPASS, true); } return; } $this->_cfg_ori_dir = $this->conf(Base::O_CDN_ORI_DIR); // In case user customized upload path if (defined('UPLOADS')) { $this->_cfg_ori_dir[] = UPLOADS; } // Check if need preg_replace $this->_cfg_url_ori = Utility::wildcard2regex($this->_cfg_url_ori); $this->_cfg_cdn_exclude = $this->conf(Base::O_CDN_EXC); if (!empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_INC_IMG])) { // Hook to srcset if (function_exists('wp_calculate_image_srcset')) { add_filter('wp_calculate_image_srcset', array($this, 'srcset'), 999); } // Hook to mime icon add_filter('wp_get_attachment_image_src', array($this, 'attach_img_src'), 999); add_filter('wp_get_attachment_url', array($this, 'url_img'), 999); } if (!empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_INC_CSS])) { add_filter('style_loader_src', array($this, 'url_css'), 999); } if (!empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_INC_JS])) { add_filter('script_loader_src', array($this, 'url_js'), 999); } add_filter('litespeed_buffer_finalize', array($this, 'finalize'), 30); } /** * Associate all filetypes with url * * @since 2.0 * @access private */ private function _append_cdn_mapping($filetype, $url) { // If filetype to url is one to many, make url be an array if (empty($this->_cfg_cdn_mapping[$filetype])) { $this->_cfg_cdn_mapping[$filetype] = $url; } elseif (is_array($this->_cfg_cdn_mapping[$filetype])) { // Append url to filetype $this->_cfg_cdn_mapping[$filetype][] = $url; } else { // Convert _cfg_cdn_mapping from string to array $this->_cfg_cdn_mapping[$filetype] = array($this->_cfg_cdn_mapping[$filetype], $url); } } /** * If include css/js in CDN * * @since 1.6.2.1 * @return bool true if included in CDN */ public function inc_type($type) { if ($type == 'css' && !empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_INC_CSS])) { return true; } if ($type == 'js' && !empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_INC_JS])) { return true; } return false; } /** * Run CDN process * NOTE: As this is after cache finalized, can NOT set any cache control anymore * * @since 1.2.3 * @access public * @return string The content that is after optimization */ public function finalize($content) { $this->content = $content; $this->_finalize(); return $this->content; } /** * Replace CDN url * * @since 1.2.3 * @access private */ private function _finalize() { if (defined(self::BYPASS)) { return; } Debug2::debug('CDN _finalize'); // Start replacing img src if (!empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_INC_IMG])) { $this->_replace_img(); $this->_replace_inline_css(); } if (!empty($this->_cfg_cdn_mapping[Base::CDN_MAPPING_FILETYPE])) { $this->_replace_file_types(); } } /** * Parse all file types * * @since 1.2.3 * @access private */ private function _replace_file_types() { $ele_to_check = $this->conf(Base::O_CDN_ATTR); foreach ($ele_to_check as $v) { if (!$v || strpos($v, '.') === false) { Debug2::debug2('[CDN] replace setting bypassed: no . attribute ' . $v); continue; } Debug2::debug2('[CDN] replace attribute ' . $v); $v = explode('.', $v); $attr = preg_quote($v[1], '#'); if ($v[0]) { $pattern = '#<' . preg_quote($v[0], '#') . '([^>]+)' . $attr . '=([\'"])(.+)\g{2}#iU'; } else { $pattern = '# ' . $attr . '=([\'"])(.+)\g{1}#iU'; } preg_match_all($pattern, $this->content, $matches); if (empty($matches[$v[0] ? 3 : 2])) { continue; } foreach ($matches[$v[0] ? 3 : 2] as $k2 => $url) { // Debug2::debug2( '[CDN] check ' . $url ); $postfix = '.' . pathinfo((string) parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); if (!array_key_exists($postfix, $this->_cfg_cdn_mapping)) { // Debug2::debug2( '[CDN] non-existed postfix ' . $postfix ); continue; } Debug2::debug2('[CDN] matched file_type ' . $postfix . ' : ' . $url); if (!($url2 = $this->rewrite($url, Base::CDN_MAPPING_FILETYPE, $postfix))) { continue; } $attr = str_replace($url, $url2, $matches[0][$k2]); $this->content = str_replace($matches[0][$k2], $attr, $this->content); } } } /** * Parse all images * * @since 1.2.3 * @access private */ private function _replace_img() { preg_match_all('#<img([^>]+?)src=([\'"\\\]*)([^\'"\s\\\>]+)([\'"\\\]*)([^>]*)>#i', $this->content, $matches); foreach ($matches[3] as $k => $url) { // Check if is a DATA-URI if (strpos($url, 'data:image') !== false) { continue; } if (!($url2 = $this->rewrite($url, Base::CDN_MAPPING_INC_IMG))) { continue; } $html_snippet = sprintf('<img %1$s src=%2$s %3$s>', $matches[1][$k], $matches[2][$k] . $url2 . $matches[4][$k], $matches[5][$k]); $this->content = str_replace($matches[0][$k], $html_snippet, $this->content); } } /** * Parse and replace all inline styles containing url() * * @since 1.2.3 * @access private */ private function _replace_inline_css() { Debug2::debug2('[CDN] _replace_inline_css', $this->_cfg_cdn_mapping); /** * Excludes `\` from URL matching * @see #959152 - WordPress LSCache CDN Mapping causing malformed URLS * @see #685485 * @since 3.0 */ preg_match_all('/url\((?![\'"]?data)[\'"]?(.+?)[\'"]?\)/i', $this->content, $matches); foreach ($matches[1] as $k => $url) { $url = str_replace(array(' ', '\t', '\n', '\r', '\0', '\x0B', '"', "'", '"', '''), '', $url); // Parse file postfix $parsed_url = parse_url($url, PHP_URL_PATH); if (!$parsed_url) { continue; } $postfix = '.' . pathinfo($parsed_url, PATHINFO_EXTENSION); if (array_key_exists($postfix, $this->_cfg_cdn_mapping)) { Debug2::debug2('[CDN] matched file_type ' . $postfix . ' : ' . $url); if (!($url2 = $this->rewrite($url, Base::CDN_MAPPING_FILETYPE, $postfix))) { continue; } } elseif (in_array($postfix, array('jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'avif'))) { if (!($url2 = $this->rewrite($url, Base::CDN_MAPPING_INC_IMG))) { continue; } } else { continue; } $attr = str_replace($matches[1][$k], $url2, $matches[0][$k]); $this->content = str_replace($matches[0][$k], $attr, $this->content); } Debug2::debug2('[CDN] _replace_inline_css done'); } /** * Hook to wp_get_attachment_image_src * * @since 1.2.3 * @since 1.7 Removed static from function * @access public * @param array $img The URL of the attachment image src, the width, the height * @return array */ public function attach_img_src($img) { if ($img && ($url = $this->rewrite($img[0], Base::CDN_MAPPING_INC_IMG))) { $img[0] = $url; } return $img; } /** * Try to rewrite one URL with CDN * * @since 1.7 * @access public */ public function url_img($url) { if ($url && ($url2 = $this->rewrite($url, Base::CDN_MAPPING_INC_IMG))) { $url = $url2; } return $url; } /** * Try to rewrite one URL with CDN * * @since 1.7 * @access public */ public function url_css($url) { if ($url && ($url2 = $this->rewrite($url, Base::CDN_MAPPING_INC_CSS))) { $url = $url2; } return $url; } /** * Try to rewrite one URL with CDN * * @since 1.7 * @access public */ public function url_js($url) { if ($url && ($url2 = $this->rewrite($url, Base::CDN_MAPPING_INC_JS))) { $url = $url2; } return $url; } /** * Hook to replace WP responsive images * * @since 1.2.3 * @since 1.7 Removed static from function * @access public * @param array $srcs * @return array */ public function srcset($srcs) { if ($srcs) { foreach ($srcs as $w => $data) { if (!($url = $this->rewrite($data['url'], Base::CDN_MAPPING_INC_IMG))) { continue; } $srcs[$w]['url'] = $url; } } return $srcs; } /** * Replace URL to CDN URL * * @since 1.2.3 * @access public * @param string $url * @return string Replaced URL */ public function rewrite($url, $mapping_kind, $postfix = false) { Debug2::debug2('[CDN] rewrite ' . $url); $url_parsed = parse_url($url); if (empty($url_parsed['path'])) { Debug2::debug2('[CDN] -rewrite bypassed: no path'); return false; } // Only images under wp-cotnent/wp-includes can be replaced $is_internal_folder = Utility::str_hit_array($url_parsed['path'], $this->_cfg_ori_dir); if (!$is_internal_folder) { Debug2::debug2('[CDN] -rewrite failed: path not match: ' . LSCWP_CONTENT_FOLDER); return false; } // Check if is external url if (!empty($url_parsed['host'])) { if (!Utility::internal($url_parsed['host']) && !$this->_is_ori_url($url)) { Debug2::debug2('[CDN] -rewrite failed: host not internal'); return false; } } $exclude = Utility::str_hit_array($url, $this->_cfg_cdn_exclude); if ($exclude) { Debug2::debug2('[CDN] -abort excludes ' . $exclude); return false; } // Fill full url before replacement if (empty($url_parsed['host'])) { $url = Utility::uri2url($url); Debug2::debug2('[CDN] -fill before rewritten: ' . $url); $url_parsed = parse_url($url); } $scheme = !empty($url_parsed['scheme']) ? $url_parsed['scheme'] . ':' : ''; if ($scheme) { // Debug2::debug2( '[CDN] -scheme from url: ' . $scheme ); } // Find the mapping url to be replaced to if (empty($this->_cfg_cdn_mapping[$mapping_kind])) { return false; } if ($mapping_kind !== Base::CDN_MAPPING_FILETYPE) { $final_url = $this->_cfg_cdn_mapping[$mapping_kind]; } else { // select from file type $final_url = $this->_cfg_cdn_mapping[$postfix]; } // If filetype to url is one to many, need to random one if (is_array($final_url)) { $final_url = $final_url[array_rand($final_url)]; } // Now lets replace CDN url foreach ($this->_cfg_url_ori as $v) { if (strpos($v, '*') !== false) { $url = preg_replace('#' . $scheme . $v . '#iU', $final_url, $url); } else { $url = str_replace($scheme . $v, $final_url, $url); } } Debug2::debug2('[CDN] -rewritten: ' . $url); return $url; } /** * Check if is original URL of CDN or not * * @since 2.1 * @access private */ private function _is_ori_url($url) { $url_parsed = parse_url($url); $scheme = !empty($url_parsed['scheme']) ? $url_parsed['scheme'] . ':' : ''; foreach ($this->_cfg_url_ori as $v) { $needle = $scheme . $v; if (strpos($v, '*') !== false) { if (preg_match('#' . $needle . '#iU', $url)) { return true; } } else { if (strpos($url, $needle) === 0) { return true; } } } return false; } /** * Check if the host is the CDN internal host * * @since 1.2.3 * */ public static function internal($host) { if (defined(self::BYPASS)) { return false; } $instance = self::cls(); return in_array($host, $instance->cdn_mapping_hosts); // todo: can add $this->_is_ori_url() check in future } } src/import.cls.php 0000644 00000010232 15162130437 0010136 0 ustar 00 <?php /** * The import/export class. * * @since 1.8.2 */ namespace LiteSpeed; defined('WPINC') || exit(); class Import extends Base { protected $_summary; const TYPE_IMPORT = 'import'; const TYPE_EXPORT = 'export'; const TYPE_RESET = 'reset'; /** * Init * * @since 1.8.2 */ public function __construct() { Debug2::debug('Import init'); $this->_summary = self::get_summary(); } /** * Export settings to file * * @since 1.8.2 * @access public */ public function export($only_data_return = false) { $raw_data = $this->get_options(true); $data = array(); foreach ($raw_data as $k => $v) { $data[] = \json_encode(array($k, $v)); } $data = implode("\n\n", $data); if ($only_data_return) { return $data; } $filename = $this->_generate_filename(); // Update log $this->_summary['export_file'] = $filename; $this->_summary['export_time'] = time(); self::save_summary(); Debug2::debug('Import: Saved to ' . $filename); @header('Content-Disposition: attachment; filename=' . $filename); echo $data; exit(); } /** * Import settings from file * * @since 1.8.2 * @access public */ public function import($file = false) { if (!$file) { if (empty($_FILES['ls_file']['name']) || substr($_FILES['ls_file']['name'], -5) != '.data' || empty($_FILES['ls_file']['tmp_name'])) { Debug2::debug('Import: Failed to import, wrong ls_file'); $msg = __('Import failed due to file error.', 'litespeed-cache'); Admin_Display::error($msg); return false; } $this->_summary['import_file'] = $_FILES['ls_file']['name']; $data = file_get_contents($_FILES['ls_file']['tmp_name']); } else { $this->_summary['import_file'] = $file; $data = file_get_contents($file); } // Update log $this->_summary['import_time'] = time(); self::save_summary(); $ori_data = array(); try { // Check if the data is v4+ or not if (strpos($data, '["_version",') === 0) { Debug2::debug('[Import] Data version: v4+'); $data = explode("\n", $data); foreach ($data as $v) { $v = trim($v); if (!$v) { continue; } list($k, $v) = \json_decode($v, true); $ori_data[$k] = $v; } } else { $ori_data = \json_decode(base64_decode($data), true); } } catch (\Exception $ex) { Debug2::debug('[Import] ❌ Failed to parse serialized data'); return false; } if (!$ori_data) { Debug2::debug('[Import] ❌ Failed to import, no data'); return false; } else { Debug2::debug('[Import] Importing data', $ori_data); } $this->cls('Conf')->update_confs($ori_data); if (!$file) { Debug2::debug('Import: Imported ' . $_FILES['ls_file']['name']); $msg = sprintf(__('Imported setting file %s successfully.', 'litespeed-cache'), $_FILES['ls_file']['name']); Admin_Display::success($msg); } else { Debug2::debug('Import: Imported ' . $file); } return true; } /** * Reset all configs to default values. * * @since 2.6.3 * @access public */ public function reset() { $options = $this->cls('Conf')->load_default_vals(); $this->cls('Conf')->update_confs($options); Debug2::debug('[Import] Reset successfully.'); $msg = __('Reset successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Generate the filename to export * * @since 1.8.2 * @access private */ private function _generate_filename() { // Generate filename $parsed_home = parse_url(get_home_url()); $filename = 'LSCWP_cfg-'; if (!empty($parsed_home['host'])) { $filename .= $parsed_home['host'] . '_'; } if (!empty($parsed_home['path'])) { $filename .= $parsed_home['path'] . '_'; } $filename = str_replace('/', '_', $filename); $filename .= '-' . date('Ymd_His') . '.data'; return $filename; } /** * Handle all request actions from main cls * * @since 1.8.2 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_IMPORT: $this->import(); break; case self::TYPE_EXPORT: $this->export(); break; case self::TYPE_RESET: $this->reset(); break; default: break; } Admin::redirect(); } } src/crawler-map.cls.php 0000644 00000035245 15162130441 0011044 0 ustar 00 <?php /** * The Crawler Sitemap Class * * @since 1.1.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class Crawler_Map extends Root { const LOG_TAG = '🐞🗺️'; const BM_MISS = 1; const BM_HIT = 2; const BM_BLACKLIST = 4; private $_home_url; // Used to simplify urls private $_tb; private $_tb_blacklist; private $__data; private $_conf_map_timeout; private $_urls = array(); /** * Instantiate the class * * @since 1.1.0 */ public function __construct() { $this->_home_url = get_home_url(); $this->__data = Data::cls(); $this->_tb = $this->__data->tb('crawler'); $this->_tb_blacklist = $this->__data->tb('crawler_blacklist'); $this->_conf_map_timeout = defined('LITESPEED_CRAWLER_MAP_TIMEOUT') ? LITESPEED_CRAWLER_MAP_TIMEOUT : 180; // Specify the timeout while parsing the sitemap } /** * Save URLs crawl status into DB * * @since 3.0 * @access public */ public function save_map_status($list, $curr_crawler) { global $wpdb; Utility::compatibility(); $total_crawler = count(Crawler::cls()->list_crawlers()); $total_crawler_pos = $total_crawler - 1; // Replace current crawler's position $curr_crawler = (int) $curr_crawler; foreach ($list as $bit => $ids) { // $ids = [ id => [ url, code ], ... ] if (!$ids) { continue; } self::debug("Update map [crawler] $curr_crawler [bit] $bit [count] " . count($ids)); // Update res first, then reason $right_pos = $total_crawler_pos - $curr_crawler; $sql_res = "CONCAT( LEFT( res, $curr_crawler ), '$bit', RIGHT( res, $right_pos ) )"; $id_all = implode(',', array_map('intval', array_keys($ids))); $wpdb->query("UPDATE `$this->_tb` SET res = $sql_res WHERE id IN ( $id_all )"); // Add blacklist if ($bit == Crawler::STATUS_BLACKLIST || $bit == Crawler::STATUS_NOCACHE) { $q = "SELECT a.id, a.url FROM `$this->_tb_blacklist` a LEFT JOIN `$this->_tb` b ON b.url=a.url WHERE b.id IN ( $id_all )"; $existing = $wpdb->get_results($q, ARRAY_A); // Update current crawler status tag in existing blacklist if ($existing) { $count = $wpdb->query("UPDATE `$this->_tb_blacklist` SET res = $sql_res WHERE id IN ( " . implode(',', array_column($existing, 'id')) . ' )'); self::debug('Update blacklist [count] ' . $count); } // Append new blacklist if (count($ids) > count($existing)) { $new_urls = array_diff(array_column($ids, 'url'), array_column($existing, 'url')); self::debug('Insert into blacklist [count] ' . count($new_urls)); $q = "INSERT INTO `$this->_tb_blacklist` ( url, res, reason ) VALUES " . implode(',', array_fill(0, count($new_urls), '( %s, %s, %s )')); $data = array(); $res = array_fill(0, $total_crawler, '-'); $res[$curr_crawler] = $bit; $res = implode('', $res); $default_reason = $total_crawler > 1 ? str_repeat(',', $total_crawler - 1) : ''; // Pre-populate default reason value first, update later foreach ($new_urls as $url) { $data[] = $url; $data[] = $res; $data[] = $default_reason; } $wpdb->query($wpdb->prepare($q, $data)); } } // Update sitemap reason w/ HTTP code $reason_array = array(); foreach ($ids as $id => $v2) { $code = (int) $v2['code']; if (empty($reason_array[$code])) { $reason_array[$code] = array(); } $reason_array[$code][] = (int) $id; } foreach ($reason_array as $code => $v2) { // Complement comma if ($curr_crawler) { $code = ',' . $code; } if ($curr_crawler < $total_crawler_pos) { $code .= ','; } $count = $wpdb->query( "UPDATE `$this->_tb` SET reason=CONCAT(SUBSTRING_INDEX(reason, ',', $curr_crawler), '$code', SUBSTRING_INDEX(reason, ',', -$right_pos)) WHERE id IN (" . implode(',', $v2) . ')' ); self::debug("Update map reason [code] $code [pos] left $curr_crawler right -$right_pos [count] $count"); // Update blacklist reason if ($bit == Crawler::STATUS_BLACKLIST || $bit == Crawler::STATUS_NOCACHE) { $count = $wpdb->query( "UPDATE `$this->_tb_blacklist` a LEFT JOIN `$this->_tb` b ON b.url = a.url SET a.reason=CONCAT(SUBSTRING_INDEX(a.reason, ',', $curr_crawler), '$code', SUBSTRING_INDEX(a.reason, ',', -$right_pos)) WHERE b.id IN (" . implode(',', $v2) . ')' ); self::debug("Update blacklist [code] $code [pos] left $curr_crawler right -$right_pos [count] $count"); } } // Reset list $list[$bit] = array(); } return $list; } /** * Add one record to blacklist * NOTE: $id is sitemap table ID * * @since 3.0 * @access public */ public function blacklist_add($id) { global $wpdb; $id = (int) $id; // Build res&reason $total_crawler = count(Crawler::cls()->list_crawlers()); $res = str_repeat(Crawler::STATUS_BLACKLIST, $total_crawler); $reason = implode(',', array_fill(0, $total_crawler, 'Man')); $row = $wpdb->get_row("SELECT a.url, b.id FROM `$this->_tb` a LEFT JOIN `$this->_tb_blacklist` b ON b.url = a.url WHERE a.id = '$id'", ARRAY_A); if (!$row) { self::debug('blacklist failed to add [id] ' . $id); return; } self::debug('Add to blacklist [url] ' . $row['url']); $q = "UPDATE `$this->_tb` SET res = %s, reason = %s WHERE id = %d"; $wpdb->query($wpdb->prepare($q, array($res, $reason, $id))); if ($row['id']) { $q = "UPDATE `$this->_tb_blacklist` SET res = %s, reason = %s WHERE id = %d"; $wpdb->query($wpdb->prepare($q, array($res, $reason, $row['id']))); } else { $q = "INSERT INTO `$this->_tb_blacklist` (url, res, reason) VALUES (%s, %s, %s)"; $wpdb->query($wpdb->prepare($q, array($row['url'], $res, $reason))); } } /** * Delete one record from blacklist * * @since 3.0 * @access public */ public function blacklist_del($id) { global $wpdb; if (!$this->__data->tb_exist('crawler_blacklist')) { return; } $id = (int) $id; self::debug('blacklist delete [id] ' . $id); $sql = sprintf( "UPDATE `%s` SET res=REPLACE(REPLACE(res, '%s', '-'), '%s', '-') WHERE url=(SELECT url FROM `%s` WHERE id=%d)", $this->_tb, Crawler::STATUS_NOCACHE, Crawler::STATUS_BLACKLIST, $this->_tb_blacklist, $id ); $wpdb->query($sql); $wpdb->query("DELETE FROM `$this->_tb_blacklist` WHERE id='$id'"); } /** * Empty blacklist * * @since 3.0 * @access public */ public function blacklist_empty() { global $wpdb; if (!$this->__data->tb_exist('crawler_blacklist')) { return; } self::debug('Truncate blacklist'); $sql = sprintf("UPDATE `%s` SET res=REPLACE(REPLACE(res, '%s', '-'), '%s', '-')", $this->_tb, Crawler::STATUS_NOCACHE, Crawler::STATUS_BLACKLIST); $wpdb->query($sql); $wpdb->query("TRUNCATE `$this->_tb_blacklist`"); } /** * List blacklist * * @since 3.0 * @access public */ public function list_blacklist($limit = false, $offset = false) { global $wpdb; if (!$this->__data->tb_exist('crawler_blacklist')) { return array(); } $q = "SELECT * FROM `$this->_tb_blacklist` ORDER BY id DESC"; if ($limit !== false) { if ($offset === false) { $total = $this->count_blacklist(); $offset = Utility::pagination($total, $limit, true); } $q .= ' LIMIT %d, %d'; $q = $wpdb->prepare($q, $offset, $limit); } return $wpdb->get_results($q, ARRAY_A); } /** * Count blacklist */ public function count_blacklist() { global $wpdb; if (!$this->__data->tb_exist('crawler_blacklist')) { return false; } $q = "SELECT COUNT(*) FROM `$this->_tb_blacklist`"; return $wpdb->get_var($q); } /** * Empty sitemap * * @since 3.0 * @access public */ public function empty_map() { Data::cls()->tb_del('crawler'); $msg = __('Sitemap cleaned successfully', 'litespeed-cache'); Admin_Display::success($msg); } /** * List generated sitemap * * @since 3.0 * @access public */ public function list_map($limit, $offset = false) { global $wpdb; if (!$this->__data->tb_exist('crawler')) { return array(); } if ($offset === false) { $total = $this->count_map(); $offset = Utility::pagination($total, $limit, true); } $type = Router::verify_type(); $where = ''; if (!empty($_POST['kw'])) { $q = "SELECT * FROM `$this->_tb` WHERE url LIKE %s"; if ($type == 'hit') { $q .= " AND res LIKE '%" . Crawler::STATUS_HIT . "%'"; } if ($type == 'miss') { $q .= " AND res LIKE '%" . Crawler::STATUS_MISS . "%'"; } if ($type == 'blacklisted') { $q .= " AND res LIKE '%" . Crawler::STATUS_BLACKLIST . "%'"; } $q .= ' ORDER BY id LIMIT %d, %d'; $where = '%' . $wpdb->esc_like($_POST['kw']) . '%'; return $wpdb->get_results($wpdb->prepare($q, $where, $offset, $limit), ARRAY_A); } $q = "SELECT * FROM `$this->_tb`"; if ($type == 'hit') { $q .= " WHERE res LIKE '%" . Crawler::STATUS_HIT . "%'"; } if ($type == 'miss') { $q .= " WHERE res LIKE '%" . Crawler::STATUS_MISS . "%'"; } if ($type == 'blacklisted') { $q .= " WHERE res LIKE '%" . Crawler::STATUS_BLACKLIST . "%'"; } $q .= ' ORDER BY id LIMIT %d, %d'; // self::debug("q=$q offset=$offset, limit=$limit"); return $wpdb->get_results($wpdb->prepare($q, $offset, $limit), ARRAY_A); } /** * Count sitemap */ public function count_map() { global $wpdb; if (!$this->__data->tb_exist('crawler')) { return false; } $q = "SELECT COUNT(*) FROM `$this->_tb`"; $type = Router::verify_type(); if ($type == 'hit') { $q .= " WHERE res LIKE '%" . Crawler::STATUS_HIT . "%'"; } if ($type == 'miss') { $q .= " WHERE res LIKE '%" . Crawler::STATUS_MISS . "%'"; } if ($type == 'blacklisted') { $q .= " WHERE res LIKE '%" . Crawler::STATUS_BLACKLIST . "%'"; } return $wpdb->get_var($q); } /** * Generate sitemap * * @since 1.1.0 * @access public */ public function gen($manual = false) { $count = $this->_gen(); if (!$count) { Admin_Display::error(__('No valid sitemap parsed for crawler.', 'litespeed-cache')); return; } if (!defined('DOING_CRON') && $manual) { $msg = sprintf(__('Sitemap created successfully: %d items', 'litespeed-cache'), $count); Admin_Display::success($msg); } } /** * Generate the sitemap * * @since 1.1.0 * @access private */ private function _gen() { global $wpdb; if (!$this->__data->tb_exist('crawler')) { $this->__data->tb_create('crawler'); } if (!$this->__data->tb_exist('crawler_blacklist')) { $this->__data->tb_create('crawler_blacklist'); } // use custom sitemap if (!($sitemap = $this->conf(Base::O_CRAWLER_SITEMAP))) { return false; } $offset = strlen($this->_home_url); $sitemap = Utility::sanitize_lines($sitemap); try { foreach ($sitemap as $this_map) { $this->_parse($this_map); } } catch (\Exception $e) { self::debug('❌ failed to parse custom sitemap: ' . $e->getMessage()); } if (is_array($this->_urls) && !empty($this->_urls)) { if (defined('LITESPEED_CRAWLER_DROP_DOMAIN') && LITESPEED_CRAWLER_DROP_DOMAIN) { foreach ($this->_urls as $k => $v) { if (stripos($v, $this->_home_url) !== 0) { unset($this->_urls[$k]); continue; } $this->_urls[$k] = substr($v, $offset); } } $this->_urls = array_unique($this->_urls); } self::debug('Truncate sitemap'); $wpdb->query("TRUNCATE `$this->_tb`"); self::debug('Generate sitemap'); // Filter URLs in blacklist $blacklist = $this->list_blacklist(); $full_blacklisted = array(); $partial_blacklisted = array(); foreach ($blacklist as $v) { if (strpos($v['res'], '-') === false) { // Full blacklisted $full_blacklisted[] = $v['url']; } else { // Replace existing reason $v['reason'] = explode(',', $v['reason']); $v['reason'] = array_map(function ($element) { return $element ? 'Existed' : ''; }, $v['reason']); $v['reason'] = implode(',', $v['reason']); $partial_blacklisted[$v['url']] = array( 'res' => $v['res'], 'reason' => $v['reason'], ); } } // Drop all blacklisted URLs $this->_urls = array_diff($this->_urls, $full_blacklisted); // Default res & reason $crawler_count = count(Crawler::cls()->list_crawlers()); $default_res = str_repeat('-', $crawler_count); $default_reason = $crawler_count > 1 ? str_repeat(',', $crawler_count - 1) : ''; $data = array(); foreach ($this->_urls as $url) { $data[] = $url; $data[] = array_key_exists($url, $partial_blacklisted) ? $partial_blacklisted[$url]['res'] : $default_res; $data[] = array_key_exists($url, $partial_blacklisted) ? $partial_blacklisted[$url]['reason'] : $default_reason; } foreach (array_chunk($data, 300) as $data2) { $this->_save($data2); } // Reset crawler Crawler::cls()->reset_pos(); return count($this->_urls); } /** * Save data to table * * @since 3.0 * @access private */ private function _save($data, $fields = 'url,res,reason') { global $wpdb; if (empty($data)) { return; } $q = "INSERT INTO `$this->_tb` ( $fields ) VALUES "; // Add placeholder $q .= Utility::chunk_placeholder($data, $fields); // Store data $wpdb->query($wpdb->prepare($q, $data)); } /** * Parse custom sitemap and return urls * * @since 1.1.1 * @access private */ private function _parse($sitemap) { /** * Read via wp func to avoid allow_url_fopen = off * @since 2.2.7 */ $response = wp_safe_remote_get($sitemap, array('timeout' => $this->_conf_map_timeout, 'sslverify' => false)); if (is_wp_error($response)) { $error_message = $response->get_error_message(); self::debug('failed to read sitemap: ' . $error_message); throw new \Exception('Failed to remote read ' . $sitemap); } $xml_object = simplexml_load_string($response['body'], null, LIBXML_NOCDATA); if (!$xml_object) { if ($this->_urls) { return; } throw new \Exception('Failed to parse xml ' . $sitemap); } // start parsing $xml_array = (array) $xml_object; if (!empty($xml_array['sitemap'])) { // parse sitemap set if (is_object($xml_array['sitemap'])) { $xml_array['sitemap'] = (array) $xml_array['sitemap']; } if (!empty($xml_array['sitemap']['loc'])) { // is single sitemap $this->_parse($xml_array['sitemap']['loc']); } else { // parse multiple sitemaps foreach ($xml_array['sitemap'] as $val) { $val = (array) $val; if (!empty($val['loc'])) { $this->_parse($val['loc']); // recursive parse sitemap } } } } elseif (!empty($xml_array['url'])) { // parse url set if (is_object($xml_array['url'])) { $xml_array['url'] = (array) $xml_array['url']; } // if only 1 element if (!empty($xml_array['url']['loc'])) { $this->_urls[] = $xml_array['url']['loc']; } else { foreach ($xml_array['url'] as $val) { $val = (array) $val; if (!empty($val['loc'])) { $this->_urls[] = $val['loc']; } } } } } } src/cloud.cls.php 0000644 00000150656 15162130443 0007746 0 ustar 00 <?php /** * Cloud service cls * * @since 3.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class Cloud extends Base { const LOG_TAG = '❄️'; const CLOUD_SERVER = 'https://api.quic.cloud'; const CLOUD_IPS = 'https://quic.cloud/ips'; const CLOUD_SERVER_DASH = 'https://my.quic.cloud'; const CLOUD_SERVER_WP = 'https://wpapi.quic.cloud'; const SVC_D_ACTIVATE = 'd/activate'; const SVC_U_ACTIVATE = 'u/wp3/activate'; const SVC_D_ENABLE_CDN = 'd/enable_cdn'; const SVC_D_LINK = 'd/link'; const SVC_D_API = 'd/api'; const SVC_D_DASH = 'd/dash'; const SVC_D_V3UPGRADE = 'd/v3upgrade'; const SVC_U_LINK = 'u/wp3/link'; const SVC_U_ENABLE_CDN = 'u/wp3/enablecdn'; const SVC_D_STATUS_CDN_CLI = 'd/status/cdn_cli'; const SVC_D_NODES = 'd/nodes'; const SVC_D_SYNC_CONF = 'd/sync_conf'; const SVC_D_USAGE = 'd/usage'; const SVC_D_SETUP_TOKEN = 'd/get_token'; const SVC_D_DEL_CDN_DNS = 'd/del_cdn_dns'; const SVC_PAGE_OPTM = 'page_optm'; const SVC_CCSS = 'ccss'; const SVC_UCSS = 'ucss'; const SVC_VPI = 'vpi'; const SVC_LQIP = 'lqip'; const SVC_QUEUE = 'queue'; const SVC_IMG_OPTM = 'img_optm'; const SVC_HEALTH = 'health'; const SVC_CDN = 'cdn'; const IMG_OPTM_DEFAULT_GROUP = 200; const IMGOPTM_TAKEN = 'img_optm-taken'; const TTL_NODE = 3; // Days before node expired const EXPIRATION_REQ = 300; // Seconds of min interval between two unfinished requests const TTL_IPS = 3; // Days for node ip list cache const API_REPORT = 'wp/report'; const API_NEWS = 'news'; const API_VER = 'ver_check'; const API_BETA_TEST = 'beta_test'; const API_REST_ECHO = 'tool/wp_rest_echo'; const API_SERVER_KEY_SIGN = 'key_sign'; private static $CENTER_SVC_SET = array( self::SVC_D_ACTIVATE, self::SVC_U_ACTIVATE, self::SVC_D_ENABLE_CDN, self::SVC_D_LINK, self::SVC_D_NODES, self::SVC_D_SYNC_CONF, self::SVC_D_USAGE, self::SVC_D_API, self::SVC_D_V3UPGRADE, self::SVC_D_DASH, self::SVC_D_STATUS_CDN_CLI, // self::API_NEWS, self::API_REPORT, // self::API_VER, // self::API_BETA_TEST, self::SVC_D_SETUP_TOKEN, self::SVC_D_DEL_CDN_DNS, ); private static $WP_SVC_SET = array(self::API_NEWS, self::API_VER, self::API_BETA_TEST, self::API_REST_ECHO); // No api key needed for these services private static $_PUB_SVC_SET = array(self::API_NEWS, self::API_REPORT, self::API_VER, self::API_BETA_TEST, self::API_REST_ECHO, self::SVC_D_V3UPGRADE, self::SVC_D_DASH); private static $_QUEUE_SVC_SET = array(self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI); public static $SERVICES_LOAD_CHECK = array( // self::SVC_CCSS, // self::SVC_UCSS, // self::SVC_VPI, self::SVC_LQIP, self::SVC_HEALTH, ); public static $SERVICES = array( self::SVC_IMG_OPTM, self::SVC_PAGE_OPTM, self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI, self::SVC_LQIP, self::SVC_CDN, self::SVC_HEALTH, // self::SVC_QUEUE, ); const TYPE_CLEAR_PROMO = 'clear_promo'; const TYPE_REDETECT_CLOUD = 'redetect_cloud'; const TYPE_CLEAR_CLOUD = 'clear_cloud'; const TYPE_ACTIVATE = 'activate'; const TYPE_LINK = 'link'; const TYPE_ENABLE_CDN = 'enablecdn'; const TYPE_API = 'api'; const TYPE_SYNC_USAGE = 'sync_usage'; const TYPE_RESET = 'reset'; const TYPE_SYNC_STATUS = 'sync_status'; protected $_summary; /** * Init * * @since 3.0 */ public function __construct() { $this->_summary = self::get_summary(); } /** * Init QC setup preparation * * @since 7.0 */ public function init_qc_prepare() { if (empty($this->_summary['sk_b64'])) { $keypair = sodium_crypto_sign_keypair(); $pk = base64_encode(sodium_crypto_sign_publickey($keypair)); $sk = base64_encode(sodium_crypto_sign_secretkey($keypair)); $this->_summary['pk_b64'] = $pk; $this->_summary['sk_b64'] = $sk; $this->save_summary(); // ATM `qc_activated` = null return true; } return false; } /** * Init QC setup * * @since 7.0 */ public function init_qc() { $this->init_qc_prepare(); $ref = $this->_get_ref_url(); // WPAPI REST echo dryrun $req_data = array( 'wp_pk_b64' => $this->_summary['pk_b64'], ); $echobox = self::post(self::API_REST_ECHO, $req_data); if ($echobox === false) { self::debugErr('REST Echo Failed!'); $msg = __('Your WP REST API seems blocked our QUIC.cloud server calls.', 'litespeed-cache'); Admin_Display::error($msg); wp_redirect($ref); return; } self::debug('echo succeeded'); // Load separate thread echoed data from storage if (empty($echobox['wpapi_ts']) || empty($echobox['wpapi_signature_b64'])) { Admin_Display::error(__('Failed to get echo data from WPAPI', 'litespeed-cache')); wp_redirect($ref); return; } $data = array( 'wp_pk_b64' => $this->_summary['pk_b64'], 'wpapi_ts' => $echobox['wpapi_ts'], 'wpapi_signature_b64' => $echobox['wpapi_signature_b64'], ); $server_ip = $this->conf(self::O_SERVER_IP); if ($server_ip) { $data['server_ip'] = $server_ip; } // Activation redirect $param = array( 'site_url' => home_url(), 'ver' => Core::VER, 'data' => $data, 'ref' => $ref, ); wp_redirect(self::CLOUD_SERVER_DASH . '/' . self::SVC_U_ACTIVATE . '?data=' . urlencode(Utility::arr2str($param))); exit(); } /** * Decide the ref */ private function _get_ref_url($ref = false) { $link = 'admin.php?page=litespeed'; if ($ref == 'cdn') { $link = 'admin.php?page=litespeed-cdn'; } if ($ref == 'online') { $link = 'admin.php?page=litespeed-general'; } if (!empty($_GET['ref']) && $_GET['ref'] == 'cdn') { $link = 'admin.php?page=litespeed-cdn'; } if (!empty($_GET['ref']) && $_GET['ref'] == 'online') { $link = 'admin.php?page=litespeed-general'; } return get_admin_url(null, $link); } /** * Init QC setup (CLI) * * @since 7.0 */ public function init_qc_cli() { $this->init_qc_prepare(); $server_ip = $this->conf(self::O_SERVER_IP); if (!$server_ip) { self::debugErr('Server IP needs to be set first!'); $msg = sprintf( __('You need to set the %1$s first. Please use the command %2$s to set.', 'litespeed-cache'), '`' . __('Server IP', 'litespeed-cache') . '`', '`wp litespeed-option set server_ip __your_ip_value__`' ); Admin_Display::error($msg); return; } // WPAPI REST echo dryrun $req_data = array( 'wp_pk_b64' => $this->_summary['pk_b64'], ); $echobox = self::post(self::API_REST_ECHO, $req_data); if ($echobox === false) { self::debugErr('REST Echo Failed!'); $msg = __('Your WP REST API seems blocked our QUIC.cloud server calls.', 'litespeed-cache'); Admin_Display::error($msg); return; } self::debug('echo succeeded'); // Load separate thread echoed data from storage if (empty($echobox['wpapi_ts']) || empty($echobox['wpapi_signature_b64'])) { self::debug('Resp: ', $echobox); Admin_Display::error(__('Failed to get echo data from WPAPI', 'litespeed-cache')); return; } $data = array( 'wp_pk_b64' => $this->_summary['pk_b64'], 'wpapi_ts' => $echobox['wpapi_ts'], 'wpapi_signature_b64' => $echobox['wpapi_signature_b64'], 'server_ip' => $server_ip, ); $res = $this->post(self::SVC_D_ACTIVATE, $data); return $res; } /** * Init QC CDN setup (CLI) * * @since 7.0 */ public function init_qc_cdn_cli($method, $cert = false, $key = false, $cf_token = false) { if (!$this->activated()) { Admin_Display::error(__('You need to activate QC first.', 'litespeed-cache')); return; } $server_ip = $this->conf(self::O_SERVER_IP); if (!$server_ip) { self::debugErr('Server IP needs to be set first!'); $msg = sprintf( __('You need to set the %1$s first. Please use the command %2$s to set.', 'litespeed-cache'), '`' . __('Server IP', 'litespeed-cache') . '`', '`wp litespeed-option set server_ip __your_ip_value__`' ); Admin_Display::error($msg); return; } if ($cert) { if (!file_exists($cert) || !file_exists($key)) { Admin_Display::error(__('Cert or key file does not exist.', 'litespeed-cache')); return; } } $data = array( 'method' => $method, 'server_ip' => $server_ip, ); if ($cert) { $data['cert'] = File::read($cert); $data['key'] = File::read($key); } if ($cf_token) { $data['cf_token'] = $cf_token; } $res = $this->post(self::SVC_D_ENABLE_CDN, $data); return $res; } /** * Link to QC setup * * @since 7.0 */ public function link_qc() { if (!$this->activated()) { Admin_Display::error(__('You need to activate QC first.', 'litespeed-cache')); return; } $data = array( 'wp_ts' => time(), ); $data['wp_signature_b64'] = $this->_sign_b64($data['wp_ts']); // Activation redirect $param = array( 'site_url' => home_url(), 'ver' => Core::VER, 'data' => $data, 'ref' => $this->_get_ref_url(), ); wp_redirect(self::CLOUD_SERVER_DASH . '/' . self::SVC_U_LINK . '?data=' . urlencode(Utility::arr2str($param))); exit(); } /** * Show QC Account CDN status * * @since 7.0 */ public function cdn_status_cli() { if (!$this->activated()) { Admin_Display::error(__('You need to activate QC first.', 'litespeed-cache')); return; } $data = array(); $res = $this->post(self::SVC_D_STATUS_CDN_CLI, $data); return $res; } /** * Link to QC Account for CLI * * @since 7.0 */ public function link_qc_cli($email, $key) { if (!$this->activated()) { Admin_Display::error(__('You need to activate QC first.', 'litespeed-cache')); return; } $data = array( 'qc_acct_email' => $email, 'qc_acct_apikey' => $key, ); $res = $this->post(self::SVC_D_LINK, $data); return $res; } /** * API link parsed call to QC * * @since 7.0 */ public function api_link_call($action2) { if (!$this->activated()) { Admin_Display::error(__('You need to activate QC first.', 'litespeed-cache')); return; } $data = array( 'action2' => $action2, ); $res = $this->post(self::SVC_D_API, $data); self::debug('API link call result: ', $res); } /** * Enable QC CDN * * @since 7.0 */ public function enable_cdn() { if (!$this->activated()) { Admin_Display::error(__('You need to activate QC first.', 'litespeed-cache')); return; } $data = array( 'wp_ts' => time(), ); $data['wp_signature_b64'] = $this->_sign_b64($data['wp_ts']); // Activation redirect $param = array( 'site_url' => home_url(), 'ver' => Core::VER, 'data' => $data, 'ref' => $this->_get_ref_url(), ); wp_redirect(self::CLOUD_SERVER_DASH . '/' . self::SVC_U_ENABLE_CDN . '?data=' . urlencode(Utility::arr2str($param))); exit(); } /** * Encrypt data for cloud req * * @since 7.0 */ private function _sign_b64($data) { if (empty($this->_summary['sk_b64'])) { self::debugErr('No sk to sign.'); return false; } $sk = base64_decode($this->_summary['sk_b64']); if (strlen($sk) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { self::debugErr('Invalid local sign sk length.'); // Reset local pk/sk unset($this->_summary['pk_b64']); unset($this->_summary['sk_b64']); $this->save_summary(); self::debug('Clear local sign pk/sk pair.'); return false; } $signature = sodium_crypto_sign_detached((string) $data, $sk); return base64_encode($signature); } /** * Load server pk from cloud * * @since 7.0 */ private function _load_server_pk($from_wpapi = false) { // Load cloud pk $server_key_url = self::CLOUD_SERVER . '/' . self::API_SERVER_KEY_SIGN; if ($from_wpapi) { $server_key_url = self::CLOUD_SERVER_WP . '/' . self::API_SERVER_KEY_SIGN; } $resp = wp_safe_remote_get($server_key_url); if (is_wp_error($resp)) { self::debugErr('Failed to load key: ' . $resp->get_error_message()); return false; } $pk = trim($resp['body']); self::debug('Loaded key from ' . $server_key_url . ': ' . $pk); $cloud_pk = base64_decode($pk); if (strlen($cloud_pk) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { self::debugErr('Invalid cloud public key length.'); return false; } $sk = base64_decode($this->_summary['sk_b64']); if (strlen($sk) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { self::debugErr('Invalid local secret key length.'); // Reset local pk/sk unset($this->_summary['pk_b64']); unset($this->_summary['sk_b64']); $this->save_summary(); self::debug('Unset local pk/sk pair.'); return false; } return $cloud_pk; } /** * WPAPI echo back to notify the sealed databox * * @since 7.0 */ public function wp_rest_echo() { self::debug('Parsing echo', $_POST); if (empty($_POST['wpapi_ts']) || empty($_POST['wpapi_signature_b64'])) { return self::err('No echo data'); } $is_valid = $this->_validate_signature($_POST['wpapi_signature_b64'], $_POST['wpapi_ts'], true); if (!$is_valid) { return self::err('Data validation from WPAPI REST Echo failed'); } $diff = time() - $_POST['wpapi_ts']; if (abs($diff) > 86400) { self::debugErr('WPAPI echo data timeout [diff] ' . $diff); return self::err('Echo data expired'); } $signature_b64 = $this->_sign_b64($_POST['wpapi_ts']); self::debug('Response to echo [signature_b64] ' . $signature_b64); return self::ok(array('signature_b64' => $signature_b64)); } /** * Validate cloud data * * @since 7.0 */ private function _validate_signature($signature_b64, $data, $from_wpapi = false) { // Try validation try { $cloud_pk = $this->_load_server_pk($from_wpapi); if (!$cloud_pk) { return false; } $signature = base64_decode($signature_b64); $is_valid = sodium_crypto_sign_verify_detached($signature, $data, $cloud_pk); } catch (\SodiumException $e) { self::debugErr('Decryption failed: ' . $e->getMessage()); return false; } self::debug('Signature validation result: ' . ($is_valid ? 'true' : 'false')); return $is_valid; } /** * Finish qc activation after redirection back from QC * * @since 7.0 */ public function finish_qc_activation($ref = false) { if (empty($_GET['qc_activated']) || empty($_GET['qc_ts']) || empty($_GET['qc_signature_b64'])) { return; } $data_to_validate_signature = array( 'wp_pk_b64' => $this->_summary['pk_b64'], 'qc_ts' => $_GET['qc_ts'], ); $is_valid = $this->_validate_signature($_GET['qc_signature_b64'], implode('', $data_to_validate_signature)); if (!$is_valid) { self::debugErr('Failed to validate qc activation data'); Admin_Display::error(sprintf(__('Failed to validate %s activation data.', 'litespeed-cache'), 'QUIC.cloud')); return; } self::debug('QC activation status: ' . $_GET['qc_activated']); if (!in_array($_GET['qc_activated'], array('anonymous', 'linked', 'cdn'))) { self::debugErr('Failed to parse qc activation status'); Admin_Display::error(sprintf(__('Failed to parse %s activation status.', 'litespeed-cache'), 'QUIC.cloud')); return; } $diff = time() - $_GET['qc_ts']; if (abs($diff) > 86400) { self::debugErr('QC activation data timeout [diff] ' . $diff); Admin_Display::error(sprintf(__('%s activation data expired.', 'litespeed-cache'), 'QUIC.cloud')); return; } $main_domain = !empty($_GET['main_domain']) ? $_GET['main_domain'] : false; $this->update_qc_activation($_GET['qc_activated'], $main_domain); wp_redirect($this->_get_ref_url($ref)); } /** * Finish qc activation process * * @since 7.0 */ public function update_qc_activation($qc_activated, $main_domain = false, $quite = false) { $this->_summary['qc_activated'] = $qc_activated; if ($main_domain) { $this->_summary['main_domain'] = $main_domain; } $this->save_summary(); $msg = sprintf(__('Congratulations, %s successfully set this domain up for the anonymous online services.', 'litespeed-cache'), 'QUIC.cloud'); if ($qc_activated == 'linked') { $msg = sprintf(__('Congratulations, %s successfully set this domain up for the online services.', 'litespeed-cache'), 'QUIC.cloud'); // Sync possible partner info $this->sync_usage(); } if ($qc_activated == 'cdn') { $msg = sprintf(__('Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache'), 'QUIC.cloud'); // Turn on CDN option $this->cls('Conf')->update_confs(array(self::O_CDN_QUIC => true)); } if (!$quite) { Admin_Display::success('🎊 ' . $msg); } $this->_clear_reset_qc_reg_msg(); $this->clear_cloud(); } /** * Load QC status for dash usage * Format to translate: `<a href="{#xxx#}" class="button button-primary">xxxx</a><a href="{#xxx#}">xxxx2</a>` * * @since 7.0 */ public function load_qc_status_for_dash($type, $force = false) { return Str::translate_qc_apis($this->_load_qc_status_for_dash($type, $force)); } private function _load_qc_status_for_dash($type, $force = false) { if ( !$force && !empty($this->_summary['mini_html']) && isset($this->_summary['mini_html'][$type]) && !empty($this->_summary['mini_html']['ttl.' . $type]) && $this->_summary['mini_html']['ttl.' . $type] > time() ) { return Str::safe_html($this->_summary['mini_html'][$type]); } // Try to update dash content $data = self::post(self::SVC_D_DASH, array('action2' => $type == 'cdn_dash_mini' ? 'cdn_dash' : $type)); if (!empty($data['qc_activated'])) { // Sync conf as changed if (empty($this->_summary['qc_activated']) || $this->_summary['qc_activated'] != $data['qc_activated']) { $msg = sprintf(__('Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache'), 'QUIC.cloud'); Admin_Display::success('🎊 ' . $msg); $this->_clear_reset_qc_reg_msg(); // Turn on CDN option $this->cls('Conf')->update_confs(array(self::O_CDN_QUIC => true)); $this->cls('CDN\Quic')->try_sync_conf(true); } $this->_summary['qc_activated'] = $data['qc_activated']; $this->save_summary(); } // Show the info if (isset($this->_summary['mini_html'][$type])) { return Str::safe_html($this->_summary['mini_html'][$type]); } return ''; } /** * Update QC status * * @since 7.0 */ public function update_cdn_status() { if (empty($_POST['qc_activated']) || !in_array($_POST['qc_activated'], array('anonymous', 'linked', 'cdn', 'deleted'))) { return self::err('lack_of_params'); } self::debug('update_cdn_status request hash: ' . $_POST['qc_activated']); if ($_POST['qc_activated'] == 'deleted') { $this->_reset_qc_reg(); } else { $this->_summary['qc_activated'] = $_POST['qc_activated']; $this->save_summary(); } if ($_POST['qc_activated'] == 'cdn') { $msg = sprintf(__('Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache'), 'QUIC.cloud'); Admin_Display::success('🎊 ' . $msg); $this->_clear_reset_qc_reg_msg(); // Turn on CDN option $this->cls('Conf')->update_confs(array(self::O_CDN_QUIC => true)); $this->cls('CDN\Quic')->try_sync_conf(true); } return self::ok(array('qc_activated' => $_POST['qc_activated'])); } /** * Reset QC setup * * @since 7.0 */ public function reset_qc() { unset($this->_summary['pk_b64']); unset($this->_summary['sk_b64']); unset($this->_summary['qc_activated']); if (!empty($this->_summary['partner'])) { unset($this->_summary['partner']); } $this->save_summary(); self::debug('Clear local QC activation.'); $this->clear_cloud(); Admin_Display::success(sprintf(__('Reset %s activation successfully.', 'litespeed-cache'), 'QUIC.cloud')); wp_redirect($this->_get_ref_url()); exit(); } /** * Show latest commit version always if is on dev * * @since 3.0 */ public function check_dev_version() { if (!preg_match('/[^\d\.]/', Core::VER)) { return; } $last_check = empty($this->_summary['last_request.' . self::API_VER]) ? 0 : $this->_summary['last_request.' . self::API_VER]; if (time() - $last_check > 86400) { $auto_v = self::version_check('dev'); if (!empty($auto_v['dev'])) { self::save_summary(array('version.dev' => $auto_v['dev'])); } } if (empty($this->_summary['version.dev'])) { return; } self::debug('Latest dev version ' . $this->_summary['version.dev']); if (version_compare($this->_summary['version.dev'], Core::VER, '<=')) { return; } // Show the dev banner require_once LSCWP_DIR . 'tpl/banner/new_version_dev.tpl.php'; } /** * Check latest version * * @since 2.9 * @access public */ public static function version_check($src = false) { $req_data = array( 'v' => defined('LSCWP_CUR_V') ? LSCWP_CUR_V : '', 'src' => $src, 'php' => phpversion(), ); if (defined('LITESPEED_ERR')) { $req_data['err'] = base64_encode(!is_string(LITESPEED_ERR) ? \json_encode(LITESPEED_ERR) : LITESPEED_ERR); } $data = self::post(self::API_VER, $req_data); return $data; } /** * Show latest news * * @since 3.0 */ public function news() { $this->_update_news(); if (empty($this->_summary['news.new'])) { return; } if (!empty($this->_summary['news.plugin']) && Activation::cls()->dash_notifier_is_plugin_active($this->_summary['news.plugin'])) { return; } require_once LSCWP_DIR . 'tpl/banner/cloud_news.tpl.php'; } /** * Update latest news * * @since 2.9.9.1 */ private function _update_news() { if (!empty($this->_summary['news.utime']) && time() - $this->_summary['news.utime'] < 86400 * 7) { return; } self::save_summary(array('news.utime' => time())); $data = self::get(self::API_NEWS); if (empty($data['id'])) { return; } // Save news if (!empty($this->_summary['news.id']) && $this->_summary['news.id'] == $data['id']) { return; } $this->_summary['news.id'] = $data['id']; $this->_summary['news.plugin'] = !empty($data['plugin']) ? $data['plugin'] : ''; $this->_summary['news.title'] = !empty($data['title']) ? $data['title'] : ''; $this->_summary['news.content'] = !empty($data['content']) ? $data['content'] : ''; $this->_summary['news.zip'] = !empty($data['zip']) ? $data['zip'] : ''; $this->_summary['news.new'] = 1; if ($this->_summary['news.plugin']) { $plugin_info = Activation::cls()->dash_notifier_get_plugin_info($this->_summary['news.plugin']); if ($plugin_info && !empty($plugin_info->name)) { $this->_summary['news.plugin_name'] = $plugin_info->name; } } self::save_summary(); } /** * Check if contains a package in a service or not * * @since 4.0 */ public function has_pkg($service, $pkg) { if (!empty($this->_summary['usage.' . $service]['pkgs']) && $this->_summary['usage.' . $service]['pkgs'] & $pkg) { return true; } return false; } /** * Get allowance of current service * * @since 3.0 * @access private */ public function allowance($service, &$err = false) { // Only auto sync usage at most one time per day if (empty($this->_summary['last_request.' . self::SVC_D_USAGE]) || time() - $this->_summary['last_request.' . self::SVC_D_USAGE] > 86400) { $this->sync_usage(); } if (in_array($service, array(self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI))) { // @since 4.2 $service = self::SVC_PAGE_OPTM; } if (empty($this->_summary['usage.' . $service])) { return 0; } $usage = $this->_summary['usage.' . $service]; // Image optm is always free $allowance_max = 0; if ($service == self::SVC_IMG_OPTM) { $allowance_max = self::IMG_OPTM_DEFAULT_GROUP; } $allowance = $usage['quota'] - $usage['used']; $err = 'out_of_quota'; if ($allowance > 0) { if ($allowance_max && $allowance_max < $allowance) { $allowance = $allowance_max; } // Daily limit @since 4.2 if (isset($usage['remaining_daily_quota']) && $usage['remaining_daily_quota'] >= 0 && $usage['remaining_daily_quota'] < $allowance) { $allowance = $usage['remaining_daily_quota']; if (!$allowance) { $err = 'out_of_daily_quota'; } } return $allowance; } // Check Pay As You Go balance if (empty($usage['pag_bal'])) { return $allowance_max; } if ($allowance_max && $allowance_max < $usage['pag_bal']) { return $allowance_max; } return $usage['pag_bal']; } /** * Sync Cloud usage summary data * * @since 3.0 * @access public */ public function sync_usage() { $usage = $this->_post(self::SVC_D_USAGE); if (!$usage) { return; } self::debug('sync_usage ' . \json_encode($usage)); foreach (self::$SERVICES as $v) { $this->_summary['usage.' . $v] = !empty($usage[$v]) ? $usage[$v] : false; } self::save_summary(); return $this->_summary; } /** * Clear all existing cloud nodes for future reconnect * * @since 3.0 * @access public */ public function clear_cloud() { foreach (self::$SERVICES as $service) { if (isset($this->_summary['server.' . $service])) { unset($this->_summary['server.' . $service]); } if (isset($this->_summary['server_date.' . $service])) { unset($this->_summary['server_date.' . $service]); } } self::save_summary(); self::debug('Cleared all local service node caches'); } /** * ping clouds to find the fastest node * * @since 3.0 * @access public */ public function detect_cloud($service, $force = false) { if (in_array($service, self::$CENTER_SVC_SET)) { return self::CLOUD_SERVER; } if (in_array($service, self::$WP_SVC_SET)) { return self::CLOUD_SERVER_WP; } // Check if the stored server needs to be refreshed if (!$force) { if ( !empty($this->_summary['server.' . $service]) && !empty($this->_summary['server_date.' . $service]) && $this->_summary['server_date.' . $service] > time() - 86400 * self::TTL_NODE ) { $server = $this->_summary['server.' . $service]; if (!strpos(self::CLOUD_SERVER, 'preview.') && !strpos($server, 'preview.')) { return $server; } if (strpos(self::CLOUD_SERVER, 'preview.') && strpos($server, 'preview.')) { return $server; } } } if (!$service || !in_array($service, self::$SERVICES)) { $msg = __('Cloud Error', 'litespeed-cache') . ': ' . $service; Admin_Display::error($msg); return false; } // Send request to Quic Online Service $json = $this->_post(self::SVC_D_NODES, array('svc' => $this->_maybe_queue($service))); // Check if get list correctly if (empty($json['list']) || !is_array($json['list'])) { self::debug('request cloud list failed: ', $json); if ($json) { $msg = __('Cloud Error', 'litespeed-cache') . ": [Service] $service [Info] " . \json_encode($json); Admin_Display::error($msg); } return false; } // Ping closest cloud $valid_clouds = false; if (!empty($json['list_preferred'])) { $valid_clouds = $this->_get_closest_nodes($json['list_preferred'], $service); } if (!$valid_clouds) { $valid_clouds = $this->_get_closest_nodes($json['list'], $service); } if (!$valid_clouds) { return false; } // Check server load if (in_array($service, self::$SERVICES_LOAD_CHECK)) { // TODO $valid_cloud_loads = array(); foreach ($valid_clouds as $k => $v) { $response = wp_safe_remote_get($v, array('timeout' => 5)); if (is_wp_error($response)) { $error_message = $response->get_error_message(); self::debug('failed to do load checker: ' . $error_message); continue; } $curr_load = \json_decode($response['body'], true); if (!empty($curr_load['_res']) && $curr_load['_res'] == 'ok' && isset($curr_load['load'])) { $valid_cloud_loads[$v] = $curr_load['load']; } } if (!$valid_cloud_loads) { $msg = __('Cloud Error', 'litespeed-cache') . ": [Service] $service [Info] " . __('No available Cloud Node after checked server load.', 'litespeed-cache'); Admin_Display::error($msg); return false; } self::debug('Closest nodes list after load check', $valid_cloud_loads); $qualified_list = array_keys($valid_cloud_loads, min($valid_cloud_loads)); } else { $qualified_list = $valid_clouds; } $closest = $qualified_list[array_rand($qualified_list)]; self::debug('Chose node: ' . $closest); // store data into option locally $this->_summary['server.' . $service] = $closest; $this->_summary['server_date.' . $service] = time(); self::save_summary(); return $this->_summary['server.' . $service]; } /** * Ping to choose the closest nodes * @since 7.0 */ private function _get_closest_nodes($list, $service) { $speed_list = array(); foreach ($list as $v) { // Exclude possible failed 503 nodes if (!empty($this->_summary['disabled_node']) && !empty($this->_summary['disabled_node'][$v]) && time() - $this->_summary['disabled_node'][$v] < 86400) { continue; } $speed_list[$v] = Utility::ping($v); } if (!$speed_list) { self::debug('nodes are in 503 failed nodes'); return false; } $min = min($speed_list); if ($min == 99999) { self::debug('failed to ping all clouds'); return false; } // Random pick same time range ip (230ms 250ms) $range_len = strlen($min); $range_num = substr($min, 0, 1); $valid_clouds = array(); foreach ($speed_list as $node => $speed) { if (strlen($speed) == $range_len && substr($speed, 0, 1) == $range_num) { $valid_clouds[] = $node; } // Append the lower speed ones elseif ($speed < $min * 4) { $valid_clouds[] = $node; } } if (!$valid_clouds) { $msg = __('Cloud Error', 'litespeed-cache') . ": [Service] $service [Info] " . __('No available Cloud Node.', 'litespeed-cache'); Admin_Display::error($msg); return false; } self::debug('Closest nodes list', $valid_clouds); return $valid_clouds; } /** * May need to convert to queue service */ private function _maybe_queue($service) { if (in_array($service, self::$_QUEUE_SVC_SET)) { return self::SVC_QUEUE; } return $service; } /** * Get data from QUIC cloud server * * @since 3.0 * @access public */ public static function get($service, $data = array()) { $instance = self::cls(); return $instance->_get($service, $data); } /** * Get data from QUIC cloud server * * @since 3.0 * @access private */ private function _get($service, $data = false) { $service_tag = $service; if (!empty($data['action'])) { $service_tag .= '-' . $data['action']; } $maybe_cloud = $this->_maybe_cloud($service_tag); if (!$maybe_cloud || $maybe_cloud === 'svc_hot') { return $maybe_cloud; } $server = $this->detect_cloud($service); if (!$server) { return; } $url = $server . '/' . $service; $param = array( 'site_url' => home_url(), 'main_domain' => !empty($this->_summary['main_domain']) ? $this->_summary['main_domain'] : '', 'ver' => Core::VER, ); if ($data) { $param['data'] = $data; } $url .= '?' . http_build_query($param); self::debug('getting from : ' . $url); self::save_summary(array('curr_request.' . $service_tag => time())); $response = wp_safe_remote_get($url, array( 'timeout' => 15, 'headers' => array('Accept' => 'application/json'), )); return $this->_parse_response($response, $service, $service_tag, $server); } /** * Check if is able to do cloud request or not * * @since 3.0 * @access private */ private function _maybe_cloud($service_tag) { $home_url = home_url(); if (!wp_http_validate_url($home_url)) { self::debug('wp_http_validate_url failed: ' . $home_url); return false; } // Deny if is IP if (preg_match('#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', Utility::parse_url_safe($home_url, PHP_URL_HOST))) { self::debug('IP home url is not allowed for cloud service.'); $msg = __('In order to use QC services, need a real domain name, cannot use an IP.', 'litespeed-cache'); Admin_Display::error($msg); return false; } /** @since 5.0 If in valid err_domains, bypass request */ if ($this->_is_err_domain($home_url)) { self::debug('home url is in err_domains, bypass request: ' . $home_url); return false; } // we don't want the `img_optm-taken` to fail at any given time if ($service_tag == self::IMGOPTM_TAKEN) { return true; } if ($service_tag == self::SVC_D_SYNC_CONF && !$this->activated()) { self::debug('Skip sync conf as QC not activated yet.'); return false; } // Check TTL if (!empty($this->_summary['ttl.' . $service_tag])) { $ttl = $this->_summary['ttl.' . $service_tag] - time(); if ($ttl > 0) { self::debug('❌ TTL limit. [srv] ' . $service_tag . ' [TTL cool down] ' . $ttl . ' seconds'); return 'svc_hot'; } } $expiration_req = self::EXPIRATION_REQ; // Limit frequent unfinished request to 5min $timestamp_tag = 'curr_request.'; if ($service_tag == self::SVC_IMG_OPTM . '-' . Img_Optm::TYPE_NEW_REQ) { $timestamp_tag = 'last_request.'; } else { // For all other requests, if is under debug mode, will always allow if ($this->conf(self::O_DEBUG)) { return true; } } if (!empty($this->_summary[$timestamp_tag . $service_tag])) { $expired = $this->_summary[$timestamp_tag . $service_tag] + $expiration_req - time(); if ($expired > 0) { self::debug("❌ try [$service_tag] after $expired seconds"); if ($service_tag !== self::API_VER) { $msg = __('Cloud Error', 'litespeed-cache') . ': ' . sprintf(__('Please try after %1$s for service %2$s.', 'litespeed-cache'), Utility::readable_time($expired, 0, true), '<code>' . $service_tag . '</code>'); Admin_Display::error(array('cloud_trylater' => $msg)); } return false; } } if (in_array($service_tag, self::$_PUB_SVC_SET)) { return true; } if (!$this->activated() && $service_tag != self::SVC_D_ACTIVATE) { Admin_Display::error(Error::msg('qc_setup_required')); return false; } return true; } /** * Check if a service tag ttl is valid or not * @since 7.1 */ public function service_hot($service_tag) { if (empty($this->_summary['ttl.' . $service_tag])) { return false; } $ttl = $this->_summary['ttl.' . $service_tag] - time(); if ($ttl <= 0) { return false; } return $ttl; } /** * Check if activated QUIC.cloud service or not * * @since 7.0 * @access public */ public function activated() { return !empty($this->_summary['sk_b64']) && !empty($this->_summary['qc_activated']); } /** * Show my.qc quick link to the domain page */ public function qc_link() { $data = array( 'site_url' => home_url(), 'ver' => LSCWP_V, 'ref' => $this->_get_ref_url(), ); return self::CLOUD_SERVER_DASH . '/u/wp3/manage?data=' . urlencode(Utility::arr2str($data)); // . (!empty($this->_summary['is_linked']) ? '?wplogin=1' : ''); } /** * Post data to QUIC.cloud server * * @since 3.0 * @access public */ public static function post($service, $data = false, $time_out = false) { $instance = self::cls(); return $instance->_post($service, $data, $time_out); } /** * Post data to cloud server * * @since 3.0 * @access private */ private function _post($service, $data = false, $time_out = false) { $service_tag = $service; if (!empty($data['action'])) { $service_tag .= '-' . $data['action']; } $maybe_cloud = $this->_maybe_cloud($service_tag); if (!$maybe_cloud || $maybe_cloud === 'svc_hot') { self::debug('Maybe cloud failed: ' . var_export($maybe_cloud, true)); return $maybe_cloud; } $server = $this->detect_cloud($service); if (!$server) { return; } $url = $server . '/' . $this->_maybe_queue($service); self::debug('posting to : ' . $url); if ($data) { $data['service_type'] = $service; // For queue distribution usage } // Encrypt service as signature // $signature_ts = time(); // $sign_data = array( // 'service_tag' => $service_tag, // 'ts' => $signature_ts, // ); // $data['signature_b64'] = $this->_sign_b64(implode('', $sign_data)); // $data['signature_ts'] = $signature_ts; self::debug('data', $data); $param = array( 'site_url' => home_url(), // Need to use home_url() as WPML case may change it for diff langs, therefore we can do auto alias 'main_domain' => !empty($this->_summary['main_domain']) ? $this->_summary['main_domain'] : '', 'wp_pk_b64' => !empty($this->_summary['pk_b64']) ? $this->_summary['pk_b64'] : '', 'ver' => Core::VER, 'data' => $data, ); self::save_summary(array('curr_request.' . $service_tag => time())); $response = wp_safe_remote_post($url, array( 'body' => $param, 'timeout' => $time_out ?: 15, 'headers' => array('Accept' => 'application/json', 'Expect' => ''), )); return $this->_parse_response($response, $service, $service_tag, $server); } /** * Parse response JSON * Mark the request successful if the response status is ok * * @since 3.0 */ private function _parse_response($response, $service, $service_tag, $server) { // If show the error or not if failed $visible_err = $service !== self::API_VER && $service !== self::API_NEWS && $service !== self::SVC_D_DASH; if (is_wp_error($response)) { $error_message = $response->get_error_message(); self::debug('failed to request: ' . $error_message); if ($visible_err) { $msg = __('Failed to request via WordPress', 'litespeed-cache') . ': ' . $error_message . " [server] $server [service] $service"; Admin_Display::error($msg); // Tmp disabled this node from reusing in 1 day if (empty($this->_summary['disabled_node'])) { $this->_summary['disabled_node'] = array(); } $this->_summary['disabled_node'][$server] = time(); self::save_summary(); // Force redetect node self::debug('Node error, redetecting node [svc] ' . $service); $this->detect_cloud($service, true); } return false; } $json = \json_decode($response['body'], true); if (!is_array($json)) { self::debugErr('failed to decode response json: ' . $response['body']); if ($visible_err) { $msg = __('Failed to request via WordPress', 'litespeed-cache') . ': ' . $response['body'] . " [server] $server [service] $service"; Admin_Display::error($msg); // Tmp disabled this node from reusing in 1 day if (empty($this->_summary['disabled_node'])) { $this->_summary['disabled_node'] = array(); } $this->_summary['disabled_node'][$server] = time(); self::save_summary(); // Force redetect node self::debugErr('Node error, redetecting node [svc] ' . $service); $this->detect_cloud($service, true); } return false; } // Check and save TTL data if (!empty($json['_ttl'])) { $ttl = intval($json['_ttl']); self::debug('Service TTL to save: ' . $ttl); if ($ttl > 0 && $ttl < 86400) { self::save_summary(array( 'ttl.' . $service_tag => $ttl + time(), )); } } if (!empty($json['_code'])) { self::debugErr('Hit err _code: ' . $json['_code']); if ($json['_code'] == 'unpulled_images') { $msg = __('Cloud server refused the current request due to unpulled images. Please pull the images first.', 'litespeed-cache'); Admin_Display::error($msg); return false; } if ($json['_code'] == 'blocklisted') { $msg = __('Your domain_key has been temporarily blocklisted to prevent abuse. You may contact support at QUIC.cloud to learn more.', 'litespeed-cache'); Admin_Display::error($msg); return false; } if ($json['_code'] == 'rate_limit') { self::debugErr('Cloud server rate limit exceeded.'); $msg = __('Cloud server refused the current request due to rate limiting. Please try again later.', 'litespeed-cache'); Admin_Display::error($msg); return false; } if ($json['_code'] == 'heavy_load' || $json['_code'] == 'redetect_node') { // Force redetect node self::debugErr('Node redetecting node [svc] ' . $service); Admin_Display::info(__('Redetected node', 'litespeed-cache') . ': ' . Error::msg($json['_code'])); $this->detect_cloud($service, true); } } if (!empty($json['_503'])) { self::debugErr('service 503 unavailable temporarily. ' . $json['_503']); $msg = __( 'We are working hard to improve your online service experience. The service will be unavailable while we work. We apologize for any inconvenience.', 'litespeed-cache' ); $msg .= ' ' . $json['_503'] . " [server] $server [service] $service"; Admin_Display::error($msg); // Force redetect node self::debugErr('Node error, redetecting node [svc] ' . $service); $this->detect_cloud($service, true); return false; } list($json, $return) = $this->extract_msg($json, $service, $server); if ($return) { return false; } self::save_summary(array( 'last_request.' . $service_tag => $this->_summary['curr_request.' . $service_tag], 'curr_request.' . $service_tag => 0, )); if ($json) { self::debug2('response ok', $json); } else { self::debug2('response ok'); } // Only successful request return Array return $json; } /** * Extract msg from json * @since 5.0 */ public function extract_msg($json, $service, $server = false, $is_callback = false) { if (!empty($json['_info'])) { self::debug('_info: ' . $json['_info']); $msg = __('Message from QUIC.cloud server', 'litespeed-cache') . ': ' . $json['_info']; $msg .= $this->_parse_link($json); Admin_Display::info($msg); unset($json['_info']); } if (!empty($json['_note'])) { self::debug('_note: ' . $json['_note']); $msg = __('Message from QUIC.cloud server', 'litespeed-cache') . ': ' . $json['_note']; $msg .= $this->_parse_link($json); Admin_Display::note($msg); unset($json['_note']); } if (!empty($json['_success'])) { self::debug('_success: ' . $json['_success']); $msg = __('Good news from QUIC.cloud server', 'litespeed-cache') . ': ' . $json['_success']; $msg .= $this->_parse_link($json); Admin_Display::success($msg); unset($json['_success']); } // Upgrade is required if (!empty($json['_err_req_v'])) { self::debug('_err_req_v: ' . $json['_err_req_v']); $msg = sprintf(__('%1$s plugin version %2$s required for this action.', 'litespeed-cache'), Core::NAME, 'v' . $json['_err_req_v'] . '+') . " [server] $server [service] $service"; // Append upgrade link $msg2 = ' ' . GUI::plugin_upgrade_link(Core::NAME, Core::PLUGIN_NAME, $json['_err_req_v']); $msg2 .= $this->_parse_link($json); Admin_Display::error($msg . $msg2); return array($json, true); } // Parse _carry_on info if (!empty($json['_carry_on'])) { self::debug('Carry_on usage', $json['_carry_on']); // Store generic info foreach (array('usage', 'promo', 'mini_html', 'partner', '_error', '_info', '_note', '_success') as $v) { if (isset($json['_carry_on'][$v])) { switch ($v) { case 'usage': $usage_svc_tag = in_array($service, array(self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI)) ? self::SVC_PAGE_OPTM : $service; $this->_summary['usage.' . $usage_svc_tag] = $json['_carry_on'][$v]; break; case 'promo': if (empty($this->_summary[$v]) || !is_array($this->_summary[$v])) { $this->_summary[$v] = array(); } $this->_summary[$v][] = $json['_carry_on'][$v]; break; case 'mini_html': foreach ($json['_carry_on'][$v] as $k2 => $v2) { if (strpos($k2, 'ttl.') === 0) { $v2 += time(); } $this->_summary[$v][$k2] = $v2; } break; case 'partner': $this->_summary[$v] = $json['_carry_on'][$v]; break; case '_error': case '_info': case '_note': case '_success': $color_mode = substr($v, 1); $msgs = $json['_carry_on'][$v]; Admin_Display::add_unique_notice($color_mode, $msgs, true); break; default: break; } } } self::save_summary(); unset($json['_carry_on']); } // Parse general error msg if (!$is_callback && (empty($json['_res']) || $json['_res'] !== 'ok')) { $json_msg = !empty($json['_msg']) ? $json['_msg'] : 'unknown'; self::debug('❌ _err: ' . $json_msg, $json); $str_translated = Error::msg($json_msg); $msg = __('Failed to communicate with QUIC.cloud server', 'litespeed-cache') . ': ' . $str_translated . " [server] $server [service] $service"; $msg .= $this->_parse_link($json); $visible_err = $service !== self::API_VER && $service !== self::API_NEWS && $service !== self::SVC_D_DASH; if ($visible_err) { Admin_Display::error($msg); } // QC may try auto alias /** @since 5.0 Store the domain as `err_domains` only for QC auto alias feature */ if ($json_msg == 'err_alias') { if (empty($this->_summary['err_domains'])) { $this->_summary['err_domains'] = array(); } $home_url = home_url(); if (!array_key_exists($home_url, $this->_summary['err_domains'])) { $this->_summary['err_domains'][$home_url] = time(); } self::save_summary(); } // Site not on QC, delete invalid domain key if ($json_msg == 'site_not_registered' || $json_msg == 'err_key') { $this->_reset_qc_reg(); } return array($json, true); } unset($json['_res']); if (!empty($json['_msg'])) { unset($json['_msg']); } return array($json, false); } /** * Clear QC linked status * @since 5.0 */ private function _reset_qc_reg() { unset($this->_summary['qc_activated']); if (!empty($this->_summary['partner'])) { unset($this->_summary['partner']); } self::save_summary(); $msg = $this->_reset_qc_reg_content(); Admin_Display::error($msg, false, true); } private function _reset_qc_reg_content() { $msg = __('Site not recognized. QUIC.cloud deactivated automatically. Please reactivate your QUIC.cloud account.', 'litespeed-cache'); $msg .= Doc::learn_more(admin_url('admin.php?page=litespeed'), __('Click here to proceed.', 'litespeed-cache'), true, false, true); $msg .= Doc::learn_more('https://docs.litespeedtech.com/lscache/lscwp/general/', false, false, false, true); return $msg; } private function _clear_reset_qc_reg_msg() { self::debug('Removed pinned reset QC reg content msg'); $msg = $this->_reset_qc_reg_content(); Admin_Display::dismiss_pin_by_content($msg, Admin_Display::NOTICE_RED, true); } /** * REST call: check if the error domain is valid call for auto alias purpose * @since 5.0 */ public function rest_err_domains() { if (empty($_POST['main_domain']) || empty($_POST['alias'])) { return self::err('lack_of_param'); } $this->extract_msg($_POST, 'Quic.cloud', false, true); if ($this->_is_err_domain($_POST['alias'])) { if ($_POST['alias'] == home_url()) { $this->_remove_domain_from_err_list($_POST['alias']); } return self::ok(); } return self::err('Not an alias req from here'); } /** * Remove a domain from err domain * @since 5.0 */ private function _remove_domain_from_err_list($url) { unset($this->_summary['err_domains'][$url]); self::save_summary(); } /** * Check if is err domain * @since 5.0 */ private function _is_err_domain($home_url) { if (empty($this->_summary['err_domains'])) { return false; } if (!array_key_exists($home_url, $this->_summary['err_domains'])) { return false; } // Auto delete if too long ago if (time() - $this->_summary['err_domains'][$home_url] > 86400 * 10) { $this->_remove_domain_from_err_list($home_url); return false; } if (time() - $this->_summary['err_domains'][$home_url] > 86400) { return false; } return true; } /** * Show promo from cloud * * @since 3.0 * @access public */ public function show_promo() { if (empty($this->_summary['promo'])) { return; } require_once LSCWP_DIR . 'tpl/banner/cloud_promo.tpl.php'; } /** * Clear promo from cloud * * @since 3.0 * @access private */ private function _clear_promo() { if (count($this->_summary['promo']) > 1) { array_shift($this->_summary['promo']); } else { $this->_summary['promo'] = array(); } self::save_summary(); } /** * Parse _links from json * * @since 1.6.5 * @since 1.6.7 Self clean the parameter * @access private */ private function _parse_link(&$json) { $msg = ''; if (!empty($json['_links'])) { foreach ($json['_links'] as $v) { $msg .= ' ' . sprintf('<a href="%s" class="%s" target="_blank">%s</a>', $v['link'], !empty($v['cls']) ? $v['cls'] : '', $v['title']); } unset($json['_links']); } return $msg; } /** * Request callback validation from Cloud * * @since 3.0 * @access public */ public function ip_validate() { if (empty($_POST['hash'])) { return self::err('lack_of_params'); } if ($_POST['hash'] != md5(substr($this->_summary['pk_b64'], 0, 4))) { self::debug('__callback IP request decryption failed'); return self::err('err_hash'); } Control::set_nocache('Cloud IP hash validation'); $resp_hash = md5(substr($this->_summary['pk_b64'], 2, 4)); self::debug('__callback IP request hash: ' . $resp_hash); return self::ok(array('hash' => $resp_hash)); } /** * Check if this visit is from cloud or not * * @since 3.0 */ public function is_from_cloud() { // return true; $check_point = time() - 86400 * self::TTL_IPS; if (empty($this->_summary['ips']) || empty($this->_summary['ips_ts']) || $this->_summary['ips_ts'] < $check_point) { self::debug('Force updating ip as ips_ts is older than ' . self::TTL_IPS . ' days'); $this->_update_ips(); } $res = $this->cls('Router')->ip_access($this->_summary['ips']); if (!$res) { self::debug('❌ Not our cloud IP'); // Auto check ip list again but need an interval limit safety. if (empty($this->_summary['ips_ts_runner']) || time() - $this->_summary['ips_ts_runner'] > 600) { self::debug('Force updating ip as ips_ts_runner is older than 10mins'); // Refresh IP list for future detection $this->_update_ips(); $res = $this->cls('Router')->ip_access($this->_summary['ips']); if (!$res) { self::debug('❌ 2nd time: Not our cloud IP'); } else { self::debug('✅ Passed Cloud IP verification'); } return $res; } } else { self::debug('✅ Passed Cloud IP verification'); } return $res; } /** * Update Cloud IP list * * @since 4.2 */ private function _update_ips() { self::debug('Load remote Cloud IP list from ' . self::CLOUD_IPS); // Prevent multiple call in a short period self::save_summary(array('ips_ts' => time(), 'ips_ts_runner' => time())); $response = wp_safe_remote_get(self::CLOUD_IPS . '?json'); if (is_wp_error($response)) { $error_message = $response->get_error_message(); self::debug('failed to get ip whitelist: ' . $error_message); throw new \Exception('Failed to fetch QUIC.cloud whitelist ' . $error_message); } $json = \json_decode($response['body'], true); self::debug('Load ips', $json); self::save_summary(array('ips' => $json)); } /** * Return succeeded response * * @since 3.0 */ public static function ok($data = array()) { $data['_res'] = 'ok'; return $data; } /** * Return error * * @since 3.0 */ public static function err($code) { self::debug("❌ Error response code: $code"); return array('_res' => 'err', '_msg' => $code); } /** * Return pong for ping to check PHP function availability * @since 6.5 */ public function ping() { $resp = array( 'v_lscwp' => Core::VER, 'v_php' => PHP_VERSION, 'v_wp' => $GLOBALS['wp_version'], 'home_url' => home_url(), ); if (!empty($_POST['funcs'])) { foreach ($_POST['funcs'] as $v) { $resp[$v] = function_exists($v) ? 'y' : 'n'; } } if (!empty($_POST['classes'])) { foreach ($_POST['classes'] as $v) { $resp[$v] = class_exists($v) ? 'y' : 'n'; } } if (!empty($_POST['consts'])) { foreach ($_POST['consts'] as $v) { $resp[$v] = defined($v) ? 'y' : 'n'; } } return self::ok($resp); } /** * Display a banner for dev env if using preview QC node. * @since 7.0 */ public function maybe_preview_banner() { if (strpos(self::CLOUD_SERVER, 'preview.')) { Admin_Display::note(__('Linked to QUIC.cloud preview environment, for testing purpose only.', 'litespeed-cache'), true, true, 'litespeed-warning-bg'); } } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_CLEAR_CLOUD: $this->clear_cloud(); break; case self::TYPE_REDETECT_CLOUD: if (!empty($_GET['svc'])) { $this->detect_cloud($_GET['svc'], true); } break; case self::TYPE_CLEAR_PROMO: $this->_clear_promo(); break; case self::TYPE_RESET: $this->reset_qc(); break; case self::TYPE_ACTIVATE: $this->init_qc(); break; case self::TYPE_LINK: $this->link_qc(); break; case self::TYPE_ENABLE_CDN: $this->enable_cdn(); break; case self::TYPE_API: if (!empty($_GET['action2'])) { $this->api_link_call($_GET['action2']); } break; case self::TYPE_SYNC_STATUS: $this->load_qc_status_for_dash('cdn_dash', true); $msg = __('Sync QUIC.cloud status successfully.', 'litespeed-cache'); Admin_Display::success($msg); break; case self::TYPE_SYNC_USAGE: $this->sync_usage(); $msg = __('Sync credit allowance with Cloud Server successfully.', 'litespeed-cache'); Admin_Display::success($msg); break; default: break; } Admin::redirect(); } } src/core.cls.php 0000644 00000047526 15162130445 0007573 0 ustar 00 <?php /** * The core plugin class. * * Note: Core doesn't allow $this->cls( 'Core' ) * * @since 1.0.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class Core extends Root { const NAME = 'LiteSpeed Cache'; const PLUGIN_NAME = 'litespeed-cache'; const PLUGIN_FILE = 'litespeed-cache/litespeed-cache.php'; const VER = LSCWP_V; const ACTION_DISMISS = 'dismiss'; const ACTION_PURGE_BY = 'PURGE_BY'; const ACTION_PURGE_EMPTYCACHE = 'PURGE_EMPTYCACHE'; const ACTION_QS_PURGE = 'PURGE'; const ACTION_QS_PURGE_SINGLE = 'PURGESINGLE'; // This will be same as `ACTION_QS_PURGE` (purge single url only) const ACTION_QS_SHOW_HEADERS = 'SHOWHEADERS'; const ACTION_QS_PURGE_ALL = 'purge_all'; const ACTION_QS_PURGE_EMPTYCACHE = 'empty_all'; const ACTION_QS_NOCACHE = 'NOCACHE'; const HEADER_DEBUG = 'X-LiteSpeed-Debug'; protected static $_debug_show_header = false; private $_footer_comment = ''; /** * Define the core functionality of the plugin. * * Set the plugin name and the plugin version that can be used throughout the plugin. * Load the dependencies, define the locale, and set the hooks for the admin area and * the public-facing side of the site. * * @since 1.0.0 */ public function __construct() { !defined('LSCWP_TS_0') && define('LSCWP_TS_0', microtime(true)); $this->cls('Conf')->init(); /** * Load API hooks * @since 3.0 */ $this->cls('API')->init(); if (defined('LITESPEED_ON')) { // Load third party detection if lscache enabled. include_once LSCWP_DIR . 'thirdparty/entry.inc.php'; } if ($this->conf(Base::O_DEBUG_DISABLE_ALL)) { !defined('LITESPEED_DISABLE_ALL') && define('LITESPEED_DISABLE_ALL', true); } /** * Register plugin activate/deactivate/uninstall hooks * NOTE: this can't be moved under after_setup_theme, otherwise activation will be bypassed somehow * @since 2.7.1 Disabled admin&CLI check to make frontend able to enable cache too */ // if( is_admin() || defined( 'LITESPEED_CLI' ) ) { $plugin_file = LSCWP_DIR . 'litespeed-cache.php'; register_activation_hook($plugin_file, array(__NAMESPACE__ . '\Activation', 'register_activation')); register_deactivation_hook($plugin_file, array(__NAMESPACE__ . '\Activation', 'register_deactivation')); register_uninstall_hook($plugin_file, __NAMESPACE__ . '\Activation::uninstall_litespeed_cache'); // } if (defined('LITESPEED_ON')) { // register purge_all actions $purge_all_events = $this->conf(Base::O_PURGE_HOOK_ALL); // purge all on upgrade if ($this->conf(Base::O_PURGE_ON_UPGRADE)) { $purge_all_events[] = 'automatic_updates_complete'; $purge_all_events[] = 'upgrader_process_complete'; $purge_all_events[] = 'admin_action_do-plugin-upgrade'; } foreach ($purge_all_events as $event) { // Don't allow hook to update_option bcos purge_all will cause infinite loop of update_option if (in_array($event, array('update_option'))) { continue; } add_action($event, __NAMESPACE__ . '\Purge::purge_all'); } // add_filter( 'upgrader_pre_download', 'Purge::filter_with_purge_all' ); // Add headers to site health check for full page cache // @since 5.4 add_filter('site_status_page_cache_supported_cache_headers', function ($cache_headers) { $is_cache_hit = function ($header_value) { return false !== strpos(strtolower($header_value), 'hit'); }; $cache_headers['x-litespeed-cache'] = $is_cache_hit; $cache_headers['x-lsadc-cache'] = $is_cache_hit; $cache_headers['x-qc-cache'] = $is_cache_hit; return $cache_headers; }); } add_action('after_setup_theme', array($this, 'init')); // Check if there is a purge request in queue if (!defined('LITESPEED_CLI')) { $purge_queue = Purge::get_option(Purge::DB_QUEUE); if ($purge_queue && $purge_queue != -1) { $this->_http_header($purge_queue); Debug2::debug('[Core] Purge Queue found&sent: ' . $purge_queue); } if ($purge_queue != -1) { Purge::update_option(Purge::DB_QUEUE, -1); // Use 0 to bypass purge while still enable db update as WP's update_option will check value===false to bypass update } $purge_queue = Purge::get_option(Purge::DB_QUEUE2); if ($purge_queue && $purge_queue != -1) { $this->_http_header($purge_queue); Debug2::debug('[Core] Purge2 Queue found&sent: ' . $purge_queue); } if ($purge_queue != -1) { Purge::update_option(Purge::DB_QUEUE2, -1); } } /** * Hook internal REST * @since 2.9.4 */ $this->cls('REST'); /** * Hook wpnonce function * * Note: ESI nonce won't be available until hook after_setup_theme ESI init due to Guest Mode concern * @since v4.1 */ if ($this->cls('Router')->esi_enabled() && !function_exists('wp_create_nonce')) { Debug2::debug('[ESI] Overwrite wp_create_nonce()'); litespeed_define_nonce_func(); } } /** * The plugin initializer. * * This function checks if the cache is enabled and ready to use, then determines what actions need to be set up based on the type of user and page accessed. Output is buffered if the cache is enabled. * * NOTE: WP user doesn't init yet * * @since 1.0.0 * @access public */ public function init() { /** * Added hook before init * 3rd party preload hooks will be fired here too (e.g. Divi disable all in edit mode) * @since 1.6.6 * @since 2.6 Added filter to all config values in Conf */ do_action('litespeed_init'); add_action('wp_ajax_async_litespeed', 'LiteSpeed\Task::async_litespeed_handler'); add_action('wp_ajax_nopriv_async_litespeed', 'LiteSpeed\Task::async_litespeed_handler'); // in `after_setup_theme`, before `init` hook $this->cls('Activation')->auto_update(); if (is_admin() && !(defined('DOING_AJAX') && DOING_AJAX)) { $this->cls('Admin'); } if (defined('LITESPEED_DISABLE_ALL') && LITESPEED_DISABLE_ALL) { Debug2::debug('[Core] Bypassed due to debug disable all setting'); return; } do_action('litespeed_initing'); ob_start(array($this, 'send_headers_force')); add_action('shutdown', array($this, 'send_headers'), 0); add_action('wp_footer', array($this, 'footer_hook')); /** * Check if is non optm simulator * @since 2.9 */ if (!empty($_GET[Router::ACTION]) && $_GET[Router::ACTION] == 'before_optm' && !apply_filters('litespeed_qs_forbidden', false)) { Debug2::debug('[Core] ⛑️ bypass_optm due to QS CTRL'); !defined('LITESPEED_NO_OPTM') && define('LITESPEED_NO_OPTM', true); } /** * Register vary filter * @since 1.6.2 */ $this->cls('Control')->init(); // 1. Init vary // 2. Init cacheable status // $this->cls('Vary')->init(); // Init Purge hooks $this->cls('Purge')->init(); $this->cls('Tag')->init(); // Load hooks that may be related to users add_action('init', array($this, 'after_user_init'), 5); // Load 3rd party hooks add_action('wp_loaded', array($this, 'load_thirdparty'), 2); // test: Simulate a purge all // if (defined( 'LITESPEED_CLI' )) Purge::add('test'.date('Ymd.His')); } /** * Run hooks after user init * * @since 2.9.8 * @access public */ public function after_user_init() { $this->cls('Router')->is_role_simulation(); // Detect if is Guest mode or not also $this->cls('Vary')->after_user_init(); /** * Preload ESI functionality for ESI request uri recovery * @since 1.8.1 * @since 4.0 ESI init needs to be after Guest mode detection to bypass ESI if is under Guest mode */ $this->cls('ESI')->init(); if (!is_admin() && !defined('LITESPEED_GUEST_OPTM') && ($result = $this->cls('Conf')->in_optm_exc_roles())) { Debug2::debug('[Core] ⛑️ bypass_optm: hit Role Excludes setting: ' . $result); !defined('LITESPEED_NO_OPTM') && define('LITESPEED_NO_OPTM', true); } // Heartbeat control $this->cls('Tool')->heartbeat(); /** * Backward compatibility for v4.2- @Ruikai * TODO: Will change to hook in future versions to make it revertable */ if (defined('LITESPEED_BYPASS_OPTM') && !defined('LITESPEED_NO_OPTM')) { define('LITESPEED_NO_OPTM', LITESPEED_BYPASS_OPTM); } if (!defined('LITESPEED_NO_OPTM') || !LITESPEED_NO_OPTM) { // Check missing static files $this->cls('Router')->serve_static(); $this->cls('Media')->init(); $this->cls('Placeholder')->init(); $this->cls('Router')->can_optm() && $this->cls('Optimize')->init(); $this->cls('Localization')->init(); // Hook cdn for attachments $this->cls('CDN')->init(); // load cron tasks $this->cls('Task')->init(); } // load litespeed actions if ($action = Router::get_action()) { $this->proceed_action($action); } // Load frontend GUI if (!is_admin()) { $this->cls('GUI')->init(); } } /** * Run frontend actions * * @since 1.1.0 * @access public */ public function proceed_action($action) { $msg = false; // handle actions switch ($action) { case self::ACTION_QS_SHOW_HEADERS: self::$_debug_show_header = true; break; case self::ACTION_QS_PURGE: case self::ACTION_QS_PURGE_SINGLE: Purge::set_purge_single(); break; case self::ACTION_QS_PURGE_ALL: Purge::purge_all(); break; case self::ACTION_PURGE_EMPTYCACHE: case self::ACTION_QS_PURGE_EMPTYCACHE: define('LSWCP_EMPTYCACHE', true); // clear all sites caches Purge::purge_all(); $msg = __('Notified LiteSpeed Web Server to purge everything.', 'litespeed-cache'); break; case self::ACTION_PURGE_BY: $this->cls('Purge')->purge_list(); $msg = __('Notified LiteSpeed Web Server to purge the list.', 'litespeed-cache'); break; case self::ACTION_DISMISS: // Even its from ajax, we don't need to register wp ajax callback function but directly use our action GUI::dismiss(); break; default: $msg = $this->cls('Router')->handler($action); break; } if ($msg && !Router::is_ajax()) { Admin_Display::add_notice(Admin_Display::NOTICE_GREEN, $msg); Admin::redirect(); return; } if (Router::is_ajax()) { exit(); } } /** * Callback used to call the detect third party action. * * The detect action is used by third party plugin integration classes to determine if they should add the rest of their hooks. * * @since 1.0.5 * @access public */ public function load_thirdparty() { do_action('litespeed_load_thirdparty'); } /** * Mark wp_footer called * * @since 1.3 * @access public */ public function footer_hook() { Debug2::debug('[Core] Footer hook called'); if (!defined('LITESPEED_FOOTER_CALLED')) { define('LITESPEED_FOOTER_CALLED', true); } } /** * Trigger comment info display hook * * @since 1.3 * @access private */ private function _check_is_html($buffer = null) { if (!defined('LITESPEED_FOOTER_CALLED')) { Debug2::debug2('[Core] CHK html bypass: miss footer const'); return; } if (defined('DOING_AJAX')) { Debug2::debug2('[Core] CHK html bypass: doing ajax'); return; } if (defined('DOING_CRON')) { Debug2::debug2('[Core] CHK html bypass: doing cron'); return; } if ($_SERVER['REQUEST_METHOD'] !== 'GET') { Debug2::debug2('[Core] CHK html bypass: not get method ' . $_SERVER['REQUEST_METHOD']); return; } if ($buffer === null) { $buffer = ob_get_contents(); } // double check to make sure it is a html file if (strlen($buffer) > 300) { $buffer = substr($buffer, 0, 300); } if (strstr($buffer, '<!--') !== false) { $buffer = preg_replace('/<!--.*?-->/s', '', $buffer); } $buffer = trim($buffer); $buffer = File::remove_zero_space($buffer); $is_html = stripos($buffer, '<html') === 0 || stripos($buffer, '<!DOCTYPE') === 0; if (!$is_html) { Debug2::debug('[Core] Footer check failed: ' . ob_get_level() . '-' . substr($buffer, 0, 100)); return; } Debug2::debug('[Core] Footer check passed'); if (!defined('LITESPEED_IS_HTML')) { define('LITESPEED_IS_HTML', true); } } /** * For compatibility with those plugins have 'Bad' logic that forced all buffer output even it is NOT their buffer :( * * Usually this is called after send_headers() if following original WP process * * @since 1.1.5 * @access public * @param string $buffer * @return string */ public function send_headers_force($buffer) { $this->_check_is_html($buffer); // Hook to modify buffer before $buffer = apply_filters('litespeed_buffer_before', $buffer); /** * Media: Image lazyload && WebP * GUI: Clean wrapper mainly for esi block NOTE: this needs to be before optimizer to avoid wrapper being removed * Optimize * CDN */ if (!defined('LITESPEED_NO_OPTM') || !LITESPEED_NO_OPTM) { Debug2::debug('[Core] run hook litespeed_buffer_finalize'); $buffer = apply_filters('litespeed_buffer_finalize', $buffer); } /** * Replace ESI preserved list * @since 3.3 Replace this in the end to avoid `Inline JS Defer` or other Page Optm features encoded ESI tags wrongly, which caused LSWS can't recognize ESI */ $buffer = $this->cls('ESI')->finalize($buffer); $this->send_headers(true); // Log ESI nonce buffer empty issue if (defined('LSCACHE_IS_ESI') && strlen($buffer) == 0) { // log ref for debug purpose error_log('ESI buffer empty ' . $_SERVER['REQUEST_URI']); } // Init comment info $running_info_showing = defined('LITESPEED_IS_HTML') || defined('LSCACHE_IS_ESI'); if (defined('LSCACHE_ESI_SILENCE')) { $running_info_showing = false; Debug2::debug('[Core] ESI silence'); } /** * Silence comment for json req * @since 2.9.3 */ if (REST::cls()->is_rest() || Router::is_ajax()) { $running_info_showing = false; Debug2::debug('[Core] Silence Comment due to REST/AJAX'); } $running_info_showing = apply_filters('litespeed_comment', $running_info_showing); if ($running_info_showing) { if ($this->_footer_comment) { $buffer .= $this->_footer_comment; } } /** * If ESI req is JSON, give the content JSON format * @since 2.9.3 * @since 2.9.4 ESI req could be from internal REST call, so moved json_encode out of this cond */ if (defined('LSCACHE_IS_ESI')) { Debug2::debug('[Core] ESI Start 👇'); if (strlen($buffer) > 500) { Debug2::debug(trim(substr($buffer, 0, 500)) . '.....'); } else { Debug2::debug($buffer); } Debug2::debug('[Core] ESI End 👆'); } if (apply_filters('litespeed_is_json', false)) { if (\json_decode($buffer, true) == null) { Debug2::debug('[Core] Buffer converting to JSON'); $buffer = \json_encode($buffer); $buffer = trim($buffer, '"'); } else { Debug2::debug('[Core] JSON Buffer'); } } // Hook to modify buffer after $buffer = apply_filters('litespeed_buffer_after', $buffer); Debug2::ended(); return $buffer; } /** * Sends the headers out at the end of processing the request. * * This will send out all LiteSpeed Cache related response headers needed for the post. * * @since 1.0.5 * @access public * @param boolean $is_forced If the header is sent following our normal finalizing logic */ public function send_headers($is_forced = false) { // Make sure header output only run once if (!defined('LITESPEED_DID_' . __FUNCTION__)) { define('LITESPEED_DID_' . __FUNCTION__, true); } else { return; } // Avoid PHP warning for header sent out already if (headers_sent()) { self::debug('❌ !!! Err: Header sent out already'); return; } $this->_check_is_html(); // NOTE: cache ctrl output needs to be done first, as currently some varies are added in 3rd party hook `litespeed_api_control`. $this->cls('Control')->finalize(); $vary_header = $this->cls('Vary')->finalize(); // If is not cacheable but Admin QS is `purge` or `purgesingle`, `tag` still needs to be generated $tag_header = $this->cls('Tag')->output(); if (!$tag_header && Control::is_cacheable()) { Control::set_nocache('empty tag header'); } // NOTE: `purge` output needs to be after `tag` output as Admin QS may need to send `tag` header $purge_header = Purge::output(); // generate `control` header in the end in case control status is changed by other headers. $control_header = $this->cls('Control')->output(); // Give one more break to avoid ff crash if (!defined('LSCACHE_IS_ESI')) { $this->_footer_comment .= "\n"; } $cache_support = 'supported'; if (defined('LITESPEED_ON')) { $cache_support = Control::is_cacheable() ? 'cached' : 'uncached'; } $this->_comment( sprintf( '%1$s %2$s by LiteSpeed Cache %4$s on %3$s', defined('LSCACHE_IS_ESI') ? 'Block' : 'Page', $cache_support, date('Y-m-d H:i:s', time() + LITESPEED_TIME_OFFSET), self::VER ) ); // send Control header if (defined('LITESPEED_ON') && $control_header) { $this->_http_header($control_header); if (!Control::is_cacheable()) { $this->_http_header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0'); // @ref: https://wordpress.org/support/topic/apply_filterslitespeed_control_cacheable-returns-false-for-cacheable/ } if (defined('LSCWP_LOG')) { $this->_comment($control_header); } } // send PURGE header (Always send regardless of cache setting disabled/enabled) if (defined('LITESPEED_ON') && $purge_header) { $this->_http_header($purge_header); Debug2::log_purge($purge_header); if (defined('LSCWP_LOG')) { $this->_comment($purge_header); } } // send Vary header if (defined('LITESPEED_ON') && $vary_header) { $this->_http_header($vary_header); if (defined('LSCWP_LOG')) { $this->_comment($vary_header); } } if (defined('LITESPEED_ON') && defined('LSCWP_LOG')) { $vary = $this->cls('Vary')->finalize_full_varies(); if ($vary) { $this->_comment('Full varies: ' . $vary); } } // Admin QS show header action if (self::$_debug_show_header) { $debug_header = self::HEADER_DEBUG . ': '; if ($control_header) { $debug_header .= $control_header . '; '; } if ($purge_header) { $debug_header .= $purge_header . '; '; } if ($tag_header) { $debug_header .= $tag_header . '; '; } if ($vary_header) { $debug_header .= $vary_header . '; '; } $this->_http_header($debug_header); } else { // Control header if (defined('LITESPEED_ON') && Control::is_cacheable() && $tag_header) { $this->_http_header($tag_header); if (defined('LSCWP_LOG')) { $this->_comment($tag_header); } } } // Object cache _comment if (defined('LSCWP_LOG') && defined('LSCWP_OBJECT_CACHE') && method_exists('WP_Object_Cache', 'debug')) { $this->_comment('Object Cache ' . \WP_Object_Cache::get_instance()->debug()); } if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { $this->_comment('Guest Mode'); } if (!empty($this->_footer_comment)) { self::debug('[footer comment] ' . $this->_footer_comment); } if ($is_forced) { Debug2::debug('--forced--'); } /** * If is CLI and contains Purge Header, then issue a HTTP req to Purge * @since v5.3 */ if (defined('LITESPEED_CLI')) { $purge_queue = Purge::get_option(Purge::DB_QUEUE); if (!$purge_queue || $purge_queue == -1) { $purge_queue = Purge::get_option(Purge::DB_QUEUE2); } if ($purge_queue && $purge_queue != -1) { self::debug('[Core] Purge Queue found, issue a HTTP req to purge: ' . $purge_queue); // Kick off HTTP req $url = admin_url('admin-ajax.php'); $resp = wp_safe_remote_get($url); if (is_wp_error($resp)) { $error_message = $resp->get_error_message(); self::debug('[URL]' . $url); self::debug('failed to request: ' . $error_message); } else { self::debug('HTTP req res: ' . $resp['body']); } } } } /** * Append one HTML comment * @since 5.5 */ public static function comment($data) { self::cls()->_comment($data); } private function _comment($data) { $this->_footer_comment .= "\n<!-- " . $data . ' -->'; } /** * Send HTTP header * @since 5.3 */ private function _http_header($header) { if (defined('LITESPEED_CLI')) { return; } @header($header); if (!defined('LSCWP_LOG')) { return; } Debug2::debug('💰 ' . $header); } } src/root.cls.php 0000644 00000031435 15162130447 0007620 0 ustar 00 <?php /** * The abstract instance * * @since 3.0 */ namespace LiteSpeed; defined('WPINC') || exit(); abstract class Root { const CONF_FILE = '.litespeed_conf.dat'; // Instance set private static $_instances; private static $_options = array(); private static $_const_options = array(); private static $_primary_options = array(); private static $_network_options = array(); /** * Check if need to separate ccss for mobile * * @since 4.7 * @access protected */ protected function _separate_mobile() { return (wp_is_mobile() || apply_filters('litespeed_is_mobile', false)) && $this->conf(Base::O_CACHE_MOBILE); } /** * Log an error message * * @since 7.0 */ public static function debugErr($msg, $backtrace_limit = false) { $msg = '❌ ' . $msg; self::debug($msg, $backtrace_limit); } /** * Log a debug message. * * @since 4.4 * @access public */ public static function debug($msg, $backtrace_limit = false) { if (!defined('LSCWP_LOG')) { return; } if (defined('static::LOG_TAG')) { $msg = static::LOG_TAG . ' ' . $msg; } Debug2::debug($msg, $backtrace_limit); } /** * Log an advanced debug message. * * @since 4.4 * @access public */ public static function debug2($msg, $backtrace_limit = false) { if (!defined('LSCWP_LOG_MORE')) { return; } if (defined('static::LOG_TAG')) { $msg = static::LOG_TAG . ' ' . $msg; } Debug2::debug2($msg, $backtrace_limit); } /** * Check if there is cache folder for that type * * @since 3.0 */ public function has_cache_folder($type) { $subsite_id = is_multisite() && !is_network_admin() ? get_current_blog_id() : ''; if (file_exists(LITESPEED_STATIC_DIR . '/' . $type . '/' . $subsite_id)) { return true; } return false; } /** * Maybe make the cache folder if not existed * * @since 4.4.2 */ protected function _maybe_mk_cache_folder($type) { if (!$this->has_cache_folder($type)) { $subsite_id = is_multisite() && !is_network_admin() ? get_current_blog_id() : ''; $path = LITESPEED_STATIC_DIR . '/' . $type . '/' . $subsite_id; mkdir($path, 0755, true); } } /** * Delete file-based cache folder for that type * * @since 3.0 */ public function rm_cache_folder($type) { if (!$this->has_cache_folder($type)) { return; } $subsite_id = is_multisite() && !is_network_admin() ? get_current_blog_id() : ''; File::rrmdir(LITESPEED_STATIC_DIR . '/' . $type . '/' . $subsite_id); // Clear All summary data self::save_summary(false, false, true); if ($type == 'ccss' || $type == 'ucss') { Debug2::debug('[CSS] Cleared ' . $type . ' queue'); } elseif ($type == 'avatar') { Debug2::debug('[Avatar] Cleared ' . $type . ' queue'); } elseif ($type == 'css' || $type == 'js') { return; } else { Debug2::debug('[' . strtoupper($type) . '] Cleared ' . $type . ' queue'); } } /** * Build the static filepath * * @since 4.0 */ protected function _build_filepath_prefix($type) { $filepath_prefix = '/' . $type . '/'; if (is_multisite()) { $filepath_prefix .= get_current_blog_id() . '/'; } return $filepath_prefix; } /** * Load current queues from data file * * @since 4.1 * @since 4.3 Elevated to root.cls */ public function load_queue($type) { $filepath_prefix = $this->_build_filepath_prefix($type); $static_path = LITESPEED_STATIC_DIR . $filepath_prefix . '.litespeed_conf.dat'; $queue = array(); if (file_exists($static_path)) { $queue = \json_decode(file_get_contents($static_path), true) ?: array(); } return $queue; } /** * Save current queues to data file * * @since 4.1 * @since 4.3 Elevated to root.cls */ public function save_queue($type, $list) { $filepath_prefix = $this->_build_filepath_prefix($type); $static_path = LITESPEED_STATIC_DIR . $filepath_prefix . '.litespeed_conf.dat'; $data = \json_encode($list); File::save($static_path, $data, true); } /** * Clear all waiting queues * * @since 3.4 * @since 4.3 Elevated to root.cls */ public function clear_q($type, $silent = false) { $filepath_prefix = $this->_build_filepath_prefix($type); $static_path = LITESPEED_STATIC_DIR . $filepath_prefix . '.litespeed_conf.dat'; if (file_exists($static_path)) { $silent = false; unlink($static_path); } if (!$silent) { $msg = __('All QUIC.cloud service queues have been cleared.', 'litespeed-cache'); Admin_Display::success($msg); } } /** * Load an instance or create it if not existed * @since 4.0 */ public static function cls($cls = false, $unset = false, $data = false) { if (!$cls) { $cls = self::ori_cls(); } $cls = __NAMESPACE__ . '\\' . $cls; $cls_tag = strtolower($cls); if (!isset(self::$_instances[$cls_tag])) { if ($unset) { return; } self::$_instances[$cls_tag] = new $cls($data); } else { if ($unset) { unset(self::$_instances[$cls_tag]); return; } } return self::$_instances[$cls_tag]; } /** * Set one conf or confs */ public function set_conf($id, $val = null) { if (is_array($id)) { foreach ($id as $k => $v) { $this->set_conf($k, $v); } return; } self::$_options[$id] = $val; } /** * Set one primary conf or confs */ public function set_primary_conf($id, $val = null) { if (is_array($id)) { foreach ($id as $k => $v) { $this->set_primary_conf($k, $v); } return; } self::$_primary_options[$id] = $val; } /** * Set one network conf */ public function set_network_conf($id, $val = null) { if (is_array($id)) { foreach ($id as $k => $v) { $this->set_network_conf($k, $v); } return; } self::$_network_options[$id] = $val; } /** * Set one const conf */ public function set_const_conf($id, $val) { self::$_const_options[$id] = $val; } /** * Check if is overwritten by const * * @since 3.0 */ public function const_overwritten($id) { if (!isset(self::$_const_options[$id]) || self::$_const_options[$id] == self::$_options[$id]) { return null; } return self::$_const_options[$id]; } /** * Check if is overwritten by primary site * * @since 3.2.2 */ public function primary_overwritten($id) { if (!isset(self::$_primary_options[$id]) || self::$_primary_options[$id] == self::$_options[$id]) { return null; } // Network admin settings is impossible to be overwritten by primary if (is_network_admin()) { return null; } return self::$_primary_options[$id]; } /** * Get the list of configured options for the blog. * * @since 1.0 */ public function get_options($ori = false) { if (!$ori) { return array_merge(self::$_options, self::$_primary_options, self::$_network_options, self::$_const_options); } return self::$_options; } /** * If has a conf or not */ public function has_conf($id) { return array_key_exists($id, self::$_options); } /** * If has a primary conf or not */ public function has_primary_conf($id) { return array_key_exists($id, self::$_primary_options); } /** * If has a network conf or not */ public function has_network_conf($id) { return array_key_exists($id, self::$_network_options); } /** * Get conf */ public function conf($id, $ori = false) { if (isset(self::$_options[$id])) { if (!$ori) { $val = $this->const_overwritten($id); if ($val !== null) { defined('LSCWP_LOG') && Debug2::debug('[Conf] 🏛️ const option ' . $id . '=' . var_export($val, true)); return $val; } $val = $this->primary_overwritten($id); // Network Use primary site settings if ($val !== null) { return $val; } } // Network original value will be in _network_options if (!is_network_admin() || !$this->has_network_conf($id)) { return self::$_options[$id]; } } if ($this->has_network_conf($id)) { if (!$ori) { $val = $this->const_overwritten($id); if ($val !== null) { defined('LSCWP_LOG') && Debug2::debug('[Conf] 🏛️ const option ' . $id . '=' . var_export($val, true)); return $val; } } return $this->network_conf($id); } defined('LSCWP_LOG') && Debug2::debug('[Conf] Invalid option ID ' . $id); return null; } /** * Get primary conf */ public function primary_conf($id) { return self::$_primary_options[$id]; } /** * Get network conf */ public function network_conf($id) { if (!$this->has_network_conf($id)) { return null; } return self::$_network_options[$id]; } /** * Get called class short name */ public static function ori_cls() { $cls = new \ReflectionClass(get_called_class()); $shortname = $cls->getShortName(); $namespace = str_replace(__NAMESPACE__ . '\\', '', $cls->getNamespaceName() . '\\'); if ($namespace) { // the left namespace after dropped LiteSpeed $shortname = $namespace . $shortname; } return $shortname; } /** * Generate conf name for wp_options record * * @since 3.0 */ public static function name($id) { $name = strtolower(self::ori_cls()); if ($name == 'conf2') { // For a certain 3.7rc correction, can be dropped after v4 $name = 'conf'; } return 'litespeed.' . $name . '.' . $id; } /** * Dropin with prefix for WP's get_option * * @since 3.0 */ public static function get_option($id, $default_v = false) { $v = get_option(self::name($id), $default_v); // Maybe decode array if (is_array($default_v)) { $v = self::_maybe_decode($v); } return $v; } /** * Dropin with prefix for WP's get_site_option * * @since 3.0 */ public static function get_site_option($id, $default_v = false) { $v = get_site_option(self::name($id), $default_v); // Maybe decode array if (is_array($default_v)) { $v = self::_maybe_decode($v); } return $v; } /** * Dropin with prefix for WP's get_blog_option * * @since 3.0 */ public static function get_blog_option($blog_id, $id, $default_v = false) { $v = get_blog_option($blog_id, self::name($id), $default_v); // Maybe decode array if (is_array($default_v)) { $v = self::_maybe_decode($v); } return $v; } /** * Dropin with prefix for WP's add_option * * @since 3.0 */ public static function add_option($id, $v) { add_option(self::name($id), self::_maybe_encode($v)); } /** * Dropin with prefix for WP's add_site_option * * @since 3.0 */ public static function add_site_option($id, $v) { add_site_option(self::name($id), self::_maybe_encode($v)); } /** * Dropin with prefix for WP's update_option * * @since 3.0 */ public static function update_option($id, $v) { update_option(self::name($id), self::_maybe_encode($v)); } /** * Dropin with prefix for WP's update_site_option * * @since 3.0 */ public static function update_site_option($id, $v) { update_site_option(self::name($id), self::_maybe_encode($v)); } /** * Decode an array * * @since 4.0 */ private static function _maybe_decode($v) { if (!is_array($v)) { $v2 = \json_decode($v, true); if ($v2 !== null) { $v = $v2; } } return $v; } /** * Encode an array * * @since 4.0 */ private static function _maybe_encode($v) { if (is_array($v)) { $v = \json_encode($v) ?: $v; // Non utf-8 encoded value will get failed, then used ori value } return $v; } /** * Dropin with prefix for WP's delete_option * * @since 3.0 */ public static function delete_option($id) { delete_option(self::name($id)); } /** * Dropin with prefix for WP's delete_site_option * * @since 3.0 */ public static function delete_site_option($id) { delete_site_option(self::name($id)); } /** * Read summary * * @since 3.0 * @access public */ public static function get_summary($field = false) { $summary = self::get_option('_summary', array()); if (!is_array($summary)) { $summary = array(); } if (!$field) { return $summary; } if (array_key_exists($field, $summary)) { return $summary[$field]; } return null; } /** * Save summary * * @since 3.0 * @access public */ public static function save_summary($data = false, $reload = false, $overwrite = false) { if ($reload || empty(static::cls()->_summary)) { self::reload_summary(); } $existing_summary = static::cls()->_summary; if ($overwrite || !is_array($existing_summary)) { $existing_summary = array(); } $new_summary = array_merge($existing_summary, $data ?: array()); // self::debug2('Save after Reloaded summary', $new_summary); static::cls()->_summary = $new_summary; self::update_option('_summary', $new_summary); } /** * Reload summary * @since 5.0 */ public static function reload_summary() { static::cls()->_summary = self::get_summary(); // self::debug2( 'Reloaded summary', static::cls()->_summary ); } /** * Get the current instance object. To be inherited. * * @since 3.0 */ public static function get_instance() { return static::cls(); } } src/lang.cls.php 0000644 00000035617 15162130450 0007556 0 ustar 00 <?php /** * The language class. * * @since 3.0 * @package LiteSpeed_Cache * @subpackage LiteSpeed_Cache/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Lang extends Base { /** * Get image status per status bit * * @since 3.0 */ public static function img_status($status = null) { $list = array( Img_Optm::STATUS_NEW => __('Images not requested', 'litespeed-cache'), Img_Optm::STATUS_RAW => __('Images ready to request', 'litespeed-cache'), Img_Optm::STATUS_REQUESTED => __('Images requested', 'litespeed-cache'), Img_Optm::STATUS_NOTIFIED => __('Images notified to pull', 'litespeed-cache'), Img_Optm::STATUS_PULLED => __('Images optimized and pulled', 'litespeed-cache'), ); if ($status !== null) { return !empty($list[$status]) ? $list[$status] : 'N/A'; } return $list; } /** * Try translating a string * * @since 4.7 */ public static function maybe_translate($raw_string) { $map = array( 'auto_alias_failed_cdn' => __('Unable to automatically add %1$s as a Domain Alias for main %2$s domain, due to potential CDN conflict.', 'litespeed-cache') . ' ' . Doc::learn_more('https://quic.cloud/docs/cdn/dns/how-to-setup-domain-alias/', false, false, false, true), 'auto_alias_failed_uid' => __('Unable to automatically add %1$s as a Domain Alias for main %2$s domain.', 'litespeed-cache') . ' ' . __('Alias is in use by another QUIC.cloud account.', 'litespeed-cache') . ' ' . Doc::learn_more('https://quic.cloud/docs/cdn/dns/how-to-setup-domain-alias/', false, false, false, true), ); // Maybe has placeholder if (strpos($raw_string, '::')) { $replacements = explode('::', $raw_string); if (empty($map[$replacements[0]])) { return $raw_string; } $tpl = $map[$replacements[0]]; unset($replacements[0]); return vsprintf($tpl, array_values($replacements)); } // Direct translation only if (empty($map[$raw_string])) { return $raw_string; } return $map[$raw_string]; } /** * Get the title of id * * @since 3.0 * @access public */ public static function title($id) { $_lang_list = array( self::O_SERVER_IP => __('Server IP', 'litespeed-cache'), self::O_GUEST_UAS => __('Guest Mode User Agents', 'litespeed-cache'), self::O_GUEST_IPS => __('Guest Mode IPs', 'litespeed-cache'), self::O_CACHE => __('Enable Cache', 'litespeed-cache'), self::O_CACHE_BROWSER => __('Browser Cache', 'litespeed-cache'), self::O_CACHE_TTL_PUB => __('Default Public Cache TTL', 'litespeed-cache'), self::O_CACHE_TTL_PRIV => __('Default Private Cache TTL', 'litespeed-cache'), self::O_CACHE_TTL_FRONTPAGE => __('Default Front Page TTL', 'litespeed-cache'), self::O_CACHE_TTL_FEED => __('Default Feed TTL', 'litespeed-cache'), self::O_CACHE_TTL_REST => __('Default REST TTL', 'litespeed-cache'), self::O_CACHE_TTL_STATUS => __('Default HTTP Status Code Page TTL', 'litespeed-cache'), self::O_CACHE_TTL_BROWSER => __('Browser Cache TTL', 'litespeed-cache'), self::O_CACHE_AJAX_TTL => __('AJAX Cache TTL', 'litespeed-cache'), self::O_AUTO_UPGRADE => __('Automatically Upgrade', 'litespeed-cache'), self::O_GUEST => __('Guest Mode', 'litespeed-cache'), self::O_GUEST_OPTM => __('Guest Optimization', 'litespeed-cache'), self::O_NEWS => __('Notifications', 'litespeed-cache'), self::O_CACHE_PRIV => __('Cache Logged-in Users', 'litespeed-cache'), self::O_CACHE_COMMENTER => __('Cache Commenters', 'litespeed-cache'), self::O_CACHE_REST => __('Cache REST API', 'litespeed-cache'), self::O_CACHE_PAGE_LOGIN => __('Cache Login Page', 'litespeed-cache'), self::O_CACHE_RES => __('Cache PHP Resources', 'litespeed-cache'), self::O_CACHE_MOBILE => __('Cache Mobile', 'litespeed-cache'), self::O_CACHE_MOBILE_RULES => __('List of Mobile User Agents', 'litespeed-cache'), self::O_CACHE_PRIV_URI => __('Private Cached URIs', 'litespeed-cache'), self::O_CACHE_DROP_QS => __('Drop Query String', 'litespeed-cache'), self::O_OBJECT => __('Object Cache', 'litespeed-cache'), self::O_OBJECT_KIND => __('Method', 'litespeed-cache'), self::O_OBJECT_HOST => __('Host', 'litespeed-cache'), self::O_OBJECT_PORT => __('Port', 'litespeed-cache'), self::O_OBJECT_LIFE => __('Default Object Lifetime', 'litespeed-cache'), self::O_OBJECT_USER => __('Username', 'litespeed-cache'), self::O_OBJECT_PSWD => __('Password', 'litespeed-cache'), self::O_OBJECT_DB_ID => __('Redis Database ID', 'litespeed-cache'), self::O_OBJECT_GLOBAL_GROUPS => __('Global Groups', 'litespeed-cache'), self::O_OBJECT_NON_PERSISTENT_GROUPS => __('Do Not Cache Groups', 'litespeed-cache'), self::O_OBJECT_PERSISTENT => __('Persistent Connection', 'litespeed-cache'), self::O_OBJECT_ADMIN => __('Cache WP-Admin', 'litespeed-cache'), self::O_OBJECT_TRANSIENTS => __('Store Transients', 'litespeed-cache'), self::O_PURGE_ON_UPGRADE => __('Purge All On Upgrade', 'litespeed-cache'), self::O_PURGE_STALE => __('Serve Stale', 'litespeed-cache'), self::O_PURGE_TIMED_URLS => __('Scheduled Purge URLs', 'litespeed-cache'), self::O_PURGE_TIMED_URLS_TIME => __('Scheduled Purge Time', 'litespeed-cache'), self::O_CACHE_FORCE_URI => __('Force Cache URIs', 'litespeed-cache'), self::O_CACHE_FORCE_PUB_URI => __('Force Public Cache URIs', 'litespeed-cache'), self::O_CACHE_EXC => __('Do Not Cache URIs', 'litespeed-cache'), self::O_CACHE_EXC_QS => __('Do Not Cache Query Strings', 'litespeed-cache'), self::O_CACHE_EXC_CAT => __('Do Not Cache Categories', 'litespeed-cache'), self::O_CACHE_EXC_TAG => __('Do Not Cache Tags', 'litespeed-cache'), self::O_CACHE_EXC_ROLES => __('Do Not Cache Roles', 'litespeed-cache'), self::O_OPTM_CSS_MIN => __('CSS Minify', 'litespeed-cache'), self::O_OPTM_CSS_COMB => __('CSS Combine', 'litespeed-cache'), self::O_OPTM_CSS_COMB_EXT_INL => __('CSS Combine External and Inline', 'litespeed-cache'), self::O_OPTM_UCSS => __('Generate UCSS', 'litespeed-cache'), self::O_OPTM_UCSS_INLINE => __('UCSS Inline', 'litespeed-cache'), self::O_OPTM_UCSS_SELECTOR_WHITELIST => __('UCSS Selector Allowlist', 'litespeed-cache'), self::O_OPTM_UCSS_FILE_EXC_INLINE => __('UCSS File Excludes and Inline', 'litespeed-cache'), self::O_OPTM_UCSS_EXC => __('UCSS URI Excludes', 'litespeed-cache'), self::O_OPTM_JS_MIN => __('JS Minify', 'litespeed-cache'), self::O_OPTM_JS_COMB => __('JS Combine', 'litespeed-cache'), self::O_OPTM_JS_COMB_EXT_INL => __('JS Combine External and Inline', 'litespeed-cache'), self::O_OPTM_HTML_MIN => __('HTML Minify', 'litespeed-cache'), self::O_OPTM_HTML_LAZY => __('HTML Lazy Load Selectors', 'litespeed-cache'), self::O_OPTM_HTML_SKIP_COMMENTS => __('HTML Keep Comments', 'litespeed-cache'), self::O_OPTM_CSS_ASYNC => __('Load CSS Asynchronously', 'litespeed-cache'), self::O_OPTM_CCSS_PER_URL => __('CCSS Per URL', 'litespeed-cache'), self::O_OPTM_CSS_ASYNC_INLINE => __('Inline CSS Async Lib', 'litespeed-cache'), self::O_OPTM_CSS_FONT_DISPLAY => __('Font Display Optimization', 'litespeed-cache'), self::O_OPTM_JS_DEFER => __('Load JS Deferred', 'litespeed-cache'), self::O_OPTM_LOCALIZE => __('Localize Resources', 'litespeed-cache'), self::O_OPTM_LOCALIZE_DOMAINS => __('Localization Files', 'litespeed-cache'), self::O_OPTM_DNS_PREFETCH => __('DNS Prefetch', 'litespeed-cache'), self::O_OPTM_DNS_PREFETCH_CTRL => __('DNS Prefetch Control', 'litespeed-cache'), self::O_OPTM_DNS_PRECONNECT => __('DNS Preconnect', 'litespeed-cache'), self::O_OPTM_CSS_EXC => __('CSS Excludes', 'litespeed-cache'), self::O_OPTM_JS_DELAY_INC => __('JS Delayed Includes', 'litespeed-cache'), self::O_OPTM_JS_EXC => __('JS Excludes', 'litespeed-cache'), self::O_OPTM_QS_RM => __('Remove Query Strings', 'litespeed-cache'), self::O_OPTM_GGFONTS_ASYNC => __('Load Google Fonts Asynchronously', 'litespeed-cache'), self::O_OPTM_GGFONTS_RM => __('Remove Google Fonts', 'litespeed-cache'), self::O_OPTM_CCSS_CON => __('Critical CSS Rules', 'litespeed-cache'), self::O_OPTM_CCSS_SEP_POSTTYPE => __('Separate CCSS Cache Post Types', 'litespeed-cache'), self::O_OPTM_CCSS_SEP_URI => __('Separate CCSS Cache URIs', 'litespeed-cache'), self::O_OPTM_CCSS_SELECTOR_WHITELIST => __('CCSS Selector Allowlist', 'litespeed-cache'), self::O_OPTM_JS_DEFER_EXC => __('JS Deferred / Delayed Excludes', 'litespeed-cache'), self::O_OPTM_GM_JS_EXC => __('Guest Mode JS Excludes', 'litespeed-cache'), self::O_OPTM_EMOJI_RM => __('Remove WordPress Emoji', 'litespeed-cache'), self::O_OPTM_NOSCRIPT_RM => __('Remove Noscript Tags', 'litespeed-cache'), self::O_OPTM_EXC => __('URI Excludes', 'litespeed-cache'), self::O_OPTM_GUEST_ONLY => __('Optimize for Guests Only', 'litespeed-cache'), self::O_OPTM_EXC_ROLES => __('Role Excludes', 'litespeed-cache'), self::O_DISCUSS_AVATAR_CACHE => __('Gravatar Cache', 'litespeed-cache'), self::O_DISCUSS_AVATAR_CRON => __('Gravatar Cache Cron', 'litespeed-cache'), self::O_DISCUSS_AVATAR_CACHE_TTL => __('Gravatar Cache TTL', 'litespeed-cache'), self::O_MEDIA_LAZY => __('Lazy Load Images', 'litespeed-cache'), self::O_MEDIA_LAZY_EXC => __('Lazy Load Image Excludes', 'litespeed-cache'), self::O_MEDIA_LAZY_CLS_EXC => __('Lazy Load Image Class Name Excludes', 'litespeed-cache'), self::O_MEDIA_LAZY_PARENT_CLS_EXC => __('Lazy Load Image Parent Class Name Excludes', 'litespeed-cache'), self::O_MEDIA_IFRAME_LAZY_CLS_EXC => __('Lazy Load Iframe Class Name Excludes', 'litespeed-cache'), self::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC => __('Lazy Load Iframe Parent Class Name Excludes', 'litespeed-cache'), self::O_MEDIA_LAZY_URI_EXC => __('Lazy Load URI Excludes', 'litespeed-cache'), self::O_MEDIA_LQIP_EXC => __('LQIP Excludes', 'litespeed-cache'), self::O_MEDIA_LAZY_PLACEHOLDER => __('Basic Image Placeholder', 'litespeed-cache'), self::O_MEDIA_PLACEHOLDER_RESP => __('Responsive Placeholder', 'litespeed-cache'), self::O_MEDIA_PLACEHOLDER_RESP_COLOR => __('Responsive Placeholder Color', 'litespeed-cache'), self::O_MEDIA_PLACEHOLDER_RESP_SVG => __('Responsive Placeholder SVG', 'litespeed-cache'), self::O_MEDIA_LQIP => __('LQIP Cloud Generator', 'litespeed-cache'), self::O_MEDIA_LQIP_QUAL => __('LQIP Quality', 'litespeed-cache'), self::O_MEDIA_LQIP_MIN_W => __('LQIP Minimum Dimensions', 'litespeed-cache'), // self::O_MEDIA_LQIP_MIN_H => __( 'LQIP Minimum Height', 'litespeed-cache' ), self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => __('Generate LQIP In Background', 'litespeed-cache'), self::O_MEDIA_IFRAME_LAZY => __('Lazy Load Iframes', 'litespeed-cache'), self::O_MEDIA_ADD_MISSING_SIZES => __('Add Missing Sizes', 'litespeed-cache'), self::O_MEDIA_VPI => __('Viewport Images', 'litespeed-cache'), self::O_MEDIA_VPI_CRON => __('Viewport Images Cron', 'litespeed-cache'), self::O_IMG_OPTM_AUTO => __('Auto Request Cron', 'litespeed-cache'), self::O_IMG_OPTM_ORI => __('Optimize Original Images', 'litespeed-cache'), self::O_IMG_OPTM_RM_BKUP => __('Remove Original Backups', 'litespeed-cache'), self::O_IMG_OPTM_WEBP => __('Next-Gen Image Format', 'litespeed-cache'), self::O_IMG_OPTM_LOSSLESS => __('Optimize Losslessly', 'litespeed-cache'), self::O_IMG_OPTM_EXIF => __('Preserve EXIF/XMP data', 'litespeed-cache'), self::O_IMG_OPTM_WEBP_ATTR => __('WebP/AVIF Attribute To Replace', 'litespeed-cache'), self::O_IMG_OPTM_WEBP_REPLACE_SRCSET => __('WebP/AVIF For Extra srcset', 'litespeed-cache'), self::O_IMG_OPTM_JPG_QUALITY => __('WordPress Image Quality Control', 'litespeed-cache'), self::O_ESI => __('Enable ESI', 'litespeed-cache'), self::O_ESI_CACHE_ADMBAR => __('Cache Admin Bar', 'litespeed-cache'), self::O_ESI_CACHE_COMMFORM => __('Cache Comment Form', 'litespeed-cache'), self::O_ESI_NONCE => __('ESI Nonces', 'litespeed-cache'), self::O_CACHE_VARY_GROUP => __('Vary Group', 'litespeed-cache'), self::O_PURGE_HOOK_ALL => __('Purge All Hooks', 'litespeed-cache'), self::O_UTIL_NO_HTTPS_VARY => __('Improve HTTP/HTTPS Compatibility', 'litespeed-cache'), self::O_UTIL_INSTANT_CLICK => __('Instant Click', 'litespeed-cache'), self::O_CACHE_EXC_COOKIES => __('Do Not Cache Cookies', 'litespeed-cache'), self::O_CACHE_EXC_USERAGENTS => __('Do Not Cache User Agents', 'litespeed-cache'), self::O_CACHE_LOGIN_COOKIE => __('Login Cookie', 'litespeed-cache'), self::O_CACHE_VARY_COOKIES => __('Vary Cookies', 'litespeed-cache'), self::O_MISC_HEARTBEAT_FRONT => __('Frontend Heartbeat Control', 'litespeed-cache'), self::O_MISC_HEARTBEAT_FRONT_TTL => __('Frontend Heartbeat TTL', 'litespeed-cache'), self::O_MISC_HEARTBEAT_BACK => __('Backend Heartbeat Control', 'litespeed-cache'), self::O_MISC_HEARTBEAT_BACK_TTL => __('Backend Heartbeat TTL', 'litespeed-cache'), self::O_MISC_HEARTBEAT_EDITOR => __('Editor Heartbeat', 'litespeed-cache'), self::O_MISC_HEARTBEAT_EDITOR_TTL => __('Editor Heartbeat TTL', 'litespeed-cache'), self::O_CDN => __('Use CDN Mapping', 'litespeed-cache'), self::CDN_MAPPING_URL => __('CDN URL', 'litespeed-cache'), self::CDN_MAPPING_INC_IMG => __('Include Images', 'litespeed-cache'), self::CDN_MAPPING_INC_CSS => __('Include CSS', 'litespeed-cache'), self::CDN_MAPPING_INC_JS => __('Include JS', 'litespeed-cache'), self::CDN_MAPPING_FILETYPE => __('Include File Types', 'litespeed-cache'), self::O_CDN_ATTR => __('HTML Attribute To Replace', 'litespeed-cache'), self::O_CDN_ORI => __('Original URLs', 'litespeed-cache'), self::O_CDN_ORI_DIR => __('Included Directories', 'litespeed-cache'), self::O_CDN_EXC => __('Exclude Path', 'litespeed-cache'), self::O_CDN_CLOUDFLARE => __('Cloudflare API', 'litespeed-cache'), self::O_CRAWLER => __('Crawler', 'litespeed-cache'), self::O_CRAWLER_CRAWL_INTERVAL => __('Crawl Interval', 'litespeed-cache'), self::O_CRAWLER_LOAD_LIMIT => __('Server Load Limit', 'litespeed-cache'), self::O_CRAWLER_ROLES => __('Role Simulation', 'litespeed-cache'), self::O_CRAWLER_COOKIES => __('Cookie Simulation', 'litespeed-cache'), self::O_CRAWLER_SITEMAP => __('Custom Sitemap', 'litespeed-cache'), self::O_DEBUG_DISABLE_ALL => __('Disable All Features', 'litespeed-cache'), self::O_DEBUG => __('Debug Log', 'litespeed-cache'), self::O_DEBUG_IPS => __('Admin IPs', 'litespeed-cache'), self::O_DEBUG_LEVEL => __('Debug Level', 'litespeed-cache'), self::O_DEBUG_FILESIZE => __('Log File Size Limit', 'litespeed-cache'), self::O_DEBUG_COLLAPSE_QS => __('Collapse Query Strings', 'litespeed-cache'), self::O_DEBUG_INC => __('Debug URI Includes', 'litespeed-cache'), self::O_DEBUG_EXC => __('Debug URI Excludes', 'litespeed-cache'), self::O_DEBUG_EXC_STRINGS => __('Debug String Excludes', 'litespeed-cache'), self::O_DB_OPTM_REVISIONS_MAX => __('Revisions Max Number', 'litespeed-cache'), self::O_DB_OPTM_REVISIONS_AGE => __('Revisions Max Age', 'litespeed-cache'), ); if (array_key_exists($id, $_lang_list)) { return $_lang_list[$id]; } return 'N/A'; } } src/purge.cls.php 0000644 00000074764 15162130452 0007767 0 ustar 00 <?php /** * The plugin purge class for X-LiteSpeed-Purge * * @since 1.1.3 * @since 2.2 Refactored. Changed access from public to private for most func and class variables. */ namespace LiteSpeed; defined('WPINC') || exit(); class Purge extends Base { const LOG_TAG = '🧹'; protected $_pub_purge = array(); protected $_pub_purge2 = array(); protected $_priv_purge = array(); protected $_purge_single = false; const X_HEADER = 'X-LiteSpeed-Purge'; const X_HEADER2 = 'X-LiteSpeed-Purge2'; const DB_QUEUE = 'queue'; const DB_QUEUE2 = 'queue2'; const TYPE_PURGE_ALL = 'purge_all'; const TYPE_PURGE_ALL_LSCACHE = 'purge_all_lscache'; const TYPE_PURGE_ALL_CSSJS = 'purge_all_cssjs'; const TYPE_PURGE_ALL_LOCALRES = 'purge_all_localres'; const TYPE_PURGE_ALL_CCSS = 'purge_all_ccss'; const TYPE_PURGE_ALL_UCSS = 'purge_all_ucss'; const TYPE_PURGE_ALL_LQIP = 'purge_all_lqip'; const TYPE_PURGE_ALL_AVATAR = 'purge_all_avatar'; const TYPE_PURGE_ALL_OBJECT = 'purge_all_object'; const TYPE_PURGE_ALL_OPCACHE = 'purge_all_opcache'; const TYPE_PURGE_FRONT = 'purge_front'; const TYPE_PURGE_UCSS = 'purge_ucss'; const TYPE_PURGE_FRONTPAGE = 'purge_frontpage'; const TYPE_PURGE_PAGES = 'purge_pages'; const TYPE_PURGE_ERROR = 'purge_error'; /** * Init hooks * * @since 3.0 */ public function init() { // Register purge actions. // Most used values: edit_post, save_post, delete_post, wp_trash_post, clean_post_cache, wp_update_comment_count $purge_post_events = apply_filters('litespeed_purge_post_events', array( 'delete_post', 'wp_trash_post', // 'clean_post_cache', // This will disable wc's not purge product when stock status not change setting 'wp_update_comment_count', // TODO: check if needed for non ESI )); foreach ($purge_post_events as $event) { // this will purge all related tags add_action($event, array($this, 'purge_post')); } // Purge post only when status is/was publish add_action('transition_post_status', array($this, 'purge_publish'), 10, 3); add_action('wp_update_comment_count', array($this, 'purge_feeds')); if ($this->conf(self::O_OPTM_UCSS)) { add_action('edit_post', __NAMESPACE__ . '\Purge::purge_ucss'); } } /** * Only purge publish related status post * * @since 3.0 * @access public */ public function purge_publish($new_status, $old_status, $post) { if ($new_status != 'publish' && $old_status != 'publish') { return; } $this->purge_post($post->ID); } /** * Handle all request actions from main cls * * @since 1.8 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_PURGE_ALL: $this->_purge_all(); break; case self::TYPE_PURGE_ALL_LSCACHE: $this->_purge_all_lscache(); break; case self::TYPE_PURGE_ALL_CSSJS: $this->_purge_all_cssjs(); break; case self::TYPE_PURGE_ALL_LOCALRES: $this->_purge_all_localres(); break; case self::TYPE_PURGE_ALL_CCSS: $this->_purge_all_ccss(); break; case self::TYPE_PURGE_ALL_UCSS: $this->_purge_all_ucss(); break; case self::TYPE_PURGE_ALL_LQIP: $this->_purge_all_lqip(); break; case self::TYPE_PURGE_ALL_AVATAR: $this->_purge_all_avatar(); break; case self::TYPE_PURGE_ALL_OBJECT: $this->_purge_all_object(); break; case self::TYPE_PURGE_ALL_OPCACHE: $this->purge_all_opcache(); break; case self::TYPE_PURGE_FRONT: $this->_purge_front(); break; case self::TYPE_PURGE_UCSS: $this->_purge_ucss(); break; case self::TYPE_PURGE_FRONTPAGE: $this->_purge_frontpage(); break; case self::TYPE_PURGE_PAGES: $this->_purge_pages(); break; case strpos($type, self::TYPE_PURGE_ERROR) === 0: $this->_purge_error(substr($type, strlen(self::TYPE_PURGE_ERROR))); break; default: break; } Admin::redirect(); } /** * Shortcut to purge all lscache * * @since 1.0.0 * @access public */ public static function purge_all($reason = false) { self::cls()->_purge_all($reason); } /** * Purge all caches (lscache/op/oc) * * @since 2.2 * @access private */ private function _purge_all($reason = false) { // if ( defined( 'LITESPEED_CLI' ) ) { // // Can't send, already has output, need to save and wait for next run // self::update_option( self::DB_QUEUE, $curr_built ); // self::debug( 'CLI request, queue stored: ' . $curr_built ); // } // else { $this->_purge_all_lscache(true); $this->_purge_all_cssjs(true); $this->_purge_all_localres(true); // $this->_purge_all_ccss( true ); // $this->_purge_all_lqip( true ); $this->_purge_all_object(true); $this->purge_all_opcache(true); // } if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = "( $reason )"; } self::debug('Purge all ' . $reason, 3); $msg = __('Purged all caches successfully.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); do_action('litespeed_purged_all'); } /** * Alerts LiteSpeed Web Server to purge all pages. * * For multisite installs, if this is called by a site admin (not network admin), * it will only purge all posts associated with that site. * * @since 2.2 * @access public */ private function _purge_all_lscache($silence = false) { $this->_add('*'); // Action to run after server was notified to delete LSCache entries. do_action('litespeed_purged_all_lscache'); if (!$silence) { $msg = __('Notified LiteSpeed Web Server to purge all LSCache entries.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Delete all critical css * * @since 2.3 * @access private */ private function _purge_all_ccss($silence = false) { do_action('litespeed_purged_all_ccss'); $this->cls('CSS')->rm_cache_folder('ccss'); $this->cls('Data')->url_file_clean('ccss'); if (!$silence) { $msg = __('Cleaned all Critical CSS files.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Delete all unique css * * @since 2.3 * @access private */ private function _purge_all_ucss($silence = false) { do_action('litespeed_purged_all_ucss'); $this->cls('CSS')->rm_cache_folder('ucss'); $this->cls('Data')->url_file_clean('ucss'); if (!$silence) { $msg = __('Cleaned all Unique CSS files.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Purge one UCSS by URL * * @since 4.5 * @access public */ public static function purge_ucss($post_id_or_url) { self::debug('Purge a single UCSS: ' . $post_id_or_url); // If is post_id, generate URL if (!preg_match('/\D/', $post_id_or_url)) { $post_id_or_url = get_permalink($post_id_or_url); } $post_id_or_url = untrailingslashit($post_id_or_url); $existing_url_files = Data::cls()->mark_as_expired($post_id_or_url, true); if ($existing_url_files) { // Add to UCSS Q self::cls('UCSS')->add_to_q($existing_url_files); } } /** * Delete all LQIP images * * @since 3.0 * @access private */ private function _purge_all_lqip($silence = false) { do_action('litespeed_purged_all_lqip'); $this->cls('Placeholder')->rm_cache_folder('lqip'); if (!$silence) { $msg = __('Cleaned all LQIP files.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Delete all avatar images * * @since 3.0 * @access private */ private function _purge_all_avatar($silence = false) { do_action('litespeed_purged_all_avatar'); $this->cls('Avatar')->rm_cache_folder('avatar'); if (!$silence) { $msg = __('Cleaned all Gravatar files.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Delete all localized JS * * @since 3.3 * @access private */ private function _purge_all_localres($silence = false) { do_action('litespeed_purged_all_localres'); $this->_add(Tag::TYPE_LOCALRES); if (!$silence) { $msg = __('Cleaned all localized resource entries.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Alerts LiteSpeed Web Server to purge pages. * * @since 1.2.2 * @access private */ private function _purge_all_cssjs($silence = false) { if (defined('DOING_CRON') || defined('LITESPEED_DID_send_headers')) { self::debug('❌ Bypassed cssjs delete as header sent (lscache purge after this point will fail) or doing cron'); return; } $this->_purge_all_lscache($silence); // Purge CSSJS must purge lscache too to avoid 404 do_action('litespeed_purged_all_cssjs'); Optimize::update_option(Optimize::ITEM_TIMESTAMP_PURGE_CSS, time()); $this->_add(Tag::TYPE_MIN); $this->cls('CSS')->rm_cache_folder('css'); $this->cls('CSS')->rm_cache_folder('js'); $this->cls('Data')->url_file_clean('css'); $this->cls('Data')->url_file_clean('js'); // Clear UCSS queue as it used combined CSS to generate $this->clear_q('ucss', true); if (!$silence) { $msg = __('Notified LiteSpeed Web Server to purge CSS/JS entries.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } } /** * Purge opcode cache * * @since 1.8.2 * @access public */ public function purge_all_opcache($silence = false) { if (!Router::opcache_enabled()) { self::debug('Failed to reset opcode cache due to opcache not enabled'); if (!$silence) { $msg = __('Opcode cache is not enabled.', 'litespeed-cache'); Admin_Display::error($msg); } return false; } // Action to run after opcache purge. do_action('litespeed_purged_all_opcache'); // Purge opcode cache opcache_reset(); self::debug('Reset opcode cache'); if (!$silence) { $msg = __('Reset the entire opcode cache successfully.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } return true; } /** * Purge object cache * * @since 3.4 * @access public */ public static function purge_all_object($silence = true) { self::cls()->_purge_all_object($silence); } /** * Purge object cache * * @since 1.8 * @access private */ private function _purge_all_object($silence = false) { if (!defined('LSCWP_OBJECT_CACHE')) { self::debug('Failed to flush object cache due to object cache not enabled'); if (!$silence) { $msg = __('Object cache is not enabled.', 'litespeed-cache'); Admin_Display::error($msg); } return false; } do_action('litespeed_purged_all_object'); $this->cls('Object_Cache')->flush(); self::debug('Flushed object cache'); if (!$silence) { $msg = __('Purge all object caches successfully.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } return true; } /** * Adds new public purge tags to the array of purge tags for the request. * * @since 1.1.3 * @access public * @param mixed $tags Tags to add to the list. */ public static function add($tags, $purge2 = false) { self::cls()->_add($tags, $purge2); } /** * Add tags to purge * * @since 2.2 * @access private */ private function _add($tags, $purge2 = false) { if (!is_array($tags)) { $tags = array($tags); } $tags = $this->_prepend_bid($tags); if (!array_diff($tags, $purge2 ? $this->_pub_purge2 : $this->_pub_purge)) { return; } if ($purge2) { $this->_pub_purge2 = array_merge($this->_pub_purge2, $tags); $this->_pub_purge2 = array_unique($this->_pub_purge2); } else { $this->_pub_purge = array_merge($this->_pub_purge, $tags); $this->_pub_purge = array_unique($this->_pub_purge); } self::debug('added ' . implode(',', $tags) . ($purge2 ? ' [Purge2]' : ''), 8); // Send purge header immediately $curr_built = $this->_build($purge2); if (defined('LITESPEED_CLI')) { // Can't send, already has output, need to save and wait for next run self::update_option($purge2 ? self::DB_QUEUE2 : self::DB_QUEUE, $curr_built); self::debug('CLI request, queue stored: ' . $curr_built); } else { @header($curr_built); if (defined('DOING_CRON') || defined('LITESPEED_DID_send_headers') || apply_filters('litespeed_delay_purge', false)) { self::update_option($purge2 ? self::DB_QUEUE2 : self::DB_QUEUE, $curr_built); self::debug('Output existed, queue stored: ' . $curr_built); } self::debug($curr_built); } } /** * Adds new private purge tags to the array of purge tags for the request. * * @since 1.1.3 * @access public * @param mixed $tags Tags to add to the list. */ public static function add_private($tags) { self::cls()->_add_private($tags); } /** * Add private ESI tag to purge list * * @since 3.0 * @access public */ public static function add_private_esi($tag) { self::add_private(Tag::TYPE_ESI . $tag); } /** * Add private all tag to purge list * * @since 3.0 * @access public */ public static function add_private_all() { self::add_private('*'); } /** * Add tags to private purge * * @since 2.2 * @access private */ private function _add_private($tags) { if (!is_array($tags)) { $tags = array($tags); } $tags = $this->_prepend_bid($tags); if (!array_diff($tags, $this->_priv_purge)) { return; } self::debug('added [private] ' . implode(',', $tags), 3); $this->_priv_purge = array_merge($this->_priv_purge, $tags); $this->_priv_purge = array_unique($this->_priv_purge); // Send purge header immediately @header($this->_build()); } /** * Incorporate blog_id into purge tags for multisite * * @since 4.0 * @access private * @param mixed $tags Tags to add to the list. */ private function _prepend_bid($tags) { if (in_array('*', $tags)) { return array('*'); } $curr_bid = is_multisite() ? get_current_blog_id() : ''; foreach ($tags as $k => $v) { $tags[$k] = $curr_bid . '_' . $v; } return $tags; } /** * Activate `purge related tags` for Admin QS. * * @since 1.1.3 * @access public * @deprecated @7.0 Drop @v7.5 */ public static function set_purge_related() { } /** * Activate `purge single url tag` for Admin QS. * * @since 1.1.3 * @access public */ public static function set_purge_single() { self::cls()->_purge_single = true; do_action('litespeed_purged_single'); } /** * Purge frontend url * * @since 1.3 * @since 2.2 Renamed from `frontend_purge`; Access changed from public * @access private */ private function _purge_front() { if (empty($_SERVER['HTTP_REFERER'])) { exit('no referer'); } $this->purge_url($_SERVER['HTTP_REFERER']); do_action('litespeed_purged_front', $_SERVER['HTTP_REFERER']); wp_redirect($_SERVER['HTTP_REFERER']); exit(); } /** * Purge single UCSS * @since 4.7 */ private function _purge_ucss() { if (empty($_SERVER['HTTP_REFERER'])) { exit('no referer'); } $url_tag = empty($_GET['url_tag']) ? $_SERVER['HTTP_REFERER'] : $_GET['url_tag']; self::debug('Purge ucss [url_tag] ' . $url_tag); do_action('litespeed_purge_ucss', $url_tag); $this->purge_url($_SERVER['HTTP_REFERER']); wp_redirect($_SERVER['HTTP_REFERER']); exit(); } /** * Alerts LiteSpeed Web Server to purge the front page. * * @since 1.0.3 * @since 2.2 Access changed from public to private, renamed from `_purge_front` * @access private */ private function _purge_frontpage() { $this->_add(Tag::TYPE_FRONTPAGE); if (LITESPEED_SERVER_TYPE !== 'LITESPEED_SERVER_OLS') { $this->_add_private(Tag::TYPE_FRONTPAGE); } $msg = __('Notified LiteSpeed Web Server to purge the front page.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); do_action('litespeed_purged_frontpage'); } /** * Alerts LiteSpeed Web Server to purge pages. * * @since 1.0.15 * @access private */ private function _purge_pages() { $this->_add(Tag::TYPE_PAGES); $msg = __('Notified LiteSpeed Web Server to purge all pages.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); do_action('litespeed_purged_pages'); } /** * Alerts LiteSpeed Web Server to purge error pages. * * @since 1.0.14 * @access private */ private function _purge_error($type = false) { $this->_add(Tag::TYPE_HTTP); if (!$type || !in_array($type, array('403', '404', '500'))) { return; } $this->_add(Tag::TYPE_HTTP . $type); $msg = __('Notified LiteSpeed Web Server to purge error pages.', 'litespeed-cache'); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success($msg); } /** * Callback to add purge tags if admin selects to purge selected category pages. * * @since 1.0.7 * @access public */ public function purge_cat($value) { $val = trim($value); if (empty($val)) { return; } if (preg_match('/^[a-zA-Z0-9-]+$/', $val) == 0) { self::debug("$val cat invalid"); return; } $cat = get_category_by_slug($val); if ($cat == false) { self::debug("$val cat not existed/published"); return; } self::add(Tag::TYPE_ARCHIVE_TERM . $cat->term_id); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success(sprintf(__('Purge category %s', 'litespeed-cache'), $val)); // Action to run after category purge. do_action('litespeed_purged_cat', $value); } /** * Callback to add purge tags if admin selects to purge selected tag pages. * * @since 1.0.7 * @access public */ public function purge_tag($val) { $val = trim($val); if (empty($val)) { return; } if (preg_match('/^[a-zA-Z0-9-]+$/', $val) == 0) { self::debug("$val tag invalid"); return; } $term = get_term_by('slug', $val, 'post_tag'); if ($term == 0) { self::debug("$val tag not exist"); return; } self::add(Tag::TYPE_ARCHIVE_TERM . $term->term_id); !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success(sprintf(__('Purge tag %s', 'litespeed-cache'), $val)); // Action to run after tag purge. do_action('litespeed_purged_tag', $val); } /** * Callback to add purge tags if admin selects to purge selected urls. * * @since 1.0.7 * @access public */ public function purge_url($url, $purge2 = false, $quite = false) { $val = trim($url); if (empty($val)) { return; } if (strpos($val, '<') !== false) { self::debug("$val url contains <"); return; } $val = Utility::make_relative($val); $hash = Tag::get_uri_tag($val); if ($hash === false) { self::debug("$val url invalid"); return; } self::add($hash, $purge2); !$quite && !defined('LITESPEED_PURGE_SILENT') && Admin_Display::success(sprintf(__('Purge url %s', 'litespeed-cache'), $val)); // Action to run after url purge. do_action('litespeed_purged_link', $url); } /** * Purge a list of pages when selected by admin. This method will look at the post arguments to determine how and what to purge. * * @since 1.0.7 * @access public */ public function purge_list() { if (!isset($_REQUEST[Admin_Display::PURGEBYOPT_SELECT]) || !isset($_REQUEST[Admin_Display::PURGEBYOPT_LIST])) { return; } $sel = $_REQUEST[Admin_Display::PURGEBYOPT_SELECT]; $list_buf = $_REQUEST[Admin_Display::PURGEBYOPT_LIST]; if (empty($list_buf)) { return; } $list_buf = str_replace(',', "\n", $list_buf); // for cli $list = explode("\n", $list_buf); switch ($sel) { case Admin_Display::PURGEBY_CAT: $cb = 'purge_cat'; break; case Admin_Display::PURGEBY_PID: $cb = 'purge_post'; break; case Admin_Display::PURGEBY_TAG: $cb = 'purge_tag'; break; case Admin_Display::PURGEBY_URL: $cb = 'purge_url'; break; default: return; } array_map(array($this, $cb), $list); // for redirection $_GET[Admin_Display::PURGEBYOPT_SELECT] = $sel; } /** * Purge ESI * * @since 3.0 * @access public */ public static function purge_esi($tag) { self::add(Tag::TYPE_ESI . $tag); do_action('litespeed_purged_esi', $tag); } /** * Purge a certain post type * * @since 3.0 * @access public */ public static function purge_posttype($post_type) { self::add(Tag::TYPE_ARCHIVE_POSTTYPE . $post_type); self::add($post_type); do_action('litespeed_purged_posttype', $post_type); } /** * Purge all related tags to a post. * * @since 1.0.0 * @access public */ public function purge_post($pid) { $pid = intval($pid); // ignore the status we don't care if (!$pid || !in_array(get_post_status($pid), array('publish', 'trash', 'private', 'draft'))) { return; } $purge_tags = $this->_get_purge_tags_by_post($pid); if (!$purge_tags) { return; } self::add($purge_tags); if ($this->conf(self::O_CACHE_REST)) { self::add(Tag::TYPE_REST); } // $this->cls( 'Control' )->set_stale(); do_action('litespeed_purged_post', $pid); } /** * Hooked to the load-widgets.php action. * Attempts to purge a single widget from cache. * If no widget id is passed in, the method will attempt to find the widget id. * * @since 1.1.3 * @access public */ public static function purge_widget($widget_id = null) { if (is_null($widget_id)) { $widget_id = $_POST['widget-id']; if (is_null($widget_id)) { return; } } self::add(Tag::TYPE_WIDGET . $widget_id); self::add_private(Tag::TYPE_WIDGET . $widget_id); do_action('litespeed_purged_widget', $widget_id); } /** * Hooked to the wp_update_comment_count action. * Purges the comment widget when the count is updated. * * @access public * @since 1.1.3 * @global type $wp_widget_factory */ public static function purge_comment_widget() { global $wp_widget_factory; if (!isset($wp_widget_factory->widgets['WP_Widget_Recent_Comments'])) { return; } $recent_comments = $wp_widget_factory->widgets['WP_Widget_Recent_Comments']; if (!is_null($recent_comments)) { self::add(Tag::TYPE_WIDGET . $recent_comments->id); self::add_private(Tag::TYPE_WIDGET . $recent_comments->id); do_action('litespeed_purged_comment_widget', $recent_comments->id); } } /** * Purges feeds on comment count update. * * @since 1.0.9 * @access public */ public function purge_feeds() { if ($this->conf(self::O_CACHE_TTL_FEED) > 0) { self::add(Tag::TYPE_FEED); } do_action('litespeed_purged_feeds'); } /** * Purges all private cache entries when the user logs out. * * @access public * @since 1.1.3 */ public static function purge_on_logout() { self::add_private_all(); do_action('litespeed_purged_on_logout'); } /** * Generate all purge tags before output * * @access private * @since 1.1.3 */ private function _finalize() { // Make sure header output only run once if (!defined('LITESPEED_DID_' . __FUNCTION__)) { define('LITESPEED_DID_' . __FUNCTION__, true); } else { return; } do_action('litespeed_purge_finalize'); // Append unique uri purge tags if Admin QS is `PURGESINGLE` or `PURGE` if ($this->_purge_single) { $tags = array(Tag::build_uri_tag()); $this->_pub_purge = array_merge($this->_pub_purge, $this->_prepend_bid($tags)); } if (!empty($this->_pub_purge)) { $this->_pub_purge = array_unique($this->_pub_purge); } if (!empty($this->_priv_purge)) { $this->_priv_purge = array_unique($this->_priv_purge); } } /** * Gathers all the purge headers. * * This will collect all site wide purge tags as well as third party plugin defined purge tags. * * @since 1.1.0 * @access public * @return string the built purge header */ public static function output() { $instance = self::cls(); $instance->_finalize(); return $instance->_build(); } /** * Build the current purge headers. * * @since 1.1.5 * @access private * @return string the built purge header */ private function _build($purge2 = false) { if ($purge2) { if (empty($this->_pub_purge2)) { return; } } else { if (empty($this->_pub_purge) && empty($this->_priv_purge)) { return; } } $purge_header = ''; // Handle purge2 @since 4.4.1 if ($purge2) { $public_tags = $this->_append_prefix($this->_pub_purge2); if (empty($public_tags)) { return; } $purge_header = self::X_HEADER2 . ': public,'; if (Control::is_stale()) { $purge_header .= 'stale,'; } $purge_header .= implode(',', $public_tags); return $purge_header; } $private_prefix = self::X_HEADER . ': private,'; if (!empty($this->_pub_purge)) { $public_tags = $this->_append_prefix($this->_pub_purge); if (empty($public_tags)) { // If this ends up empty, private will also end up empty return; } $purge_header = self::X_HEADER . ': public,'; if (Control::is_stale()) { $purge_header .= 'stale,'; } $purge_header .= implode(',', $public_tags); $private_prefix = ';private,'; } // Handle priv purge tags if (!empty($this->_priv_purge)) { $private_tags = $this->_append_prefix($this->_priv_purge, true); $purge_header .= $private_prefix . implode(',', $private_tags); } return $purge_header; } /** * Append prefix to an array of purge headers * * @since 1.1.0 * @access private */ private function _append_prefix($purge_tags, $is_private = false) { $curr_bid = is_multisite() ? get_current_blog_id() : ''; if (!in_array('*', $purge_tags)) { $tags = array(); foreach ($purge_tags as $val) { $tags[] = LSWCP_TAG_PREFIX . $val; } return $tags; } // Purge All need to check if need to reset crawler or not if (!$is_private && $this->conf(self::O_CRAWLER)) { Crawler::cls()->reset_pos(); } if ((defined('LSWCP_EMPTYCACHE') && LSWCP_EMPTYCACHE) || $is_private) { return array('*'); } if (is_multisite() && !$this->_is_subsite_purge()) { $blogs = Activation::get_network_ids(); if (empty($blogs)) { self::debug('build_purge_headers: blog list is empty'); return ''; } $tags = array(); foreach ($blogs as $blog_id) { $tags[] = LSWCP_TAG_PREFIX . $blog_id . '_'; } return $tags; } else { return array(LSWCP_TAG_PREFIX . $curr_bid . '_'); } } /** * Check if this purge belongs to a subsite purge * * @since 4.0 */ private function _is_subsite_purge() { if (!is_multisite()) { return false; } if (is_network_admin()) { return false; } if (defined('LSWCP_EMPTYCACHE') && LSWCP_EMPTYCACHE) { return false; } // Would only use multisite and network admin except is_network_admin is false for ajax calls, which is used by wordpress updates v4.6+ if (Router::is_ajax() && (check_ajax_referer('updates', false, false) || check_ajax_referer('litespeed-purgeall-network', false, false))) { return false; } return true; } /** * Gets all the purge tags correlated with the post about to be purged. * * If the purge all pages configuration is set, all pages will be purged. * * This includes site wide post types (e.g. front page) as well as any third party plugin specific post tags. * * @since 1.0.0 * @access private */ private function _get_purge_tags_by_post($post_id) { // If this is a valid post we want to purge the post, the home page and any associated tags & cats // If not, purge everything on the site. $purge_tags = array(); if ($this->conf(self::O_PURGE_POST_ALL)) { // ignore the rest if purge all return array('*'); } // now do API hook action for post purge do_action('litespeed_api_purge_post', $post_id); // post $purge_tags[] = Tag::TYPE_POST . $post_id; $post_status = get_post_status($post_id); if (function_exists('is_post_status_viewable')) { $viewable = is_post_status_viewable($post_status); if ($viewable) { $purge_tags[] = Tag::get_uri_tag(wp_make_link_relative(get_permalink($post_id))); } } // for archive of categories|tags|custom tax global $post; $original_post = $post; $post = get_post($post_id); $post_type = $post->post_type; global $wp_widget_factory; // recent_posts $recent_posts = isset($wp_widget_factory->widgets['WP_Widget_Recent_Posts']) ? $wp_widget_factory->widgets['WP_Widget_Recent_Posts'] : null; if (!is_null($recent_posts)) { $purge_tags[] = Tag::TYPE_WIDGET . $recent_posts->id; } // get adjacent posts id as related post tag if ($post_type == 'post') { $prev_post = get_previous_post(); $next_post = get_next_post(); if (!empty($prev_post->ID)) { $purge_tags[] = Tag::TYPE_POST . $prev_post->ID; self::debug('--------purge_tags prev is: ' . $prev_post->ID); } if (!empty($next_post->ID)) { $purge_tags[] = Tag::TYPE_POST . $next_post->ID; self::debug('--------purge_tags next is: ' . $next_post->ID); } } if ($this->conf(self::O_PURGE_POST_TERM)) { $taxonomies = get_object_taxonomies($post_type); //self::debug('purge by post, check tax = ' . var_export($taxonomies, true)); foreach ($taxonomies as $tax) { $terms = get_the_terms($post_id, $tax); if (!empty($terms)) { foreach ($terms as $term) { $purge_tags[] = Tag::TYPE_ARCHIVE_TERM . $term->term_id; } } } } if ($this->conf(self::O_CACHE_TTL_FEED)) { $purge_tags[] = Tag::TYPE_FEED; } // author, for author posts and feed list if ($this->conf(self::O_PURGE_POST_AUTHOR)) { $purge_tags[] = Tag::TYPE_AUTHOR . get_post_field('post_author', $post_id); } // archive and feed of post type // todo: check if type contains space if ($this->conf(self::O_PURGE_POST_POSTTYPE)) { if (get_post_type_archive_link($post_type)) { $purge_tags[] = Tag::TYPE_ARCHIVE_POSTTYPE . $post_type; $purge_tags[] = $post_type; } } if ($this->conf(self::O_PURGE_POST_FRONTPAGE)) { $purge_tags[] = Tag::TYPE_FRONTPAGE; } if ($this->conf(self::O_PURGE_POST_HOMEPAGE)) { $purge_tags[] = Tag::TYPE_HOME; } if ($this->conf(self::O_PURGE_POST_PAGES)) { $purge_tags[] = Tag::TYPE_PAGES; } if ($this->conf(self::O_PURGE_POST_PAGES_WITH_RECENT_POSTS)) { $purge_tags[] = Tag::TYPE_PAGES_WITH_RECENT_POSTS; } // if configured to have archived by date $date = $post->post_date; $date = strtotime($date); if ($this->conf(self::O_PURGE_POST_DATE)) { $purge_tags[] = Tag::TYPE_ARCHIVE_DATE . date('Ymd', $date); } if ($this->conf(self::O_PURGE_POST_MONTH)) { $purge_tags[] = Tag::TYPE_ARCHIVE_DATE . date('Ym', $date); } if ($this->conf(self::O_PURGE_POST_YEAR)) { $purge_tags[] = Tag::TYPE_ARCHIVE_DATE . date('Y', $date); } // Set back to original post as $post_id might affecting the global $post value $post = $original_post; return array_unique($purge_tags); } /** * The dummy filter for purge all * * @since 1.1.5 * @access public * @param string $val The filter value * @return string The filter value */ public static function filter_with_purge_all($val) { self::purge_all(); return $val; } } src/file.cls.php 0000644 00000024734 15162130455 0007557 0 ustar 00 <?php /** * LiteSpeed File Operator Library Class * Append/Replace content to a file * * @since 1.1.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class File { const MARKER = 'LiteSpeed Operator'; /** * Detect if an URL is 404 * * @since 3.3 */ public static function is_404($url) { $response = wp_safe_remote_get($url); $code = wp_remote_retrieve_response_code($response); if ($code == 404) { return true; } return false; } /** * Delete folder * * @since 2.1 */ public static function rrmdir($dir) { $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $file) { is_dir("$dir/$file") ? self::rrmdir("$dir/$file") : unlink("$dir/$file"); } return rmdir($dir); } public static function count_lines($filename) { if (!file_exists($filename)) { return 0; } $file = new \SplFileObject($filename); $file->seek(PHP_INT_MAX); return $file->key() + 1; } /** * Read data from file * * @since 1.1.0 * @param string $filename * @param int $start_line * @param int $lines */ public static function read($filename, $start_line = null, $lines = null) { if (!file_exists($filename)) { return ''; } if (!is_readable($filename)) { return false; } if ($start_line !== null) { $res = array(); $file = new \SplFileObject($filename); $file->seek($start_line); if ($lines === null) { while (!$file->eof()) { $res[] = rtrim($file->current(), "\n"); $file->next(); } } else { for ($i = 0; $i < $lines; $i++) { if ($file->eof()) { break; } $res[] = rtrim($file->current(), "\n"); $file->next(); } } unset($file); return $res; } $content = file_get_contents($filename); $content = self::remove_zero_space($content); return $content; } /** * Append data to file * * @since 1.1.5 * @access public * @param string $filename * @param string $data * @param boolean $mkdir * @param boolean $silence Used to avoid WP's functions are used */ public static function append($filename, $data, $mkdir = false, $silence = true) { return self::save($filename, $data, $mkdir, true, $silence); } /** * Save data to file * * @since 1.1.0 * @param string $filename * @param string $data * @param boolean $mkdir * @param boolean $append If the content needs to be appended * @param boolean $silence Used to avoid WP's functions are used */ public static function save($filename, $data, $mkdir = false, $append = false, $silence = true) { if (is_null($filename)) { return $silence ? false : __('Filename is empty!', 'litespeed-cache'); } $error = false; $folder = dirname($filename); // mkdir if folder does not exist if (!file_exists($folder)) { if (!$mkdir) { return $silence ? false : sprintf(__('Folder does not exist: %s', 'litespeed-cache'), $folder); } set_error_handler('litespeed_exception_handler'); try { mkdir($folder, 0755, true); // Create robots.txt file to forbid search engine indexes if (!file_exists(LITESPEED_STATIC_DIR . '/robots.txt')) { file_put_contents(LITESPEED_STATIC_DIR . '/robots.txt', "User-agent: *\nDisallow: /\n"); } } catch (\ErrorException $ex) { return $silence ? false : sprintf(__('Can not create folder: %1$s. Error: %2$s', 'litespeed-cache'), $folder, $ex->getMessage()); } restore_error_handler(); } if (!file_exists($filename)) { if (!is_writable($folder)) { return $silence ? false : sprintf(__('Folder is not writable: %s.', 'litespeed-cache'), $folder); } set_error_handler('litespeed_exception_handler'); try { touch($filename); } catch (\ErrorException $ex) { return $silence ? false : sprintf(__('File %s is not writable.', 'litespeed-cache'), $filename); } restore_error_handler(); } elseif (!is_writable($filename)) { return $silence ? false : sprintf(__('File %s is not writable.', 'litespeed-cache'), $filename); } $data = self::remove_zero_space($data); $ret = file_put_contents($filename, $data, $append ? FILE_APPEND : LOCK_EX); if ($ret === false) { return $silence ? false : sprintf(__('Failed to write to %s.', 'litespeed-cache'), $filename); } return true; } /** * Remove Unicode zero-width space <200b><200c> * * @since 2.1.2 * @since 2.9 changed to public */ public static function remove_zero_space($content) { if (is_array($content)) { $content = array_map(__CLASS__ . '::remove_zero_space', $content); return $content; } // Remove UTF-8 BOM if present if (substr($content, 0, 3) === "\xEF\xBB\xBF") { $content = substr($content, 3); } $content = str_replace("\xe2\x80\x8b", '', $content); $content = str_replace("\xe2\x80\x8c", '', $content); $content = str_replace("\xe2\x80\x8d", '', $content); return $content; } /** * Appends an array of strings into a file (.htaccess ), placing it between * BEGIN and END markers. * * Replaces existing marked info. Retains surrounding * data. Creates file if none exists. * * @param string $filename Filename to alter. * @param string $marker The marker to alter. * @param array|string $insertion The new content to insert. * @param bool $prepend Prepend insertion if not exist. * @return bool True on write success, false on failure. */ public static function insert_with_markers($filename, $insertion = false, $marker = false, $prepend = false) { if (!$marker) { $marker = self::MARKER; } if (!$insertion) { $insertion = array(); } return self::_insert_with_markers($filename, $marker, $insertion, $prepend); //todo: capture exceptions } /** * Return wrapped block data with marker * * @param string $insertion * @param string $marker * @return string The block data */ public static function wrap_marker_data($insertion, $marker = false) { if (!$marker) { $marker = self::MARKER; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_data = implode("\n", array_merge(array($start_marker), $insertion, array($end_marker))); return $new_data; } /** * Touch block data from file, return with marker * * @param string $filename * @param string $marker * @return string The current block data */ public static function touch_marker_data($filename, $marker = false) { if (!$marker) { $marker = self::MARKER; } $result = self::_extract_from_markers($filename, $marker); if (!$result) { return false; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_data = implode("\n", array_merge(array($start_marker), $result, array($end_marker))); return $new_data; } /** * Extracts strings from between the BEGIN and END markers in the .htaccess file. * * @param string $filename * @param string $marker * @return array An array of strings from a file (.htaccess ) from between BEGIN and END markers. */ public static function extract_from_markers($filename, $marker = false) { if (!$marker) { $marker = self::MARKER; } return self::_extract_from_markers($filename, $marker); } /** * Extracts strings from between the BEGIN and END markers in the .htaccess file. * * @param string $filename * @param string $marker * @return array An array of strings from a file (.htaccess ) from between BEGIN and END markers. */ private static function _extract_from_markers($filename, $marker) { $result = array(); if (!file_exists($filename)) { return $result; } if ($markerdata = explode("\n", implode('', file($filename)))) { $state = false; foreach ($markerdata as $markerline) { if (strpos($markerline, '# END ' . $marker) !== false) { $state = false; } if ($state) { $result[] = $markerline; } if (strpos($markerline, '# BEGIN ' . $marker) !== false) { $state = true; } } } return array_map('trim', $result); } /** * Inserts an array of strings into a file (.htaccess ), placing it between BEGIN and END markers. * * Replaces existing marked info. Retains surrounding data. Creates file if none exists. * * NOTE: will throw error if failed * * @since 3.0- * @since 3.0 Throw errors if failed * @access private */ private static function _insert_with_markers($filename, $marker, $insertion, $prepend = false) { if (!file_exists($filename)) { if (!is_writable(dirname($filename))) { Error::t('W', dirname($filename)); } set_error_handler('litespeed_exception_handler'); try { touch($filename); } catch (\ErrorException $ex) { Error::t('W', $filename); } restore_error_handler(); } elseif (!is_writable($filename)) { Error::t('W', $filename); } if (!is_array($insertion)) { $insertion = explode("\n", $insertion); } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $fp = fopen($filename, 'r+'); if (!$fp) { Error::t('W', $filename); } // Attempt to get a lock. If the filesystem supports locking, this will block until the lock is acquired. flock($fp, LOCK_EX); $lines = array(); while (!feof($fp)) { $lines[] = rtrim(fgets($fp), "\r\n"); } // Split out the existing file into the preceding lines, and those that appear after the marker $pre_lines = $post_lines = $existing_lines = array(); $found_marker = $found_end_marker = false; foreach ($lines as $line) { if (!$found_marker && false !== strpos($line, $start_marker)) { $found_marker = true; continue; } elseif (!$found_end_marker && false !== strpos($line, $end_marker)) { $found_end_marker = true; continue; } if (!$found_marker) { $pre_lines[] = $line; } elseif ($found_marker && $found_end_marker) { $post_lines[] = $line; } else { $existing_lines[] = $line; } } // Check to see if there was a change if ($existing_lines === $insertion) { flock($fp, LOCK_UN); fclose($fp); return true; } // Check if need to prepend data if not exist if ($prepend && !$post_lines) { // Generate the new file data $new_file_data = implode("\n", array_merge(array($start_marker), $insertion, array($end_marker), $pre_lines)); } else { // Generate the new file data $new_file_data = implode("\n", array_merge($pre_lines, array($start_marker), $insertion, array($end_marker), $post_lines)); } // Write to the start of the file, and truncate it to that length fseek($fp, 0); $bytes = fwrite($fp, $new_file_data); if ($bytes) { ftruncate($fp, ftell($fp)); } fflush($fp); flock($fp, LOCK_UN); fclose($fp); return (bool) $bytes; } } src/db-optm.cls.php 0000644 00000023513 15162130460 0010170 0 ustar 00 <?php /** * The admin optimize tool * * * @since 1.2.1 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class DB_Optm extends Root { private static $_hide_more = false; private static $TYPES = array( 'revision', 'orphaned_post_meta', 'auto_draft', 'trash_post', 'spam_comment', 'trash_comment', 'trackback-pingback', 'expired_transient', 'all_transients', 'optimize_tables', ); const TYPE_CONV_TB = 'conv_innodb'; /** * Show if there are more sites in hidden * * @since 3.0 */ public static function hide_more() { return self::$_hide_more; } /** * Clean/Optimize WP tables * * @since 1.2.1 * @access public * @param string $type The type to clean * @param bool $ignore_multisite If ignore multisite check * @return int The rows that will be affected */ public function db_count($type, $ignore_multisite = false) { if ($type === 'all') { $num = 0; foreach (self::$TYPES as $v) { $num += $this->db_count($v); } return $num; } if (!$ignore_multisite) { if (is_multisite() && is_network_admin()) { $num = 0; $blogs = Activation::get_network_ids(); foreach ($blogs as $k => $blog_id) { if ($k > 3) { self::$_hide_more = true; break; } switch_to_blog($blog_id); $num += $this->db_count($type, true); restore_current_blog(); } return $num; } } global $wpdb; switch ($type) { case 'revision': $rev_max = (int) $this->conf(Base::O_DB_OPTM_REVISIONS_MAX); $rev_age = (int) $this->conf(Base::O_DB_OPTM_REVISIONS_AGE); $sql_add = ''; if ($rev_age) { $sql_add = " and post_modified < DATE_SUB( NOW(), INTERVAL $rev_age DAY ) "; } $sql = "SELECT COUNT(*) FROM `$wpdb->posts` WHERE post_type = 'revision' $sql_add"; if (!$rev_max) { return $wpdb->get_var($sql); } // Has count limit $sql = "SELECT COUNT(*)-$rev_max FROM `$wpdb->posts` WHERE post_type = 'revision' $sql_add GROUP BY post_parent HAVING count(*)>$rev_max"; $res = $wpdb->get_results($sql, ARRAY_N); Utility::compatibility(); return array_sum(array_column($res, 0)); case 'orphaned_post_meta': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->postmeta` a LEFT JOIN `$wpdb->posts` b ON b.ID=a.post_id WHERE b.ID IS NULL"); case 'auto_draft': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->posts` WHERE post_status = 'auto-draft'"); case 'trash_post': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->posts` WHERE post_status = 'trash'"); case 'spam_comment': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->comments` WHERE comment_approved = 'spam'"); case 'trash_comment': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->comments` WHERE comment_approved = 'trash'"); case 'trackback-pingback': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->comments` WHERE comment_type = 'trackback' OR comment_type = 'pingback'"); case 'expired_transient': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->options` WHERE option_name LIKE '_transient_timeout%' AND option_value < " . time()); case 'all_transients': return $wpdb->get_var("SELECT COUNT(*) FROM `$wpdb->options` WHERE option_name LIKE '%_transient_%'"); case 'optimize_tables': return $wpdb->get_var("SELECT COUNT(*) FROM information_schema.tables WHERE TABLE_SCHEMA = '" . DB_NAME . "' and ENGINE <> 'InnoDB' and DATA_FREE > 0"); } return '-'; } /** * Clean/Optimize WP tables * * @since 1.2.1 * @since 3.0 changed to private * @access private */ private function _db_clean($type) { if ($type === 'all') { foreach (self::$TYPES as $v) { $this->_db_clean($v); } return __('Clean all successfully.', 'litespeed-cache'); } global $wpdb; switch ($type) { case 'revision': $rev_max = (int) $this->conf(Base::O_DB_OPTM_REVISIONS_MAX); $rev_age = (int) $this->conf(Base::O_DB_OPTM_REVISIONS_AGE); $postmeta = "`$wpdb->postmeta`"; $posts = "`$wpdb->posts`"; $sql_postmeta_join = function ($table) use ($postmeta, $posts) { return " $postmeta CROSS JOIN $table ON $posts.ID = $postmeta.post_id "; }; $sql_where = "WHERE $posts.post_type = 'revision'"; $sql_add = $rev_age ? "AND $posts.post_modified < DATE_SUB( NOW(), INTERVAL $rev_age DAY )" : ''; if (!$rev_max) { $sql_where = "$sql_where $sql_add"; $sql_postmeta = $sql_postmeta_join($posts); $wpdb->query("DELETE $postmeta FROM $sql_postmeta $sql_where"); $wpdb->query("DELETE FROM $posts $sql_where"); } else { // Has count limit $sql = " SELECT COUNT(*) - $rev_max AS del_max, post_parent FROM $posts WHERE post_type = 'revision' $sql_add GROUP BY post_parent HAVING COUNT(*) > $rev_max "; $res = $wpdb->get_results($sql); $sql_where = " $sql_where AND post_parent = %d ORDER BY ID LIMIT %d "; $sql_postmeta = $sql_postmeta_join("(SELECT ID FROM $posts $sql_where) AS $posts"); foreach ($res as $v) { $args = array($v->post_parent, $v->del_max); $sql = $wpdb->prepare("DELETE $postmeta FROM $sql_postmeta", $args); $wpdb->query($sql); $sql = $wpdb->prepare("DELETE FROM $posts $sql_where", $args); $wpdb->query($sql); } } return __('Clean post revisions successfully.', 'litespeed-cache'); case 'orphaned_post_meta': $wpdb->query("DELETE a FROM `$wpdb->postmeta` a LEFT JOIN `$wpdb->posts` b ON b.ID=a.post_id WHERE b.ID IS NULL"); return __('Clean orphaned post meta successfully.', 'litespeed-cache'); case 'auto_draft': $wpdb->query("DELETE FROM `$wpdb->posts` WHERE post_status = 'auto-draft'"); return __('Clean auto drafts successfully.', 'litespeed-cache'); case 'trash_post': $wpdb->query("DELETE FROM `$wpdb->posts` WHERE post_status = 'trash'"); return __('Clean trashed posts and pages successfully.', 'litespeed-cache'); case 'spam_comment': $wpdb->query("DELETE FROM `$wpdb->comments` WHERE comment_approved = 'spam'"); return __('Clean spam comments successfully.', 'litespeed-cache'); case 'trash_comment': $wpdb->query("DELETE FROM `$wpdb->comments` WHERE comment_approved = 'trash'"); return __('Clean trashed comments successfully.', 'litespeed-cache'); case 'trackback-pingback': $wpdb->query("DELETE FROM `$wpdb->comments` WHERE comment_type = 'trackback' OR comment_type = 'pingback'"); return __('Clean trackbacks and pingbacks successfully.', 'litespeed-cache'); case 'expired_transient': $wpdb->query("DELETE FROM `$wpdb->options` WHERE option_name LIKE '_transient_timeout%' AND option_value < " . time()); return __('Clean expired transients successfully.', 'litespeed-cache'); case 'all_transients': $wpdb->query("DELETE FROM `$wpdb->options` WHERE option_name LIKE '%\\_transient\\_%'"); return __('Clean all transients successfully.', 'litespeed-cache'); case 'optimize_tables': $sql = "SELECT table_name, DATA_FREE FROM information_schema.tables WHERE TABLE_SCHEMA = '" . DB_NAME . "' and ENGINE <> 'InnoDB' and DATA_FREE > 0"; $result = $wpdb->get_results($sql); if ($result) { foreach ($result as $row) { $wpdb->query('OPTIMIZE TABLE ' . $row->table_name); } } return __('Optimized all tables.', 'litespeed-cache'); } } /** * Get all myisam tables * * @since 3.0 * @access public */ public function list_myisam() { global $wpdb; $q = "SELECT * FROM information_schema.tables WHERE TABLE_SCHEMA = '" . DB_NAME . "' and ENGINE = 'myisam' AND TABLE_NAME LIKE '{$wpdb->prefix}%'"; return $wpdb->get_results($q); } /** * Convert tables to InnoDB * * @since 3.0 * @access private */ private function _conv_innodb() { global $wpdb; if (empty($_GET['tb'])) { Admin_Display::error('No table to convert'); return; } $tb = false; $list = $this->list_myisam(); foreach ($list as $v) { if ($v->TABLE_NAME == $_GET['tb']) { $tb = $v->TABLE_NAME; break; } } if (!$tb) { Admin_Display::error('No existing table'); return; } $q = 'ALTER TABLE ' . DB_NAME . '.' . $tb . ' ENGINE = InnoDB'; $wpdb->query($q); Debug2::debug("[DB] Converted $tb to InnoDB"); $msg = __('Converted to InnoDB successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Count all autoload size * * @since 3.0 * @access public */ public function autoload_summary() { global $wpdb; $autoloads = function_exists('wp_autoload_values_to_autoload') ? wp_autoload_values_to_autoload() : array('yes', 'on', 'auto-on', 'auto'); $autoloads = '("' . implode('","', $autoloads) . '")'; $summary = $wpdb->get_row("SELECT SUM(LENGTH(option_value)) AS autoload_size,COUNT(*) AS autload_entries FROM `$wpdb->options` WHERE autoload IN " . $autoloads); $summary->autoload_toplist = $wpdb->get_results( "SELECT option_name, LENGTH(option_value) AS option_value_length, autoload FROM `$wpdb->options` WHERE autoload IN " . $autoloads . ' ORDER BY option_value_length DESC LIMIT 20' ); return $summary; } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case 'all': case in_array($type, self::$TYPES): if (is_multisite() && is_network_admin()) { $blogs = Activation::get_network_ids(); foreach ($blogs as $blog_id) { switch_to_blog($blog_id); $msg = $this->_db_clean($type); restore_current_blog(); } } else { $msg = $this->_db_clean($type); } Admin_Display::success($msg); break; case self::TYPE_CONV_TB: $this->_conv_innodb(); break; default: break; } Admin::redirect(); } } src/htaccess.cls.php 0000644 00000060241 15162130463 0010425 0 ustar 00 <?php /** * The htaccess rewrite rule operation class * * * @since 1.0.0 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Htaccess extends Root { private $frontend_htaccess = null; private $_default_frontend_htaccess = null; private $backend_htaccess = null; private $_default_backend_htaccess = null; private $theme_htaccess = null; // Not used yet private $frontend_htaccess_readable = false; private $frontend_htaccess_writable = false; private $backend_htaccess_readable = false; private $backend_htaccess_writable = false; private $theme_htaccess_readable = false; private $theme_htaccess_writable = false; private $__rewrite_on; const LS_MODULE_START = '<IfModule LiteSpeed>'; const EXPIRES_MODULE_START = '<IfModule mod_expires.c>'; const LS_MODULE_END = '</IfModule>'; const LS_MODULE_REWRITE_START = '<IfModule mod_rewrite.c>'; const REWRITE_ON = 'RewriteEngine on'; const LS_MODULE_DONOTEDIT = '## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##'; const MARKER = 'LSCACHE'; const MARKER_NONLS = 'NON_LSCACHE'; const MARKER_LOGIN_COOKIE = '### marker LOGIN COOKIE'; const MARKER_ASYNC = '### marker ASYNC'; const MARKER_CRAWLER = '### marker CRAWLER'; const MARKER_MOBILE = '### marker MOBILE'; const MARKER_NOCACHE_COOKIES = '### marker NOCACHE COOKIES'; const MARKER_NOCACHE_USER_AGENTS = '### marker NOCACHE USER AGENTS'; const MARKER_CACHE_RESOURCE = '### marker CACHE RESOURCE'; const MARKER_BROWSER_CACHE = '### marker BROWSER CACHE'; const MARKER_MINIFY = '### marker MINIFY'; const MARKER_CORS = '### marker CORS'; const MARKER_WEBP = '### marker WEBP'; const MARKER_DROPQS = '### marker DROPQS'; const MARKER_START = ' start ###'; const MARKER_END = ' end ###'; const RW_PATTERN_RES = '/.*/[^/]*(responsive|css|js|dynamic|loader|fonts)\.php'; /** * Initialize the class and set its properties. * * @since 1.0.7 */ public function __construct() { $this->_path_set(); $this->_default_frontend_htaccess = $this->frontend_htaccess; $this->_default_backend_htaccess = $this->backend_htaccess; $frontend_htaccess = defined('LITESPEED_CFG_HTACCESS') ? LITESPEED_CFG_HTACCESS : false; if ($frontend_htaccess && substr($frontend_htaccess, -10) === '/.htaccess') { $this->frontend_htaccess = $frontend_htaccess; } $backend_htaccess = defined('LITESPEED_CFG_HTACCESS_BACKEND') ? LITESPEED_CFG_HTACCESS_BACKEND : false; if ($backend_htaccess && substr($backend_htaccess, -10) === '/.htaccess') { $this->backend_htaccess = $backend_htaccess; } // Filter for frontend&backend htaccess path $this->frontend_htaccess = apply_filters('litespeed_frontend_htaccess', $this->frontend_htaccess); $this->backend_htaccess = apply_filters('litespeed_backend_htaccess', $this->backend_htaccess); clearstatcache(); // frontend .htaccess privilege $test_permissions = file_exists($this->frontend_htaccess) ? $this->frontend_htaccess : dirname($this->frontend_htaccess); if (is_readable($test_permissions)) { $this->frontend_htaccess_readable = true; } if (is_writable($test_permissions)) { $this->frontend_htaccess_writable = true; } $this->__rewrite_on = array( self::REWRITE_ON, 'CacheLookup on', 'RewriteRule .* - [E=Cache-Control:no-autoflush]', 'RewriteRule ' . preg_quote(LITESPEED_DATA_FOLDER) . '/debug/.*\.log$ - [F,L]', 'RewriteRule ' . preg_quote(self::CONF_FILE) . ' - [F,L]', ); // backend .htaccess privilege if ($this->frontend_htaccess === $this->backend_htaccess) { $this->backend_htaccess_readable = $this->frontend_htaccess_readable; $this->backend_htaccess_writable = $this->frontend_htaccess_writable; } else { $test_permissions = file_exists($this->backend_htaccess) ? $this->backend_htaccess : dirname($this->backend_htaccess); if (is_readable($test_permissions)) { $this->backend_htaccess_readable = true; } if (is_writable($test_permissions)) { $this->backend_htaccess_writable = true; } } } /** * Get if htaccess file is readable * * @since 1.1.0 * @return string */ private function _readable($kind = 'frontend') { if ($kind === 'frontend') { return $this->frontend_htaccess_readable; } if ($kind === 'backend') { return $this->backend_htaccess_readable; } } /** * Get if htaccess file is writable * * @since 1.1.0 * @return string */ public function writable($kind = 'frontend') { if ($kind === 'frontend') { return $this->frontend_htaccess_writable; } if ($kind === 'backend') { return $this->backend_htaccess_writable; } } /** * Get frontend htaccess path * * @since 1.1.0 * @return string */ public static function get_frontend_htaccess($show_default = false) { if ($show_default) { return self::cls()->_default_frontend_htaccess; } return self::cls()->frontend_htaccess; } /** * Get backend htaccess path * * @since 1.1.0 * @return string */ public static function get_backend_htaccess($show_default = false) { if ($show_default) { return self::cls()->_default_backend_htaccess; } return self::cls()->backend_htaccess; } /** * Check to see if .htaccess exists starting at $start_path and going up directories until it hits DOCUMENT_ROOT. * * As dirname() strips the ending '/', paths passed in must exclude the final '/' * * @since 1.0.11 * @access private */ private function _htaccess_search($start_path) { while (!file_exists($start_path . '/.htaccess')) { if ($start_path === '/' || !$start_path) { return false; } if (!empty($_SERVER['DOCUMENT_ROOT']) && wp_normalize_path($start_path) === wp_normalize_path($_SERVER['DOCUMENT_ROOT'])) { return false; } if (dirname($start_path) === $start_path) { return false; } $start_path = dirname($start_path); } return $start_path; } /** * Set the path class variables. * * @since 1.0.11 * @access private */ private function _path_set() { $frontend = Router::frontend_path(); $frontend_htaccess_search = $this->_htaccess_search($frontend); // The existing .htaccess path to be used for frontend .htaccess $this->frontend_htaccess = ($frontend_htaccess_search ?: $frontend) . '/.htaccess'; $backend = realpath(ABSPATH); // /home/user/public_html/backend/ if ($frontend == $backend) { $this->backend_htaccess = $this->frontend_htaccess; return; } // Backend is a different path $backend_htaccess_search = $this->_htaccess_search($backend); // Found affected .htaccess if ($backend_htaccess_search) { $this->backend_htaccess = $backend_htaccess_search . '/.htaccess'; return; } // Frontend path is the parent of backend path if (stripos($backend, $frontend . '/') === 0) { // backend use frontend htaccess $this->backend_htaccess = $this->frontend_htaccess; return; } $this->backend_htaccess = $backend . '/.htaccess'; } /** * Get corresponding htaccess path * * @since 1.1.0 * @param string $kind Frontend or backend * @return string Path */ public function htaccess_path($kind = 'frontend') { switch ($kind) { case 'backend': $path = $this->backend_htaccess; break; case 'frontend': default: $path = $this->frontend_htaccess; break; } return $path; } /** * Get the content of the rules file. * * NOTE: will throw error if failed * * @since 1.0.4 * @since 2.9 Used exception for failed reading * @access public */ public function htaccess_read($kind = 'frontend') { $path = $this->htaccess_path($kind); if (!$path || !file_exists($path)) { return "\n"; } if (!$this->_readable($kind)) { Error::t('HTA_R'); } $content = File::read($path); if ($content === false) { Error::t('HTA_GET'); } // Remove ^M characters. $content = str_ireplace("\x0D", '', $content); return $content; } /** * Try to backup the .htaccess file if we didn't save one before. * * NOTE: will throw error if failed * * @since 1.0.10 * @access private */ private function _htaccess_backup($kind = 'frontend') { $path = $this->htaccess_path($kind); if (!file_exists($path)) { return; } if (file_exists($path . '.bk')) { return; } $res = copy($path, $path . '.bk'); // Failed to backup, abort if (!$res) { Error::t('HTA_BK'); } } /** * Get mobile view rule from htaccess file * * NOTE: will throw error if failed * * @since 1.1.0 */ public function current_mobile_agents() { $rules = $this->_get_rule_by(self::MARKER_MOBILE); if (!isset($rules[0])) { Error::t('HTA_DNF', self::MARKER_MOBILE); } $rule = trim($rules[0]); // 'RewriteCond %{HTTP_USER_AGENT} ' . Utility::arr2regex( $cfg[ $id ], true ) . ' [NC]'; $match = substr($rule, strlen('RewriteCond %{HTTP_USER_AGENT} '), -strlen(' [NC]')); if (!$match) { Error::t('HTA_DNF', __('Mobile Agent Rules', 'litespeed-cache')); } return $match; } /** * Parse rewrites rule from the .htaccess file. * * NOTE: will throw error if failed * * @since 1.1.0 * @access public */ public function current_login_cookie($kind = 'frontend') { $rule = $this->_get_rule_by(self::MARKER_LOGIN_COOKIE, $kind); if (!$rule) { Error::t('HTA_DNF', self::MARKER_LOGIN_COOKIE); } if (strpos($rule, 'RewriteRule .? - [E=') !== 0) { Error::t('HTA_LOGIN_COOKIE_INVALID'); } $rule_cookie = substr($rule, strlen('RewriteRule .? - [E='), -1); if (LITESPEED_SERVER_TYPE === 'LITESPEED_SERVER_OLS') { $rule_cookie = trim($rule_cookie, '"'); } // Drop `Cache-Vary:` $rule_cookie = substr($rule_cookie, strlen('Cache-Vary:')); return $rule_cookie; } /** * Get rewrite rules based on the marker * * @since 2.0 * @access private */ private function _get_rule_by($cond, $kind = 'frontend') { clearstatcache(); $path = $this->htaccess_path($kind); if (!$this->_readable($kind)) { return false; } $rules = File::extract_from_markers($path, self::MARKER); if (!in_array($cond . self::MARKER_START, $rules) || !in_array($cond . self::MARKER_END, $rules)) { return false; } $key_start = array_search($cond . self::MARKER_START, $rules); $key_end = array_search($cond . self::MARKER_END, $rules); if ($key_start === false || $key_end === false) { return false; } $results = array_slice($rules, $key_start + 1, $key_end - $key_start - 1); if (!$results) { return false; } if (count($results) == 1) { return trim($results[0]); } return array_filter($results); } /** * Generate browser cache rules * * @since 1.3 * @access private * @return array Rules set */ private function _browser_cache_rules($cfg) { /** * Add ttl setting * @since 1.6.3 */ $id = Base::O_CACHE_TTL_BROWSER; $ttl = $cfg[$id]; $rules = array( self::EXPIRES_MODULE_START, // '<FilesMatch "\.(pdf|ico|svg|xml|jpg|jpeg|png|gif|webp|ogg|mp4|webm|js|css|woff|woff2|ttf|eot)(\.gz)?$">', 'ExpiresActive on', 'ExpiresByType application/pdf A' . $ttl, 'ExpiresByType image/x-icon A' . $ttl, 'ExpiresByType image/vnd.microsoft.icon A' . $ttl, 'ExpiresByType image/svg+xml A' . $ttl, '', 'ExpiresByType image/jpg A' . $ttl, 'ExpiresByType image/jpeg A' . $ttl, 'ExpiresByType image/png A' . $ttl, 'ExpiresByType image/gif A' . $ttl, 'ExpiresByType image/webp A' . $ttl, 'ExpiresByType image/avif A' . $ttl, '', 'ExpiresByType video/ogg A' . $ttl, 'ExpiresByType audio/ogg A' . $ttl, 'ExpiresByType video/mp4 A' . $ttl, 'ExpiresByType video/webm A' . $ttl, '', 'ExpiresByType text/css A' . $ttl, 'ExpiresByType text/javascript A' . $ttl, 'ExpiresByType application/javascript A' . $ttl, 'ExpiresByType application/x-javascript A' . $ttl, '', 'ExpiresByType application/x-font-ttf A' . $ttl, 'ExpiresByType application/x-font-woff A' . $ttl, 'ExpiresByType application/font-woff A' . $ttl, 'ExpiresByType application/font-woff2 A' . $ttl, 'ExpiresByType application/vnd.ms-fontobject A' . $ttl, 'ExpiresByType font/ttf A' . $ttl, 'ExpiresByType font/otf A' . $ttl, 'ExpiresByType font/woff A' . $ttl, 'ExpiresByType font/woff2 A' . $ttl, '', // '</FilesMatch>', self::LS_MODULE_END, ); return $rules; } /** * Generate CORS rules for fonts * * @since 1.5 * @access private * @return array Rules set */ private function _cors_rules() { return array( '<FilesMatch "\.(ttf|ttc|otf|eot|woff|woff2|font\.css)$">', '<IfModule mod_headers.c>', 'Header set Access-Control-Allow-Origin "*"', '</IfModule>', '</FilesMatch>', ); } /** * Generate rewrite rules based on settings * * @since 1.3 * @access private * @param array $cfg The settings to be used for rewrite rule * @return array Rules array */ private function _generate_rules($cfg) { $new_rules = array(); $new_rules_nonls = array(); $new_rules_backend = array(); $new_rules_backend_nonls = array(); # continual crawler // $id = Base::O_CRAWLER; // if (!empty($cfg[$id])) { $new_rules[] = self::MARKER_ASYNC . self::MARKER_START; $new_rules[] = 'RewriteCond %{REQUEST_URI} /wp-admin/admin-ajax\.php'; $new_rules[] = 'RewriteCond %{QUERY_STRING} action=async_litespeed'; $new_rules[] = 'RewriteRule .* - [E=noabort:1]'; $new_rules[] = self::MARKER_ASYNC . self::MARKER_END; $new_rules[] = ''; // } // mobile agents $id = Base::O_CACHE_MOBILE_RULES; if ((!empty($cfg[Base::O_CACHE_MOBILE]) || !empty($cfg[Base::O_GUEST])) && !empty($cfg[$id])) { $new_rules[] = self::MARKER_MOBILE . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} ' . Utility::arr2regex($cfg[$id], true) . ' [NC]'; $new_rules[] = 'RewriteRule .* - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+ismobile]'; $new_rules[] = self::MARKER_MOBILE . self::MARKER_END; $new_rules[] = ''; } // nocache cookie $id = Base::O_CACHE_EXC_COOKIES; if (!empty($cfg[$id])) { $new_rules[] = self::MARKER_NOCACHE_COOKIES . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_COOKIE} ' . Utility::arr2regex($cfg[$id], true); $new_rules[] = 'RewriteRule .* - [E=Cache-Control:no-cache]'; $new_rules[] = self::MARKER_NOCACHE_COOKIES . self::MARKER_END; $new_rules[] = ''; } // nocache user agents $id = Base::O_CACHE_EXC_USERAGENTS; if (!empty($cfg[$id])) { $new_rules[] = self::MARKER_NOCACHE_USER_AGENTS . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} ' . Utility::arr2regex($cfg[$id], true) . ' [NC]'; $new_rules[] = 'RewriteRule .* - [E=Cache-Control:no-cache]'; $new_rules[] = self::MARKER_NOCACHE_USER_AGENTS . self::MARKER_END; $new_rules[] = ''; } // caching php resource TODO: consider drop $id = Base::O_CACHE_RES; if (!empty($cfg[$id])) { $new_rules[] = $new_rules_backend[] = self::MARKER_CACHE_RESOURCE . self::MARKER_START; $new_rules[] = $new_rules_backend[] = 'RewriteRule ' . LSCWP_CONTENT_FOLDER . self::RW_PATTERN_RES . ' - [E=cache-control:max-age=3600]'; $new_rules[] = $new_rules_backend[] = self::MARKER_CACHE_RESOURCE . self::MARKER_END; $new_rules[] = $new_rules_backend[] = ''; } // check login cookie $vary_cookies = $cfg[Base::O_CACHE_VARY_COOKIES]; $id = Base::O_CACHE_LOGIN_COOKIE; if (!empty($cfg[$id])) { $vary_cookies[] = $cfg[$id]; } if (LITESPEED_SERVER_TYPE === 'LITESPEED_SERVER_OLS') { // Need to keep this due to different behavior of OLS when handling response vary header @Sep/22/2018 if (defined('COOKIEHASH')) { $vary_cookies[] = ',wp-postpass_' . COOKIEHASH; } } $vary_cookies = apply_filters('litespeed_vary_cookies', $vary_cookies); // todo: test if response vary header can work in latest OLS, drop the above two lines // frontend and backend if ($vary_cookies) { $env = 'Cache-Vary:' . implode(',', $vary_cookies); // if (LITESPEED_SERVER_TYPE === 'LITESPEED_SERVER_OLS') { // } $env = '"' . $env . '"'; $new_rules[] = $new_rules_backend[] = self::MARKER_LOGIN_COOKIE . self::MARKER_START; $new_rules[] = $new_rules_backend[] = 'RewriteRule .? - [E=' . $env . ']'; $new_rules[] = $new_rules_backend[] = self::MARKER_LOGIN_COOKIE . self::MARKER_END; $new_rules[] = ''; } // CORS font rules $id = Base::O_CDN; if (!empty($cfg[$id])) { $new_rules[] = self::MARKER_CORS . self::MARKER_START; $new_rules = array_merge($new_rules, $this->_cors_rules()); //todo: network $new_rules[] = self::MARKER_CORS . self::MARKER_END; $new_rules[] = ''; } // webp support $id = Base::O_IMG_OPTM_WEBP; if (!empty($cfg[$id])) { $webP_rule = 'RewriteRule .* - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+webp]'; $next_gen_format = 'webp'; if ($cfg[$id] == 2) { $next_gen_format = 'avif'; } $new_rules[] = self::MARKER_WEBP . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_ACCEPT} "image/' . $next_gen_format . '"'; $new_rules[] = $webP_rule; $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} iPhone.*Version/(\d{2}).*Safari'; $new_rules[] = 'RewriteCond %1 >13'; $new_rules[] = $webP_rule; $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} Firefox/([0-9]+)'; $new_rules[] = 'RewriteCond %1 >=65'; $new_rules[] = $webP_rule; $new_rules[] = self::MARKER_WEBP . self::MARKER_END; $new_rules[] = ''; } // drop qs support $id = Base::O_CACHE_DROP_QS; if (!empty($cfg[$id])) { $new_rules[] = self::MARKER_DROPQS . self::MARKER_START; foreach ($cfg[$id] as $v) { $new_rules[] = 'CacheKeyModify -qs:' . $v; } $new_rules[] = self::MARKER_DROPQS . self::MARKER_END; $new_rules[] = ''; } // Browser cache $id = Base::O_CACHE_BROWSER; if (!empty($cfg[$id])) { $new_rules_nonls[] = $new_rules_backend_nonls[] = self::MARKER_BROWSER_CACHE . self::MARKER_START; $new_rules_nonls = array_merge($new_rules_nonls, $this->_browser_cache_rules($cfg)); $new_rules_backend_nonls = array_merge($new_rules_backend_nonls, $this->_browser_cache_rules($cfg)); $new_rules_nonls[] = $new_rules_backend_nonls[] = self::MARKER_BROWSER_CACHE . self::MARKER_END; $new_rules_nonls[] = ''; } // Add module wrapper for LiteSpeed rules if ($new_rules) { $new_rules = $this->_wrap_ls_module($new_rules); } if ($new_rules_backend) { $new_rules_backend = $this->_wrap_ls_module($new_rules_backend); } return array($new_rules, $new_rules_backend, $new_rules_nonls, $new_rules_backend_nonls); } /** * Add LitSpeed module wrapper with rewrite on * * @since 2.1.1 * @access private */ private function _wrap_ls_module($rules = array()) { return array_merge(array(self::LS_MODULE_START), $this->__rewrite_on, array(''), $rules, array(self::LS_MODULE_END)); } /** * Insert LitSpeed module wrapper with rewrite on * * @since 2.1.1 * @access public */ public function insert_ls_wrapper() { $rules = $this->_wrap_ls_module(); $this->_insert_wrapper($rules); } /** * wrap rules with module on info * * @since 1.1.5 * @param array $rules * @return array wrapped rules with module info */ private function _wrap_do_no_edit($rules) { // When to clear rules, don't need DONOTEDIT msg if ($rules === false || !is_array($rules)) { return $rules; } $rules = array_merge(array(self::LS_MODULE_DONOTEDIT), $rules, array(self::LS_MODULE_DONOTEDIT)); return $rules; } /** * Write to htaccess with rules * * NOTE: will throw error if failed * * @since 1.1.0 * @access private */ private function _insert_wrapper($rules = array(), $kind = false, $marker = false) { if ($kind != 'backend') { $kind = 'frontend'; } // Default marker is LiteSpeed marker `LSCACHE` if ($marker === false) { $marker = self::MARKER; } $this->_htaccess_backup($kind); File::insert_with_markers($this->htaccess_path($kind), $this->_wrap_do_no_edit($rules), $marker, true); } /** * Update rewrite rules based on setting * * NOTE: will throw error if failed * * @since 1.3 * @access public */ public function update($cfg) { list($frontend_rules, $backend_rules, $frontend_rules_nonls, $backend_rules_nonls) = $this->_generate_rules($cfg); // Check frontend content list($rules, $rules_nonls) = $this->_extract_rules(); // Check Non-LiteSpeed rules if ($this->_wrap_do_no_edit($frontend_rules_nonls) != $rules_nonls) { Debug2::debug('[Rules] Update non-ls frontend rules'); // Need to update frontend htaccess try { $this->_insert_wrapper($frontend_rules_nonls, false, self::MARKER_NONLS); } catch (\Exception $e) { $manual_guide_codes = $this->_rewrite_codes_msg($this->frontend_htaccess, $frontend_rules_nonls, self::MARKER_NONLS); Debug2::debug('[Rules] Update Failed'); throw new \Exception($manual_guide_codes); } } // Check LiteSpeed rules if ($this->_wrap_do_no_edit($frontend_rules) != $rules) { Debug2::debug('[Rules] Update frontend rules'); // Need to update frontend htaccess try { $this->_insert_wrapper($frontend_rules); } catch (\Exception $e) { Debug2::debug('[Rules] Update Failed'); $manual_guide_codes = $this->_rewrite_codes_msg($this->frontend_htaccess, $frontend_rules); throw new \Exception($manual_guide_codes); } } if ($this->frontend_htaccess !== $this->backend_htaccess) { list($rules, $rules_nonls) = $this->_extract_rules('backend'); // Check Non-LiteSpeed rules for backend if ($this->_wrap_do_no_edit($backend_rules_nonls) != $rules_nonls) { Debug2::debug('[Rules] Update non-ls backend rules'); // Need to update frontend htaccess try { $this->_insert_wrapper($backend_rules_nonls, 'backend', self::MARKER_NONLS); } catch (\Exception $e) { Debug2::debug('[Rules] Update Failed'); $manual_guide_codes = $this->_rewrite_codes_msg($this->backend_htaccess, $backend_rules_nonls, self::MARKER_NONLS); throw new \Exception($manual_guide_codes); } } // Check backend content if ($this->_wrap_do_no_edit($backend_rules) != $rules) { Debug2::debug('[Rules] Update backend rules'); // Need to update backend htaccess try { $this->_insert_wrapper($backend_rules, 'backend'); } catch (\Exception $e) { Debug2::debug('[Rules] Update Failed'); $manual_guide_codes = $this->_rewrite_codes_msg($this->backend_htaccess, $backend_rules); throw new \Exception($manual_guide_codes); } } } return true; } /** * Get existing rewrite rules * * NOTE: will throw error if failed * * @since 1.3 * @access private * @param string $kind Frontend or backend .htaccess file */ private function _extract_rules($kind = 'frontend') { clearstatcache(); $path = $this->htaccess_path($kind); if (!$this->_readable($kind)) { Error::t('E_HTA_R'); } $rules = File::extract_from_markers($path, self::MARKER); $rules_nonls = File::extract_from_markers($path, self::MARKER_NONLS); return array($rules, $rules_nonls); } /** * Output the msg with rules plain data for manual insert * * @since 1.1.5 * @param string $file * @param array $rules * @return string final msg to output */ private function _rewrite_codes_msg($file, $rules, $marker = false) { return sprintf( __('<p>Please add/replace the following codes into the beginning of %1$s:</p> %2$s', 'litespeed-cache'), $file, '<textarea style="width:100%;" rows="10" readonly>' . htmlspecialchars($this->_wrap_rules_with_marker($rules, $marker)) . '</textarea>' ); } /** * Generate rules plain data for manual insert * * @since 1.1.5 */ private function _wrap_rules_with_marker($rules, $marker = false) { // Default marker is LiteSpeed marker `LSCACHE` if ($marker === false) { $marker = self::MARKER; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_file_data = implode("\n", array_merge(array($start_marker), $this->_wrap_do_no_edit($rules), array($end_marker))); return $new_file_data; } /** * Clear the rules file of any changes added by the plugin specifically. * * @since 1.0.4 * @access public */ public function clear_rules() { $this->_insert_wrapper(false); // Use false to avoid do-not-edit msg // Clear non ls rules $this->_insert_wrapper(false, false, self::MARKER_NONLS); if ($this->frontend_htaccess !== $this->backend_htaccess) { $this->_insert_wrapper(false, 'backend'); $this->_insert_wrapper(false, 'backend', self::MARKER_NONLS); } } } src/gui.cls.php 0000644 00000066745 15162130466 0007436 0 ustar 00 <?php /** * The frontend GUI class. * * @since 1.3 * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class GUI extends Base { private static $_clean_counter = 0; private $_promo_true; // [ file_tag => [ days, litespeed_only ], ... ] private $_promo_list = array( 'new_version' => array(7, false), 'score' => array(14, false), // 'slack' => array( 3, false ), ); const LIB_GUEST_JS = 'assets/js/guest.min.js'; const LIB_GUEST_DOCREF_JS = 'assets/js/guest.docref.min.js'; const PHP_GUEST = 'guest.vary.php'; const TYPE_DISMISS_WHM = 'whm'; const TYPE_DISMISS_EXPIRESDEFAULT = 'ExpiresDefault'; const TYPE_DISMISS_PROMO = 'promo'; const TYPE_DISMISS_PIN = 'pin'; const WHM_MSG = 'lscwp_whm_install'; const WHM_MSG_VAL = 'whm_install'; protected $_summary; /** * Instance * * @since 1.3 */ public function __construct() { $this->_summary = self::get_summary(); } /** * Frontend Init * * @since 3.0 */ public function init() { Debug2::debug2('[GUI] init'); if (is_admin_bar_showing() && current_user_can('manage_options')) { add_action('wp_enqueue_scripts', array($this, 'frontend_enqueue_style')); add_action('admin_bar_menu', array($this, 'frontend_shortcut'), 95); } /** * Turn on instant click * @since 1.8.2 */ if ($this->conf(self::O_UTIL_INSTANT_CLICK)) { add_action('wp_enqueue_scripts', array($this, 'frontend_enqueue_style_public')); } // NOTE: this needs to be before optimizer to avoid wrapper being removed add_filter('litespeed_buffer_finalize', array($this, 'finalize'), 8); } /** * Print a loading message when redirecting CCSS/UCSS page to avoid whiteboard confusion */ public static function print_loading($counter, $type) { echo '<div style="font-size: 25px; text-align: center; padding-top: 150px; width: 100%; position: absolute;">'; echo "<img width='35' src='" . LSWCP_PLUGIN_URL . "assets/img/Litespeed.icon.svg' /> "; echo sprintf(__('%1$s %2$s files left in queue', 'litespeed-cache'), $counter, $type); echo '<p><a href="' . admin_url('admin.php?page=litespeed-page_optm') . '">' . __('Cancel', 'litespeed-cache') . '</a></p>'; echo '</div>'; } /** * Display a pie * * @since 1.6.6 */ public static function pie($percent, $width = 50, $finished_tick = false, $without_percentage = false, $append_cls = false) { $percentage = '<text x="50%" y="50%">' . $percent . ($without_percentage ? '' : '%') . '</text>'; if ($percent == 100 && $finished_tick) { $percentage = '<text x="50%" y="50%" class="litespeed-pie-done">✓</text>'; } return " <svg class='litespeed-pie $append_cls' viewbox='0 0 33.83098862 33.83098862' width='$width' height='$width' xmlns='http://www.w3.org/2000/svg'> <circle class='litespeed-pie_bg' cx='16.91549431' cy='16.91549431' r='15.91549431' /> <circle class='litespeed-pie_circle' cx='16.91549431' cy='16.91549431' r='15.91549431' stroke-dasharray='$percent,100' /> <g class='litespeed-pie_info'>$percentage</g> </svg> "; } /** * Display a tiny pie with a tooltip * * @since 3.0 */ public static function pie_tiny($percent, $width = 50, $tooltip = '', $tooltip_pos = 'up', $append_cls = false) { // formula C = 2πR $dasharray = 2 * 3.1416 * 9 * ($percent / 100); return " <button type='button' data-balloon-break data-balloon-pos='$tooltip_pos' aria-label='$tooltip' class='litespeed-btn-pie'> <svg class='litespeed-pie litespeed-pie-tiny $append_cls' viewbox='0 0 30 30' width='$width' height='$width' xmlns='http://www.w3.org/2000/svg'> <circle class='litespeed-pie_bg' cx='15' cy='15' r='9' /> <circle class='litespeed-pie_circle' cx='15' cy='15' r='9' stroke-dasharray='$dasharray,100' /> <g class='litespeed-pie_info'><text x='50%' y='50%'>i</text></g> </svg> </button> "; } /** * Get classname of PageSpeed Score * * Scale: * 90-100 (fast) * 50-89 (average) * 0-49 (slow) * * @since 2.9 * @access public */ public function get_cls_of_pagescore($score) { if ($score >= 90) { return 'success'; } if ($score >= 50) { return 'warning'; } return 'danger'; } /** * Dismiss banner * * @since 1.0 * @access public */ public static function dismiss() { $_instance = self::cls(); switch (Router::verify_type()) { case self::TYPE_DISMISS_WHM: self::dismiss_whm(); break; case self::TYPE_DISMISS_EXPIRESDEFAULT: self::update_option(Admin_Display::DB_DISMISS_MSG, Admin_Display::RULECONFLICT_DISMISSED); break; case self::TYPE_DISMISS_PIN: admin_display::dismiss_pin(); break; case self::TYPE_DISMISS_PROMO: if (empty($_GET['promo_tag'])) { break; } $promo_tag = sanitize_key($_GET['promo_tag']); if (empty($_instance->_promo_list[$promo_tag])) { break; } defined('LSCWP_LOG') && Debug2::debug('[GUI] Dismiss promo ' . $promo_tag); // Forever dismiss if (!empty($_GET['done'])) { $_instance->_summary[$promo_tag] = 'done'; } elseif (!empty($_GET['later'])) { // Delay the banner to half year later $_instance->_summary[$promo_tag] = time() + 86400 * 180; } else { // Update welcome banner to 30 days after $_instance->_summary[$promo_tag] = time() + 86400 * 30; } self::save_summary(); break; default: break; } if (Router::is_ajax()) { // All dismiss actions are considered as ajax call, so just exit exit(\json_encode(array('success' => 1))); } // Plain click link, redirect to referral url Admin::redirect(); } /** * Check if has rule conflict notice * * @since 1.1.5 * @access public * @return boolean */ public static function has_msg_ruleconflict() { $db_dismiss_msg = self::get_option(Admin_Display::DB_DISMISS_MSG); if (!$db_dismiss_msg) { self::update_option(Admin_Display::DB_DISMISS_MSG, -1); } return $db_dismiss_msg == Admin_Display::RULECONFLICT_ON; } /** * Check if has whm notice * * @since 1.1.1 * @access public * @return boolean */ public static function has_whm_msg() { $val = self::get_option(self::WHM_MSG); if (!$val) { self::dismiss_whm(); return false; } return $val == self::WHM_MSG_VAL; } /** * Delete whm msg tag * * @since 1.1.1 * @access public */ public static function dismiss_whm() { self::update_option(self::WHM_MSG, -1); } /** * Set current page a litespeed page * * @since 2.9 */ private function _is_litespeed_page() { if ( !empty($_GET['page']) && in_array($_GET['page'], array( 'litespeed-settings', 'litespeed-dash', Admin::PAGE_EDIT_HTACCESS, 'litespeed-optimization', 'litespeed-crawler', 'litespeed-import', 'litespeed-report', )) ) { return true; } return false; } /** * Display promo banner * * @since 2.1 * @access public */ public function show_promo($check_only = false) { $is_litespeed_page = $this->_is_litespeed_page(); // Bypass showing info banner if disabled all in debug if (defined('LITESPEED_DISABLE_ALL') && LITESPEED_DISABLE_ALL) { if ($is_litespeed_page && !$check_only) { include_once LSCWP_DIR . 'tpl/inc/disabled_all.php'; } return false; } if (file_exists(ABSPATH . '.litespeed_no_banner')) { defined('LSCWP_LOG') && Debug2::debug('[GUI] Bypass banners due to silence file'); return false; } foreach ($this->_promo_list as $promo_tag => $v) { list($delay_days, $litespeed_page_only) = $v; if ($litespeed_page_only && !$is_litespeed_page) { continue; } // first time check if (empty($this->_summary[$promo_tag])) { $this->_summary[$promo_tag] = time() + 86400 * $delay_days; self::save_summary(); continue; } $promo_timestamp = $this->_summary[$promo_tag]; // was ticked as done if ($promo_timestamp == 'done') { continue; } // Not reach the dateline yet if (time() < $promo_timestamp) { continue; } // try to load, if can pass, will set $this->_promo_true = true $this->_promo_true = false; include LSCWP_DIR . "tpl/banner/$promo_tag.php"; // If not defined, means it didn't pass the display workflow in tpl. if (!$this->_promo_true) { continue; } if ($check_only) { return $promo_tag; } defined('LSCWP_LOG') && Debug2::debug('[GUI] Show promo ' . $promo_tag); // Only contain one break; } return false; } /** * Load frontend public script * * @since 1.8.2 * @access public */ public function frontend_enqueue_style_public() { wp_enqueue_script(Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/js/instant_click.min.js', array(), Core::VER, true); } /** * Load frontend menu shortcut * * @since 1.3 * @access public */ public function frontend_enqueue_style() { wp_enqueue_style(Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/css/litespeed.css', array(), Core::VER, 'all'); } /** * Load frontend menu shortcut * * @since 1.3 * @access public */ public function frontend_shortcut() { global $wp_admin_bar; $wp_admin_bar->add_menu(array( 'id' => 'litespeed-menu', 'title' => '<span class="ab-icon"></span>', 'href' => get_admin_url(null, 'admin.php?page=litespeed'), 'meta' => array('tabindex' => 0, 'class' => 'litespeed-top-toolbar'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-single', 'title' => __('Purge this page', 'litespeed-cache') . ' - LSCache', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_FRONT, false, true), 'meta' => array('tabindex' => '0'), )); if ($this->has_cache_folder('ucss')) { $possible_url_tag = UCSS::get_url_tag(); $append_arr = array(); if ($possible_url_tag) { $append_arr['url_tag'] = $possible_url_tag; } $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-single-ucss', 'title' => __('Purge this page', 'litespeed-cache') . ' - UCSS', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_UCSS, false, true, $append_arr), 'meta' => array('tabindex' => '0'), )); } $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-single-action', 'title' => __('Mark this page as ', 'litespeed-cache'), 'meta' => array('tabindex' => '0'), )); if (!empty($_SERVER['REQUEST_URI'])) { $append_arr = array( Conf::TYPE_SET . '[' . self::O_CACHE_FORCE_URI . '][]' => $_SERVER['REQUEST_URI'] . '$', 'redirect' => $_SERVER['REQUEST_URI'], ); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-forced_cache', 'title' => __('Forced cacheable', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr), )); $append_arr = array( Conf::TYPE_SET . '[' . self::O_CACHE_EXC . '][]' => $_SERVER['REQUEST_URI'] . '$', 'redirect' => $_SERVER['REQUEST_URI'], ); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-noncache', 'title' => __('Non cacheable', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr), )); $append_arr = array( Conf::TYPE_SET . '[' . self::O_CACHE_PRIV_URI . '][]' => $_SERVER['REQUEST_URI'] . '$', 'redirect' => $_SERVER['REQUEST_URI'], ); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-private', 'title' => __('Private cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr), )); $append_arr = array( Conf::TYPE_SET . '[' . self::O_OPTM_EXC . '][]' => $_SERVER['REQUEST_URI'] . '$', 'redirect' => $_SERVER['REQUEST_URI'], ); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-nonoptimize', 'title' => __('No optimization', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr), )); } $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-more', 'title' => __('More settings', 'litespeed-cache'), 'href' => get_admin_url(null, 'admin.php?page=litespeed-cache'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all', 'title' => __('Purge All', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL, false, '_ori'), 'meta' => array('tabindex' => '0'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all-lscache', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('LSCache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LSCACHE, false, '_ori'), 'meta' => array('tabindex' => '0'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cssjs', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('CSS/JS Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CSSJS, false, '_ori'), 'meta' => array('tabindex' => '0'), )); if ($this->conf(self::O_CDN_CLOUDFLARE)) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cloudflare', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Cloudflare', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_CDN_CLOUDFLARE, CDN\Cloudflare::TYPE_PURGE_ALL), 'meta' => array('tabindex' => '0'), )); } if (defined('LSCWP_OBJECT_CACHE')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-object', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Object Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OBJECT, false, '_ori'), 'meta' => array('tabindex' => '0'), )); } if (Router::opcache_enabled()) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-opcache', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Opcode Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OPCACHE, false, '_ori'), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('ccss')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ccss', 'title' => __('Purge All', 'litespeed-cache') . ' - CCSS', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CCSS, false, '_ori'), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('ucss')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ucss', 'title' => __('Purge All', 'litespeed-cache') . ' - UCSS', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_UCSS, false, '_ori'), )); } if ($this->has_cache_folder('localres')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-localres', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Localized Resources', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LOCALRES, false, '_ori'), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('lqip')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-placeholder', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('LQIP Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LQIP, false, '_ori'), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('avatar')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-avatar', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Gravatar Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_AVATAR, false, '_ori'), 'meta' => array('tabindex' => '0'), )); } do_action('litespeed_frontend_shortcut'); } /** * Hooked to wp_before_admin_bar_render. * Adds a link to the admin bar so users can quickly purge all. * * @access public * @global WP_Admin_Bar $wp_admin_bar * @since 1.7.2 Moved from admin_display.cls to gui.cls; Renamed from `add_quick_purge` to `backend_shortcut` */ public function backend_shortcut() { global $wp_admin_bar; // if ( defined( 'LITESPEED_ON' ) ) { $wp_admin_bar->add_menu(array( 'id' => 'litespeed-menu', 'title' => '<span class="ab-icon" title="' . __('LiteSpeed Cache Purge All', 'litespeed-cache') . ' - ' . __('LSCache', 'litespeed-cache') . '"></span>', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LSCACHE), 'meta' => array('tabindex' => 0, 'class' => 'litespeed-top-toolbar'), )); // } // else { // $wp_admin_bar->add_menu( array( // 'id' => 'litespeed-menu', // 'title' => '<span class="ab-icon" title="' . __( 'LiteSpeed Cache', 'litespeed-cache' ) . '"></span>', // 'meta' => array( 'tabindex' => 0, 'class' => 'litespeed-top-toolbar' ), // ) ); // } $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-bar-manage', 'title' => __('Manage', 'litespeed-cache'), 'href' => 'admin.php?page=litespeed', 'meta' => array('tabindex' => '0'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-bar-setting', 'title' => __('Settings', 'litespeed-cache'), 'href' => 'admin.php?page=litespeed-cache', 'meta' => array('tabindex' => '0'), )); if (!is_network_admin()) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-bar-imgoptm', 'title' => __('Image Optimization', 'litespeed-cache'), 'href' => 'admin.php?page=litespeed-img_optm', 'meta' => array('tabindex' => '0'), )); } $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all', 'title' => __('Purge All', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL), 'meta' => array('tabindex' => '0'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all-lscache', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('LSCache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LSCACHE), 'meta' => array('tabindex' => '0'), )); $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cssjs', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('CSS/JS Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CSSJS), 'meta' => array('tabindex' => '0'), )); if ($this->conf(self::O_CDN_CLOUDFLARE)) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cloudflare', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Cloudflare', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_CDN_CLOUDFLARE, CDN\Cloudflare::TYPE_PURGE_ALL), 'meta' => array('tabindex' => '0'), )); } if (defined('LSCWP_OBJECT_CACHE')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-object', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Object Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OBJECT), 'meta' => array('tabindex' => '0'), )); } if (Router::opcache_enabled()) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-opcache', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Opcode Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OPCACHE), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('ccss')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ccss', 'title' => __('Purge All', 'litespeed-cache') . ' - CCSS', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CCSS), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('ucss')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ucss', 'title' => __('Purge All', 'litespeed-cache') . ' - UCSS', 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_UCSS), )); } if ($this->has_cache_folder('localres')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-localres', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Localized Resources', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LOCALRES), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('lqip')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-placeholder', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('LQIP Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LQIP), 'meta' => array('tabindex' => '0'), )); } if ($this->has_cache_folder('avatar')) { $wp_admin_bar->add_menu(array( 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-avatar', 'title' => __('Purge All', 'litespeed-cache') . ' - ' . __('Gravatar Cache', 'litespeed-cache'), 'href' => Utility::build_url(Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_AVATAR), 'meta' => array('tabindex' => '0'), )); } do_action('litespeed_backend_shortcut'); } /** * Clear unfinished data * * @since 2.4.2 * @access public */ public static function img_optm_clean_up($unfinished_num) { return sprintf( '<a href="%1$s" class="button litespeed-btn-warning" data-balloon-pos="up" aria-label="%2$s"><span class="dashicons dashicons-editor-removeformatting"></span> %3$s</a>', Utility::build_url(Router::ACTION_IMG_OPTM, Img_Optm::TYPE_CLEAN), __('Remove all previous unfinished image optimization requests.', 'litespeed-cache'), __('Clean Up Unfinished Data', 'litespeed-cache') . ($unfinished_num ? ': ' . Admin_Display::print_plural($unfinished_num, 'image') : '') ); } /** * Generate install link * * @since 2.4.2 * @access public */ public static function plugin_install_link($title, $name, $v) { $url = wp_nonce_url(self_admin_url('update.php?action=install-plugin&plugin=' . $name), 'install-plugin_' . $name); $action = sprintf( '<a href="%1$s" class="install-now" data-slug="%2$s" data-name="%3$s" aria-label="%4$s">%5$s</a>', esc_url($url), esc_attr($name), esc_attr($title), esc_attr(sprintf(__('Install %s', 'litespeed-cache'), $title)), __('Install Now', 'litespeed-cache') ); return $action; // $msg .= " <a href='$upgrade_link' class='litespeed-btn-success' target='_blank'>" . __( 'Click here to upgrade', 'litespeed-cache' ) . '</a>'; } /** * Generate upgrade link * * @since 2.4.2 * @access public */ public static function plugin_upgrade_link($title, $name, $v) { $details_url = self_admin_url('plugin-install.php?tab=plugin-information&plugin=' . $name . '§ion=changelog&TB_iframe=true&width=600&height=800'); $file = $name . '/' . $name . '.php'; $msg = sprintf( __('<a href="%1$s" %2$s>View version %3$s details</a> or <a href="%4$s" %5$s target="_blank">update now</a>.', 'litespeed-cache'), esc_url($details_url), sprintf('class="thickbox open-plugin-details-modal" aria-label="%s"', esc_attr(sprintf(__('View %1$s version %2$s details', 'litespeed-cache'), $title, $v))), $v, wp_nonce_url(self_admin_url('update.php?action=upgrade-plugin&plugin=') . $file, 'upgrade-plugin_' . $file), sprintf('class="update-link" aria-label="%s"', esc_attr(sprintf(__('Update %s now', 'litespeed-cache'), $title))) ); return $msg; } /** * Finalize buffer by GUI class * * @since 1.6 * @access public */ public function finalize($buffer) { $buffer = $this->_clean_wrapper($buffer); // Maybe restore doc.ref if ($this->conf(Base::O_GUEST) && strpos($buffer, '<head>') !== false && defined('LITESPEED_IS_HTML')) { $buffer = $this->_enqueue_guest_docref_js($buffer); } if (defined('LITESPEED_GUEST') && LITESPEED_GUEST && strpos($buffer, '</body>') !== false && defined('LITESPEED_IS_HTML')) { $buffer = $this->_enqueue_guest_js($buffer); } return $buffer; } /** * Append guest restore doc.ref JS for organic traffic count * * @since 4.4.6 */ private function _enqueue_guest_docref_js($buffer) { $js_con = File::read(LSCWP_DIR . self::LIB_GUEST_DOCREF_JS); $buffer = preg_replace('/<head>/', '<head><script data-no-optimize="1">' . $js_con . '</script>', $buffer, 1); return $buffer; } /** * Append guest JS to update vary * * @since 4.0 */ private function _enqueue_guest_js($buffer) { $js_con = File::read(LSCWP_DIR . self::LIB_GUEST_JS); // $guest_update_url = add_query_arg( 'litespeed_guest', 1, home_url( '/' ) ); $guest_update_url = parse_url(LSWCP_PLUGIN_URL . self::PHP_GUEST, PHP_URL_PATH); $js_con = str_replace('litespeed_url', esc_url($guest_update_url), $js_con); $buffer = preg_replace('/<\/body>/', '<script data-no-optimize="1">' . $js_con . '</script></body>', $buffer, 1); return $buffer; } /** * Clean wrapper from buffer * * @since 1.4 * @since 1.6 converted to private with adding prefix _ * @access private */ private function _clean_wrapper($buffer) { if (self::$_clean_counter < 1) { Debug2::debug2('GUI bypassed by no counter'); return $buffer; } Debug2::debug2('GUI start cleaning counter ' . self::$_clean_counter); for ($i = 1; $i <= self::$_clean_counter; $i++) { // If miss beginning $start = strpos($buffer, self::clean_wrapper_begin($i)); if ($start === false) { $buffer = str_replace(self::clean_wrapper_end($i), '', $buffer); Debug2::debug2("GUI lost beginning wrapper $i"); continue; } // If miss end $end_wrapper = self::clean_wrapper_end($i); $end = strpos($buffer, $end_wrapper); if ($end === false) { $buffer = str_replace(self::clean_wrapper_begin($i), '', $buffer); Debug2::debug2("GUI lost ending wrapper $i"); continue; } // Now replace wrapped content $buffer = substr_replace($buffer, '', $start, $end - $start + strlen($end_wrapper)); Debug2::debug2("GUI cleaned wrapper $i"); } return $buffer; } /** * Display a to-be-removed html wrapper * * @since 1.4 * @access public */ public static function clean_wrapper_begin($counter = false) { if ($counter === false) { self::$_clean_counter++; $counter = self::$_clean_counter; Debug2::debug("GUI clean wrapper $counter begin"); } return '<!-- LiteSpeed To Be Removed begin ' . $counter . ' -->'; } /** * Display a to-be-removed html wrapper * * @since 1.4 * @access public */ public static function clean_wrapper_end($counter = false) { if ($counter === false) { $counter = self::$_clean_counter; Debug2::debug("GUI clean wrapper $counter end"); } return '<!-- LiteSpeed To Be Removed end ' . $counter . ' -->'; } } src/placeholder.cls.php 0000644 00000034130 15162130472 0011110 0 ustar 00 <?php /** * The PlaceHolder class * * @since 3.0 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Placeholder extends Base { const TYPE_GENERATE = 'generate'; const TYPE_CLEAR_Q = 'clear_q'; private $_conf_placeholder_resp; private $_conf_placeholder_resp_svg; private $_conf_lqip; private $_conf_lqip_qual; private $_conf_lqip_min_w; private $_conf_lqip_min_h; private $_conf_placeholder_resp_color; private $_conf_placeholder_resp_async; private $_conf_ph_default; private $_placeholder_resp_dict = array(); private $_ph_queue = array(); protected $_summary; /** * Init * * @since 3.0 */ public function __construct() { $this->_conf_placeholder_resp = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_MEDIA_PLACEHOLDER_RESP); $this->_conf_placeholder_resp_svg = $this->conf(self::O_MEDIA_PLACEHOLDER_RESP_SVG); $this->_conf_lqip = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_MEDIA_LQIP); $this->_conf_lqip_qual = $this->conf(self::O_MEDIA_LQIP_QUAL); $this->_conf_lqip_min_w = $this->conf(self::O_MEDIA_LQIP_MIN_W); $this->_conf_lqip_min_h = $this->conf(self::O_MEDIA_LQIP_MIN_H); $this->_conf_placeholder_resp_async = $this->conf(self::O_MEDIA_PLACEHOLDER_RESP_ASYNC); $this->_conf_placeholder_resp_color = $this->conf(self::O_MEDIA_PLACEHOLDER_RESP_COLOR); $this->_conf_ph_default = $this->conf(self::O_MEDIA_LAZY_PLACEHOLDER) ?: LITESPEED_PLACEHOLDER; $this->_summary = self::get_summary(); } /** * Init Placeholder */ public function init() { Debug2::debug2('[LQIP] init'); add_action('litspeed_after_admin_init', array($this, 'after_admin_init')); } /** * Display column in Media * * @since 3.0 * @access public */ public function after_admin_init() { if ($this->_conf_lqip) { add_filter('manage_media_columns', array($this, 'media_row_title')); add_filter('manage_media_custom_column', array($this, 'media_row_actions'), 10, 2); add_action('litespeed_media_row_lqip', array($this, 'media_row_con')); } } /** * Media Admin Menu -> LQIP col * * @since 3.0 * @access public */ public function media_row_title($posts_columns) { $posts_columns['lqip'] = __('LQIP', 'litespeed-cache'); return $posts_columns; } /** * Media Admin Menu -> LQIP Column * * @since 3.0 * @access public */ public function media_row_actions($column_name, $post_id) { if ($column_name !== 'lqip') { return; } do_action('litespeed_media_row_lqip', $post_id); } /** * Display LQIP column * * @since 3.0 * @access public */ public function media_row_con($post_id) { $meta_value = wp_get_attachment_metadata($post_id); if (empty($meta_value['file'])) { return; } $total_files = 0; // List all sizes $all_sizes = array($meta_value['file']); $size_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/'; foreach ($meta_value['sizes'] as $v) { $all_sizes[] = $size_path . $v['file']; } foreach ($all_sizes as $short_path) { $lqip_folder = LITESPEED_STATIC_DIR . '/lqip/' . $short_path; if (is_dir($lqip_folder)) { Debug2::debug('[LQIP] Found folder: ' . $short_path); // List all files foreach (scandir($lqip_folder) as $v) { if ($v == '.' || $v == '..') { continue; } if ($total_files == 0) { echo '<div class="litespeed-media-lqip"><img src="' . Str::trim_quotes(File::read($lqip_folder . '/' . $v)) . '" alt="' . sprintf(__('LQIP image preview for size %s', 'litespeed-cache'), $v) . '"></div>'; } echo '<div class="litespeed-media-size"><a href="' . Str::trim_quotes(File::read($lqip_folder . '/' . $v)) . '" target="_blank">' . $v . '</a></div>'; $total_files++; } } } if ($total_files == 0) { echo '—'; } } /** * Replace image with placeholder * * @since 3.0 * @access public */ public function replace($html, $src, $size) { // Check if need to enable responsive placeholder or not $this_placeholder = $this->_placeholder($src, $size) ?: $this->_conf_ph_default; $additional_attr = ''; if ($this->_conf_lqip && $this_placeholder != $this->_conf_ph_default) { Debug2::debug2('[LQIP] Use resp LQIP [size] ' . $size); $additional_attr = ' data-placeholder-resp="' . Str::trim_quotes($size) . '"'; } $snippet = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_NOSCRIPT_RM) ? '' : '<noscript>' . $html . '</noscript>'; $html = str_replace(array(' src=', ' srcset=', ' sizes='), array(' data-src=', ' data-srcset=', ' data-sizes='), $html); $html = str_replace('<img ', '<img data-lazyloaded="1"' . $additional_attr . ' src="' . Str::trim_quotes($this_placeholder) . '" ', $html); $snippet = $html . $snippet; return $snippet; } /** * Generate responsive placeholder * * @since 2.5.1 * @access private */ private function _placeholder($src, $size) { // Low Quality Image Placeholders if (!$size) { Debug2::debug2('[LQIP] no size ' . $src); return false; } if (!$this->_conf_placeholder_resp) { return false; } // If use local generator if (!$this->_conf_lqip || !$this->_lqip_size_check($size)) { return $this->_generate_placeholder_locally($size); } Debug2::debug2('[LQIP] Resp LQIP process [src] ' . $src . ' [size] ' . $size); $arr_key = $size . ' ' . $src; // Check if its already in dict or not if (!empty($this->_placeholder_resp_dict[$arr_key])) { Debug2::debug2('[LQIP] already in dict'); return $this->_placeholder_resp_dict[$arr_key]; } // Need to generate the responsive placeholder $placeholder_realpath = $this->_placeholder_realpath($src, $size); // todo: give offload API if (file_exists($placeholder_realpath)) { Debug2::debug2('[LQIP] file exists'); $this->_placeholder_resp_dict[$arr_key] = File::read($placeholder_realpath); return $this->_placeholder_resp_dict[$arr_key]; } // Add to cron queue // Prevent repeated requests if (in_array($arr_key, $this->_ph_queue)) { Debug2::debug2('[LQIP] file bypass generating due to in queue'); return $this->_generate_placeholder_locally($size); } if ($hit = Utility::str_hit_array($src, $this->conf(self::O_MEDIA_LQIP_EXC))) { Debug2::debug2('[LQIP] file bypass generating due to exclude setting [hit] ' . $hit); return $this->_generate_placeholder_locally($size); } $this->_ph_queue[] = $arr_key; // Send request to generate placeholder if (!$this->_conf_placeholder_resp_async) { // If requested recently, bypass if ($this->_summary && !empty($this->_summary['curr_request']) && time() - $this->_summary['curr_request'] < 300) { Debug2::debug2('[LQIP] file bypass generating due to interval limit'); return false; } // Generate immediately $this->_placeholder_resp_dict[$arr_key] = $this->_generate_placeholder($arr_key); return $this->_placeholder_resp_dict[$arr_key]; } // Prepare default svg placeholder as tmp placeholder $tmp_placeholder = $this->_generate_placeholder_locally($size); // Store it to prepare for cron $queue = $this->load_queue('lqip'); if (in_array($arr_key, $queue)) { Debug2::debug2('[LQIP] already in queue'); return $tmp_placeholder; } if (count($queue) > 500) { Debug2::debug2('[LQIP] queue is full'); return $tmp_placeholder; } $queue[] = $arr_key; $this->save_queue('lqip', $queue); Debug2::debug('[LQIP] Added placeholder queue'); return $tmp_placeholder; } /** * Generate realpath of placeholder file * * @since 2.5.1 * @access private */ private function _placeholder_realpath($src, $size) { // Use LQIP Cloud generator, each image placeholder will be separately stored // Compatibility with WebP and AVIF $src = Utility::drop_webp($src); $filepath_prefix = $this->_build_filepath_prefix('lqip'); // External images will use cache folder directly $domain = parse_url($src, PHP_URL_HOST); if ($domain && !Utility::internal($domain)) { // todo: need to improve `util:internal()` to include `CDN::internal()` $md5 = md5($src); return LITESPEED_STATIC_DIR . $filepath_prefix . 'remote/' . substr($md5, 0, 1) . '/' . substr($md5, 1, 1) . '/' . $md5 . '.' . $size; } // Drop domain $short_path = Utility::att_short_path($src); return LITESPEED_STATIC_DIR . $filepath_prefix . $short_path . '/' . $size; } /** * Cron placeholder generation * * @since 2.5.1 * @access public */ public static function cron($continue = false) { $_instance = self::cls(); $queue = $_instance->load_queue('lqip'); if (empty($queue)) { return; } // For cron, need to check request interval too if (!$continue) { if (!empty($_instance->_summary['curr_request']) && time() - $_instance->_summary['curr_request'] < 300) { Debug2::debug('[LQIP] Last request not done'); return; } } foreach ($queue as $v) { Debug2::debug('[LQIP] cron job [size] ' . $v); $res = $_instance->_generate_placeholder($v, true); // Exit queue if out of quota if ($res === 'out_of_quota') { return; } // only request first one if (!$continue) { return; } } } /** * Generate placeholder locally * * @since 3.0 * @access private */ private function _generate_placeholder_locally($size) { Debug2::debug2('[LQIP] _generate_placeholder local [size] ' . $size); $size = explode('x', $size); $svg = str_replace(array('{width}', '{height}', '{color}'), array($size[0], $size[1], $this->_conf_placeholder_resp_color), $this->_conf_placeholder_resp_svg); return 'data:image/svg+xml;base64,' . base64_encode($svg); } /** * Send to LiteSpeed API to generate placeholder * * @since 2.5.1 * @access private */ private function _generate_placeholder($raw_size_and_src, $from_cron = false) { // Parse containing size and src info $size_and_src = explode(' ', $raw_size_and_src, 2); $size = $size_and_src[0]; if (empty($size_and_src[1])) { $this->_popup_and_save($raw_size_and_src); Debug2::debug('[LQIP] ❌ No src [raw] ' . $raw_size_and_src); return $this->_generate_placeholder_locally($size); } $src = $size_and_src[1]; $file = $this->_placeholder_realpath($src, $size); // Local generate SVG to serve ( Repeatedly doing this here to remove stored cron queue in case the setting _conf_lqip is changed ) if (!$this->_conf_lqip || !$this->_lqip_size_check($size)) { $data = $this->_generate_placeholder_locally($size); } else { $err = false; $allowance = Cloud::cls()->allowance(Cloud::SVC_LQIP, $err); if (!$allowance) { Debug2::debug('[LQIP] ❌ No credit: ' . $err); $err && Admin_Display::error(Error::msg($err)); if ($from_cron) { return 'out_of_quota'; } return $this->_generate_placeholder_locally($size); } // Generate LQIP list($width, $height) = explode('x', $size); $req_data = array( 'width' => $width, 'height' => $height, 'url' => Utility::drop_webp($src), 'quality' => $this->_conf_lqip_qual, ); // CHeck if the image is 404 first if (File::is_404($req_data['url'])) { $this->_popup_and_save($raw_size_and_src, true); $this->_append_exc($src); Debug2::debug('[LQIP] 404 before request [src] ' . $req_data['url']); return $this->_generate_placeholder_locally($size); } // Update request status $this->_summary['curr_request'] = time(); self::save_summary(); $json = Cloud::post(Cloud::SVC_LQIP, $req_data, 120); if (!is_array($json)) { return $this->_generate_placeholder_locally($size); } if (empty($json['lqip']) || strpos($json['lqip'], 'data:image/svg+xml') !== 0) { // image error, pop up the current queue $this->_popup_and_save($raw_size_and_src, true); $this->_append_exc($src); Debug2::debug('[LQIP] wrong response format', $json); return $this->_generate_placeholder_locally($size); } $data = $json['lqip']; Debug2::debug('[LQIP] _generate_placeholder LQIP'); } // Write to file File::save($file, $data, true); // Save summary data $this->_summary['last_spent'] = time() - $this->_summary['curr_request']; $this->_summary['last_request'] = $this->_summary['curr_request']; $this->_summary['curr_request'] = 0; self::save_summary(); $this->_popup_and_save($raw_size_and_src); Debug2::debug('[LQIP] saved LQIP ' . $file); return $data; } /** * Check if the size is valid to send LQIP request or not * * @since 3.0 */ private function _lqip_size_check($size) { $size = explode('x', $size); if ($size[0] >= $this->_conf_lqip_min_w || $size[1] >= $this->_conf_lqip_min_h) { return true; } Debug2::debug2('[LQIP] Size too small'); return false; } /** * Add to LQIP exclude list * * @since 3.4 */ private function _append_exc($src) { $val = $this->conf(self::O_MEDIA_LQIP_EXC); $val[] = $src; $this->cls('Conf')->update(self::O_MEDIA_LQIP_EXC, $val); Debug2::debug('[LQIP] Appended to LQIP Excludes [URL] ' . $src); } /** * Pop up the current request and save * * @since 3.0 */ private function _popup_and_save($raw_size_and_src, $append_to_exc = false) { $queue = $this->load_queue('lqip'); if (!empty($queue) && in_array($raw_size_and_src, $queue)) { unset($queue[array_search($raw_size_and_src, $queue)]); } if ($append_to_exc) { $size_and_src = explode(' ', $raw_size_and_src, 2); $this_src = $size_and_src[1]; // Append to lqip exc setting first $this->_append_exc($this_src); // Check if other queues contain this src or not if ($queue) { foreach ($queue as $k => $raw_size_and_src) { $size_and_src = explode(' ', $raw_size_and_src, 2); if (empty($size_and_src[1])) { continue; } if ($size_and_src[1] == $this_src) { unset($queue[$k]); } } } } $this->save_queue('lqip', $queue); } /** * Handle all request actions from main cls * * @since 2.5.1 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_GENERATE: self::cron(true); break; case self::TYPE_CLEAR_Q: $this->clear_q('lqip'); break; default: break; } Admin::redirect(); } } src/router.cls.php 0000644 00000047007 15162130475 0010160 0 ustar 00 <?php /** * The core plugin router class. * * This generate the valid action. * * @since 1.1.0 * @since 1.5 Moved into /inc */ namespace LiteSpeed; defined('WPINC') || exit(); class Router extends Base { const LOG_TAG = '[Router]'; const NONCE = 'LSCWP_NONCE'; const ACTION = 'LSCWP_CTRL'; const ACTION_SAVE_SETTINGS_NETWORK = 'save-settings-network'; const ACTION_DB_OPTM = 'db_optm'; const ACTION_PLACEHOLDER = 'placeholder'; const ACTION_AVATAR = 'avatar'; const ACTION_SAVE_SETTINGS = 'save-settings'; const ACTION_CLOUD = 'cloud'; const ACTION_IMG_OPTM = 'img_optm'; const ACTION_HEALTH = 'health'; const ACTION_CRAWLER = 'crawler'; const ACTION_PURGE = 'purge'; const ACTION_CONF = 'conf'; const ACTION_ACTIVATION = 'activation'; const ACTION_CSS = 'css'; const ACTION_UCSS = 'ucss'; const ACTION_VPI = 'vpi'; const ACTION_PRESET = 'preset'; const ACTION_IMPORT = 'import'; const ACTION_REPORT = 'report'; const ACTION_DEBUG2 = 'debug2'; const ACTION_CDN_CLOUDFLARE = 'CDN\Cloudflare'; const ACTION_ADMIN_DISPLAY = 'admin_display'; // List all handlers here private static $_HANDLERS = array( self::ACTION_ADMIN_DISPLAY, self::ACTION_ACTIVATION, self::ACTION_AVATAR, self::ACTION_CDN_CLOUDFLARE, self::ACTION_CLOUD, self::ACTION_CONF, self::ACTION_CRAWLER, self::ACTION_CSS, self::ACTION_UCSS, self::ACTION_VPI, self::ACTION_DB_OPTM, self::ACTION_DEBUG2, self::ACTION_HEALTH, self::ACTION_IMG_OPTM, self::ACTION_PRESET, self::ACTION_IMPORT, self::ACTION_PLACEHOLDER, self::ACTION_PURGE, self::ACTION_REPORT, ); const TYPE = 'litespeed_type'; const ITEM_HASH = 'hash'; const ITEM_FLASH_HASH = 'flash_hash'; private static $_esi_enabled; private static $_is_ajax; private static $_is_logged_in; private static $_ip; private static $_action; private static $_is_admin_ip; private static $_frontend_path; /** * Redirect to self to continue operation * * Note: must return when use this func. CLI/Cron call won't die in this func. * * @since 3.0 * @access public */ public static function self_redirect($action, $type) { if (defined('LITESPEED_CLI') || defined('DOING_CRON')) { Admin_Display::success('To be continued'); // Show for CLI return; } // Add i to avoid browser too many redirected warning $i = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0; $i++; $link = Utility::build_url($action, $type, false, null, array('litespeed_i' => $i)); $url = html_entity_decode($link); exit("<meta http-equiv='refresh' content='0;url=$url'>"); } /** * Check if can run optimize * * @since 1.3 * @since 2.3.1 Relocated from cdn.cls * @access public */ public function can_optm() { $can = true; if (is_user_logged_in() && $this->conf(self::O_OPTM_GUEST_ONLY)) { $can = false; } elseif (is_admin()) { $can = false; } elseif (is_feed()) { $can = false; } elseif (is_preview()) { $can = false; } elseif (self::is_ajax()) { $can = false; } if (self::_is_login_page()) { Debug2::debug('[Router] Optm bypassed: login/reg page'); $can = false; } $can_final = apply_filters('litespeed_can_optm', $can); if ($can_final != $can) { Debug2::debug('[Router] Optm bypassed: filter'); } return $can_final; } /** * Check referer page to see if its from admin * * @since 2.4.2.1 * @access public */ public static function from_admin() { return !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], get_admin_url()) === 0; } /** * Check if it can use CDN replacement * * @since 1.2.3 * @since 2.3.1 Relocated from cdn.cls * @access public */ public static function can_cdn() { $can = true; if (is_admin()) { if (!self::is_ajax()) { Debug2::debug2('[Router] CDN bypassed: is not ajax call'); $can = false; } if (self::from_admin()) { Debug2::debug2('[Router] CDN bypassed: ajax call from admin'); $can = false; } } elseif (is_feed()) { $can = false; } elseif (is_preview()) { $can = false; } /** * Bypass cron to avoid deregister jq notice `Do not deregister the <code>jquery-core</code> script in the administration area.` * @since 2.7.2 */ if (defined('DOING_CRON')) { $can = false; } /** * Bypass login/reg page * @since 1.6 */ if (self::_is_login_page()) { Debug2::debug('[Router] CDN bypassed: login/reg page'); $can = false; } /** * Bypass post/page link setting * @since 2.9.8.5 */ $rest_prefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : apply_filters('rest_url_prefix', 'wp-json'); if ( !empty($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], $rest_prefix . '/wp/v2/media') !== false && isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'wp-admin') !== false ) { Debug2::debug('[Router] CDN bypassed: wp-json on admin page'); $can = false; } $can_final = apply_filters('litespeed_can_cdn', $can); if ($can_final != $can) { Debug2::debug('[Router] CDN bypassed: filter'); } return $can_final; } /** * Check if is login page or not * * @since 2.3.1 * @access protected */ protected static function _is_login_page() { if (in_array($GLOBALS['pagenow'], array('wp-login.php', 'wp-register.php'), true)) { return true; } return false; } /** * UCSS/Crawler role simulator * * @since 1.9.1 * @since 3.3 Renamed from `is_crawler_role_simulation` */ public function is_role_simulation() { if (is_admin()) { return; } if (empty($_COOKIE['litespeed_hash']) && empty($_COOKIE['litespeed_flash_hash'])) { return; } self::debug('🪪 starting role validation'); // Check if is from crawler // if ( empty( $_SERVER[ 'HTTP_USER_AGENT' ] ) || strpos( $_SERVER[ 'HTTP_USER_AGENT' ], Crawler::FAST_USER_AGENT ) !== 0 ) { // Debug2::debug( '[Router] user agent not match' ); // return; // } $server_ip = $this->conf(self::O_SERVER_IP); if (!$server_ip || self::get_ip() !== $server_ip) { self::debug('❌❌ Role simulate uid denied! Not localhost visit!'); Control::set_nocache('Role simulate uid denied'); return; } // Flash hash validation if (!empty($_COOKIE['litespeed_flash_hash'])) { $hash_data = self::get_option(self::ITEM_FLASH_HASH, array()); if ($hash_data && is_array($hash_data) && !empty($hash_data['hash']) && !empty($hash_data['ts']) && !empty($hash_data['uid'])) { if (time() - $hash_data['ts'] < 120 && $_COOKIE['litespeed_flash_hash'] == $hash_data['hash']) { self::debug('🪪 Role simulator flash hash matched, escalating user to be uid=' . $hash_data['uid']); self::delete_option(self::ITEM_FLASH_HASH); wp_set_current_user($hash_data['uid']); return; } } } // Hash validation if (!empty($_COOKIE['litespeed_hash'])) { $hash_data = self::get_option(self::ITEM_HASH, array()); if ($hash_data && is_array($hash_data) && !empty($hash_data['hash']) && !empty($hash_data['ts']) && !empty($hash_data['uid'])) { $RUN_DURATION = $this->cls('Crawler')->get_crawler_duration(); if (time() - $hash_data['ts'] < $RUN_DURATION && $_COOKIE['litespeed_hash'] == $hash_data['hash']) { self::debug('🪪 Role simulator hash matched, escalating user to be uid=' . $hash_data['uid']); wp_set_current_user($hash_data['uid']); return; } } } self::debug('❌ WARNING: role simulator hash not match'); } /** * Get a short ttl hash (2mins) * * @since 6.4 */ public function get_flash_hash($uid) { $hash_data = self::get_option(self::ITEM_FLASH_HASH, array()); if ($hash_data && is_array($hash_data) && !empty($hash_data['hash']) && !empty($hash_data['ts'])) { if (time() - $hash_data['ts'] < 60) { return $hash_data['hash']; } } // Check if this user has editor access or not if (user_can($uid, 'edit_posts')) { self::debug('🛑 The user with id ' . $uid . ' has editor access, which is not allowed for the role simulator.'); return ''; } $hash = Str::rrand(32); self::update_option(self::ITEM_FLASH_HASH, array('hash' => $hash, 'ts' => time(), 'uid' => $uid)); return $hash; } /** * Get a security hash * * @since 3.3 */ public function get_hash($uid) { // Check if this user has editor access or not if (user_can($uid, 'edit_posts')) { self::debug('🛑 The user with id ' . $uid . ' has editor access, which is not allowed for the role simulator.'); return ''; } // As this is called only when starting crawling, not per page, no need to reuse $hash = Str::rrand(32); self::update_option(self::ITEM_HASH, array('hash' => $hash, 'ts' => time(), 'uid' => $uid)); return $hash; } /** * Get user role * * @since 1.6.2 */ public static function get_role($uid = null) { if (defined('LITESPEED_WP_ROLE')) { return LITESPEED_WP_ROLE; } if ($uid === null) { $uid = get_current_user_id(); } $role = false; if ($uid) { $user = get_userdata($uid); if (isset($user->roles) && is_array($user->roles)) { $tmp = array_values($user->roles); $role = implode(',', $tmp); // Combine for PHP5.3 const comaptibility } } Debug2::debug('[Router] get_role: ' . $role); if (!$role) { return $role; // Guest user Debug2::debug('[Router] role: guest'); /** * Fix double login issue * The previous user init refactoring didn't fix this bcos this is in login process and the user role could change * @see https://github.com/litespeedtech/lscache_wp/commit/69e7bc71d0de5cd58961bae953380b581abdc088 * @since 2.9.8 Won't assign const if in login process */ if (substr_compare(wp_login_url(), $GLOBALS['pagenow'], -strlen($GLOBALS['pagenow'])) === 0) { return $role; } } define('LITESPEED_WP_ROLE', $role); return LITESPEED_WP_ROLE; } /** * Get frontend path * * @since 1.2.2 * @access public * @return boolean */ public static function frontend_path() { //todo: move to htaccess.cls ? if (!isset(self::$_frontend_path)) { $frontend = rtrim(ABSPATH, '/'); // /home/user/public_html/frontend // get home path failed. Trac ticket #37668 (e.g. frontend:/blog backend:/wordpress) if (!$frontend) { Debug2::debug('[Router] No ABSPATH, generating from home option'); $frontend = parse_url(get_option('home')); $frontend = !empty($frontend['path']) ? $frontend['path'] : ''; $frontend = $_SERVER['DOCUMENT_ROOT'] . $frontend; } $frontend = realpath($frontend); self::$_frontend_path = $frontend; } return self::$_frontend_path; } /** * Check if ESI is enabled or not * * @since 1.2.0 * @access public * @return boolean */ public function esi_enabled() { if (!isset(self::$_esi_enabled)) { self::$_esi_enabled = defined('LITESPEED_ON') && $this->conf(self::O_ESI); if (!empty($_REQUEST[self::ACTION])) { self::$_esi_enabled = false; } } return self::$_esi_enabled; } /** * Check if crawler is enabled on server level * * @since 1.1.1 * @access public */ public static function can_crawl() { if (isset($_SERVER['X-LSCACHE']) && strpos($_SERVER['X-LSCACHE'], 'crawler') === false) { return false; } // CLI will bypass this check as crawler library can always do the 428 check if (defined('LITESPEED_CLI')) { return true; } return true; } /** * Check action * * @since 1.1.0 * @access public * @return string */ public static function get_action() { if (!isset(self::$_action)) { self::$_action = false; self::cls()->verify_action(); if (self::$_action) { defined('LSCWP_LOG') && Debug2::debug('[Router] LSCWP_CTRL verified: ' . var_export(self::$_action, true)); } } return self::$_action; } /** * Check if is logged in * * @since 1.1.3 * @access public * @return boolean */ public static function is_logged_in() { if (!isset(self::$_is_logged_in)) { self::$_is_logged_in = is_user_logged_in(); } return self::$_is_logged_in; } /** * Check if is ajax call * * @since 1.1.0 * @access public * @return boolean */ public static function is_ajax() { if (!isset(self::$_is_ajax)) { self::$_is_ajax = defined('DOING_AJAX') && DOING_AJAX; } return self::$_is_ajax; } /** * Check if is admin ip * * @since 1.1.0 * @access public * @return boolean */ public function is_admin_ip() { if (!isset(self::$_is_admin_ip)) { $ips = $this->conf(self::O_DEBUG_IPS); self::$_is_admin_ip = $this->ip_access($ips); } return self::$_is_admin_ip; } /** * Get type value * * @since 1.6 * @access public */ public static function verify_type() { if (empty($_REQUEST[self::TYPE])) { Debug2::debug('[Router] no type', 2); return false; } Debug2::debug('[Router] parsed type: ' . $_REQUEST[self::TYPE], 2); return $_REQUEST[self::TYPE]; } /** * Check privilege and nonce for the action * * @since 1.1.0 * @access private */ private function verify_action() { if (empty($_REQUEST[Router::ACTION])) { Debug2::debug2('[Router] LSCWP_CTRL bypassed empty'); return; } $action = stripslashes($_REQUEST[Router::ACTION]); if (!$action) { return; } $_is_public_action = false; // Each action must have a valid nonce unless its from admin ip and is public action // Validate requests nonce (from admin logged in page or cli) if (!$this->verify_nonce($action)) { // check if it is from admin ip if (!$this->is_admin_ip()) { Debug2::debug('[Router] LSCWP_CTRL query string - did not match admin IP: ' . $action); return; } // check if it is public action if ( !in_array($action, array( Core::ACTION_QS_NOCACHE, Core::ACTION_QS_PURGE, Core::ACTION_QS_PURGE_SINGLE, Core::ACTION_QS_SHOW_HEADERS, Core::ACTION_QS_PURGE_ALL, Core::ACTION_QS_PURGE_EMPTYCACHE, )) ) { Debug2::debug('[Router] LSCWP_CTRL query string - did not match admin IP Actions: ' . $action); return; } if (apply_filters('litespeed_qs_forbidden', false)) { Debug2::debug('[Router] LSCWP_CTRL forbidden by hook litespeed_qs_forbidden'); return; } $_is_public_action = true; } /* Now it is a valid action, lets log and check the permission */ Debug2::debug('[Router] LSCWP_CTRL: ' . $action); // OK, as we want to do something magic, lets check if its allowed $_is_multisite = is_multisite(); $_is_network_admin = $_is_multisite && is_network_admin(); $_can_network_option = $_is_network_admin && current_user_can('manage_network_options'); $_can_option = current_user_can('manage_options'); switch ($action) { case self::ACTION_SAVE_SETTINGS_NETWORK: // Save network settings if ($_can_network_option) { self::$_action = $action; } return; case Core::ACTION_PURGE_BY: if (defined('LITESPEED_ON') && ($_can_network_option || $_can_option || self::is_ajax())) { //here may need more security self::$_action = $action; } return; case self::ACTION_DB_OPTM: if ($_can_network_option || $_can_option) { self::$_action = $action; } return; case Core::ACTION_PURGE_EMPTYCACHE: // todo: moved to purge.cls type action if ((defined('LITESPEED_ON') || $_is_network_admin) && ($_can_network_option || (!$_is_multisite && $_can_option))) { self::$_action = $action; } return; case Core::ACTION_QS_NOCACHE: case Core::ACTION_QS_PURGE: case Core::ACTION_QS_PURGE_SINGLE: case Core::ACTION_QS_SHOW_HEADERS: case Core::ACTION_QS_PURGE_ALL: case Core::ACTION_QS_PURGE_EMPTYCACHE: if (defined('LITESPEED_ON') && ($_is_public_action || self::is_ajax())) { self::$_action = $action; } return; case self::ACTION_ADMIN_DISPLAY: case self::ACTION_PLACEHOLDER: case self::ACTION_AVATAR: case self::ACTION_IMG_OPTM: case self::ACTION_CLOUD: case self::ACTION_CDN_CLOUDFLARE: case self::ACTION_CRAWLER: case self::ACTION_PRESET: case self::ACTION_IMPORT: case self::ACTION_REPORT: case self::ACTION_CSS: case self::ACTION_UCSS: case self::ACTION_VPI: case self::ACTION_CONF: case self::ACTION_ACTIVATION: case self::ACTION_HEALTH: case self::ACTION_SAVE_SETTINGS: // Save settings if ($_can_option && !$_is_network_admin) { self::$_action = $action; } return; case self::ACTION_PURGE: case self::ACTION_DEBUG2: if ($_can_network_option || $_can_option) { self::$_action = $action; } return; case Core::ACTION_DISMISS: /** * Non ajax call can dismiss too * @since 2.9 */ // if ( self::is_ajax() ) { self::$_action = $action; // } return; default: Debug2::debug('[Router] LSCWP_CTRL match failed: ' . $action); return; } } /** * Verify nonce * * @since 1.1.0 * @access public * @param string $action * @return bool */ public function verify_nonce($action) { if (!isset($_REQUEST[Router::NONCE]) || !wp_verify_nonce($_REQUEST[Router::NONCE], $action)) { return false; } else { return true; } } /** * Check if the ip is in the range * * @since 1.1.0 * @access public */ public function ip_access($ip_list) { if (!$ip_list) { return false; } if (!isset(self::$_ip)) { self::$_ip = self::get_ip(); } if (!self::$_ip) { return false; } // $uip = explode('.', $_ip); // if(empty($uip) || count($uip) != 4) Return false; // foreach($ip_list as $key => $ip) $ip_list[$key] = explode('.', trim($ip)); // foreach($ip_list as $key => $ip) { // if(count($ip) != 4) continue; // for($i = 0; $i <= 3; $i++) if($ip[$i] == '*') $ip_list[$key][$i] = $uip[$i]; // } return in_array(self::$_ip, $ip_list); } /** * Get client ip * * @since 1.1.0 * @since 1.6.5 changed to public * @access public * @return string */ public static function get_ip() { $_ip = ''; // if ( function_exists( 'apache_request_headers' ) ) { // $apache_headers = apache_request_headers(); // $_ip = ! empty( $apache_headers['True-Client-IP'] ) ? $apache_headers['True-Client-IP'] : false; // if ( ! $_ip ) { // $_ip = ! empty( $apache_headers['X-Forwarded-For'] ) ? $apache_headers['X-Forwarded-For'] : false; // $_ip = explode( ',', $_ip ); // $_ip = $_ip[ 0 ]; // } // } if (!$_ip) { $_ip = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false; } return $_ip; } /** * Check if opcode cache is enabled * * @since 1.8.2 * @access public */ public static function opcache_enabled() { return function_exists('opcache_reset') && ini_get('opcache.enable'); } /** * Handle static files * * @since 3.0 */ public function serve_static() { if (!empty($_SERVER['SCRIPT_URI'])) { if (strpos($_SERVER['SCRIPT_URI'], LITESPEED_STATIC_URL . '/') !== 0) { return; } $path = substr($_SERVER['SCRIPT_URI'], strlen(LITESPEED_STATIC_URL . '/')); } elseif (!empty($_SERVER['REQUEST_URI'])) { $static_path = parse_url(LITESPEED_STATIC_URL, PHP_URL_PATH) . '/'; if (strpos($_SERVER['REQUEST_URI'], $static_path) !== 0) { return; } $path = substr(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), strlen($static_path)); } else { return; } $path = explode('/', $path, 2); if (empty($path[0]) || empty($path[1])) { return; } switch ($path[0]) { case 'avatar': $this->cls('Avatar')->serve_static($path[1]); break; case 'localres': $this->cls('Localization')->serve_static($path[1]); break; default: break; } } /** * Handle all request actions from main cls * * This is different than other handlers * * @since 3.0 * @access public */ public function handler($cls) { if (!in_array($cls, self::$_HANDLERS)) { return; } return $this->cls($cls)->handler(); } } src/ucss.cls.php 0000644 00000034261 15162130500 0007600 0 ustar 00 <?php /** * The ucss class. * * @since 5.1 */ namespace LiteSpeed; defined('WPINC') || exit(); class UCSS extends Base { const LOG_TAG = '[UCSS]'; const TYPE_GEN = 'gen'; const TYPE_CLEAR_Q = 'clear_q'; protected $_summary; private $_ucss_whitelist; private $_queue; /** * Init * * @since 3.0 */ public function __construct() { $this->_summary = self::get_summary(); add_filter('litespeed_ucss_whitelist', array($this->cls('Data'), 'load_ucss_whitelist')); } /** * Uniform url tag for ucss usage * @since 4.7 */ public static function get_url_tag($request_url = false) { $url_tag = $request_url; if (is_404()) { $url_tag = '404'; } elseif (apply_filters('litespeed_ucss_per_pagetype', false)) { $url_tag = Utility::page_type(); self::debug('litespeed_ucss_per_pagetype filter altered url to ' . $url_tag); } return $url_tag; } /** * Get UCSS path * * @since 4.0 */ public function load($request_url, $dry_run = false) { // Check UCSS URI excludes $ucss_exc = apply_filters('litespeed_ucss_exc', $this->conf(self::O_OPTM_UCSS_EXC)); if ($ucss_exc && ($hit = Utility::str_hit_array($request_url, $ucss_exc))) { self::debug('UCSS bypassed due to UCSS URI Exclude setting: ' . $hit); Core::comment('QUIC.cloud UCSS bypassed by setting'); return false; } $filepath_prefix = $this->_build_filepath_prefix('ucss'); $url_tag = self::get_url_tag($request_url); $vary = $this->cls('Vary')->finalize_full_varies(); $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'ucss'); if ($filename) { $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css'; if (file_exists($static_file)) { self::debug2('existing ucss ' . $static_file); // Check if is error comment inside only $tmp = File::read($static_file); if (substr($tmp, 0, 2) == '/*' && substr(trim($tmp), -2) == '*/') { self::debug2('existing ucss is error only: ' . $tmp); Core::comment('QUIC.cloud UCSS bypassed due to generation error ❌ ' . $filepath_prefix . $filename . '.css'); return false; } Core::comment('QUIC.cloud UCSS loaded ✅'); return $filename . '.css'; } } if ($dry_run) { return false; } Core::comment('QUIC.cloud UCSS in queue'); $uid = get_current_user_id(); $ua = $this->_get_ua(); // Store it for cron $this->_queue = $this->load_queue('ucss'); if (count($this->_queue) > 500) { self::debug('UCSS Queue is full - 500'); return false; } $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag; $this->_queue[$queue_k] = array( 'url' => apply_filters('litespeed_ucss_url', $request_url), 'user_agent' => substr($ua, 0, 200), 'is_mobile' => $this->_separate_mobile(), 'is_webp' => $this->cls('Media')->webp_support() ? 1 : 0, 'uid' => $uid, 'vary' => $vary, 'url_tag' => $url_tag, ); // Current UA will be used to request $this->save_queue('ucss', $this->_queue); self::debug('Added queue_ucss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] ' . $uid); // Prepare cache tag for later purge Tag::add('UCSS.' . md5($queue_k)); return false; } /** * Get User Agent * * @since 5.3 */ private function _get_ua() { return !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; } /** * Add rows to q * * @since 5.3 */ public function add_to_q($url_files) { // Store it for cron $this->_queue = $this->load_queue('ucss'); if (count($this->_queue) > 500) { self::debug('UCSS Queue is full - 500'); return false; } $ua = $this->_get_ua(); foreach ($url_files as $url_file) { $vary = $url_file['vary']; $request_url = $url_file['url']; $is_mobile = $url_file['mobile']; $is_webp = $url_file['webp']; $url_tag = self::get_url_tag($request_url); $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag; $q = array( 'url' => apply_filters('litespeed_ucss_url', $request_url), 'user_agent' => substr($ua, 0, 200), 'is_mobile' => $is_mobile, 'is_webp' => $is_webp, 'uid' => false, 'vary' => $vary, 'url_tag' => $url_tag, ); // Current UA will be used to request self::debug('Added queue_ucss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] false'); $this->_queue[$queue_k] = $q; } $this->save_queue('ucss', $this->_queue); } /** * Generate UCSS * * @since 4.0 */ public static function cron($continue = false) { $_instance = self::cls(); return $_instance->_cron_handler($continue); } /** * Handle UCSS cron * * @since 4.2 */ private function _cron_handler($continue) { $this->_queue = $this->load_queue('ucss'); if (empty($this->_queue)) { return; } // For cron, need to check request interval too if (!$continue) { if (!empty($this->_summary['curr_request']) && time() - $this->_summary['curr_request'] < 300 && !$this->conf(self::O_DEBUG)) { self::debug('Last request not done'); return; } } $i = 0; foreach ($this->_queue as $k => $v) { if (!empty($v['_status'])) { continue; } self::debug('cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']); if (!isset($v['is_webp'])) { $v['is_webp'] = false; } $i++; $res = $this->_send_req($v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $v['is_mobile'], $v['is_webp']); if (!$res) { // Status is wrong, drop this this->_queue $this->_queue = $this->load_queue('ucss'); unset($this->_queue[$k]); $this->save_queue('ucss', $this->_queue); if (!$continue) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), 'UCSS'); return Router::self_redirect(Router::ACTION_UCSS, self::TYPE_GEN); } continue; } // Exit queue if out of quota or service is hot if ($res === 'out_of_quota' || $res === 'svc_hot') { return; } $this->_queue = $this->load_queue('ucss'); $this->_queue[$k]['_status'] = 'requested'; $this->save_queue('ucss', $this->_queue); self::debug('Saved to queue [k] ' . $k); // only request first one if (!$continue) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), 'UCSS'); return Router::self_redirect(Router::ACTION_UCSS, self::TYPE_GEN); } } } /** * Send to QC API to generate UCSS * * @since 2.3 * @access private */ private function _send_req($request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $is_mobile, $is_webp) { // Check if has credit to push or not $err = false; $allowance = $this->cls('Cloud')->allowance(Cloud::SVC_UCSS, $err); if (!$allowance) { self::debug('❌ No credit: ' . $err); $err && Admin_Display::error(Error::msg($err)); return 'out_of_quota'; } set_time_limit(120); // Update css request status $this->_summary['curr_request'] = time(); self::save_summary(); // Gather guest HTML to send $html = $this->cls('CSS')->prepare_html($request_url, $user_agent, $uid); if (!$html) { return false; } // Parse HTML to gather all CSS content before requesting $css = false; list(, $html) = $this->prepare_css($html, $is_webp, true); // Use this to drop CSS from HTML as we don't need those CSS to generate UCSS $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'css'); $filepath_prefix = $this->_build_filepath_prefix('css'); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css'; self::debug('Checking combined file ' . $static_file); if (file_exists($static_file)) { $css = File::read($static_file); } if (!$css) { self::debug('❌ No combined css'); return false; } $data = array( 'url' => $request_url, 'queue_k' => $queue_k, 'user_agent' => $user_agent, 'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet 'is_webp' => $is_webp ? 1 : 0, 'html' => $html, 'css' => $css, ); if (!isset($this->_ucss_whitelist)) { $this->_ucss_whitelist = $this->_filter_whitelist(); } $data['whitelist'] = $this->_ucss_whitelist; self::debug('Generating: ', $data); $json = Cloud::post(Cloud::SVC_UCSS, $data, 30); if (!is_array($json)) { return $json; } // Old version compatibility if (empty($json['status'])) { if (!empty($json['ucss'])) { $this->_save_con('ucss', $json['ucss'], $queue_k, $is_mobile, $is_webp); } // Delete the row return false; } // Unknown status, remove this line if ($json['status'] != 'queued') { return false; } // Save summary data $this->_summary['last_spent'] = time() - $this->_summary['curr_request']; $this->_summary['last_request'] = $this->_summary['curr_request']; $this->_summary['curr_request'] = 0; self::save_summary(); return true; } /** * Save UCSS content * * @since 4.2 */ private function _save_con($type, $css, $queue_k, $is_mobile, $is_webp) { // Add filters $css = apply_filters('litespeed_' . $type, $css, $queue_k); self::debug2('con: ', $css); if (substr($css, 0, 2) == '/*' && substr($css, -2) == '*/') { self::debug('❌ empty ' . $type . ' [content] ' . $css); // continue; // Save the error info too } // Write to file $filecon_md5 = md5($css); $filepath_prefix = $this->_build_filepath_prefix($type); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css'; File::save($static_file, $css, true); $url_tag = $this->_queue[$queue_k]['url_tag']; $vary = $this->_queue[$queue_k]['vary']; self::debug2("Save URL to file [file] $static_file [vary] $vary"); $this->cls('Data')->save_url($url_tag, $vary, $type, $filecon_md5, dirname($static_file), $is_mobile, $is_webp); Purge::add(strtoupper($type) . '.' . md5($queue_k)); } /** * Prepare CSS from HTML for CCSS generation only. UCSS will used combined CSS directly. * Prepare refined HTML for both CCSS and UCSS. * * @since 3.4.3 */ public function prepare_css($html, $is_webp = false, $dryrun = false) { $css = ''; preg_match_all('#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>#isU', $html, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $debug_info = ''; if (strpos($match[0], '<link') === 0) { $attrs = Utility::parse_attr($match[1]); if (empty($attrs['rel'])) { continue; } if ($attrs['rel'] != 'stylesheet') { if ($attrs['rel'] != 'preload' || empty($attrs['as']) || $attrs['as'] != 'style') { continue; } } if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) { continue; } if (empty($attrs['href'])) { continue; } // Check Google fonts hit if (strpos($attrs['href'], 'fonts.googleapis.com') !== false) { $html = str_replace($match[0], '', $html); continue; } $debug_info = $attrs['href']; // Load CSS content if (!$dryrun) { // Dryrun will not load CSS but just drop them $con = $this->cls('Optimizer')->load_file($attrs['href']); if (!$con) { continue; } } else { $con = ''; } } else { // Inline style $attrs = Utility::parse_attr($match[2]); if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) { continue; } Debug2::debug2('[CSS] Load inline CSS ' . substr($match[3], 0, 100) . '...', $attrs); $con = $match[3]; $debug_info = '__INLINE__'; } $con = Optimizer::minify_css($con); if ($is_webp && $this->cls('Media')->webp_support()) { $con = $this->cls('Media')->replace_background_webp($con); } if (!empty($attrs['media']) && $attrs['media'] !== 'all') { $con = '@media ' . $attrs['media'] . '{' . $con . "}\n"; } else { $con = $con . "\n"; } $con = '/* ' . $debug_info . ' */' . $con; $css .= $con; $html = str_replace($match[0], '', $html); } return array($css, $html); } /** * Filter the comment content, add quotes to selector from whitelist. Return the json * * @since 3.3 */ private function _filter_whitelist() { $whitelist = array(); $list = apply_filters('litespeed_ucss_whitelist', $this->conf(self::O_OPTM_UCSS_SELECTOR_WHITELIST)); foreach ($list as $k => $v) { if (substr($v, 0, 2) === '//') { continue; } // Wrap in quotes for selectors if (substr($v, 0, 1) !== '/' && strpos($v, '"') === false && strpos($v, "'") === false) { // $v = "'$v'"; } $whitelist[] = $v; } return $whitelist; } /** * Notify finished from server * @since 5.1 */ public function notify() { $post_data = \json_decode(file_get_contents('php://input'), true); if (is_null($post_data)) { $post_data = $_POST; } self::debug('notify() data', $post_data); $this->_queue = $this->load_queue('ucss'); list($post_data) = $this->cls('Cloud')->extract_msg($post_data, 'ucss'); $notified_data = $post_data['data']; if (empty($notified_data) || !is_array($notified_data)) { self::debug('❌ notify exit: no notified data'); return Cloud::err('no notified data'); } // Check if its in queue or not $valid_i = 0; foreach ($notified_data as $v) { if (empty($v['request_url'])) { self::debug('❌ notify bypass: no request_url', $v); continue; } if (empty($v['queue_k'])) { self::debug('❌ notify bypass: no queue_k', $v); continue; } if (empty($this->_queue[$v['queue_k']])) { self::debug('❌ notify bypass: no this queue [q_k]' . $v['queue_k']); continue; } // Save data if (!empty($v['data_ucss'])) { $is_mobile = $this->_queue[$v['queue_k']]['is_mobile']; $is_webp = $this->_queue[$v['queue_k']]['is_webp']; $this->_save_con('ucss', $v['data_ucss'], $v['queue_k'], $is_mobile, $is_webp); $valid_i++; } unset($this->_queue[$v['queue_k']]); self::debug('notify data handled, unset queue [q_k] ' . $v['queue_k']); } $this->save_queue('ucss', $this->_queue); self::debug('notified'); return Cloud::ok(array('count' => $valid_i)); } /** * Handle all request actions from main cls * * @since 2.3 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_GEN: self::cron(true); break; case self::TYPE_CLEAR_Q: $this->clear_q('ucss'); break; default: break; } Admin::redirect(); } } src/localization.cls.php 0000644 00000006617 15162130503 0011322 0 ustar 00 <?php /** * The localization class. * * @since 3.3 */ namespace LiteSpeed; defined('WPINC') || exit(); class Localization extends Base { const LOG_TAG = '🛍️'; /** * Init optimizer * * @since 3.0 * @access protected */ public function init() { add_filter('litespeed_buffer_finalize', array($this, 'finalize'), 23); // After page optm } /** * Localize Resources * * @since 3.3 */ public function serve_static($uri) { $url = base64_decode($uri); if (!$this->conf(self::O_OPTM_LOCALIZE)) { // wp_redirect( $url ); exit('Not supported'); } if (substr($url, -3) !== '.js') { // wp_redirect( $url ); // exit( 'Not supported ' . $uri ); } $match = false; $domains = $this->conf(self::O_OPTM_LOCALIZE_DOMAINS); foreach ($domains as $v) { if (!$v || strpos($v, '#') === 0) { continue; } $type = 'js'; $domain = $v; // Try to parse space split value if (strpos($v, ' ')) { $v = explode(' ', $v); if (!empty($v[1])) { $type = strtolower($v[0]); $domain = $v[1]; } } if (strpos($domain, 'https://') !== 0) { continue; } if ($type != 'js') { continue; } // if ( strpos( $url, $domain ) !== 0 ) { if ($url != $domain) { continue; } $match = true; break; } if (!$match) { // wp_redirect( $url ); exit('Not supported2'); } header('Content-Type: application/javascript'); // Generate $this->_maybe_mk_cache_folder('localres'); $file = $this->_realpath($url); self::debug('localize [url] ' . $url); $response = wp_safe_remote_get($url, array('timeout' => 180, 'stream' => true, 'filename' => $file)); // Parse response data if (is_wp_error($response)) { $error_message = $response->get_error_message(); file_exists($file) && unlink($file); self::debug('failed to get: ' . $error_message); wp_redirect($url); exit(); } $url = $this->_rewrite($url); wp_redirect($url); exit(); } /** * Get the final URL of local avatar * * @since 4.5 */ private function _rewrite($url) { return LITESPEED_STATIC_URL . '/localres/' . $this->_filepath($url); } /** * Generate realpath of the cache file * * @since 4.5 * @access private */ private function _realpath($url) { return LITESPEED_STATIC_DIR . '/localres/' . $this->_filepath($url); } /** * Get filepath * * @since 4.5 */ private function _filepath($url) { $filename = md5($url) . '.js'; if (is_multisite()) { $filename = get_current_blog_id() . '/' . $filename; } return $filename; } /** * Localize JS/Fonts * * @since 3.3 * @access public */ public function finalize($content) { if (is_admin()) { return $content; } if (!$this->conf(self::O_OPTM_LOCALIZE)) { return $content; } $domains = $this->conf(self::O_OPTM_LOCALIZE_DOMAINS); if (!$domains) { return $content; } foreach ($domains as $v) { if (!$v || strpos($v, '#') === 0) { continue; } $type = 'js'; $domain = $v; // Try to parse space split value if (strpos($v, ' ')) { $v = explode(' ', $v); if (!empty($v[1])) { $type = strtolower($v[0]); $domain = $v[1]; } } if (strpos($domain, 'https://') !== 0) { continue; } if ($type != 'js') { continue; } $content = str_replace($domain, LITESPEED_STATIC_URL . '/localres/' . base64_encode($domain), $content); } return $content; } } src/str.cls.php 0000644 00000004575 15162130506 0007446 0 ustar 00 <?php /** * LiteSpeed String Operator Library Class * * @since 1.3 */ namespace LiteSpeed; defined('WPINC') || exit(); class Str { /** * Translate QC HTML links from html. Convert `<a href="{#xxx#}">xxxx</a>` to `<a href="xxx">xxxx</a>` * * @since 7.0 */ public static function translate_qc_apis($html) { preg_match_all('/<a href="{#(\w+)#}"/U', $html, $matches); if (!$matches) { return $html; } foreach ($matches[0] as $k => $html_to_be_replaced) { $link = '<a href="' . Utility::build_url(Router::ACTION_CLOUD, Cloud::TYPE_API, false, null, array('action2' => $matches[1][$k])) . '"'; $html = str_replace($html_to_be_replaced, $link, $html); } return $html; } /** * Return safe HTML * * @since 7.0 */ public static function safe_html($html) { $common_attrs = array( 'style' => array(), 'class' => array(), 'target' => array(), 'src' => array(), 'color' => array(), 'href' => array(), ); $tags = array('hr', 'h3', 'h4', 'h5', 'ul', 'li', 'br', 'strong', 'p', 'span', 'img', 'a', 'div', 'font'); $allowed_tags = array(); foreach ($tags as $tag) { $allowed_tags[$tag] = $common_attrs; } return wp_kses($html, $allowed_tags); } /** * Generate random string * * @since 1.3 * @access public * @param int $len Length of string * @param int $type 1-Number 2-LowerChar 4-UpperChar * @return string */ public static function rrand($len, $type = 7) { switch ($type) { case 0: $charlist = '012'; break; case 1: $charlist = '0123456789'; break; case 2: $charlist = 'abcdefghijklmnopqrstuvwxyz'; break; case 3: $charlist = '0123456789abcdefghijklmnopqrstuvwxyz'; break; case 4: $charlist = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 5: $charlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 6: $charlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 7: $charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; } $str = ''; $max = strlen($charlist) - 1; for ($i = 0; $i < $len; $i++) { $str .= $charlist[random_int(0, $max)]; } return $str; } /** * Trim double quotes from a string to be used as a preformatted src in HTML. * @since 6.5.3 */ public static function trim_quotes($string) { return str_replace('"', '', $string); } } src/admin-display.cls.php 0000644 00000106620 15162130510 0011356 0 ustar 00 <?php /** * The admin-panel specific functionality of the plugin. * * * @since 1.0.0 * @package LiteSpeed * @subpackage LiteSpeed/admin * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Admin_Display extends Base { const LOG_TAG = '👮♀️'; const NOTICE_BLUE = 'notice notice-info'; const NOTICE_GREEN = 'notice notice-success'; const NOTICE_RED = 'notice notice-error'; const NOTICE_YELLOW = 'notice notice-warning'; const DB_MSG = 'messages'; const DB_MSG_PIN = 'msg_pin'; const PURGEBY_CAT = '0'; const PURGEBY_PID = '1'; const PURGEBY_TAG = '2'; const PURGEBY_URL = '3'; const PURGEBYOPT_SELECT = 'purgeby'; const PURGEBYOPT_LIST = 'purgebylist'; const DB_DISMISS_MSG = 'dismiss'; const RULECONFLICT_ON = 'ExpiresDefault_1'; const RULECONFLICT_DISMISSED = 'ExpiresDefault_0'; const TYPE_QC_HIDE_BANNER = 'qc_hide_banner'; const COOKIE_QC_HIDE_BANNER = 'litespeed_qc_hide_banner'; protected $messages = array(); protected $default_settings = array(); protected $_is_network_admin = false; protected $_is_multisite = false; private $_btn_i = 0; /** * Initialize the class and set its properties. * * @since 1.0.7 */ public function __construct() { // main css add_action('admin_enqueue_scripts', array($this, 'enqueue_style')); // Main js add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts')); $this->_is_network_admin = is_network_admin(); $this->_is_multisite = is_multisite(); // Quick access menu if (is_multisite() && $this->_is_network_admin) { $manage = 'manage_network_options'; } else { $manage = 'manage_options'; } if (current_user_can($manage)) { if (!defined('LITESPEED_DISABLE_ALL') || !LITESPEED_DISABLE_ALL) { add_action('wp_before_admin_bar_render', array(GUI::cls(), 'backend_shortcut')); } // `admin_notices` is after `admin_enqueue_scripts` // @see wp-admin/admin-header.php add_action($this->_is_network_admin ? 'network_admin_notices' : 'admin_notices', array($this, 'display_messages')); } /** * In case this is called outside the admin page * @see https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network * @since 2.0 */ if (!function_exists('is_plugin_active_for_network')) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } // add menus ( Also check for mu-plugins) if ($this->_is_network_admin && (is_plugin_active_for_network(LSCWP_BASENAME) || defined('LSCWP_MU_PLUGIN'))) { add_action('network_admin_menu', array($this, 'register_admin_menu')); } else { add_action('admin_menu', array($this, 'register_admin_menu')); } $this->cls('Metabox')->register_settings(); } /** * Show the title of one line * * @since 3.0 * @access public */ public function title($id) { echo Lang::title($id); } /** * Register the admin menu display. * * @since 1.0.0 * @access public */ public function register_admin_menu() { $capability = $this->_is_network_admin ? 'manage_network_options' : 'manage_options'; if (current_user_can($capability)) { // root menu add_menu_page('LiteSpeed Cache', 'LiteSpeed Cache', 'manage_options', 'litespeed'); // sub menus $this->_add_submenu(__('Dashboard', 'litespeed-cache'), 'litespeed', 'show_menu_dash'); !$this->_is_network_admin && $this->_add_submenu(__('Presets', 'litespeed-cache'), 'litespeed-presets', 'show_menu_presets'); $this->_add_submenu(__('General', 'litespeed-cache'), 'litespeed-general', 'show_menu_general'); $this->_add_submenu(__('Cache', 'litespeed-cache'), 'litespeed-cache', 'show_menu_cache'); !$this->_is_network_admin && $this->_add_submenu(__('CDN', 'litespeed-cache'), 'litespeed-cdn', 'show_menu_cdn'); $this->_add_submenu(__('Image Optimization', 'litespeed-cache'), 'litespeed-img_optm', 'show_img_optm'); !$this->_is_network_admin && $this->_add_submenu(__('Page Optimization', 'litespeed-cache'), 'litespeed-page_optm', 'show_page_optm'); $this->_add_submenu(__('Database', 'litespeed-cache'), 'litespeed-db_optm', 'show_db_optm'); !$this->_is_network_admin && $this->_add_submenu(__('Crawler', 'litespeed-cache'), 'litespeed-crawler', 'show_crawler'); $this->_add_submenu(__('Toolbox', 'litespeed-cache'), 'litespeed-toolbox', 'show_toolbox'); // sub menus under options add_options_page('LiteSpeed Cache', 'LiteSpeed Cache', $capability, 'litespeed-cache-options', array($this, 'show_menu_cache')); } } /** * Helper function to set up a submenu page. * * @since 1.0.4 * @access private * @param string $menu_title The title that appears on the menu. * @param string $menu_slug The slug of the page. * @param string $callback The callback to call if selected. */ private function _add_submenu($menu_title, $menu_slug, $callback) { add_submenu_page('litespeed', $menu_title, $menu_title, 'manage_options', $menu_slug, array($this, $callback)); } /** * Register the stylesheets for the admin area. * * @since 1.0.14 * @access public */ public function enqueue_style() { wp_enqueue_style(Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/css/litespeed.css', array(), Core::VER, 'all'); } /** * Register the JavaScript for the admin area. * * @since 1.0.0 * @access public */ public function enqueue_scripts() { wp_register_script(Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/js/litespeed-cache-admin.js', array(), Core::VER, false); $localize_data = array(); if (GUI::has_whm_msg()) { $ajax_url_dismiss_whm = Utility::build_url(Core::ACTION_DISMISS, GUI::TYPE_DISMISS_WHM, true); $localize_data['ajax_url_dismiss_whm'] = $ajax_url_dismiss_whm; } if (GUI::has_msg_ruleconflict()) { $ajax_url = Utility::build_url(Core::ACTION_DISMISS, GUI::TYPE_DISMISS_EXPIRESDEFAULT, true); $localize_data['ajax_url_dismiss_ruleconflict'] = $ajax_url; } $promo_tag = GUI::cls()->show_promo(true); if ($promo_tag) { $ajax_url_promo = Utility::build_url(Core::ACTION_DISMISS, GUI::TYPE_DISMISS_PROMO, true, null, array('promo_tag' => $promo_tag)); $localize_data['ajax_url_promo'] = $ajax_url_promo; } // Injection to LiteSpeed pages global $pagenow; if ($pagenow == 'admin.php' && !empty($_GET['page']) && (strpos($_GET['page'], 'litespeed-') === 0 || $_GET['page'] == 'litespeed')) { // Admin footer add_filter('admin_footer_text', array($this, 'admin_footer_text'), 1); if ($_GET['page'] == 'litespeed-crawler' || $_GET['page'] == 'litespeed-cdn') { // Babel JS type correction add_filter('script_loader_tag', array($this, 'babel_type'), 10, 3); wp_enqueue_script(Core::PLUGIN_NAME . '-lib-react', LSWCP_PLUGIN_URL . 'assets/js/react.min.js', array(), Core::VER, false); wp_enqueue_script(Core::PLUGIN_NAME . '-lib-babel', LSWCP_PLUGIN_URL . 'assets/js/babel.min.js', array(), Core::VER, false); } // Crawler Cookie Simulation if ($_GET['page'] == 'litespeed-crawler') { wp_enqueue_script(Core::PLUGIN_NAME . '-crawler', LSWCP_PLUGIN_URL . 'assets/js/component.crawler.js', array(), Core::VER, false); $localize_data['lang'] = array(); $localize_data['lang']['cookie_name'] = __('Cookie Name', 'litespeed-cache'); $localize_data['lang']['cookie_value'] = __('Cookie Values', 'litespeed-cache'); $localize_data['lang']['one_per_line'] = Doc::one_per_line(true); $localize_data['lang']['remove_cookie_simulation'] = __('Remove cookie simulation', 'litespeed-cache'); $localize_data['lang']['add_cookie_simulation_row'] = __('Add new cookie to simulate', 'litespeed-cache'); empty($localize_data['ids']) && ($localize_data['ids'] = array()); $localize_data['ids']['crawler_cookies'] = self::O_CRAWLER_COOKIES; } // CDN mapping if ($_GET['page'] == 'litespeed-cdn') { $home_url = home_url('/'); $parsed = parse_url($home_url); $home_url = str_replace($parsed['scheme'] . ':', '', $home_url); $cdn_url = 'https://cdn.' . substr($home_url, 2); wp_enqueue_script(Core::PLUGIN_NAME . '-cdn', LSWCP_PLUGIN_URL . 'assets/js/component.cdn.js', array(), Core::VER, false); $localize_data['lang'] = array(); $localize_data['lang']['cdn_mapping_url'] = Lang::title(self::CDN_MAPPING_URL); $localize_data['lang']['cdn_mapping_inc_img'] = Lang::title(self::CDN_MAPPING_INC_IMG); $localize_data['lang']['cdn_mapping_inc_css'] = Lang::title(self::CDN_MAPPING_INC_CSS); $localize_data['lang']['cdn_mapping_inc_js'] = Lang::title(self::CDN_MAPPING_INC_JS); $localize_data['lang']['cdn_mapping_filetype'] = Lang::title(self::CDN_MAPPING_FILETYPE); $localize_data['lang']['cdn_mapping_url_desc'] = sprintf(__('CDN URL to be used. For example, %s', 'litespeed-cache'), '<code>' . $cdn_url . '</code>'); $localize_data['lang']['one_per_line'] = Doc::one_per_line(true); $localize_data['lang']['cdn_mapping_remove'] = __('Remove CDN URL', 'litespeed-cache'); $localize_data['lang']['add_cdn_mapping_row'] = __('Add new CDN URL', 'litespeed-cache'); $localize_data['lang']['on'] = __('ON', 'litespeed-cache'); $localize_data['lang']['off'] = __('OFF', 'litespeed-cache'); empty($localize_data['ids']) && ($localize_data['ids'] = array()); $localize_data['ids']['cdn_mapping'] = self::O_CDN_MAPPING; } // If on Server IP setting page, append getIP link if ($_GET['page'] == 'litespeed-general') { $localize_data['ajax_url_getIP'] = function_exists('get_rest_url') ? get_rest_url(null, 'litespeed/v1/tool/check_ip') : '/'; $localize_data['nonce'] = wp_create_nonce('wp_rest'); } // Activate or deactivate a specific crawler if ($_GET['page'] == 'litespeed-crawler') { $localize_data['ajax_url_crawler_switch'] = function_exists('get_rest_url') ? get_rest_url(null, 'litespeed/v1/toggle_crawler_state') : '/'; $localize_data['nonce'] = wp_create_nonce('wp_rest'); } } if ($localize_data) { wp_localize_script(Core::PLUGIN_NAME, 'litespeed_data', $localize_data); } wp_enqueue_script(Core::PLUGIN_NAME); } /** * Babel type for crawler * * @since 3.6 */ public function babel_type($tag, $handle, $src) { if ($handle != Core::PLUGIN_NAME . '-crawler' && $handle != Core::PLUGIN_NAME . '-cdn') { return $tag; } return '<script src="' . Str::trim_quotes($src) . '" type="text/babel"></script>'; } /** * Callback that adds LiteSpeed Cache's action links. * * @since 1.0.0 * @access public * @param array $links Previously added links from other plugins. * @return array Links array with the litespeed cache one appended. */ public function add_plugin_links($links) { // $links[] = '<a href="' . admin_url('options-general.php?page=litespeed-cache') . '">' . __('Settings', 'litespeed-cache') . '</a>'; $links[] = '<a href="' . admin_url('admin.php?page=litespeed-cache') . '">' . __('Settings', 'litespeed-cache') . '</a>'; return $links; } /** * Change the admin footer text on LiteSpeed Cache admin pages. * * @since 1.0.13 * @param string $footer_text * @return string */ public function admin_footer_text($footer_text) { require_once LSCWP_DIR . 'tpl/inc/admin_footer.php'; return $footer_text; } /** * Builds the html for a single notice. * * @since 1.0.7 * @access public * @param string $color The color to use for the notice. * @param string $str The notice message. * @return string The built notice html. */ public static function build_notice($color, $str, $irremovable = false, $additional_classes = '') { $cls = $color; if ($irremovable) { $cls .= ' litespeed-irremovable'; } else { $cls .= ' is-dismissible'; } if ($additional_classes) { $cls .= ' ' . $additional_classes; } // possible translation $str = Lang::maybe_translate($str); return '<div class="litespeed_icon ' . $cls . '"><p>' . wp_kses_post($str) . '</p></div>'; } /** * Display info notice * * @since 1.6.5 * @access public */ public static function info($msg, $echo = false, $irremovable = false, $additional_classes = '') { self::add_notice(self::NOTICE_BLUE, $msg, $echo, $irremovable, $additional_classes); } /** * Display note notice * * @since 1.6.5 * @access public */ public static function note($msg, $echo = false, $irremovable = false, $additional_classes = '') { self::add_notice(self::NOTICE_YELLOW, $msg, $echo, $irremovable, $additional_classes); } /** * Display success notice * * @since 1.6 * @access public */ public static function success($msg, $echo = false, $irremovable = false, $additional_classes = '') { self::add_notice(self::NOTICE_GREEN, $msg, $echo, $irremovable, $additional_classes); } /** @deprecated 4.7 */ /** will drop in v7.5 */ public static function succeed($msg, $echo = false, $irremovable = false, $additional_classes = '') { self::success($msg, $echo, $irremovable, $additional_classes); } /** * Display error notice * * @since 1.6 * @access public */ public static function error($msg, $echo = false, $irremovable = false, $additional_classes = '') { self::add_notice(self::NOTICE_RED, $msg, $echo, $irremovable, $additional_classes); } /** * Add irremovable msg * @since 4.7 */ public static function add_unique_notice($color_mode, $msgs, $irremovable = false) { if (!is_array($msgs)) { $msgs = array($msgs); } $color_map = array( 'info' => self::NOTICE_BLUE, 'note' => self::NOTICE_YELLOW, 'success' => self::NOTICE_GREEN, 'error' => self::NOTICE_RED, ); if (empty($color_map[$color_mode])) { self::debug('Wrong admin display color mode!'); return; } $color = $color_map[$color_mode]; // Go through to make sure unique $filtered_msgs = array(); foreach ($msgs as $k => $str) { if (is_numeric($k)) { $k = md5($str); } // Use key to make it overwritable to previous same msg $filtered_msgs[$k] = $str; } self::add_notice($color, $filtered_msgs, false, $irremovable); } /** * Adds a notice to display on the admin page * * @since 1.0.7 * @access public */ public static function add_notice($color, $msg, $echo = false, $irremovable = false, $additional_classes = '') { // self::debug("add_notice msg", $msg); // Bypass adding for CLI or cron if (defined('LITESPEED_CLI') || defined('DOING_CRON')) { // WP CLI will show the info directly if (defined('WP_CLI') && WP_CLI) { if (!is_array($msg)) { $msg = array($msg); } foreach ($msg as $v) { $v = strip_tags($v); if ($color == self::NOTICE_RED) { \WP_CLI::error($v, false); } else { \WP_CLI::success($v); } } } return; } if ($echo) { echo self::build_notice($color, $msg, $irremovable, $additional_classes); return; } $msg_name = $irremovable ? self::DB_MSG_PIN : self::DB_MSG; $messages = self::get_option($msg_name, array()); if (!is_array($messages)) { $messages = array(); } if (is_array($msg)) { foreach ($msg as $k => $str) { $messages[$k] = self::build_notice($color, $str, $irremovable, $additional_classes); } } else { $messages[] = self::build_notice($color, $msg, $irremovable, $additional_classes); } $messages = array_unique($messages); self::update_option($msg_name, $messages); } /** * Display notices and errors in dashboard * * @since 1.1.0 * @access public */ public function display_messages() { if (!defined('LITESPEED_CONF_LOADED')) { $this->_in_upgrading(); } if (GUI::has_whm_msg()) { $this->show_display_installed(); } Data::cls()->check_upgrading_msg(); // If is in dev version, always check latest update Cloud::cls()->check_dev_version(); // One time msg $messages = self::get_option(self::DB_MSG, array()); $added_thickbox = false; if (is_array($messages)) { foreach ($messages as $msg) { // Added for popup links if (strpos($msg, 'TB_iframe') && !$added_thickbox) { add_thickbox(); $added_thickbox = true; } echo wp_kses_post($msg); } } if ($messages != -1) { self::update_option(self::DB_MSG, -1); } // Pinned msg $messages = self::get_option(self::DB_MSG_PIN, array()); if (is_array($messages)) { foreach ($messages as $k => $msg) { // Added for popup links if (strpos($msg, 'TB_iframe') && !$added_thickbox) { add_thickbox(); $added_thickbox = true; } // Append close btn if (substr($msg, -6) == '</div>') { $link = Utility::build_url(Core::ACTION_DISMISS, GUI::TYPE_DISMISS_PIN, false, null, array('msgid' => $k)); $msg = substr($msg, 0, -6) . '<p><a href="' . $link . '" class="button litespeed-btn-primary litespeed-btn-mini">' . __('Dismiss', 'litespeed-cache') . '</a>' . '</p></div>'; } echo wp_kses_post($msg); } } // if ( $messages != -1 ) { // self::update_option( self::DB_MSG_PIN, -1 ); // } if (empty($_GET['page']) || strpos($_GET['page'], 'litespeed') !== 0) { global $pagenow; if ($pagenow != 'plugins.php') { // && $pagenow != 'index.php' return; } } // Show disable all warning if (defined('LITESPEED_DISABLE_ALL') && LITESPEED_DISABLE_ALL) { Admin_Display::error(Error::msg('disabled_all'), true); } if (!$this->conf(self::O_NEWS)) { return; } // Show promo from cloud Cloud::cls()->show_promo(); /** * Check promo msg first * @since 2.9 */ GUI::cls()->show_promo(); // Show version news Cloud::cls()->news(); } /** * Dismiss pinned msg * * @since 3.5.2 * @access public */ public static function dismiss_pin() { if (!isset($_GET['msgid'])) { return; } $messages = self::get_option(self::DB_MSG_PIN, array()); if (!is_array($messages) || empty($messages[$_GET['msgid']])) { return; } unset($messages[$_GET['msgid']]); if (!$messages) { $messages = -1; } self::update_option(self::DB_MSG_PIN, $messages); } /** * Dismiss pinned msg by msg content * * @since 7.0 * @access public */ public static function dismiss_pin_by_content($content, $color, $irremovable) { $content = self::build_notice($color, $content, $irremovable); $messages = self::get_option(self::DB_MSG_PIN, array()); $hit = false; if ($messages != -1) { foreach ($messages as $k => $v) { if ($v == $content) { unset($messages[$k]); $hit = true; self::debug('✅ pinned msg content hit. Removed'); break; } } } if ($hit) { if (!$messages) { $messages = -1; } self::update_option(self::DB_MSG_PIN, $messages); } else { self::debug('❌ No pinned msg content hit'); } } /** * Hooked to the in_widget_form action. * Appends LiteSpeed Cache settings to the widget edit settings screen. * This will append the esi on/off selector and ttl text. * * @since 1.1.0 * @access public */ public function show_widget_edit($widget, $return, $instance) { require LSCWP_DIR . 'tpl/esi_widget_edit.php'; } /** * Displays the dashboard page. * * @since 3.0 * @access public */ public function show_menu_dash() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/dash/entry.tpl.php'; } /** * Displays the General page. * * @since 5.3 * @access public */ public function show_menu_presets() { require_once LSCWP_DIR . 'tpl/presets/entry.tpl.php'; } /** * Displays the General page. * * @since 3.0 * @access public */ public function show_menu_general() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/general/entry.tpl.php'; } /** * Displays the CDN page. * * @since 3.0 * @access public */ public function show_menu_cdn() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/cdn/entry.tpl.php'; } /** * Outputs the LiteSpeed Cache settings page. * * @since 1.0.0 * @access public */ public function show_menu_cache() { if ($this->_is_network_admin) { require_once LSCWP_DIR . 'tpl/cache/entry_network.tpl.php'; } else { require_once LSCWP_DIR . 'tpl/cache/entry.tpl.php'; } } /** * Tools page * * @since 3.0 * @access public */ public function show_toolbox() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/toolbox/entry.tpl.php'; } /** * Outputs the crawler operation page. * * @since 1.1.0 * @access public */ public function show_crawler() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/crawler/entry.tpl.php'; } /** * Outputs the optimization operation page. * * @since 1.6 * @access public */ public function show_img_optm() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/img_optm/entry.tpl.php'; } /** * Page optm page. * * @since 3.0 * @access public */ public function show_page_optm() { $this->cls('Cloud')->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/page_optm/entry.tpl.php'; } /** * DB optm page. * * @since 3.0 * @access public */ public function show_db_optm() { require_once LSCWP_DIR . 'tpl/db_optm/entry.tpl.php'; } /** * Outputs a notice to the admin panel when the plugin is installed * via the WHM plugin. * * @since 1.0.12 * @access public */ public function show_display_installed() { require_once LSCWP_DIR . 'tpl/inc/show_display_installed.php'; } /** * Display error cookie msg. * * @since 1.0.12 * @access public */ public static function show_error_cookie() { require_once LSCWP_DIR . 'tpl/inc/show_error_cookie.php'; } /** * Display warning if lscache is disabled * * @since 2.1 * @access public */ public function cache_disabled_warning() { include LSCWP_DIR . 'tpl/inc/check_cache_disabled.php'; } /** * Display conf data upgrading banner * * @since 2.1 * @access private */ private function _in_upgrading() { include LSCWP_DIR . 'tpl/inc/in_upgrading.php'; } /** * Output litespeed form info * * @since 3.0 * @access public */ public function form_action($action = false, $type = false, $has_upload = false) { if (!$action) { $action = Router::ACTION_SAVE_SETTINGS; } $has_upload = $has_upload ? 'enctype="multipart/form-data"' : ''; if (!defined('LITESPEED_CONF_LOADED')) { echo '<div class="litespeed-relative"'; } else { echo '<form method="post" action="' . wp_unslash($_SERVER['REQUEST_URI']) . '" class="litespeed-relative" ' . $has_upload . '>'; } echo '<input type="hidden" name="' . Router::ACTION . '" value="' . $action . '" />'; if ($type) { echo '<input type="hidden" name="' . Router::TYPE . '" value="' . $type . '" />'; } wp_nonce_field($action, Router::NONCE); } /** * Output litespeed form info END * * @since 3.0 * @access public */ public function form_end($disable_reset = false) { echo "<div class='litespeed-top20'></div>"; if (!defined('LITESPEED_CONF_LOADED')) { submit_button(__('Save Changes', 'litespeed-cache'), 'secondary litespeed-duplicate-float', 'litespeed-submit', true, array('disabled' => 'disabled')); echo '</div>'; } else { submit_button(__('Save Changes', 'litespeed-cache'), 'primary litespeed-duplicate-float', 'litespeed-submit', true, array( 'id' => 'litespeed-submit-' . $this->_btn_i++, )); echo '</form>'; } } /** * Register this setting to save * * @since 3.0 * @access public */ public function enroll($id) { echo '<input type="hidden" name="' . Admin_Settings::ENROLL . '[]" value="' . $id . '" />'; } /** * Build a textarea * * @since 1.1.0 * @access public */ public function build_textarea($id, $cols = false, $val = null) { if ($val === null) { $val = $this->conf($id, true); if (is_array($val)) { $val = implode("\n", $val); } } if (!$cols) { $cols = 80; } $rows = 5; $lines = substr_count($val, "\n") + 2; if ($lines > $rows) { $rows = $lines; } if ($rows > 40) { $rows = 40; } $this->enroll($id); echo "<textarea name='$id' rows='$rows' cols='$cols'>" . esc_textarea($val) . '</textarea>'; $this->_check_overwritten($id); } /** * Build a text input field * * @since 1.1.0 * @access public */ public function build_input($id, $cls = null, $val = null, $type = 'text', $disabled = false) { if ($val === null) { $val = $this->conf($id, true); // Mask pswds if ($this->_conf_pswd($id) && $val) { $val = str_repeat('*', strlen($val)); } } $label_id = preg_replace('/\W/', '', $id); if ($type == 'text') { $cls = "regular-text $cls"; } if ($disabled) { echo "<input type='$type' class='$cls' value='" . esc_textarea($val) . "' id='input_$label_id' disabled /> "; } else { $this->enroll($id); echo "<input type='$type' class='$cls' name='$id' value='" . esc_textarea($val) . "' id='input_$label_id' /> "; } $this->_check_overwritten($id); } /** * Build a checkbox html snippet * * @since 1.1.0 * @access public * @param string $id * @param string $title * @param bool $checked */ public function build_checkbox($id, $title, $checked = null, $value = 1) { if ($checked === null && $this->conf($id, true)) { $checked = true; } $checked = $checked ? ' checked ' : ''; $label_id = preg_replace('/\W/', '', $id); if ($value !== 1) { $label_id .= '_' . $value; } $this->enroll($id); echo "<div class='litespeed-tick'> <input type='checkbox' name='$id' id='input_checkbox_$label_id' value='$value' $checked /> <label for='input_checkbox_$label_id'>$title</label> </div>"; $this->_check_overwritten($id); } /** * Build a toggle checkbox html snippet * * @since 1.7 */ public function build_toggle($id, $checked = null, $title_on = null, $title_off = null) { if ($checked === null && $this->conf($id, true)) { $checked = true; } if ($title_on === null) { $title_on = __('ON', 'litespeed-cache'); $title_off = __('OFF', 'litespeed-cache'); } $cls = $checked ? 'primary' : 'default litespeed-toggleoff'; echo "<div class='litespeed-toggle litespeed-toggle-btn litespeed-toggle-btn-$cls' data-litespeed-toggle-on='primary' data-litespeed-toggle-off='default' data-litespeed_toggle_id='$id' > <input name='$id' type='hidden' value='$checked' /> <div class='litespeed-toggle-group'> <label class='litespeed-toggle-btn litespeed-toggle-btn-primary litespeed-toggle-on'>$title_on</label> <label class='litespeed-toggle-btn litespeed-toggle-btn-default litespeed-toggle-active litespeed-toggle-off'>$title_off</label> <span class='litespeed-toggle-handle litespeed-toggle-btn litespeed-toggle-btn-default'></span> </div> </div>"; } /** * Build a switch div html snippet * * @since 1.1.0 * @since 1.7 removed param $disable * @access public */ public function build_switch($id, $title_list = false) { $this->enroll($id); echo '<div class="litespeed-switch">'; if (!$title_list) { $title_list = array(__('OFF', 'litespeed-cache'), __('ON', 'litespeed-cache')); } foreach ($title_list as $k => $v) { $this->_build_radio($id, $k, $v); } echo '</div>'; $this->_check_overwritten($id); } /** * Build a radio input html codes and output * * @since 1.1.0 * @access private */ private function _build_radio($id, $val, $txt) { $id_attr = 'input_radio_' . preg_replace('/\W/', '', $id) . '_' . $val; $default = isset(self::$_default_options[$id]) ? self::$_default_options[$id] : self::$_default_site_options[$id]; if (!is_string($default)) { $checked = (int) $this->conf($id, true) === (int) $val ? ' checked ' : ''; } else { $checked = $this->conf($id, true) === $val ? ' checked ' : ''; } echo "<input type='radio' autocomplete='off' name='$id' id='$id_attr' value='$val' $checked /> <label for='$id_attr'>$txt</label>"; } /** * Show overwritten msg if there is a const defined * * @since 3.0 */ protected function _check_overwritten($id) { $const_val = $this->const_overwritten($id); $primary_val = $this->primary_overwritten($id); if ($const_val === null && $primary_val === null) { return; } $val = $const_val !== null ? $const_val : $primary_val; $default = isset(self::$_default_options[$id]) ? self::$_default_options[$id] : self::$_default_site_options[$id]; if (is_bool($default)) { $val = $val ? __('ON', 'litespeed-cache') : __('OFF', 'litespeed-cache'); } else { if (is_array($default)) { $val = implode("\n", $val); } $val = esc_textarea($val); } echo '<div class="litespeed-desc litespeed-warning">⚠️ '; if ($const_val !== null) { echo sprintf(__('This setting is overwritten by the PHP constant %s', 'litespeed-cache'), '<code>' . Base::conf_const($id) . '</code>'); } else { if (get_current_blog_id() != BLOG_ID_CURRENT_SITE && $this->conf(self::NETWORK_O_USE_PRIMARY)) { echo __('This setting is overwritten by the primary site setting', 'litespeed-cache'); } else { echo __('This setting is overwritten by the Network setting', 'litespeed-cache'); } } echo ', ' . sprintf(__('currently set to %s', 'litespeed-cache'), "<code>$val</code>") . '</div>'; } /** * Display seconds text and readable layout * * @since 3.0 * @access public */ public function readable_seconds() { echo __('seconds', 'litespeed-cache'); echo ' <span data-litespeed-readable=""></span>'; } /** * Display default value * * @since 1.1.1 * @access public */ public function recommended($id) { if (!$this->default_settings) { $this->default_settings = $this->load_default_vals(); } $val = $this->default_settings[$id]; if ($val) { if (is_array($val)) { $rows = 5; $cols = 30; // Flexible rows/cols $lines = count($val) + 1; $rows = min(max($lines, $rows), 40); foreach ($val as $v) { $cols = max(strlen($v), $cols); } $cols = min($cols, 150); $val = implode("\n", $val); $val = esc_textarea($val); $val = '<div class="litespeed-desc">' . __('Default value', 'litespeed-cache') . ':</div>' . "<textarea readonly rows='$rows' cols='$cols'>$val</textarea>"; } else { $val = esc_textarea($val); $val = "<code>$val</code>"; $val = __('Default value', 'litespeed-cache') . ': ' . $val; } echo $val; } } /** * Validate rewrite rules regex syntax * * @since 3.0 */ protected function _validate_syntax($id) { $val = $this->conf($id, true); if (!$val) { return; } if (!is_array($val)) { $val = array($val); } foreach ($val as $v) { if (!Utility::syntax_checker($v)) { echo '<br /><font class="litespeed-warning"> ❌ ' . __('Invalid rewrite rule', 'litespeed-cache') . ': <code>' . $v . '</code></font>'; } } } /** * Validate if the htaccess path is valid * * @since 3.0 */ protected function _validate_htaccess_path($id) { $val = $this->conf($id, true); if (!$val) { return; } if (substr($val, -10) !== '/.htaccess') { echo '<br /><font class="litespeed-warning"> ❌ ' . sprintf(__('Path must end with %s', 'litespeed-cache'), '<code>/.htaccess</code>') . '</font>'; } } /** * Check ttl instead of error when saving * * @since 3.0 */ protected function _validate_ttl($id, $min = false, $max = false, $allow_zero = false) { $val = $this->conf($id, true); if ($allow_zero && !$val) { // return; } $tip = array(); if ($min && $val < $min && (!$allow_zero || $val != 0)) { $tip[] = __('Minimum value', 'litespeed-cache') . ': <code>' . $min . '</code>.'; } if ($max && $val > $max) { $tip[] = __('Maximum value', 'litespeed-cache') . ': <code>' . $max . '</code>.'; } echo '<br />'; if ($tip) { echo '<font class="litespeed-warning"> ❌ ' . implode(' ', $tip) . '</font>'; } $range = ''; if ($allow_zero) { $range .= __('Zero, or', 'litespeed-cache') . ' '; } if ($min && $max) { $range .= $min . ' - ' . $max; } elseif ($min) { $range .= __('Larger than', 'litespeed-cache') . ' ' . $min; } elseif ($max) { $range .= __('Smaller than', 'litespeed-cache') . ' ' . $max; } echo __('Value range', 'litespeed-cache') . ': <code>' . $range . '</code>'; } /** * Check if ip is valid * * @since 3.0 */ protected function _validate_ip($id) { $val = $this->conf($id, true); if (!$val) { return; } if (!is_array($val)) { $val = array($val); } $tip = array(); foreach ($val as $v) { if (!$v) { continue; } if (!\WP_Http::is_ip_address($v)) { $tip[] = __('Invalid IP', 'litespeed-cache') . ': <code>' . esc_textarea($v) . '</code>.'; } } if ($tip) { echo '<br /><font class="litespeed-warning"> ❌ ' . implode(' ', $tip) . '</font>'; } } /** * Display API environment variable support * * @since 1.8.3 * @access protected */ protected function _api_env_var() { $args = func_get_args(); $s = '<code>' . implode('</code>, <code>', $args) . '</code>'; echo '<font class="litespeed-success"> ' . __('API', 'litespeed-cache') . ': ' . sprintf(__('Server variable(s) %s available to override this setting.', 'litespeed-cache'), $s); Doc::learn_more('https://docs.litespeedtech.com/lscache/lscwp/admin/#limiting-the-crawler'); } /** * Display URI setting example * * @since 2.6.1 * @access protected */ protected function _uri_usage_example() { echo __('The URLs will be compared to the REQUEST_URI server variable.', 'litespeed-cache'); echo ' ' . sprintf(__('For example, for %s, %s can be used here.', 'litespeed-cache'), '<code>/mypath/mypage?aa=bb</code>', '<code>mypage?aa=</code>'); echo '<br /><i>'; echo sprintf(__('To match the beginning, add %s to the beginning of the item.', 'litespeed-cache'), '<code>^</code>'); echo ' ' . sprintf(__('To do an exact match, add %s to the end of the URL.', 'litespeed-cache'), '<code>$</code>'); echo ' ' . __('One per line.', 'litespeed-cache'); echo '</i>'; } /** * Return groups string * * @since 2.0 * @access public */ public static function print_plural($num, $kind = 'group') { if ($num > 1) { switch ($kind) { case 'group': return sprintf(__('%s groups', 'litespeed-cache'), $num); case 'image': return sprintf(__('%s images', 'litespeed-cache'), $num); default: return $num; } } switch ($kind) { case 'group': return sprintf(__('%s group', 'litespeed-cache'), $num); case 'image': return sprintf(__('%s image', 'litespeed-cache'), $num); default: return $num; } } /** * Return guidance html * * @since 2.0 * @access public */ public static function guidance($title, $steps, $current_step) { if ($current_step === 'done') { $current_step = count($steps) + 1; } $percentage = ' (' . floor((($current_step - 1) * 100) / count($steps)) . '%)'; $html = '<div class="litespeed-guide">' . '<h2>' . $title . $percentage . '</h2>' . '<ol>'; foreach ($steps as $k => $v) { $step = $k + 1; if ($current_step > $step) { $html .= '<li class="litespeed-guide-done">'; } else { $html .= '<li>'; } $html .= $v . '</li>'; } $html .= '</ol></div>'; return $html; } /** * Check if has qc hide banner cookie or not * @since 7.1 */ public static function has_qc_hide_banner() { return isset($_COOKIE[self::COOKIE_QC_HIDE_BANNER]); } /** * Set qc hide banner cookie * @since 7.1 */ public static function set_qc_hide_banner() { $expire = time() + 86400 * 365; self::debug('Set qc hide banner cookie'); setcookie(self::COOKIE_QC_HIDE_BANNER, time(), $expire, COOKIEPATH, COOKIE_DOMAIN); } /** * Handle all request actions from main cls * * @since 7.1 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_QC_HIDE_BANNER: self::set_qc_hide_banner(); break; default: break; } Admin::redirect(); } } src/optimizer.cls.php 0000644 00000022625 15162130513 0010652 0 ustar 00 <?php /** * The optimize4 class. * * @since 1.9 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Optimizer extends Root { private $_conf_css_font_display; /** * Init optimizer * * @since 1.9 */ public function __construct() { $this->_conf_css_font_display = $this->conf(Base::O_OPTM_CSS_FONT_DISPLAY); } /** * Run HTML minify process and return final content * * @since 1.9 * @access public */ public function html_min($content, $force_inline_minify = false) { if (!apply_filters('litespeed_html_min', true)) { Debug2::debug2('[Optmer] html_min bypassed via litespeed_html_min filter'); return $content; } $options = array(); if ($force_inline_minify) { $options['jsMinifier'] = __CLASS__ . '::minify_js'; } $skip_comments = $this->conf(Base::O_OPTM_HTML_SKIP_COMMENTS); if ($skip_comments) { $options['skipComments'] = $skip_comments; } /** * Added exception capture when minify * @since 2.2.3 */ try { $obj = new Lib\HTML_MIN($content, $options); $content_final = $obj->process(); // check if content from minification is empty if ($content_final == '') { Debug2::debug('Failed to minify HTML: HTML minification resulted in empty HTML'); return $content; } if (!defined('LSCACHE_ESI_SILENCE')) { $content_final .= "\n" . '<!-- Page optimized by LiteSpeed Cache @' . date('Y-m-d H:i:s', time() + LITESPEED_TIME_OFFSET) . ' -->'; } return $content_final; } catch (\Exception $e) { Debug2::debug('******[Optmer] html_min failed: ' . $e->getMessage()); error_log('****** LiteSpeed Optimizer html_min failed: ' . $e->getMessage()); return $content; } } /** * Run minify process and save content * * @since 1.9 * @access public */ public function serve($request_url, $file_type, $minify, $src_list) { // Try Unique CSS if ($file_type == 'css') { $content = false; if (defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_OPTM_UCSS)) { $filename = $this->cls('UCSS')->load($request_url); if ($filename) { return array($filename, 'ucss'); } } } // Before generated, don't know the contented hash filename yet, so used url hash as tmp filename $file_path_prefix = $this->_build_filepath_prefix($file_type); $url_tag = $request_url; $url_tag_for_file = md5($request_url); if (is_404()) { $url_tag_for_file = $url_tag = '404'; } elseif ($file_type == 'css' && apply_filters('litespeed_ucss_per_pagetype', false)) { $url_tag_for_file = $url_tag = Utility::page_type(); } $static_file = LITESPEED_STATIC_DIR . $file_path_prefix . $url_tag_for_file . '.' . $file_type; // Create tmp file to avoid conflict $tmp_static_file = $static_file . '.tmp'; if (file_exists($tmp_static_file) && time() - filemtime($tmp_static_file) <= 600) { // some other request is generating return false; } // File::save( $tmp_static_file, '/* ' . ( is_404() ? '404' : $request_url ) . ' */', true ); // Can't use this bcos this will get filecon md5 changed File::save($tmp_static_file, '', true); // Load content $real_files = array(); foreach ($src_list as $src_info) { $is_min = false; if (!empty($src_info['inl'])) { // Load inline $content = $src_info['src']; } else { // Load file $content = $this->load_file($src_info['src'], $file_type); if (!$content) { continue; } $is_min = $this->is_min($src_info['src']); } $content = $this->optm_snippet($content, $file_type, $minify && !$is_min, $src_info['src'], !empty($src_info['media']) ? $src_info['media'] : false); // Write to file File::save($tmp_static_file, $content, true, true); } // if CSS - run the minification on the saved file. // Will move imports to the top of file and remove extra spaces. if ($file_type == 'css') { $obj = new Lib\CSS_JS_MIN\Minify\CSS(); $file_content_combined = $obj->moveImportsToTop(File::read($tmp_static_file)); File::save($tmp_static_file, $file_content_combined); } // validate md5 $filecon_md5 = md5_file($tmp_static_file); $final_file_path = $file_path_prefix . $filecon_md5 . '.' . $file_type; $realfile = LITESPEED_STATIC_DIR . $final_file_path; if (!file_exists($realfile)) { rename($tmp_static_file, $realfile); Debug2::debug2('[Optmer] Saved static file [path] ' . $realfile); } else { unlink($tmp_static_file); } $vary = $this->cls('Vary')->finalize_full_varies(); Debug2::debug2("[Optmer] Save URL to file for [file_type] $file_type [file] $filecon_md5 [vary] $vary "); $this->cls('Data')->save_url($url_tag, $vary, $file_type, $filecon_md5, dirname($realfile)); return array($filecon_md5 . '.' . $file_type, $file_type); } /** * Load a single file * @since 4.0 */ public function optm_snippet($content, $file_type, $minify, $src, $media = false) { // CSS related features if ($file_type == 'css') { // Font optimize if ($this->_conf_css_font_display) { $content = preg_replace('#(@font\-face\s*\{)#isU', '${1}font-display:swap;', $content); } $content = preg_replace('/@charset[^;]+;\\s*/', '', $content); if ($media) { $content = '@media ' . $media . '{' . $content . "\n}"; } if ($minify) { $content = self::minify_css($content); } $content = $this->cls('CDN')->finalize($content); if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP)) && $this->cls('Media')->webp_support()) { $content = $this->cls('Media')->replace_background_webp($content); } } else { if ($minify) { $content = self::minify_js($content); } else { $content = $this->_null_minifier($content); } $content .= "\n;"; } // Add filter $content = apply_filters('litespeed_optm_cssjs', $content, $file_type, $src); return $content; } /** * Load remote resource from cache if existed * * @since 4.7 */ private function load_cached_file($url, $file_type) { $file_path_prefix = $this->_build_filepath_prefix($file_type); $folder_name = LITESPEED_STATIC_DIR . $file_path_prefix; $to_be_deleted_folder = $folder_name . date('Ymd', strtotime('-2 days')); if (file_exists($to_be_deleted_folder)) { Debug2::debug('[Optimizer] ❌ Clearing folder [name] ' . $to_be_deleted_folder); File::rrmdir($to_be_deleted_folder); } $today_file = $folder_name . date('Ymd') . '/' . md5($url); if (file_exists($today_file)) { return File::read($today_file); } // Write file $res = wp_safe_remote_get($url); $res_code = wp_remote_retrieve_response_code($res); if (is_wp_error($res) || $res_code != 200) { Debug2::debug2('[Optimizer] ❌ Load Remote error [code] ' . $res_code); return false; } $con = wp_remote_retrieve_body($res); if (!$con) { return false; } Debug2::debug('[Optimizer] ✅ Save remote file to cache [name] ' . $today_file); File::save($today_file, $con, true); return $con; } /** * Load remote/local resource * * @since 3.5 */ public function load_file($src, $file_type = 'css') { $real_file = Utility::is_internal_file($src); $postfix = pathinfo(parse_url($src, PHP_URL_PATH), PATHINFO_EXTENSION); if (!$real_file || $postfix != $file_type) { Debug2::debug2('[CSS] Load Remote [' . $file_type . '] ' . $src); $this_url = substr($src, 0, 2) == '//' ? set_url_scheme($src) : $src; $con = $this->load_cached_file($this_url, $file_type); if ($file_type == 'css') { $dirname = dirname($this_url) . '/'; $con = Lib\UriRewriter::prepend($con, $dirname); } } else { Debug2::debug2('[CSS] Load local [' . $file_type . '] ' . $real_file[0]); $con = File::read($real_file[0]); if ($file_type == 'css') { $dirname = dirname($real_file[0]); $con = Lib\UriRewriter::rewrite($con, $dirname); } } return $con; } /** * Minify CSS * * @since 2.2.3 * @access private */ public static function minify_css($data) { try { $obj = new Lib\CSS_JS_MIN\Minify\CSS(); $obj->add($data); return $obj->minify(); } catch (\Exception $e) { Debug2::debug('******[Optmer] minify_css failed: ' . $e->getMessage()); error_log('****** LiteSpeed Optimizer minify_css failed: ' . $e->getMessage()); return $data; } } /** * Minify JS * * Added exception capture when minify * * @since 2.2.3 * @access private */ public static function minify_js($data, $js_type = '') { // For inline JS optimize, need to check if it's js type if ($js_type) { preg_match('#type=([\'"])(.+)\g{1}#isU', $js_type, $matches); if ($matches && $matches[2] != 'text/javascript') { Debug2::debug('******[Optmer] minify_js bypass due to type: ' . $matches[2]); return $data; } } try { $obj = new Lib\CSS_JS_MIN\Minify\JS(); $obj->add($data); return $obj->minify(); } catch (\Exception $e) { Debug2::debug('******[Optmer] minify_js failed: ' . $e->getMessage()); // error_log( '****** LiteSpeed Optimizer minify_js failed: ' . $e->getMessage() ); return $data; } } /** * Basic minifier * * @access private */ private function _null_minifier($content) { $content = str_replace("\r\n", "\n", $content); return trim($content); } /** * Check if the file is already min file * * @since 1.9 */ public function is_min($filename) { $basename = basename($filename); if (preg_match('/[-\.]min\.(?:[a-zA-Z]+)$/i', $basename)) { return true; } return false; } } src/report.cls.php 0000644 00000014221 15162130517 0010140 0 ustar 00 <?php /** * The report class * * * @since 1.1.0 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Report extends Base { const TYPE_SEND_REPORT = 'send_report'; /** * Handle all request actions from main cls * * @since 1.6.5 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_SEND_REPORT: $this->post_env(); break; default: break; } Admin::redirect(); } /** * post env report number to ls center server * * @since 1.6.5 * @access public */ public function post_env() { $report_con = $this->generate_environment_report(); // Generate link $link = !empty($_POST['link']) ? esc_url($_POST['link']) : ''; $notes = !empty($_POST['notes']) ? esc_html($_POST['notes']) : ''; $php_info = !empty($_POST['attach_php']) ? esc_html($_POST['attach_php']) : ''; $report_php = $php_info === '1' ? $this->generate_php_report() : ''; if ($report_php) { $report_con .= "\nPHPINFO\n" . $report_php; } $data = array( 'env' => $report_con, 'link' => $link, 'notes' => $notes, ); $json = Cloud::post(Cloud::API_REPORT, $data); if (!is_array($json)) { return; } $num = !empty($json['num']) ? $json['num'] : '--'; $summary = array( 'num' => $num, 'dateline' => time(), ); self::save_summary($summary); return $num; } /** * Gathers the PHP information. * * @since 7.0 * @access public */ public function generate_php_report($flags = INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES) { // INFO_ENVIRONMENT $report = ''; ob_start(); phpinfo($flags); $report = ob_get_contents(); ob_end_clean(); preg_match('%<style type="text/css">(.*?)</style>.*?<body>(.*?)</body>%s', $report, $report); return $report[2]; } /** * Gathers the environment details and creates the report. * Will write to the environment report file. * * @since 1.0.12 * @access public */ public function generate_environment_report($options = null) { global $wp_version, $_SERVER; $frontend_htaccess = Htaccess::get_frontend_htaccess(); $backend_htaccess = Htaccess::get_backend_htaccess(); $paths = array($frontend_htaccess); if ($frontend_htaccess != $backend_htaccess) { $paths[] = $backend_htaccess; } if (is_multisite()) { $active_plugins = get_site_option('active_sitewide_plugins'); if (!empty($active_plugins)) { $active_plugins = array_keys($active_plugins); } } else { $active_plugins = get_option('active_plugins'); } if (function_exists('wp_get_theme')) { $theme_obj = wp_get_theme(); $active_theme = $theme_obj->get('Name'); } else { $active_theme = get_current_theme(); } $extras = array( 'wordpress version' => $wp_version, 'siteurl' => get_option('siteurl'), 'home' => get_option('home'), 'home_url' => home_url(), 'locale' => get_locale(), 'active theme' => $active_theme, ); $extras['active plugins'] = $active_plugins; $extras['cloud'] = Cloud::get_summary(); foreach (array('mini_html', 'pk_b64', 'sk_b64', 'cdn_dash', 'ips') as $v) { if (!empty($extras['cloud'][$v])) { unset($extras['cloud'][$v]); } } if (is_null($options)) { $options = $this->get_options(true); if (is_multisite()) { $options2 = $this->get_options(); foreach ($options2 as $k => $v) { if (isset($options[$k]) && $options[$k] !== $v) { $options['[Overwritten] ' . $k] = $v; } } } } if (!is_null($options) && is_multisite()) { $blogs = Activation::get_network_ids(); if (!empty($blogs)) { $i = 0; foreach ($blogs as $blog_id) { if (++$i > 3) { // Only log 3 subsites break; } $opts = $this->cls('Conf')->load_options($blog_id, true); if (isset($opts[self::O_CACHE])) { $options['blog ' . $blog_id . ' radio select'] = $opts[self::O_CACHE]; } } } } // Security: Remove cf key in report $secure_fields = array(self::O_CDN_CLOUDFLARE_KEY, self::O_OBJECT_PSWD); foreach ($secure_fields as $v) { if (!empty($options[$v])) { $options[$v] = str_repeat('*', strlen($options[$v])); } } $report = $this->build_environment_report($_SERVER, $options, $extras, $paths); return $report; } /** * Builds the environment report buffer with the given parameters * * @access private */ private function build_environment_report($server, $options, $extras = array(), $htaccess_paths = array()) { $server_keys = array( 'DOCUMENT_ROOT' => '', 'SERVER_SOFTWARE' => '', 'X-LSCACHE' => '', 'HTTP_X_LSCACHE' => '', ); $server_vars = array_intersect_key($server, $server_keys); $server_vars[] = 'LSWCP_TAG_PREFIX = ' . LSWCP_TAG_PREFIX; $server_vars = array_merge($server_vars, $this->cls('Base')->server_vars()); $buf = $this->_format_report_section('Server Variables', $server_vars); $buf .= $this->_format_report_section('WordPress Specific Extras', $extras); $buf .= $this->_format_report_section('LSCache Plugin Options', $options); if (empty($htaccess_paths)) { return $buf; } foreach ($htaccess_paths as $path) { if (!file_exists($path) || !is_readable($path)) { $buf .= $path . " does not exist or is not readable.\n"; continue; } $content = file_get_contents($path); if ($content === false) { $buf .= $path . " returned false for file_get_contents.\n"; continue; } $buf .= $path . " contents:\n" . $content . "\n\n"; } return $buf; } /** * Creates a part of the environment report based on a section header and an array for the section parameters. * * @since 1.0.12 * @access private */ private function _format_report_section($section_header, $section) { $tab = ' '; // four spaces if (empty($section)) { return 'No matching ' . $section_header . "\n\n"; } $buf = $section_header; foreach ($section as $k => $v) { $buf .= "\n" . $tab; if (!is_numeric($k)) { $buf .= $k . ' = '; } if (!is_string($v)) { $v = var_export($v, true); } else { $v = esc_html($v); } $buf .= $v; } return $buf . "\n\n"; } } src/conf.cls.php 0000644 00000042613 15162130522 0007554 0 ustar 00 <?php /** * The core plugin config class. * * This maintains all the options and settings for this plugin. * * @since 1.0.0 * @since 1.5 Moved into /inc * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Conf extends Base { const TYPE_SET = 'set'; private $_updated_ids = array(); private $_is_primary = false; /** * Specify init logic to avoid infinite loop when calling conf.cls instance * * @since 3.0 * @access public */ public function init() { // Check if conf exists or not. If not, create them in DB (won't change version if is converting v2.9- data) // Conf may be stale, upgrade later $this->_conf_db_init(); /** * Detect if has quic.cloud set * @since 2.9.7 */ if ($this->conf(self::O_CDN_QUIC)) { !defined('LITESPEED_ALLOWED') && define('LITESPEED_ALLOWED', true); } add_action('litespeed_conf_append', array($this, 'option_append'), 10, 2); add_action('litespeed_conf_force', array($this, 'force_option'), 10, 2); $this->define_cache(); } /** * Init conf related data * * @since 3.0 * @access private */ private function _conf_db_init() { /** * Try to load options first, network sites can override this later * * NOTE: Load before run `conf_upgrade()` to avoid infinite loop when getting conf in `conf_upgrade()` */ $this->load_options(); // Check if debug is on // Init debug as early as possible if ($this->conf(Base::O_DEBUG)) { $this->cls('Debug2')->init(); } $ver = $this->conf(self::_VER); /** * Version is less than v3.0, or, is a new installation */ $ver_check_tag = ''; if (!$ver) { // Try upgrade first (network will upgrade inside too) $ver_check_tag = Data::cls()->try_upgrade_conf_3_0(); } else { defined('LSCWP_CUR_V') || define('LSCWP_CUR_V', $ver); /** * Upgrade conf */ if ($ver != Core::VER) { // Plugin version will be set inside // Site plugin upgrade & version change will do in load_site_conf $ver_check_tag = Data::cls()->conf_upgrade($ver); } } /** * Sync latest new options */ if (!$ver || $ver != Core::VER) { // Load default values $this->load_default_vals(); if (!$ver) { // New install $this->set_conf(self::$_default_options); $ver_check_tag .= ' activate' . (defined('LSCWP_REF') ? '_' . LSCWP_REF : ''); } // Init new default/missing options foreach (self::$_default_options as $k => $v) { // If the option existed, bypass updating // Bcos we may ask clients to deactivate for debug temporarily, we need to keep the current cfg in deactivation, hence we need to only try adding default cfg when activating. self::add_option($k, $v); } // Force correct version in case a rare unexpected case that `_ver` exists but empty self::update_option(Base::_VER, Core::VER); if ($ver_check_tag) { Cloud::version_check($ver_check_tag); } } /** * Network sites only * * Override conf if is network subsites and chose `Use Primary Config` */ $this->_try_load_site_options(); // Mark as conf loaded defined('LITESPEED_CONF_LOADED') || define('LITESPEED_CONF_LOADED', true); if (!$ver || $ver != Core::VER) { // Only trigger once in upgrade progress, don't run always $this->update_confs(); // Files only get corrected in activation or saving settings actions. } } /** * Load all latest options from DB * * @since 3.0 * @access public */ public function load_options($blog_id = null, $dry_run = false) { $options = array(); foreach (self::$_default_options as $k => $v) { if (!is_null($blog_id)) { $options[$k] = self::get_blog_option($blog_id, $k, $v); } else { $options[$k] = self::get_option($k, $v); } // Correct value type $options[$k] = $this->type_casting($options[$k], $k); } if ($dry_run) { return $options; } // Bypass site special settings if ($blog_id !== null) { // This is to load the primary settings ONLY // These options are the ones that can be overwritten by primary $options = array_diff_key($options, array_flip(self::$SINGLE_SITE_OPTIONS)); $this->set_primary_conf($options); } else { $this->set_conf($options); } // Append const options if (defined('LITESPEED_CONF') && LITESPEED_CONF) { foreach (self::$_default_options as $k => $v) { $const = Base::conf_const($k); if (defined($const)) { $this->set_const_conf($k, $this->type_casting(constant($const), $k)); } } } } /** * For multisite installations, the single site options need to be updated with the network wide options. * * @since 1.0.13 * @access private */ private function _try_load_site_options() { if (!$this->_if_need_site_options()) { return; } $this->_conf_site_db_init(); $this->_is_primary = get_current_blog_id() == BLOG_ID_CURRENT_SITE; // If network set to use primary setting if ($this->network_conf(self::NETWORK_O_USE_PRIMARY) && !$this->_is_primary) { // subsites or network admin // Get the primary site settings // If it's just upgraded, 2nd blog is being visited before primary blog, can just load default config (won't hurt as this could only happen shortly) $this->load_options(BLOG_ID_CURRENT_SITE); } // Overwrite single blog options with site options foreach (self::$_default_options as $k => $v) { if (!$this->has_network_conf($k)) { continue; } // $this->_options[ $k ] = $this->_network_options[ $k ]; // Special handler to `Enable Cache` option if the value is set to OFF if ($k == self::O_CACHE) { if ($this->_is_primary) { if ($this->conf($k) != $this->network_conf($k)) { if ($this->conf($k) != self::VAL_ON2) { continue; } } } else { if ($this->network_conf(self::NETWORK_O_USE_PRIMARY)) { if ($this->has_primary_conf($k) && $this->primary_conf($k) != self::VAL_ON2) { // This case will use primary_options override always continue; } } else { if ($this->conf($k) != self::VAL_ON2) { continue; } } } } // primary_options will store primary settings + network settings, OR, store the network settings for subsites $this->set_primary_conf($k, $this->network_conf($k)); } // var_dump($this->_options); } /** * Check if needs to load site_options for network sites * * @since 3.0 * @access private */ private function _if_need_site_options() { if (!is_multisite()) { return false; } // Check if needs to use site_options or not // todo: check if site settings are separate bcos it will affect .htaccess /** * In case this is called outside the admin page * @see https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network * @since 2.0 */ if (!function_exists('is_plugin_active_for_network')) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } // If is not activated on network, it will not have site options if (!is_plugin_active_for_network(Core::PLUGIN_FILE)) { if ((int) $this->conf(self::O_CACHE) == self::VAL_ON2) { // Default to cache on $this->set_conf(self::_CACHE, true); } return false; } return true; } /** * Init site conf and upgrade if necessary * * @since 3.0 * @access private */ private function _conf_site_db_init() { $this->load_site_options(); $ver = $this->network_conf(self::_VER); /** * Don't upgrade or run new installations other than from backend visit * In this case, just use default conf */ if (!$ver || $ver != Core::VER) { if (!is_admin() && !defined('LITESPEED_CLI')) { $this->set_network_conf($this->load_default_site_vals()); return; } } /** * Upgrade conf */ if ($ver && $ver != Core::VER) { // Site plugin version will change inside Data::cls()->conf_site_upgrade($ver); } /** * Is a new installation */ if (!$ver || $ver != Core::VER) { // Load default values $this->load_default_site_vals(); // Init new default/missing options foreach (self::$_default_site_options as $k => $v) { // If the option existed, bypass updating self::add_site_option($k, $v); } } } /** * Get the plugin's site wide options. * * If the site wide options are not set yet, set it to default. * * @since 1.0.2 * @access public */ public function load_site_options() { if (!is_multisite()) { return null; } // Load all site options foreach (self::$_default_site_options as $k => $v) { $val = self::get_site_option($k, $v); $val = $this->type_casting($val, $k, true); $this->set_network_conf($k, $val); } } /** * Append a 3rd party option to default options * * This will not be affected by network use primary site setting. * * NOTE: If it is a multi switch option, need to call `_conf_multi_switch()` first * * @since 3.0 * @access public */ public function option_append($name, $default) { self::$_default_options[$name] = $default; $this->set_conf($name, self::get_option($name, $default)); $this->set_conf($name, $this->type_casting($this->conf($name), $name)); } /** * Force an option to a certain value * * @since 2.6 * @access public */ public function force_option($k, $v) { if (!$this->has_conf($k)) { return; } $v = $this->type_casting($v, $k); if ($this->conf($k) === $v) { return; } Debug2::debug("[Conf] ** $k forced from " . var_export($this->conf($k), true) . ' to ' . var_export($v, true)); $this->set_conf($k, $v); } /** * Define `_CACHE` const in options ( for both single and network ) * * @since 3.0 * @access public */ public function define_cache() { // Init global const cache on setting $this->set_conf(self::_CACHE, false); if ((int) $this->conf(self::O_CACHE) == self::VAL_ON || $this->conf(self::O_CDN_QUIC)) { $this->set_conf(self::_CACHE, true); } // Check network if (!$this->_if_need_site_options()) { // Set cache on $this->_define_cache_on(); return; } // If use network setting if ((int) $this->conf(self::O_CACHE) == self::VAL_ON2 && $this->network_conf(self::O_CACHE)) { $this->set_conf(self::_CACHE, true); } $this->_define_cache_on(); } /** * Define `LITESPEED_ON` * * @since 2.1 * @access private */ private function _define_cache_on() { if (!$this->conf(self::_CACHE)) { return; } defined('LITESPEED_ALLOWED') && !defined('LITESPEED_ON') && define('LITESPEED_ON', true); } /** * Get an option value * * @since 3.0 * @access public * @deprecated 4.0 Use $this->conf() instead */ public static function val($id, $ori = false) { error_log('Called deprecated function \LiteSpeed\Conf::val(). Please use API call instead.'); return self::cls()->conf($id, $ori); } /** * Save option * * @since 3.0 * @access public */ public function update_confs($the_matrix = false) { if ($the_matrix) { foreach ($the_matrix as $id => $val) { $this->update($id, $val); } } if ($this->_updated_ids) { foreach ($this->_updated_ids as $id) { // Check if need to do a purge all or not if ($this->_conf_purge_all($id)) { Purge::purge_all('conf changed [id] ' . $id); } // Check if need to purge a tag if ($tag = $this->_conf_purge_tag($id)) { Purge::add($tag); } // Update cron if ($this->_conf_cron($id)) { $this->cls('Task')->try_clean($id); } // Reset crawler bypassed list when any of the options WebP replace, guest mode, or cache mobile got changed if ($id == self::O_IMG_OPTM_WEBP || $id == self::O_GUEST || $id == self::O_CACHE_MOBILE) { $this->cls('Crawler')->clear_disabled_list(); } } } do_action('litespeed_update_confs', $the_matrix); // Update related tables $this->cls('Data')->correct_tb_existence(); // Update related files $this->cls('Activation')->update_files(); /** * CDN related actions - Cloudflare */ $this->cls('CDN\Cloudflare')->try_refresh_zone(); /** * CDN related actions - QUIC.cloud * @since 2.3 */ $this->cls('CDN\Quic')->try_sync_conf(); } /** * Save option * * Note: this is direct save, won't trigger corresponding file update or data sync. To save settings normally, always use `Conf->update_confs()` * * @since 3.0 * @access public */ public function update($id, $val) { // Bypassed this bcos $this->_options could be changed by force_option() // if ( $this->_options[ $id ] === $val ) { // return; // } if ($id == self::_VER) { return; } if ($id == self::O_SERVER_IP) { if ($val && !Utility::valid_ipv4($val)) { $msg = sprintf(__('Saving option failed. IPv4 only for %s.', 'litespeed-cache'), Lang::title(Base::O_SERVER_IP)); Admin_Display::error($msg); return; } } if (!array_key_exists($id, self::$_default_options)) { defined('LSCWP_LOG') && Debug2::debug('[Conf] Invalid option ID ' . $id); return; } if ($val && $this->_conf_pswd($id) && !preg_match('/[^\*]/', $val)) { return; } // Special handler for CDN Original URLs if ($id == self::O_CDN_ORI && !$val) { $home_url = home_url('/'); $parsed = parse_url($home_url); $home_url = str_replace($parsed['scheme'] . ':', '', $home_url); $val = $home_url; } // Validate type $val = $this->type_casting($val, $id); // Save data self::update_option($id, $val); // Handle purge if setting changed if ($this->conf($id) != $val) { $this->_updated_ids[] = $id; // Check if need to fire a purge or not (Here has to stay inside `update()` bcos need comparing old value) if ($this->_conf_purge($id)) { $diff = array_diff($val, $this->conf($id)); $diff2 = array_diff($this->conf($id), $val); $diff = array_merge($diff, $diff2); // If has difference foreach ($diff as $v) { $v = ltrim($v, '^'); $v = rtrim($v, '$'); $this->cls('Purge')->purge_url($v); } } } // Update in-memory data $this->set_conf($id, $val); } /** * Save network option * * @since 3.0 * @access public */ public function network_update($id, $val) { if (!array_key_exists($id, self::$_default_site_options)) { defined('LSCWP_LOG') && Debug2::debug('[Conf] Invalid network option ID ' . $id); return; } if ($val && $this->_conf_pswd($id) && !preg_match('/[^\*]/', $val)) { return; } // Validate type if (is_bool(self::$_default_site_options[$id])) { $max = $this->_conf_multi_switch($id); if ($max && $val > 1) { $val %= $max + 1; } else { $val = (bool) $val; } } elseif (is_array(self::$_default_site_options[$id])) { // from textarea input if (!is_array($val)) { $val = Utility::sanitize_lines($val, $this->_conf_filter($id)); } } elseif (!is_string(self::$_default_site_options[$id])) { $val = (int) $val; } else { // Check if the string has a limit set $val = $this->_conf_string_val($id, $val); } // Save data self::update_site_option($id, $val); // Handle purge if setting changed if ($this->network_conf($id) != $val) { // Check if need to do a purge all or not if ($this->_conf_purge_all($id)) { Purge::purge_all('[Conf] Network conf changed [id] ' . $id); } // Update in-memory data $this->set_network_conf($id, $val); } // No need to update cron here, Cron will register in each init if ($this->has_conf($id)) { $this->set_conf($id, $val); } } /** * Check if one user role is in exclude optimization group settings * * @since 1.6 * @access public * @param string $role The user role * @return int The set value if already set */ public function in_optm_exc_roles($role = null) { // Get user role if ($role === null) { $role = Router::get_role(); } if (!$role) { return false; } $roles = explode(',', $role); $found = array_intersect($roles, $this->conf(self::O_OPTM_EXC_ROLES)); return $found ? implode(',', $found) : false; } /** * Set one config value directly * * @since 2.9 * @access private */ private function _set_conf() { /** * NOTE: For URL Query String setting, * 1. If append lines to an array setting e.g. `cache-force_uri`, use `set[cache-force_uri][]=the_url`. * 2. If replace the array setting with one line, use `set[cache-force_uri]=the_url`. * 3. If replace the array setting with multi lines value, use 2 then 1. */ if (empty($_GET[self::TYPE_SET]) || !is_array($_GET[self::TYPE_SET])) { return; } $the_matrix = array(); foreach ($_GET[self::TYPE_SET] as $id => $v) { if (!$this->has_conf($id)) { continue; } // Append new item to array type settings if (is_array($v) && is_array($this->conf($id))) { $v = array_merge($this->conf($id), $v); Debug2::debug('[Conf] Appended to settings [' . $id . ']: ' . var_export($v, true)); } else { Debug2::debug('[Conf] Set setting [' . $id . ']: ' . var_export($v, true)); } $the_matrix[$id] = $v; } if (!$the_matrix) { return; } $this->update_confs($the_matrix); $msg = __('Changed setting successfully.', 'litespeed-cache'); Admin_Display::success($msg); // Redirect if changed frontend URL if (!empty($_GET['redirect'])) { wp_redirect($_GET['redirect']); exit(); } } /** * Handle all request actions from main cls * * @since 2.9 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_SET: $this->_set_conf(); break; default: break; } Admin::redirect(); } } src/cdn/quic.cls.php 0000644 00000005700 15162130525 0010333 0 ustar 00 <?php /** * The quic.cloud class. * * @since 2.4.1 * @package LiteSpeed * @subpackage LiteSpeed/src/cdn * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed\CDN; use LiteSpeed\Cloud; use LiteSpeed\Base; defined('WPINC') || exit(); class Quic extends Base { const LOG_TAG = '☁️'; const TYPE_REG = 'reg'; protected $_summary; private $_force = false; public function __construct() { $this->_summary = self::get_summary(); } /** * Notify CDN new config updated * * @access public */ public function try_sync_conf($force = false) { if ($force) { $this->_force = $force; } if (!$this->conf(self::O_CDN_QUIC)) { if (!empty($this->_summary['conf_md5'])) { self::debug('❌ No QC CDN, clear conf md5!'); self::save_summary(array('conf_md5' => '')); } return false; } // Notice: Sync conf must be after `wp_loaded` hook, to get 3rd party vary injected (e.g. `woocommerce_cart_hash`). if (!did_action('wp_loaded')) { add_action('wp_loaded', array($this, 'try_sync_conf'), 999); self::debug('WP not loaded yet, delay sync to wp_loaded:999'); return; } $options = $this->get_options(); $options['_tp_cookies'] = apply_filters('litespeed_vary_cookies', array()); // Build necessary options only $options_needed = array( self::O_CACHE_DROP_QS, self::O_CACHE_EXC_COOKIES, self::O_CACHE_EXC_USERAGENTS, self::O_CACHE_LOGIN_COOKIE, self::O_CACHE_VARY_COOKIES, self::O_CACHE_MOBILE_RULES, self::O_CACHE_MOBILE, self::O_CACHE_RES, self::O_CACHE_BROWSER, self::O_CACHE_TTL_BROWSER, self::O_IMG_OPTM_WEBP, self::O_GUEST, '_tp_cookies', ); $consts_needed = array('WP_CONTENT_DIR', 'LSCWP_CONTENT_DIR', 'LSCWP_CONTENT_FOLDER', 'LSWCP_TAG_PREFIX'); $options_for_md5 = array(); foreach ($options_needed as $v) { if (isset($options[$v])) { $options_for_md5[$v] = $options[$v]; // Remove overflow multi lines fields if (is_array($options_for_md5[$v]) && count($options_for_md5[$v]) > 30) { $options_for_md5[$v] = array_slice($options_for_md5[$v], 0, 30); } } } $server_vars = $this->server_vars(); foreach ($consts_needed as $v) { if (isset($server_vars[$v])) { if (empty($options_for_md5['_server'])) { $options_for_md5['_server'] = array(); } $options_for_md5['_server'][$v] = $server_vars[$v]; } } $conf_md5 = md5(\json_encode($options_for_md5)); if (!empty($this->_summary['conf_md5'])) { if ($conf_md5 == $this->_summary['conf_md5']) { if (!$this->_force) { self::debug('Bypass sync conf to QC due to same md5', $conf_md5); return; } self::debug('!!!Force sync conf even same md5'); } else { self::debug('[conf_md5] ' . $conf_md5 . ' [existing_conf_md5] ' . $this->_summary['conf_md5']); } } self::save_summary(array('conf_md5' => $conf_md5)); self::debug('sync conf to QC'); Cloud::post(Cloud::SVC_D_SYNC_CONF, $options_for_md5); } } src/cdn/cloudflare.cls.php 0000644 00000016030 15162130530 0011504 0 ustar 00 <?php /** * The cloudflare CDN class. * * @since 2.1 * @package LiteSpeed * @subpackage LiteSpeed/src/cdn * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed\CDN; use LiteSpeed\Core; use LiteSpeed\Base; use LiteSpeed\Debug2; use LiteSpeed\Router; use LiteSpeed\Admin; use LiteSpeed\Admin_Display; defined('WPINC') || exit(); class Cloudflare extends Base { const TYPE_PURGE_ALL = 'purge_all'; const TYPE_GET_DEVMODE = 'get_devmode'; const TYPE_SET_DEVMODE_ON = 'set_devmode_on'; const TYPE_SET_DEVMODE_OFF = 'set_devmode_off'; const ITEM_STATUS = 'status'; /** * Update zone&name based on latest settings * * @since 3.0 * @access public */ public function try_refresh_zone() { if (!$this->conf(self::O_CDN_CLOUDFLARE)) { return; } $zone = $this->_fetch_zone(); if ($zone) { $this->cls('Conf')->update(self::O_CDN_CLOUDFLARE_NAME, $zone['name']); $this->cls('Conf')->update(self::O_CDN_CLOUDFLARE_ZONE, $zone['id']); Debug2::debug("[Cloudflare] Get zone successfully \t\t[ID] $zone[id]"); } else { $this->cls('Conf')->update(self::O_CDN_CLOUDFLARE_ZONE, ''); Debug2::debug('[Cloudflare] ❌ Get zone failed, clean zone'); } } /** * Get Cloudflare development mode * * @since 1.7.2 * @access private */ private function _get_devmode($show_msg = true) { Debug2::debug('[Cloudflare] _get_devmode'); $zone = $this->_zone(); if (!$zone) { return; } $url = 'https://api.cloudflare.com/client/v4/zones/' . $zone . '/settings/development_mode'; $res = $this->_cloudflare_call($url, 'GET', false, $show_msg); if (!$res) { return; } Debug2::debug('[Cloudflare] _get_devmode result ', $res); // Make sure is array: #992174 $curr_status = self::get_option(self::ITEM_STATUS, array()) ?: array(); $curr_status['devmode'] = $res['value']; $curr_status['devmode_expired'] = $res['time_remaining'] + time(); // update status self::update_option(self::ITEM_STATUS, $curr_status); } /** * Set Cloudflare development mode * * @since 1.7.2 * @access private */ private function _set_devmode($type) { Debug2::debug('[Cloudflare] _set_devmode'); $zone = $this->_zone(); if (!$zone) { return; } $url = 'https://api.cloudflare.com/client/v4/zones/' . $zone . '/settings/development_mode'; $new_val = $type == self::TYPE_SET_DEVMODE_ON ? 'on' : 'off'; $data = array('value' => $new_val); $res = $this->_cloudflare_call($url, 'PATCH', $data); if (!$res) { return; } $res = $this->_get_devmode(false); if ($res) { $msg = sprintf(__('Notified Cloudflare to set development mode to %s successfully.', 'litespeed-cache'), strtoupper($new_val)); Admin_Display::success($msg); } } /** * Purge Cloudflare cache * * @since 1.7.2 * @access private */ private function _purge_all() { Debug2::debug('[Cloudflare] _purge_all'); $cf_on = $this->conf(self::O_CDN_CLOUDFLARE); if (!$cf_on) { $msg = __('Cloudflare API is set to off.', 'litespeed-cache'); Admin_Display::error($msg); return; } $zone = $this->_zone(); if (!$zone) { return; } $url = 'https://api.cloudflare.com/client/v4/zones/' . $zone . '/purge_cache'; $data = array('purge_everything' => true); $res = $this->_cloudflare_call($url, 'DELETE', $data); if ($res) { $msg = __('Notified Cloudflare to purge all successfully.', 'litespeed-cache'); Admin_Display::success($msg); } } /** * Get current Cloudflare zone from cfg * * @since 1.7.2 * @access private */ private function _zone() { $zone = $this->conf(self::O_CDN_CLOUDFLARE_ZONE); if (!$zone) { $msg = __('No available Cloudflare zone', 'litespeed-cache'); Admin_Display::error($msg); return false; } return $zone; } /** * Get Cloudflare zone settings * * @since 1.7.2 * @access private */ private function _fetch_zone() { $kw = $this->conf(self::O_CDN_CLOUDFLARE_NAME); $url = 'https://api.cloudflare.com/client/v4/zones?status=active&match=all'; // Try exact match first if ($kw && strpos($kw, '.')) { $zones = $this->_cloudflare_call($url . '&name=' . $kw, 'GET', false, false); if ($zones) { Debug2::debug('[Cloudflare] fetch_zone exact matched'); return $zones[0]; } } // Can't find, try to get default one $zones = $this->_cloudflare_call($url, 'GET', false, false); if (!$zones) { Debug2::debug('[Cloudflare] fetch_zone no zone'); return false; } if (!$kw) { Debug2::debug('[Cloudflare] fetch_zone no set name, use first one by default'); return $zones[0]; } foreach ($zones as $v) { if (strpos($v['name'], $kw) !== false) { Debug2::debug('[Cloudflare] fetch_zone matched ' . $kw . ' [name] ' . $v['name']); return $v; } } // Can't match current name, return default one Debug2::debug('[Cloudflare] fetch_zone failed match name, use first one by default'); return $zones[0]; } /** * Cloudflare API * * @since 1.7.2 * @access private */ private function _cloudflare_call($url, $method = 'GET', $data = false, $show_msg = true) { Debug2::debug("[Cloudflare] _cloudflare_call \t\t[URL] $url"); if (40 == strlen($this->conf(self::O_CDN_CLOUDFLARE_KEY))) { $headers = array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->conf(self::O_CDN_CLOUDFLARE_KEY), ); } else { $headers = array( 'Content-Type' => 'application/json', 'X-Auth-Email' => $this->conf(self::O_CDN_CLOUDFLARE_EMAIL), 'X-Auth-Key' => $this->conf(self::O_CDN_CLOUDFLARE_KEY), ); } $wp_args = array( 'method' => $method, 'headers' => $headers, ); if ($data) { if (is_array($data)) { $data = \json_encode($data); } $wp_args['body'] = $data; } $resp = wp_remote_request($url, $wp_args); if (is_wp_error($resp)) { Debug2::debug('[Cloudflare] error in response'); if ($show_msg) { $msg = __('Failed to communicate with Cloudflare', 'litespeed-cache'); Admin_Display::error($msg); } return false; } $result = wp_remote_retrieve_body($resp); $json = \json_decode($result, true); if ($json && $json['success'] && $json['result']) { Debug2::debug('[Cloudflare] _cloudflare_call called successfully'); if ($show_msg) { $msg = __('Communicated with Cloudflare successfully.', 'litespeed-cache'); Admin_Display::success($msg); } return $json['result']; } Debug2::debug("[Cloudflare] _cloudflare_call called failed: $result"); if ($show_msg) { $msg = __('Failed to communicate with Cloudflare', 'litespeed-cache'); Admin_Display::error($msg); } return false; } /** * Handle all request actions from main cls * * @since 1.7.2 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_PURGE_ALL: $this->_purge_all(); break; case self::TYPE_GET_DEVMODE: $this->_get_devmode(); break; case self::TYPE_SET_DEVMODE_ON: case self::TYPE_SET_DEVMODE_OFF: $this->_set_devmode($type); break; default: break; } Admin::redirect(); } } src/vpi.cls.php 0000644 00000016304 15162130533 0007425 0 ustar 00 <?php /** * The viewport image class. * * @since 4.7 */ namespace LiteSpeed; defined('WPINC') || exit(); class VPI extends Base { const LOG_TAG = '[VPI]'; const TYPE_GEN = 'gen'; const TYPE_CLEAR_Q = 'clear_q'; protected $_summary; private $_queue; /** * Init * * @since 4.7 */ public function __construct() { $this->_summary = self::get_summary(); } /** * The VPI content of the current page * * @since 4.7 */ public function add_to_queue() { $is_mobile = $this->_separate_mobile(); global $wp; $request_url = home_url($wp->request); $ua = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; // Store it to prepare for cron $this->_queue = $this->load_queue('vpi'); if (count($this->_queue) > 500) { self::debug('Queue is full - 500'); return; } $home_id = get_option('page_for_posts'); if (!is_singular() && !($home_id > 0 && is_home())) { self::debug('not single post ID'); return; } $post_id = is_home() ? $home_id : get_the_ID(); $queue_k = ($is_mobile ? 'mobile' : '') . ' ' . $request_url; if (!empty($this->_queue[$queue_k])) { self::debug('queue k existed ' . $queue_k); return; } $this->_queue[$queue_k] = array( 'url' => apply_filters('litespeed_vpi_url', $request_url), 'post_id' => $post_id, 'user_agent' => substr($ua, 0, 200), 'is_mobile' => $this->_separate_mobile(), ); // Current UA will be used to request $this->save_queue('vpi', $this->_queue); self::debug('Added queue_vpi [url] ' . $queue_k . ' [UA] ' . $ua); // Prepare cache tag for later purge Tag::add('VPI.' . md5($queue_k)); return null; } /** * Notify finished from server * @since 4.7 */ public function notify() { $post_data = \json_decode(file_get_contents('php://input'), true); if (is_null($post_data)) { $post_data = $_POST; } self::debug('notify() data', $post_data); $this->_queue = $this->load_queue('vpi'); list($post_data) = $this->cls('Cloud')->extract_msg($post_data, 'vpi'); $notified_data = $post_data['data']; if (empty($notified_data) || !is_array($notified_data)) { self::debug('❌ notify exit: no notified data'); return Cloud::err('no notified data'); } // Check if its in queue or not $valid_i = 0; foreach ($notified_data as $v) { if (empty($v['request_url'])) { self::debug('❌ notify bypass: no request_url', $v); continue; } if (empty($v['queue_k'])) { self::debug('❌ notify bypass: no queue_k', $v); continue; } // $queue_k = ( $is_mobile ? 'mobile' : '' ) . ' ' . $v[ 'request_url' ]; $queue_k = $v['queue_k']; if (empty($this->_queue[$queue_k])) { self::debug('❌ notify bypass: no this queue [q_k]' . $queue_k); continue; } // Save data if (!empty($v['data_vpi'])) { $post_id = $this->_queue[$queue_k]['post_id']; $name = !empty($v['is_mobile']) ? 'litespeed_vpi_list_mobile' : 'litespeed_vpi_list'; $urldecode = is_array($v['data_vpi']) ? array_map('urldecode', $v['data_vpi']) : urldecode($v['data_vpi']); self::debug('save data_vpi', $urldecode); $this->cls('Metabox')->save($post_id, $name, $urldecode); $valid_i++; } unset($this->_queue[$queue_k]); self::debug('notify data handled, unset queue [q_k] ' . $queue_k); } $this->save_queue('vpi', $this->_queue); self::debug('notified'); return Cloud::ok(array('count' => $valid_i)); } /** * Cron * * @since 4.7 */ public static function cron($continue = false) { $_instance = self::cls(); return $_instance->_cron_handler($continue); } /** * Cron generation * * @since 4.7 */ private function _cron_handler($continue = false) { self::debug('cron start'); $this->_queue = $this->load_queue('vpi'); if (empty($this->_queue)) { return; } // For cron, need to check request interval too if (!$continue) { if (!empty($this->_summary['curr_request_vpi']) && time() - $this->_summary['curr_request_vpi'] < 300 && !$this->conf(self::O_DEBUG)) { self::debug('Last request not done'); return; } } $i = 0; foreach ($this->_queue as $k => $v) { if (!empty($v['_status'])) { continue; } self::debug('cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']); $i++; $res = $this->_send_req($v['url'], $k, $v['user_agent'], $v['is_mobile']); if (!$res) { // Status is wrong, drop this this->_queue $this->_queue = $this->load_queue('vpi'); unset($this->_queue[$k]); $this->save_queue('vpi', $this->_queue); if (!$continue) { return; } // if ( $i > 3 ) { GUI::print_loading(count($this->_queue), 'VPI'); return Router::self_redirect(Router::ACTION_VPI, self::TYPE_GEN); // } continue; } // Exit queue if out of quota or service is hot if ($res === 'out_of_quota' || $res === 'svc_hot') { return; } $this->_queue = $this->load_queue('vpi'); $this->_queue[$k]['_status'] = 'requested'; $this->save_queue('vpi', $this->_queue); self::debug('Saved to queue [k] ' . $k); // only request first one if (!$continue) { return; } // if ( $i > 3 ) { GUI::print_loading(count($this->_queue), 'VPI'); return Router::self_redirect(Router::ACTION_VPI, self::TYPE_GEN); // } } } /** * Send to QC API to generate VPI * * @since 4.7 * @access private */ private function _send_req($request_url, $queue_k, $user_agent, $is_mobile) { $svc = Cloud::SVC_VPI; // Check if has credit to push or not $err = false; $allowance = $this->cls('Cloud')->allowance($svc, $err); if (!$allowance) { self::debug('❌ No credit: ' . $err); $err && Admin_Display::error(Error::msg($err)); return 'out_of_quota'; } set_time_limit(120); // Update css request status self::save_summary(array('curr_request_vpi' => time()), true); // Gather guest HTML to send $html = $this->cls('CSS')->prepare_html($request_url, $user_agent); if (!$html) { return false; } // Parse HTML to gather all CSS content before requesting $css = false; list($css, $html) = $this->cls('CSS')->prepare_css($html); if (!$css) { self::debug('❌ No css'); return false; } $data = array( 'url' => $request_url, 'queue_k' => $queue_k, 'user_agent' => $user_agent, 'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet 'html' => $html, 'css' => $css, ); self::debug('Generating: ', $data); $json = Cloud::post($svc, $data, 30); if (!is_array($json)) { return $json; } // Unknown status, remove this line if ($json['status'] != 'queued') { return false; } // Save summary data self::reload_summary(); $this->_summary['last_spent_vpi'] = time() - $this->_summary['curr_request_vpi']; $this->_summary['last_request_vpi'] = $this->_summary['curr_request_vpi']; $this->_summary['curr_request_vpi'] = 0; self::save_summary(); return true; } /** * Handle all request actions from main cls * * @since 4.7 */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_GEN: self::cron(true); break; case self::TYPE_CLEAR_Q: $this->clear_q('vpi'); break; default: break; } Admin::redirect(); } } src/crawler.cls.php 0000644 00000121213 15162130536 0010265 0 ustar 00 <?php /** * The crawler class * * @since 1.1.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class Crawler extends Root { const LOG_TAG = '🕸️'; const TYPE_REFRESH_MAP = 'refresh_map'; const TYPE_EMPTY = 'empty'; const TYPE_BLACKLIST_EMPTY = 'blacklist_empty'; const TYPE_BLACKLIST_DEL = 'blacklist_del'; const TYPE_BLACKLIST_ADD = 'blacklist_add'; const TYPE_START = 'start'; const TYPE_RESET = 'reset'; const USER_AGENT = 'lscache_walker'; const FAST_USER_AGENT = 'lscache_runner'; const CHUNKS = 10000; const STATUS_WAIT = 'W'; const STATUS_HIT = 'H'; const STATUS_MISS = 'M'; const STATUS_BLACKLIST = 'B'; const STATUS_NOCACHE = 'N'; private $_sitemeta = 'meta.data'; private $_resetfile; private $_end_reason; private $_ncpu = 1; private $_server_ip; private $_crawler_conf = array( 'cookies' => array(), 'headers' => array(), 'ua' => '', ); private $_crawlers = array(); private $_cur_threads = -1; private $_max_run_time; private $_cur_thread_time; private $_map_status_list = array( 'H' => array(), 'M' => array(), 'B' => array(), 'N' => array(), ); protected $_summary; /** * Initialize crawler, assign sitemap path * * @since 1.1.0 */ public function __construct() { if (is_multisite()) { $this->_sitemeta = 'meta' . get_current_blog_id() . '.data'; } $this->_resetfile = LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta . '.reset'; $this->_summary = self::get_summary(); $this->_ncpu = $this->_get_server_cpu(); $this->_server_ip = $this->conf(Base::O_SERVER_IP); self::debug('Init w/ CPU cores=' . $this->_ncpu); } /** * Try get server CPUs * @since 5.2 */ private function _get_server_cpu() { $cpuinfo_file = '/proc/cpuinfo'; $setting_open_dir = ini_get('open_basedir'); if ($setting_open_dir) { return 1; } // Server has limit try { if (!@is_file($cpuinfo_file)) { return 1; } } catch (\Exception $e) { return 1; } $cpuinfo = file_get_contents($cpuinfo_file); preg_match_all('/^processor/m', $cpuinfo, $matches); return count($matches[0]) ?: 1; } /** * Check whether the current crawler is active/runable/useable/enabled/want it to work or not * * @since 4.3 */ public function is_active($curr) { $bypass_list = self::get_option('bypass_list', array()); return !in_array($curr, $bypass_list); } /** * Toggle the current crawler's activeness state, i.e., runable/useable/enabled/want it to work or not, and return the updated state * * @since 4.3 */ public function toggle_activeness($curr) { // param type: int $bypass_list = self::get_option('bypass_list', array()); if (in_array($curr, $bypass_list)) { // when the ith opt was off / in the bypassed list, turn it on / remove it from the list unset($bypass_list[array_search($curr, $bypass_list)]); $bypass_list = array_values($bypass_list); self::update_option('bypass_list', $bypass_list); return true; } else { // when the ith opt was on / not in the bypassed list, turn it off / add it to the list $bypass_list[] = (int) $curr; self::update_option('bypass_list', $bypass_list); return false; } } /** * Clear bypassed list * * @since 4.3 * @access public */ public function clear_disabled_list() { self::update_option('bypass_list', array()); $msg = __('Crawler disabled list is cleared! All crawlers are set to active! ', 'litespeed-cache'); Admin_Display::note($msg); self::debug('All crawlers are set to active...... '); } /** * Overwrite get_summary to init elements * * @since 3.0 * @access public */ public static function get_summary($field = false) { $_default = array( 'list_size' => 0, 'last_update_time' => 0, 'curr_crawler' => 0, 'curr_crawler_beginning_time' => 0, 'last_pos' => 0, 'last_count' => 0, 'last_crawled' => 0, 'last_start_time' => 0, 'last_status' => '', 'is_running' => 0, 'end_reason' => '', 'meta_save_time' => 0, 'pos_reset_check' => 0, 'done' => 0, 'this_full_beginning_time' => 0, 'last_full_time_cost' => 0, 'last_crawler_total_cost' => 0, 'crawler_stats' => array(), // this will store all crawlers hit/miss crawl status ); wp_cache_delete('alloptions', 'options'); // ensure the summary is current $summary = parent::get_summary(); $summary = array_merge($_default, $summary); if (!$field) { return $summary; } if (array_key_exists($field, $summary)) { return $summary[$field]; } return null; } /** * Overwrite save_summary * * @since 3.0 * @access public */ public static function save_summary($data = false, $reload = false, $overwrite = false) { $instance = self::cls(); $instance->_summary['meta_save_time'] = time(); if (!$data) { $data = $instance->_summary; } parent::save_summary($data, $reload, $overwrite); File::save(LITESPEED_STATIC_DIR . '/crawler/' . $instance->_sitemeta, \json_encode($data), true); } /** * Cron start async crawling * * @since 5.5 */ public static function start_async_cron() { Task::async_call('crawler'); } /** * Manually start async crawling * * @since 5.5 */ public static function start_async() { Task::async_call('crawler_force'); $msg = __('Started async crawling', 'litespeed-cache'); Admin_Display::success($msg); } /** * Ajax crawl handler * * @since 5.5 */ public static function async_handler($manually_run = false) { self::debug('------------async-------------start_async_handler'); // check_ajax_referer('async_crawler', 'nonce'); self::start($manually_run); } /** * Proceed crawling * * @since 1.1.0 * @access public */ public static function start($manually_run = false) { if (!Router::can_crawl()) { self::debug('......crawler is NOT allowed by the server admin......'); return false; } if ($manually_run) { self::debug('......crawler manually ran......'); } self::cls()->_crawl_data($manually_run); } /** * Crawling start * * @since 1.1.0 * @access private */ private function _crawl_data($manually_run) { if (!defined('LITESPEED_LANE_HASH')) { define('LITESPEED_LANE_HASH', Str::rrand(8)); } if ($this->_check_valid_lane()) { $this->_take_over_lane(); } else { self::debug('⚠️ lane in use'); return; // if ($manually_run) { // self::debug('......crawler started (manually_rund)......'); // // Log pid to prevent from multi running // if (defined('LITESPEED_CLI')) { // // Take over lane // self::debug('⚠️⚠️⚠️ Forced take over lane (CLI)'); // $this->_take_over_lane(); // } // } } self::debug('......crawler started......'); // for the first time running if (!$this->_summary || !Data::cls()->tb_exist('crawler') || !Data::cls()->tb_exist('crawler_blacklist')) { $this->cls('Crawler_Map')->gen(); } // if finished last time, regenerate sitemap if ($this->_summary['done'] === 'touchedEnd') { // check whole crawling interval $last_finished_at = $this->_summary['last_full_time_cost'] + $this->_summary['this_full_beginning_time']; if (!$manually_run && time() - $last_finished_at < $this->conf(Base::O_CRAWLER_CRAWL_INTERVAL)) { self::debug('Cron abort: cache warmed already.'); // if not reach whole crawling interval, exit $this->Release_lane(); return; } self::debug('TouchedEnd. regenerate sitemap....'); $this->cls('Crawler_Map')->gen(); } $this->list_crawlers(); // Skip the crawlers that in bypassed list while (!$this->is_active($this->_summary['curr_crawler']) && $this->_summary['curr_crawler'] < count($this->_crawlers)) { self::debug('Skipped the Crawler #' . $this->_summary['curr_crawler'] . ' ......'); $this->_summary['curr_crawler']++; } if ($this->_summary['curr_crawler'] >= count($this->_crawlers)) { $this->_end_reason = 'end'; $this->_terminate_running(); $this->Release_lane(); return; } // In case crawlers are all done but not reload, reload it if (empty($this->_summary['curr_crawler']) || empty($this->_crawlers[$this->_summary['curr_crawler']])) { $this->_summary['curr_crawler'] = 0; $this->_summary['crawler_stats'][$this->_summary['curr_crawler']] = array(); } $res = $this->load_conf(); if (!$res) { self::debug('Load conf failed'); $this->_terminate_running(); $this->Release_lane(); return; } try { $this->_engine_start(); $this->Release_lane(); } catch (\Exception $e) { self::debug('🛑 ' . $e->getMessage()); } } /** * Load conf before running crawler * * @since 3.0 * @access private */ private function load_conf() { $this->_crawler_conf['base'] = home_url(); $current_crawler = $this->_crawlers[$this->_summary['curr_crawler']]; /** * Check cookie crawler * @since 2.8 */ foreach ($current_crawler as $k => $v) { if (strpos($k, 'cookie:') !== 0) { continue; } if ($v == '_null') { continue; } $this->_crawler_conf['cookies'][substr($k, 7)] = $v; } /** * Set WebP simulation * @since 1.9.1 */ if (!empty($current_crawler['webp'])) { $this->_crawler_conf['headers'][] = 'Accept: image/' . ($this->conf(Base::O_IMG_OPTM_WEBP) == 2 ? 'avif' : 'webp') . ',*/*'; } /** * Set mobile crawler * @since 2.8 */ if (!empty($current_crawler['mobile'])) { $this->_crawler_conf['ua'] = 'Mobile iPhone'; } /** * Limit delay to use server setting * @since 1.8.3 */ $this->_crawler_conf['run_delay'] = 500; // microseconds if (defined('LITESPEED_CRAWLER_USLEEP') && LITESPEED_CRAWLER_USLEEP > $this->_crawler_conf['run_delay']) { $this->_crawler_conf['run_delay'] = LITESPEED_CRAWLER_USLEEP; } if (!empty($_SERVER[Base::ENV_CRAWLER_USLEEP]) && $_SERVER[Base::ENV_CRAWLER_USLEEP] > $this->_crawler_conf['run_delay']) { $this->_crawler_conf['run_delay'] = $_SERVER[Base::ENV_CRAWLER_USLEEP]; } $this->_crawler_conf['run_duration'] = $this->get_crawler_duration(); $this->_crawler_conf['load_limit'] = $this->conf(Base::O_CRAWLER_LOAD_LIMIT); if (!empty($_SERVER[Base::ENV_CRAWLER_LOAD_LIMIT_ENFORCE])) { $this->_crawler_conf['load_limit'] = $_SERVER[Base::ENV_CRAWLER_LOAD_LIMIT_ENFORCE]; } elseif (!empty($_SERVER[Base::ENV_CRAWLER_LOAD_LIMIT]) && $_SERVER[Base::ENV_CRAWLER_LOAD_LIMIT] < $this->_crawler_conf['load_limit']) { $this->_crawler_conf['load_limit'] = $_SERVER[Base::ENV_CRAWLER_LOAD_LIMIT]; } if ($this->_crawler_conf['load_limit'] == 0) { self::debug('🛑 Terminated crawler due to load limit set to 0'); return false; } /** * Set role simulation * @since 1.9.1 */ if (!empty($current_crawler['uid'])) { if (!$this->_server_ip) { self::debug('🛑 Terminated crawler due to Server IP not set'); return false; } // Get role simulation vary name $vary_name = $this->cls('Vary')->get_vary_name(); $vary_val = $this->cls('Vary')->finalize_default_vary($current_crawler['uid']); $this->_crawler_conf['cookies'][$vary_name] = $vary_val; $this->_crawler_conf['cookies']['litespeed_hash'] = Router::cls()->get_hash($current_crawler['uid']); } return true; } /** * Get crawler duration allowance * * @since 7.0 */ public function get_crawler_duration() { $RUN_DURATION = defined('LITESPEED_CRAWLER_DURATION') ? LITESPEED_CRAWLER_DURATION : 900; if ($RUN_DURATION > 900) { $RUN_DURATION = 900; // reset to default value if defined in conf file is higher than 900 seconds for security enhancement } return $RUN_DURATION; } /** * Start crawler * * @since 1.1.0 * @access private */ private function _engine_start() { // check if is running // if ($this->_summary['is_running'] && time() - $this->_summary['is_running'] < $this->_crawler_conf['run_duration']) { // $this->_end_reason = 'stopped'; // self::debug('The crawler is running.'); // return; // } // check current load $this->_adjust_current_threads(); if ($this->_cur_threads == 0) { $this->_end_reason = 'stopped_highload'; self::debug('Stopped due to heavy load.'); return; } // log started time self::save_summary(array('last_start_time' => time())); // set time limit $maxTime = (int) ini_get('max_execution_time'); self::debug('ini_get max_execution_time=' . $maxTime); if ($maxTime == 0) { $maxTime = 300; // hardlimit } else { $maxTime -= 5; } if ($maxTime >= $this->_crawler_conf['run_duration']) { $maxTime = $this->_crawler_conf['run_duration']; self::debug('Use run_duration setting as max_execution_time=' . $maxTime); } elseif (ini_set('max_execution_time', $this->_crawler_conf['run_duration'] + 15) !== false) { $maxTime = $this->_crawler_conf['run_duration']; self::debug('ini_set max_execution_time=' . $maxTime); } self::debug('final max_execution_time=' . $maxTime); $this->_max_run_time = $maxTime + time(); // mark running $this->_prepare_running(); // run crawler $this->_do_running(); $this->_terminate_running(); } /** * Get server load * * @since 5.5 */ public function get_server_load() { /** * If server is windows, exit * @see https://wordpress.org/support/topic/crawler-keeps-causing-crashes/ */ if (!function_exists('sys_getloadavg')) { return -1; } $curload = sys_getloadavg(); $curload = $curload[0]; self::debug('Server load: ' . $curload); return $curload; } /** * Adjust threads dynamically * * @since 1.1.0 * @access private */ private function _adjust_current_threads() { $curload = $this->get_server_load(); if ($curload == -1) { self::debug('set threads=0 due to func sys_getloadavg not exist!'); $this->_cur_threads = 0; return; } $curload /= $this->_ncpu; // $curload = 1; $CRAWLER_THREADS = defined('LITESPEED_CRAWLER_THREADS') ? LITESPEED_CRAWLER_THREADS : 3; if ($this->_cur_threads == -1) { // init if ($curload > $this->_crawler_conf['load_limit']) { $curthreads = 0; } elseif ($curload >= $this->_crawler_conf['load_limit'] - 1) { $curthreads = 1; } else { $curthreads = intval($this->_crawler_conf['load_limit'] - $curload); if ($curthreads > $CRAWLER_THREADS) { $curthreads = $CRAWLER_THREADS; } } } else { // adjust $curthreads = $this->_cur_threads; if ($curload >= $this->_crawler_conf['load_limit'] + 1) { sleep(5); // sleep 5 secs if ($curthreads >= 1) { $curthreads--; } } elseif ($curload >= $this->_crawler_conf['load_limit']) { // if ( $curthreads > 1 ) {// if already 1, keep $curthreads--; // } } elseif ($curload + 1 < $this->_crawler_conf['load_limit']) { if ($curthreads < $CRAWLER_THREADS) { $curthreads++; } } } // $log = 'set current threads = ' . $curthreads . ' previous=' . $this->_cur_threads // . ' max_allowed=' . $CRAWLER_THREADS . ' load_limit=' . $this->_crawler_conf[ 'load_limit' ] . ' current_load=' . $curload; $this->_cur_threads = $curthreads; $this->_cur_thread_time = time(); } /** * Mark running status * * @since 1.1.0 * @access private */ private function _prepare_running() { $this->_summary['is_running'] = time(); $this->_summary['done'] = 0; // reset done status $this->_summary['last_status'] = 'prepare running'; $this->_summary['last_crawled'] = 0; // Current crawler starttime mark if ($this->_summary['last_pos'] == 0) { $this->_summary['curr_crawler_beginning_time'] = time(); } if ($this->_summary['curr_crawler'] == 0 && $this->_summary['last_pos'] == 0) { $this->_summary['this_full_beginning_time'] = time(); $this->_summary['list_size'] = $this->cls('Crawler_Map')->count_map(); } if ($this->_summary['end_reason'] == 'end' && $this->_summary['last_pos'] == 0) { $this->_summary['crawler_stats'][$this->_summary['curr_crawler']] = array(); } self::save_summary(); } /** * Take over lane * @since 6.1 */ private function _take_over_lane() { self::debug('Take over lane as lane is free: ' . $this->json_local_path() . '.pid'); file::save($this->json_local_path() . '.pid', LITESPEED_LANE_HASH); } /** * Update lane file * @since 6.1 */ private function _touch_lane() { touch($this->json_local_path() . '.pid'); } /** * Release lane file * @since 6.1 */ public function Release_lane() { $lane_file = $this->json_local_path() . '.pid'; if (!file_exists($lane_file)) { return; } self::debug('Release lane'); unlink($lane_file); } /** * Check if lane is used by other crawlers * @since 6.1 */ private function _check_valid_lane($strict_mode = false) { // Check lane hash $lane_file = $this->json_local_path() . '.pid'; if ($strict_mode) { if (!file_exists($lane_file)) { self::debug("lane file not existed, strict mode is false [file] $lane_file"); return false; } } $pid = file::read($lane_file); if ($pid && LITESPEED_LANE_HASH != $pid) { // If lane file is older than 1h, ignore if (time() - filemtime($lane_file) > 3600) { self::debug('Lane file is older than 1h, releasing lane'); $this->Release_lane(); return true; } return false; } return true; } /** * Test port for simulator * * @since 7.0 * @access private * @return bool true if success and can continue crawling, false if failed and need to stop */ private function _test_port() { if (empty($this->_crawler_conf['cookies']) || empty($this->_crawler_conf['cookies']['litespeed_hash'])) { return true; } if (!$this->_server_ip) { self::debug('❌ Server IP not set'); return false; } if (defined('LITESPEED_CRAWLER_LOCAL_PORT')) { self::debug('✅ LITESPEED_CRAWLER_LOCAL_PORT already defined'); return true; } // Don't repeat testing in 120s if (!empty($this->_summary['test_port_tts']) && time() - $this->_summary['test_port_tts'] < 120) { if (!empty($this->_summary['test_port'])) { self::debug('✅ Use tested local port: ' . $this->_summary['test_port']); define('LITESPEED_CRAWLER_LOCAL_PORT', $this->_summary['test_port']); return true; } return false; } $this->_summary['test_port_tts'] = time(); self::save_summary(); $options = $this->_get_curl_options(); $home = home_url(); File::save(LITESPEED_STATIC_DIR . '/crawler/test_port.txt', $home, true); $url = LITESPEED_STATIC_URL . '/crawler/test_port.txt'; $parsed_url = parse_url($url); if (empty($parsed_url['host'])) { self::debug('❌ Test port failed, invalid URL: ' . $url); return false; } $resolved = $parsed_url['host'] . ':443:' . $this->_server_ip; $options[CURLOPT_RESOLVE] = array($resolved); $options[CURLOPT_DNS_USE_GLOBAL_CACHE] = false; $options[CURLOPT_HEADER] = false; self::debug('Test local 443 port for ' . $resolved); $ch = curl_init(); curl_setopt_array($ch, $options); curl_setopt($ch, CURLOPT_URL, $url); $result = curl_exec($ch); $test_result = false; if (curl_errno($ch) || $result !== $home) { if (curl_errno($ch)) { self::debug('❌ Test port curl error: [errNo] ' . curl_errno($ch) . ' [err] ' . curl_error($ch)); } elseif ($result !== $home) { self::debug('❌ Test port response is wrong: ' . $result); } self::debug('❌ Test local 443 port failed, try port 80'); // Try port 80 $resolved = $parsed_url['host'] . ':80:' . $this->_server_ip; $options[CURLOPT_RESOLVE] = array($resolved); $url = str_replace('https://', 'http://', $url); if (!in_array('X-Forwarded-Proto: https', $options[CURLOPT_HTTPHEADER])) { $options[CURLOPT_HTTPHEADER][] = 'X-Forwarded-Proto: https'; } // $options[CURLOPT_HTTPHEADER][] = 'X-Forwarded-SSL: on'; $ch = curl_init(); curl_setopt_array($ch, $options); curl_setopt($ch, CURLOPT_URL, $url); $result = curl_exec($ch); if (curl_errno($ch)) { self::debug('❌ Test port curl error: [errNo] ' . curl_errno($ch) . ' [err] ' . curl_error($ch)); } elseif ($result !== $home) { self::debug('❌ Test port response is wrong: ' . $result); } else { self::debug('✅ Test local 80 port successfully'); define('LITESPEED_CRAWLER_LOCAL_PORT', 80); $this->_summary['test_port'] = 80; $test_result = true; } // self::debug('Response data: ' . $result); // $this->Release_lane(); // exit($result); } else { self::debug('✅ Tested local 443 port successfully'); define('LITESPEED_CRAWLER_LOCAL_PORT', 443); $this->_summary['test_port'] = 443; $test_result = true; } self::save_summary(); curl_close($ch); return $test_result; } /** * Run crawler * * @since 1.1.0 * @access private */ private function _do_running() { $options = $this->_get_curl_options(true); // If is role simulator and not defined local port, check port once $test_result = $this->_test_port(); if (!$test_result) { $this->_end_reason = 'port_test_failed'; self::debug('❌ Test port failed, crawler stopped.'); return; } while ($urlChunks = $this->cls('Crawler_Map')->list_map(self::CHUNKS, $this->_summary['last_pos'])) { // self::debug('$urlChunks=' . count($urlChunks) . ' $this->_cur_threads=' . $this->_cur_threads); // start crawling $urlChunks = array_chunk($urlChunks, $this->_cur_threads); // self::debug('$urlChunks after array_chunk: ' . count($urlChunks)); foreach ($urlChunks as $rows) { if (!$this->_check_valid_lane(true)) { $this->_end_reason = 'lane_invalid'; self::debug('🛑 The crawler lane is used by newer crawler.'); throw new \Exception('invalid crawler lane'); } // Update time $this->_touch_lane(); // self::debug('chunk fetching count($rows)= ' . count($rows)); // multi curl $rets = $this->_multi_request($rows, $options); // check result headers foreach ($rows as $row) { // self::debug('chunk fetching 553'); if (empty($rets[$row['id']])) { // If already in blacklist, no curl happened, no corresponding record continue; } // self::debug('chunk fetching 557'); // check response if ($rets[$row['id']]['code'] == 428) { // HTTP/1.1 428 Precondition Required (need to test) $this->_end_reason = 'crawler_disabled'; self::debug('crawler_disabled'); return; } $status = $this->_status_parse($rets[$row['id']]['header'], $rets[$row['id']]['code'], $row['url']); // B or H or M or N(nocache) self::debug('[status] ' . $this->_status2title($status) . "\t\t [url] " . $row['url']); $this->_map_status_list[$status][$row['id']] = array( 'url' => $row['url'], 'code' => $rets[$row['id']]['code'], // 201 or 200 or 404 ); if (empty($this->_summary['crawler_stats'][$this->_summary['curr_crawler']][$status])) { $this->_summary['crawler_stats'][$this->_summary['curr_crawler']][$status] = 0; } $this->_summary['crawler_stats'][$this->_summary['curr_crawler']][$status]++; } // update offset position $_time = time(); $this->_summary['last_count'] = count($rows); $this->_summary['last_pos'] += $this->_summary['last_count']; $this->_summary['last_crawled'] += $this->_summary['last_count']; $this->_summary['last_update_time'] = $_time; $this->_summary['last_status'] = 'updated position'; // self::debug("chunk fetching 604 last_pos:{$this->_summary['last_pos']} last_count:{$this->_summary['last_count']} last_crawled:{$this->_summary['last_crawled']}"); // check duration if ($this->_summary['last_update_time'] > $this->_max_run_time) { $this->_end_reason = 'stopped_maxtime'; self::debug('Terminated due to maxtime'); return; // return __('Stopped due to exceeding defined Maximum Run Time', 'litespeed-cache'); } // make sure at least each 10s save meta & map status once if ($_time - $this->_summary['meta_save_time'] > 10) { $this->_map_status_list = $this->cls('Crawler_Map')->save_map_status($this->_map_status_list, $this->_summary['curr_crawler']); self::save_summary(); } // self::debug('chunk fetching 597'); // check if need to reset pos each 5s if ($_time > $this->_summary['pos_reset_check']) { $this->_summary['pos_reset_check'] = $_time + 5; if (file_exists($this->_resetfile) && unlink($this->_resetfile)) { self::debug('Terminated due to reset file'); $this->_summary['last_pos'] = 0; $this->_summary['curr_crawler'] = 0; $this->_summary['crawler_stats'][$this->_summary['curr_crawler']] = array(); // reset done status $this->_summary['done'] = 0; $this->_summary['this_full_beginning_time'] = 0; $this->_end_reason = 'stopped_reset'; return; // return __('Stopped due to reset meta position', 'litespeed-cache'); } } // self::debug('chunk fetching 615'); // check loads if ($this->_summary['last_update_time'] - $this->_cur_thread_time > 60) { $this->_adjust_current_threads(); if ($this->_cur_threads == 0) { $this->_end_reason = 'stopped_highload'; self::debug('🛑 Terminated due to highload'); return; // return __('Stopped due to load over limit', 'litespeed-cache'); } } $this->_summary['last_status'] = 'sleeping ' . $this->_crawler_conf['run_delay'] . 'ms'; usleep($this->_crawler_conf['run_delay']); } // self::debug('chunk fetching done'); } // All URLs are done for current crawler $this->_end_reason = 'end'; $this->_summary['crawler_stats'][$this->_summary['curr_crawler']]['W'] = 0; self::debug('Crawler #' . $this->_summary['curr_crawler'] . ' touched end'); } /** * Send multi curl requests * If res=B, bypass request and won't return * * @since 1.1.0 * @access private */ private function _multi_request($rows, $options) { if (!function_exists('curl_multi_init')) { exit('curl_multi_init disabled'); } $mh = curl_multi_init(); $CRAWLER_DROP_DOMAIN = defined('LITESPEED_CRAWLER_DROP_DOMAIN') ? LITESPEED_CRAWLER_DROP_DOMAIN : false; $curls = array(); foreach ($rows as $row) { if (substr($row['res'], $this->_summary['curr_crawler'], 1) == self::STATUS_BLACKLIST) { continue; } if (substr($row['res'], $this->_summary['curr_crawler'], 1) == self::STATUS_NOCACHE) { continue; } if (!function_exists('curl_init')) { exit('curl_init disabled'); } $curls[$row['id']] = curl_init(); // Append URL $url = $row['url']; if ($CRAWLER_DROP_DOMAIN) { $url = $this->_crawler_conf['base'] . $row['url']; } // IP resolve if (!empty($this->_crawler_conf['cookies']) && !empty($this->_crawler_conf['cookies']['litespeed_hash'])) { $parsed_url = parse_url($url); // self::debug('Crawl role simulator, required to use localhost for resolve'); if (!empty($parsed_url['host'])) { $dom = $parsed_url['host']; $port = defined('LITESPEED_CRAWLER_LOCAL_PORT') ? LITESPEED_CRAWLER_LOCAL_PORT : '443'; $resolved = $dom . ':' . $port . ':' . $this->_server_ip; $options[CURLOPT_RESOLVE] = array($resolved); $options[CURLOPT_DNS_USE_GLOBAL_CACHE] = false; // $options[CURLOPT_PORT] = $port; if ($port == 80) { $url = str_replace('https://', 'http://', $url); if (!in_array('X-Forwarded-Proto: https', $options[CURLOPT_HTTPHEADER])) { $options[CURLOPT_HTTPHEADER][] = 'X-Forwarded-Proto: https'; } } self::debug('Resolved DNS for ' . $resolved); } } curl_setopt($curls[$row['id']], CURLOPT_URL, $url); self::debug('Crawling [url] ' . $url . ($url == $row['url'] ? '' : ' [ori] ' . $row['url'])); curl_setopt_array($curls[$row['id']], $options); curl_multi_add_handle($mh, $curls[$row['id']]); } // execute curl if ($curls) { do { $status = curl_multi_exec($mh, $active); if ($active) { curl_multi_select($mh); } } while ($active && $status == CURLM_OK); } // curl done $ret = array(); foreach ($rows as $row) { if (substr($row['res'], $this->_summary['curr_crawler'], 1) == self::STATUS_BLACKLIST) { continue; } if (substr($row['res'], $this->_summary['curr_crawler'], 1) == self::STATUS_NOCACHE) { continue; } // self::debug('-----debug3'); $ch = $curls[$row['id']]; // Parse header $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $content = curl_multi_getcontent($ch); $header = substr($content, 0, $header_size); $ret[$row['id']] = array( 'header' => $header, 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), ); // self::debug('-----debug4'); curl_multi_remove_handle($mh, $ch); curl_close($ch); } // self::debug('-----debug5'); curl_multi_close($mh); // self::debug('-----debug6'); return $ret; } /** * Translate the status to title * @since 6.0 */ private function _status2title($status) { if ($status == self::STATUS_HIT) { return '✅ Hit'; } if ($status == self::STATUS_MISS) { return '😊 Miss'; } if ($status == self::STATUS_BLACKLIST) { return '😅 Blacklisted'; } if ($status == self::STATUS_NOCACHE) { return '😅 Blacklisted'; } return '🛸 Unknown'; } /** * Check returned curl header to find if cached or not * * @since 2.0 * @access private */ private function _status_parse($header, $code, $url) { // self::debug('http status code: ' . $code . ' [headers]', $header); if ($code == 201) { return self::STATUS_HIT; } if (stripos($header, 'X-Litespeed-Cache-Control: no-cache') !== false) { // If is from DIVI, taken as miss if (defined('LITESPEED_CRAWLER_IGNORE_NONCACHEABLE') && LITESPEED_CRAWLER_IGNORE_NONCACHEABLE) { return self::STATUS_MISS; } // If blacklist is disabled if ((defined('LITESPEED_CRAWLER_DISABLE_BLOCKLIST') && LITESPEED_CRAWLER_DISABLE_BLOCKLIST) || apply_filters('litespeed_crawler_disable_blocklist', false, $url)) { return self::STATUS_MISS; } return self::STATUS_NOCACHE; // Blacklist } $_cache_headers = array('x-qc-cache', 'x-lsadc-cache', 'x-litespeed-cache'); foreach ($_cache_headers as $_header) { if (stripos($header, $_header) !== false) { if (stripos($header, $_header . ': miss') !== false) { return self::STATUS_MISS; // Miss } return self::STATUS_HIT; // Hit } } // If blacklist is disabled if ((defined('LITESPEED_CRAWLER_DISABLE_BLOCKLIST') && LITESPEED_CRAWLER_DISABLE_BLOCKLIST) || apply_filters('litespeed_crawler_disable_blocklist', false, $url)) { return self::STATUS_MISS; } return self::STATUS_BLACKLIST; // Blacklist } /** * Get curl_options * * @since 1.1.0 * @access private */ private function _get_curl_options($crawler_only = false) { $CRAWLER_TIMEOUT = defined('LITESPEED_CRAWLER_TIMEOUT') ? LITESPEED_CRAWLER_TIMEOUT : 30; $options = array( CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_FOLLOWLOCATION => false, CURLOPT_ENCODING => 'gzip', CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $CRAWLER_TIMEOUT, // Larger timeout to avoid incorrect blacklist addition #900171 CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_NOBODY => false, CURLOPT_HTTPHEADER => $this->_crawler_conf['headers'], ); $options[CURLOPT_HTTPHEADER][] = 'Cache-Control: max-age=0'; /** * Try to enable http2 connection (only available since PHP7+) * @since 1.9.1 * @since 2.2.7 Commented due to cause no-cache issue * @since 2.9.1+ Fixed wrongly usage of CURL_HTTP_VERSION_1_1 const */ $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; // $options[ CURL_HTTP_VERSION_2 ] = 1; // if is walker // $options[ CURLOPT_FRESH_CONNECT ] = true; // Referer if (isset($_SERVER['HTTP_HOST']) && isset($_SERVER['REQUEST_URI'])) { $options[CURLOPT_REFERER] = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } // User Agent if ($crawler_only) { if (strpos($this->_crawler_conf['ua'], Crawler::FAST_USER_AGENT) !== 0) { $this->_crawler_conf['ua'] = Crawler::FAST_USER_AGENT . ' ' . $this->_crawler_conf['ua']; } } $options[CURLOPT_USERAGENT] = $this->_crawler_conf['ua']; // Cookies $cookies = array(); foreach ($this->_crawler_conf['cookies'] as $k => $v) { if (!$v) { continue; } $cookies[] = $k . '=' . urlencode($v); } if ($cookies) { $options[CURLOPT_COOKIE] = implode('; ', $cookies); } return $options; } /** * Self curl to get HTML content * * @since 3.3 */ public function self_curl($url, $ua, $uid = false, $accept = false) { // $accept not in use yet $this->_crawler_conf['base'] = home_url(); $this->_crawler_conf['ua'] = $ua; if ($accept) { $this->_crawler_conf['headers'] = array('Accept: ' . $accept); } $options = $this->_get_curl_options(); if ($uid) { $this->_crawler_conf['cookies']['litespeed_flash_hash'] = Router::cls()->get_flash_hash($uid); $parsed_url = parse_url($url); if (!empty($parsed_url['host'])) { $dom = $parsed_url['host']; $port = defined('LITESPEED_CRAWLER_LOCAL_PORT') ? LITESPEED_CRAWLER_LOCAL_PORT : '443'; $resolved = $dom . ':' . $port . ':' . $this->_server_ip; $options[CURLOPT_RESOLVE] = array($resolved); $options[CURLOPT_DNS_USE_GLOBAL_CACHE] = false; $options[CURLOPT_PORT] = $port; self::debug('Resolved DNS for ' . $resolved); } } $options[CURLOPT_HEADER] = false; $options[CURLOPT_FOLLOWLOCATION] = true; $ch = curl_init(); curl_setopt_array($ch, $options); curl_setopt($ch, CURLOPT_URL, $url); $result = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code != 200) { self::debug('❌ Response code is not 200 in self_curl() [code] ' . var_export($code, true)); return false; } return $result; } /** * Terminate crawling * * @since 1.1.0 * @access private */ private function _terminate_running() { $this->_map_status_list = $this->cls('Crawler_Map')->save_map_status($this->_map_status_list, $this->_summary['curr_crawler']); if ($this->_end_reason == 'end') { // Current crawler is fully done // $end_reason = sprintf( __( 'Crawler %s reached end of sitemap file.', 'litespeed-cache' ), '#' . ( $this->_summary['curr_crawler'] + 1 ) ); $this->_summary['curr_crawler']++; // Jump to next crawler // $this->_summary[ 'crawler_stats' ][ $this->_summary[ 'curr_crawler' ] ] = array(); // reset this at next crawl time $this->_summary['last_pos'] = 0; // reset last position $this->_summary['last_crawler_total_cost'] = time() - $this->_summary['curr_crawler_beginning_time']; $count_crawlers = count($this->list_crawlers()); if ($this->_summary['curr_crawler'] >= $count_crawlers) { self::debug('_terminate_running Touched end, whole crawled. Reload crawler!'); $this->_summary['curr_crawler'] = 0; // $this->_summary[ 'crawler_stats' ][ $this->_summary[ 'curr_crawler' ] ] = array(); $this->_summary['done'] = 'touchedEnd'; // log done status $this->_summary['last_full_time_cost'] = time() - $this->_summary['this_full_beginning_time']; } } $this->_summary['last_status'] = 'stopped'; $this->_summary['is_running'] = 0; $this->_summary['end_reason'] = $this->_end_reason; self::save_summary(); } /** * List all crawlers ( tagA => [ valueA => titleA, ... ] ...) * * @since 1.9.1 * @access public */ public function list_crawlers() { if ($this->_crawlers) { return $this->_crawlers; } $crawler_factors = array(); // Add default Guest crawler $crawler_factors['uid'] = array(0 => __('Guest', 'litespeed-cache')); // WebP on/off if ($this->conf(Base::O_IMG_OPTM_WEBP)) { $crawler_factors['webp'] = array(1 => $this->cls('Media')->next_gen_image_title()); if (apply_filters('litespeed_crawler_webp', false)) { $crawler_factors['webp'][0] = ''; } } // Guest Mode on/off if ($this->conf(Base::O_GUEST)) { $vary_name = $this->cls('Vary')->get_vary_name(); $vary_val = 'guest_mode:1'; if (!defined('LSCWP_LOG')) { $vary_val = md5($this->conf(Base::HASH) . $vary_val); } $crawler_factors['cookie:' . $vary_name] = array($vary_val => '', '_null' => '<font data-balloon-pos="up" aria-label="Guest Mode">👒</font>'); } // Mobile crawler if ($this->conf(Base::O_CACHE_MOBILE)) { $crawler_factors['mobile'] = array(1 => '<font data-balloon-pos="up" aria-label="Mobile">📱</font>', 0 => ''); } // Get roles set // List all roles foreach ($this->conf(Base::O_CRAWLER_ROLES) as $v) { $role_title = ''; $udata = get_userdata($v); if (isset($udata->roles) && is_array($udata->roles)) { $tmp = array_values($udata->roles); $role_title = array_shift($tmp); } if (!$role_title) { continue; } $crawler_factors['uid'][$v] = ucfirst($role_title); } // Cookie crawler foreach ($this->conf(Base::O_CRAWLER_COOKIES) as $v) { if (empty($v['name'])) { continue; } $this_cookie_key = 'cookie:' . $v['name']; $crawler_factors[$this_cookie_key] = array(); foreach ($v['vals'] as $v2) { $crawler_factors[$this_cookie_key][$v2] = $v2 == '_null' ? '' : '<font data-balloon-pos="up" aria-label="Cookie">🍪</font>' . esc_html($v['name']) . '=' . esc_html($v2); } } // Crossing generate the crawler list $this->_crawlers = $this->_recursive_build_crawler($crawler_factors); return $this->_crawlers; } /** * Build a crawler list recursively * * @since 2.8 * @access private */ private function _recursive_build_crawler($crawler_factors, $group = array(), $i = 0) { $current_factor = array_keys($crawler_factors); $current_factor = $current_factor[$i]; $if_touch_end = $i + 1 >= count($crawler_factors); $final_list = array(); foreach ($crawler_factors[$current_factor] as $k => $v) { // Don't alter $group bcos of loop usage $item = $group; $item['title'] = !empty($group['title']) ? $group['title'] : ''; if ($v) { if ($item['title']) { $item['title'] .= ' - '; } $item['title'] .= $v; } $item[$current_factor] = $k; if ($if_touch_end) { $final_list[] = $item; } else { // Inception: next layer $final_list = array_merge($final_list, $this->_recursive_build_crawler($crawler_factors, $item, $i + 1)); } } return $final_list; } /** * Return crawler meta file local path * * @since 6.1 * @access public */ public function json_local_path() { // if (!file_exists(LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta)) { // return false; // } return LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta; } /** * Return crawler meta file * * @since 1.1.0 * @access public */ public function json_path() { if (!file_exists(LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta)) { return false; } return LITESPEED_STATIC_URL . '/crawler/' . $this->_sitemeta; } /** * Create reset pos file * * @since 1.1.0 * @access public */ public function reset_pos() { File::save($this->_resetfile, time(), true); self::save_summary(array('is_running' => 0)); } /** * Display status based by matching crawlers order * * @since 3.0 * @access public */ public function display_status($status_row, $reason_set) { if (!$status_row) { return ''; } $_status_list = array( '-' => 'default', self::STATUS_MISS => 'primary', self::STATUS_HIT => 'success', self::STATUS_BLACKLIST => 'danger', self::STATUS_NOCACHE => 'warning', ); $reason_set = explode(',', $reason_set); $status = ''; foreach (str_split($status_row) as $k => $v) { $reason = $reason_set[$k]; if ($reason == 'Man') { $reason = __('Manually added to blocklist', 'litespeed-cache'); } if ($reason == 'Existed') { $reason = __('Previously existed in blocklist', 'litespeed-cache'); } if ($reason) { $reason = 'data-balloon-pos="up" aria-label="' . $reason . '"'; } $status .= '<i class="litespeed-dot litespeed-bg-' . $_status_list[$v] . '" ' . $reason . '>' . ($k + 1) . '</i>'; } return $status; } /** * Output info and exit * * @since 1.1.0 * @access protected * @param string $error Error info */ protected function output($msg) { if (defined('DOING_CRON')) { echo $msg; // exit(); } else { echo "<script>alert('" . htmlspecialchars($msg) . "');</script>"; // exit; } } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_REFRESH_MAP: $this->cls('Crawler_Map')->gen(true); break; case self::TYPE_EMPTY: $this->cls('Crawler_Map')->empty_map(); break; case self::TYPE_BLACKLIST_EMPTY: $this->cls('Crawler_Map')->blacklist_empty(); break; case self::TYPE_BLACKLIST_DEL: if (!empty($_GET['id'])) { $this->cls('Crawler_Map')->blacklist_del($_GET['id']); } break; case self::TYPE_BLACKLIST_ADD: if (!empty($_GET['id'])) { $this->cls('Crawler_Map')->blacklist_add($_GET['id']); } break; case self::TYPE_START: // Handle the ajax request to proceed crawler manually by admin self::start_async(); break; case self::TYPE_RESET: $this->reset_pos(); break; default: break; } Admin::redirect(); } } src/avatar.cls.php 0000644 00000014107 15162130541 0010103 0 ustar 00 <?php /** * The avatar cache class * * @since 3.0 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Avatar extends Base { const TYPE_GENERATE = 'generate'; private $_conf_cache_ttl; private $_tb; private $_avatar_realtime_gen_dict = array(); protected $_summary; /** * Init * * @since 1.4 */ public function __construct() { if (!$this->conf(self::O_DISCUSS_AVATAR_CACHE)) { return; } Debug2::debug2('[Avatar] init'); $this->_tb = $this->cls('Data')->tb('avatar'); $this->_conf_cache_ttl = $this->conf(self::O_DISCUSS_AVATAR_CACHE_TTL); add_filter('get_avatar_url', array($this, 'crawl_avatar')); $this->_summary = self::get_summary(); } /** * Check if need db table or not * * @since 3.0 * @access public */ public function need_db() { if ($this->conf(self::O_DISCUSS_AVATAR_CACHE)) { return true; } return false; } /** * Get gravatar URL from DB and regenerate * * @since 3.0 * @access public */ public function serve_static($md5) { global $wpdb; Debug2::debug('[Avatar] is avatar request'); if (strlen($md5) !== 32) { Debug2::debug('[Avatar] wrong md5 ' . $md5); return; } $q = "SELECT url FROM `$this->_tb` WHERE md5=%s"; $url = $wpdb->get_var($wpdb->prepare($q, $md5)); if (!$url) { Debug2::debug('[Avatar] no matched url for md5 ' . $md5); return; } $url = $this->_generate($url); wp_redirect($url); exit(); } /** * Localize gravatar * * @since 3.0 * @access public */ public function crawl_avatar($url) { if (!$url) { return $url; } // Check if its already in dict or not if (!empty($this->_avatar_realtime_gen_dict[$url])) { Debug2::debug2('[Avatar] already in dict [url] ' . $url); return $this->_avatar_realtime_gen_dict[$url]; } $realpath = $this->_realpath($url); if (file_exists($realpath) && time() - filemtime($realpath) <= $this->_conf_cache_ttl) { Debug2::debug2('[Avatar] cache file exists [url] ' . $url); return $this->_rewrite($url, filemtime($realpath)); } if (!strpos($url, 'gravatar.com')) { return $url; } // Send request if (!empty($this->_summary['curr_request']) && time() - $this->_summary['curr_request'] < 300) { Debug2::debug2('[Avatar] Bypass generating due to interval limit [url] ' . $url); return $url; } // Generate immediately $this->_avatar_realtime_gen_dict[$url] = $this->_generate($url); return $this->_avatar_realtime_gen_dict[$url]; } /** * Read last time generated info * * @since 3.0 * @access public */ public function queue_count() { global $wpdb; // If var not exists, mean table not exists // todo: not true if (!$this->_tb) { return false; } $q = "SELECT COUNT(*) FROM `$this->_tb` WHERE dateline<" . (time() - $this->_conf_cache_ttl); return $wpdb->get_var($q); } /** * Get the final URL of local avatar * * Check from db also * * @since 3.0 */ private function _rewrite($url, $time = null) { return LITESPEED_STATIC_URL . '/avatar/' . $this->_filepath($url) . ($time ? '?ver=' . $time : ''); } /** * Generate realpath of the cache file * * @since 3.0 * @access private */ private function _realpath($url) { return LITESPEED_STATIC_DIR . '/avatar/' . $this->_filepath($url); } /** * Get filepath * * @since 4.0 */ private function _filepath($url) { $filename = md5($url) . '.jpg'; if (is_multisite()) { $filename = get_current_blog_id() . '/' . $filename; } return $filename; } /** * Cron generation * * @since 3.0 * @access public */ public static function cron($force = false) { global $wpdb; $_instance = self::cls(); if (!$_instance->queue_count()) { Debug2::debug('[Avatar] no queue'); return; } // For cron, need to check request interval too if (!$force) { if (!empty($_instance->_summary['curr_request']) && time() - $_instance->_summary['curr_request'] < 300) { Debug2::debug('[Avatar] curr_request too close'); return; } } $q = "SELECT url FROM `$_instance->_tb` WHERE dateline < %d ORDER BY id DESC LIMIT %d"; $q = $wpdb->prepare($q, array(time() - $_instance->_conf_cache_ttl, apply_filters('litespeed_avatar_limit', 30))); $list = $wpdb->get_results($q); Debug2::debug('[Avatar] cron job [count] ' . count($list)); foreach ($list as $v) { Debug2::debug('[Avatar] cron job [url] ' . $v->url); $_instance->_generate($v->url); } } /** * Remote generator * * @since 3.0 * @access private */ private function _generate($url) { global $wpdb; // Record the data $file = $this->_realpath($url); // Update request status self::save_summary(array('curr_request' => time())); // Generate $this->_maybe_mk_cache_folder('avatar'); $response = wp_safe_remote_get($url, array('timeout' => 180, 'stream' => true, 'filename' => $file)); Debug2::debug('[Avatar] _generate [url] ' . $url); // Parse response data if (is_wp_error($response)) { $error_message = $response->get_error_message(); file_exists($file) && unlink($file); Debug2::debug('[Avatar] failed to get: ' . $error_message); return $url; } // Save summary data self::save_summary(array( 'last_spent' => time() - $this->_summary['curr_request'], 'last_request' => $this->_summary['curr_request'], 'curr_request' => 0, )); // Update DB $md5 = md5($url); $q = "UPDATE `$this->_tb` SET dateline=%d WHERE md5=%s"; $existed = $wpdb->query($wpdb->prepare($q, array(time(), $md5))); if (!$existed) { $q = "INSERT INTO `$this->_tb` SET url=%s, md5=%s, dateline=%d"; $wpdb->query($wpdb->prepare($q, array($url, $md5, time()))); } Debug2::debug('[Avatar] saved avatar ' . $file); return $this->_rewrite($url); } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_GENERATE: self::cron(true); break; default: break; } Admin::redirect(); } } src/css.cls.php 0000644 00000036215 15162130544 0007424 0 ustar 00 <?php /** * The optimize css class. * * @since 2.3 */ namespace LiteSpeed; defined('WPINC') || exit(); class CSS extends Base { const LOG_TAG = '[CSS]'; const TYPE_GEN_CCSS = 'gen_ccss'; const TYPE_CLEAR_Q_CCSS = 'clear_q_ccss'; protected $_summary; private $_ccss_whitelist; private $_queue; /** * Init * * @since 3.0 */ public function __construct() { $this->_summary = self::get_summary(); add_filter('litespeed_ccss_whitelist', array($this->cls('Data'), 'load_ccss_whitelist')); } /** * HTML lazyload CSS * @since 4.0 */ public function prepare_html_lazy() { return '<style>' . implode(',', $this->conf(self::O_OPTM_HTML_LAZY)) . '{content-visibility:auto;contain-intrinsic-size:1px 1000px;}</style>'; } /** * Output critical css * * @since 1.3 * @access public */ public function prepare_ccss() { // Get critical css for current page // Note: need to consider mobile $rules = $this->_ccss(); if (!$rules) { return null; } $error_tag = ''; if (substr($rules, 0, 2) == '/*' && substr($rules, -2) == '*/') { Core::comment('QUIC.cloud CCSS bypassed due to generation error ❌'); $error_tag = ' data-error="failed to generate"'; } // Append default critical css $rules .= $this->conf(self::O_OPTM_CCSS_CON); return '<style id="litespeed-ccss"' . $error_tag . '>' . $rules . '</style>'; } /** * Generate CCSS url tag * * @since 4.0 */ private function _gen_ccss_file_tag($request_url) { if (is_404()) { return '404'; } if ($this->conf(self::O_OPTM_CCSS_PER_URL)) { return $request_url; } $sep_uri = $this->conf(self::O_OPTM_CCSS_SEP_URI); if ($sep_uri && ($hit = Utility::str_hit_array($request_url, $sep_uri))) { Debug2::debug('[CCSS] Separate CCSS due to separate URI setting: ' . $hit); return $request_url; } $pt = Utility::page_type(); $sep_pt = $this->conf(self::O_OPTM_CCSS_SEP_POSTTYPE); if (in_array($pt, $sep_pt)) { Debug2::debug('[CCSS] Separate CCSS due to posttype setting: ' . $pt); return $request_url; } // Per posttype return $pt; } /** * The critical css content of the current page * * @since 2.3 */ private function _ccss() { global $wp; $request_url = get_permalink(); // Backup, in case get_permalink() fails. if (!$request_url) { $request_url = home_url($wp->request); } $filepath_prefix = $this->_build_filepath_prefix('ccss'); $url_tag = $this->_gen_ccss_file_tag($request_url); $vary = $this->cls('Vary')->finalize_full_varies(); $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'ccss'); if ($filename) { $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css'; if (file_exists($static_file)) { Debug2::debug2('[CSS] existing ccss ' . $static_file); Core::comment('QUIC.cloud CCSS loaded ✅ ' . $filepath_prefix . $filename . '.css'); return File::read($static_file); } } $uid = get_current_user_id(); $ua = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; // Store it to prepare for cron Core::comment('QUIC.cloud CCSS in queue'); $this->_queue = $this->load_queue('ccss'); if (count($this->_queue) > 500) { self::debug('CCSS Queue is full - 500'); return null; } $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag; $this->_queue[$queue_k] = array( 'url' => apply_filters('litespeed_ccss_url', $request_url), 'user_agent' => substr($ua, 0, 200), 'is_mobile' => $this->_separate_mobile(), 'is_webp' => $this->cls('Media')->webp_support() ? 1 : 0, 'uid' => $uid, 'vary' => $vary, 'url_tag' => $url_tag, ); // Current UA will be used to request $this->save_queue('ccss', $this->_queue); self::debug('Added queue_ccss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] ' . $uid); // Prepare cache tag for later purge Tag::add('CCSS.' . md5($queue_k)); // For v4.1- clean up if (isset($this->_summary['ccss_type_history']) || isset($this->_summary['ccss_history']) || isset($this->_summary['queue_ccss'])) { if (isset($this->_summary['ccss_type_history'])) { unset($this->_summary['ccss_type_history']); } if (isset($this->_summary['ccss_history'])) { unset($this->_summary['ccss_history']); } if (isset($this->_summary['queue_ccss'])) { unset($this->_summary['queue_ccss']); } self::save_summary(); } return null; } /** * Cron ccss generation * * @since 2.3 * @access private */ public static function cron_ccss($continue = false) { $_instance = self::cls(); return $_instance->_cron_handler('ccss', $continue); } /** * Handle UCSS/CCSS cron * * @since 4.2 */ private function _cron_handler($type, $continue) { $this->_queue = $this->load_queue($type); if (empty($this->_queue)) { return; } $type_tag = strtoupper($type); // For cron, need to check request interval too if (!$continue) { if (!empty($this->_summary['curr_request_' . $type]) && time() - $this->_summary['curr_request_' . $type] < 300 && !$this->conf(self::O_DEBUG)) { Debug2::debug('[' . $type_tag . '] Last request not done'); return; } } $i = 0; foreach ($this->_queue as $k => $v) { if (!empty($v['_status'])) { continue; } Debug2::debug('[' . $type_tag . '] cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']); if ($type == 'ccss' && empty($v['url_tag'])) { unset($this->_queue[$k]); $this->save_queue($type, $this->_queue); Debug2::debug('[CCSS] wrong queue_ccss format'); continue; } if (!isset($v['is_webp'])) { $v['is_webp'] = false; } $i++; $res = $this->_send_req($v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $type, $v['is_mobile'], $v['is_webp']); if (!$res) { // Status is wrong, drop this this->_queue unset($this->_queue[$k]); $this->save_queue($type, $this->_queue); if (!$continue) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), $type_tag); return Router::self_redirect(Router::ACTION_CSS, CSS::TYPE_GEN_CCSS); } continue; } // Exit queue if out of quota or service is hot if ($res === 'out_of_quota' || $res === 'svc_hot') { return; } $this->_queue[$k]['_status'] = 'requested'; $this->save_queue($type, $this->_queue); // only request first one if (!$continue) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), $type_tag); return Router::self_redirect(Router::ACTION_CSS, CSS::TYPE_GEN_CCSS); } } } /** * Send to QC API to generate CCSS/UCSS * * @since 2.3 * @access private */ private function _send_req($request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $type, $is_mobile, $is_webp) { // Check if has credit to push or not $err = false; $allowance = $this->cls('Cloud')->allowance(Cloud::SVC_CCSS, $err); if (!$allowance) { Debug2::debug('[CCSS] ❌ No credit: ' . $err); $err && Admin_Display::error(Error::msg($err)); return 'out_of_quota'; } set_time_limit(120); // Update css request status $this->_summary['curr_request_' . $type] = time(); self::save_summary(); // Gather guest HTML to send $html = $this->prepare_html($request_url, $user_agent, $uid); if (!$html) { return false; } // Parse HTML to gather all CSS content before requesting list($css, $html) = $this->prepare_css($html, $is_webp); if (!$css) { $type_tag = strtoupper($type); Debug2::debug('[' . $type_tag . '] ❌ No combined css'); return false; } // Generate critical css $data = array( 'url' => $request_url, 'queue_k' => $queue_k, 'user_agent' => $user_agent, 'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet 'is_webp' => $is_webp ? 1 : 0, 'html' => $html, 'css' => $css, ); if (!isset($this->_ccss_whitelist)) { $this->_ccss_whitelist = $this->_filter_whitelist(); } $data['whitelist'] = $this->_ccss_whitelist; self::debug('Generating: ', $data); $json = Cloud::post(Cloud::SVC_CCSS, $data, 30); if (!is_array($json)) { return $json; } // Old version compatibility if (empty($json['status'])) { if (!empty($json[$type])) { $this->_save_con($type, $json[$type], $queue_k, $is_mobile, $is_webp); } // Delete the row return false; } // Unknown status, remove this line if ($json['status'] != 'queued') { return false; } // Save summary data $this->_summary['last_spent_' . $type] = time() - $this->_summary['curr_request_' . $type]; $this->_summary['last_request_' . $type] = $this->_summary['curr_request_' . $type]; $this->_summary['curr_request_' . $type] = 0; self::save_summary(); return true; } /** * Save CCSS/UCSS content * * @since 4.2 */ private function _save_con($type, $css, $queue_k, $mobile, $webp) { // Add filters $css = apply_filters('litespeed_' . $type, $css, $queue_k); Debug2::debug2('[CSS] con: ' . $css); if (substr($css, 0, 2) == '/*' && substr($css, -2) == '*/') { self::debug('❌ empty ' . $type . ' [content] ' . $css); // continue; // Save the error info too } // Write to file $filecon_md5 = md5($css); $filepath_prefix = $this->_build_filepath_prefix($type); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css'; File::save($static_file, $css, true); $url_tag = $this->_queue[$queue_k]['url_tag']; $vary = $this->_queue[$queue_k]['vary']; Debug2::debug2("[CSS] Save URL to file [file] $static_file [vary] $vary"); $this->cls('Data')->save_url($url_tag, $vary, $type, $filecon_md5, dirname($static_file), $mobile, $webp); Purge::add(strtoupper($type) . '.' . md5($queue_k)); } /** * Play for fun * * @since 3.4.3 */ public function test_url($request_url) { $user_agent = $_SERVER['HTTP_USER_AGENT']; $html = $this->prepare_html($request_url, $user_agent); list($css, $html) = $this->prepare_css($html, true, true); // var_dump( $css ); // $html = <<<EOT // EOT; // $css = <<<EOT // EOT; $data = array( 'url' => $request_url, 'ccss_type' => 'test', 'user_agent' => $user_agent, 'is_mobile' => 0, 'html' => $html, 'css' => $css, 'type' => 'CCSS', ); // self::debug( 'Generating: ', $data ); $json = Cloud::post(Cloud::SVC_CCSS, $data, 180); var_dump($json); } /** * Prepare HTML from URL * * @since 3.4.3 */ public function prepare_html($request_url, $user_agent, $uid = false) { $html = $this->cls('Crawler')->self_curl(add_query_arg('LSCWP_CTRL', 'before_optm', $request_url), $user_agent, $uid); Debug2::debug2('[CSS] self_curl result....', $html); if (!$html) { return false; } $html = $this->cls('Optimizer')->html_min($html, true); // Drop <noscript>xxx</noscript> $html = preg_replace('#<noscript>.*</noscript>#isU', '', $html); return $html; } /** * Prepare CSS from HTML for CCSS generation only. UCSS will used combined CSS directly. * Prepare refined HTML for both CCSS and UCSS. * * @since 3.4.3 */ public function prepare_css($html, $is_webp = false, $dryrun = false) { $css = ''; preg_match_all('#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>#isU', $html, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $debug_info = ''; if (strpos($match[0], '<link') === 0) { $attrs = Utility::parse_attr($match[1]); if (empty($attrs['rel'])) { continue; } if ($attrs['rel'] != 'stylesheet') { if ($attrs['rel'] != 'preload' || empty($attrs['as']) || $attrs['as'] != 'style') { continue; } } if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) { continue; } if (empty($attrs['href'])) { continue; } // Check Google fonts hit if (strpos($attrs['href'], 'fonts.googleapis.com') !== false) { $html = str_replace($match[0], '', $html); continue; } $debug_info = $attrs['href']; // Load CSS content if (!$dryrun) { // Dryrun will not load CSS but just drop them $con = $this->cls('Optimizer')->load_file($attrs['href']); if (!$con) { continue; } } else { $con = ''; } } else { // Inline style $attrs = Utility::parse_attr($match[2]); if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) { continue; } Debug2::debug2('[CSS] Load inline CSS ' . substr($match[3], 0, 100) . '...', $attrs); $con = $match[3]; $debug_info = '__INLINE__'; } $con = Optimizer::minify_css($con); if ($is_webp && $this->cls('Media')->webp_support()) { $con = $this->cls('Media')->replace_background_webp($con); } if (!empty($attrs['media']) && $attrs['media'] !== 'all') { $con = '@media ' . $attrs['media'] . '{' . $con . "}\n"; } else { $con = $con . "\n"; } $con = '/* ' . $debug_info . ' */' . $con; $css .= $con; $html = str_replace($match[0], '', $html); } return array($css, $html); } /** * Filter the comment content, add quotes to selector from whitelist. Return the json * * @since 7.1 */ private function _filter_whitelist() { $whitelist = array(); $list = apply_filters('litespeed_ccss_whitelist', $this->conf(self::O_OPTM_CCSS_SELECTOR_WHITELIST)); foreach ($list as $v) { if (substr($v, 0, 2) === '//') { continue; } $whitelist[] = $v; } return $whitelist; } /** * Notify finished from server * @since 7.1 */ public function notify() { $post_data = \json_decode(file_get_contents('php://input'), true); if (is_null($post_data)) { $post_data = $_POST; } self::debug('notify() data', $post_data); $this->_queue = $this->load_queue('ccss'); list($post_data) = $this->cls('Cloud')->extract_msg($post_data, 'ccss'); $notified_data = $post_data['data']; if (empty($notified_data) || !is_array($notified_data)) { self::debug('❌ notify exit: no notified data'); return Cloud::err('no notified data'); } // Check if its in queue or not $valid_i = 0; foreach ($notified_data as $v) { if (empty($v['request_url'])) { self::debug('❌ notify bypass: no request_url', $v); continue; } if (empty($v['queue_k'])) { self::debug('❌ notify bypass: no queue_k', $v); continue; } if (empty($this->_queue[$v['queue_k']])) { self::debug('❌ notify bypass: no this queue [q_k]' . $v['queue_k']); continue; } // Save data if (!empty($v['data_ccss'])) { $is_mobile = $this->_queue[$v['queue_k']]['is_mobile']; $is_webp = $this->_queue[$v['queue_k']]['is_webp']; $this->_save_con('ccss', $v['data_ccss'], $v['queue_k'], $is_mobile, $is_webp); $valid_i++; } unset($this->_queue[$v['queue_k']]); self::debug('notify data handled, unset queue [q_k] ' . $v['queue_k']); } $this->save_queue('ccss', $this->_queue); self::debug('notified'); return Cloud::ok(array('count' => $valid_i)); } /** * Handle all request actions from main cls * * @since 2.3 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_GEN_CCSS: self::cron_ccss(true); break; case self::TYPE_CLEAR_Q_CCSS: $this->clear_q('ccss'); break; default: break; } Admin::redirect(); } } src/esi.cls.php 0000644 00000065701 15162130547 0007421 0 ustar 00 <?php /** * The ESI class. * * This is used to define all esi related functions. * * @since 1.1.3 * @package LiteSpeed * @subpackage LiteSpeed/src * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class ESI extends Root { const LOG_TAG = '⏺'; private static $has_esi = false; private static $_combine_ids = array(); private $esi_args = null; private $_esi_preserve_list = array(); private $_nonce_actions = array(-1 => ''); // val is cache control const QS_ACTION = 'lsesi'; const QS_PARAMS = 'esi'; const COMBO = '__combo'; // ESI include combine='main' handler const PARAM_ARGS = 'args'; const PARAM_ID = 'id'; const PARAM_INSTANCE = 'instance'; const PARAM_NAME = 'name'; const WIDGET_O_ESIENABLE = 'widget_esi_enable'; const WIDGET_O_TTL = 'widget_ttl'; /** * Confructor of ESI * * @since 1.2.0 * @since 4.0 Change to be after Vary init in hook 'after_setup_theme' */ public function init() { /** * Bypass ESI related funcs if disabled ESI to fix potential DIVI compatibility issue * @since 2.9.7.2 */ if (Router::is_ajax() || !$this->cls('Router')->esi_enabled()) { return; } // Guest mode, don't need to use ESI if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { return; } if (defined('LITESPEED_ESI_OFF')) { return; } // If page is not cacheable if (defined('DONOTCACHEPAGE') && apply_filters('litespeed_const_DONOTCACHEPAGE', DONOTCACHEPAGE)) { return; } // Init ESI in `after_setup_theme` hook after detected if LITESPEED_DISABLE_ALL is ON or not $this->_hooks(); /** * Overwrite wp_create_nonce func * @since 2.9.5 */ $this->_transform_nonce(); !defined('LITESPEED_ESI_INITED') && define('LITESPEED_ESI_INITED', true); } /** * Init ESI related hooks * * Load delayed by hook to give the ability to bypass by LITESPEED_DISABLE_ALL const * * @since 2.9.7.2 * @since 4.0 Changed to private from public * @access private */ private function _hooks() { add_filter('template_include', array($this, 'esi_template'), 99999); add_action('load-widgets.php', __NAMESPACE__ . '\Purge::purge_widget'); add_action('wp_update_comment_count', __NAMESPACE__ . '\Purge::purge_comment_widget'); /** * Recover REQUEST_URI * @since 1.8.1 */ if (!empty($_GET[self::QS_ACTION])) { self::debug('ESI req'); $this->_register_esi_actions(); } /** * Shortcode ESI * * To use it, just change the original shortcode as below: * old: [someshortcode aa='bb'] * new: [esi someshortcode aa='bb' cache='private,no-vary' ttl='600'] * * 1. `cache` attribute is optional, default to 'public,no-vary'. * 2. `ttl` attribute is optional, default is your public TTL setting. * 3. `_ls_silence` attribute is optional, default is false. * * @since 2.8 * @since 2.8.1 Check is_admin for Elementor compatibility #726013 */ if (!is_admin()) { add_shortcode('esi', array($this, 'shortcode')); } } /** * Take over all nonce calls and transform to ESI * * @since 2.9.5 */ private function _transform_nonce() { if (is_admin()) { return; } // Load ESI nonces in conf $nonces = $this->conf(Base::O_ESI_NONCE); add_filter('litespeed_esi_nonces', array($this->cls('Data'), 'load_esi_nonces')); if ($nonces = apply_filters('litespeed_esi_nonces', $nonces)) { foreach ($nonces as $action) { $this->nonce_action($action); } } add_action('litespeed_nonce', array($this, 'nonce_action')); } /** * Register a new nonce action to convert it to ESI * * @since 2.9.5 */ public function nonce_action($action) { // Split the Cache Control $action = explode(' ', $action); $control = !empty($action[1]) ? $action[1] : ''; $action = $action[0]; // Wildcard supported $action = Utility::wildcard2regex($action); if (array_key_exists($action, $this->_nonce_actions)) { return; } $this->_nonce_actions[$action] = $control; // Debug2::debug('[ESI] Appended nonce action to nonce list [action] ' . $action); } /** * Check if an action is registered to replace ESI * * @since 2.9.5 */ public function is_nonce_action($action) { // If GM not run yet, then ESI not init yet, then ESI nonce will not be allowed even nonce func replaced. if (!defined('LITESPEED_ESI_INITED')) { return null; } if (is_admin()) { return null; } if (defined('LITESPEED_ESI_OFF')) { return null; } foreach ($this->_nonce_actions as $k => $v) { if (strpos($k, '*') !== false) { if (preg_match('#' . $k . '#iU', $action)) { return $v; } } else { if ($k == $action) { return $v; } } } return null; } /** * Shortcode ESI * * @since 2.8 * @access public */ public function shortcode($atts) { if (empty($atts[0])) { Debug2::debug('[ESI] ===shortcode wrong format', $atts); return 'Wrong shortcode esi format'; } $cache = 'public,no-vary'; if (!empty($atts['cache'])) { $cache = $atts['cache']; unset($atts['cache']); } $silence = false; if (!empty($atts['_ls_silence'])) { $silence = true; } do_action('litespeed_esi_shortcode-' . $atts[0]); // Show ESI link return $this->sub_esi_block('esi', 'esi-shortcode', $atts, $cache, $silence); } /** * Check if the requested page has esi elements. If so, return esi on * header. * * @since 1.1.3 * @access public * @return string Esi On header if request has esi, empty string otherwise. */ public static function has_esi() { return self::$has_esi; } /** * Sets that the requested page has esi elements. * * @since 1.1.3 * @access public */ public static function set_has_esi() { self::$has_esi = true; } /** * Register all of the hooks related to the esi logic of the plugin. * Specifically when the page IS an esi page. * * @since 1.1.3 * @access private */ private function _register_esi_actions() { /** * This hook is in `init` * For any plugin need to check if page is ESI, use `LSCACHE_IS_ESI` check after `init` hook */ !defined('LSCACHE_IS_ESI') && define('LSCACHE_IS_ESI', $_GET[self::QS_ACTION]); // Reused this to ESI block ID !empty($_SERVER['ESI_REFERER']) && defined('LSCWP_LOG') && Debug2::debug('[ESI] ESI_REFERER: ' . $_SERVER['ESI_REFERER']); /** * Only when ESI's parent is not REST, replace REQUEST_URI to avoid breaking WP5 editor REST call * @since 2.9.3 */ if (!empty($_SERVER['ESI_REFERER']) && !$this->cls('REST')->is_rest($_SERVER['ESI_REFERER'])) { self::debug('overwrite REQUEST_URI to ESI_REFERER [from] ' . $_SERVER['REQUEST_URI'] . ' [to] ' . $_SERVER['ESI_REFERER']); if (!empty($_SERVER['ESI_REFERER'])) { $_SERVER['REQUEST_URI'] = $_SERVER['ESI_REFERER']; if (substr(get_option('permalink_structure'), -1) === '/' && strpos($_SERVER['ESI_REFERER'], '?') === false) { $_SERVER['REQUEST_URI'] = trailingslashit($_SERVER['ESI_REFERER']); } } # Prevent from 301 redirecting if (!empty($_SERVER['SCRIPT_URI'])) { $SCRIPT_URI = parse_url($_SERVER['SCRIPT_URI']); $SCRIPT_URI['path'] = $_SERVER['REQUEST_URI']; Utility::compatibility(); $_SERVER['SCRIPT_URI'] = http_build_url($SCRIPT_URI); } } if (!empty($_SERVER['ESI_CONTENT_TYPE']) && strpos($_SERVER['ESI_CONTENT_TYPE'], 'application/json') === 0) { add_filter('litespeed_is_json', '__return_true'); } /** * Make REST call be able to parse ESI * NOTE: Not effective due to ESI req are all to `/` yet * @since 2.9.4 */ add_action('rest_api_init', array($this, 'load_esi_block'), 101); // Register ESI blocks add_action('litespeed_esi_load-widget', array($this, 'load_widget_block')); add_action('litespeed_esi_load-admin-bar', array($this, 'load_admin_bar_block')); add_action('litespeed_esi_load-comment-form', array($this, 'load_comment_form_block')); add_action('litespeed_esi_load-nonce', array($this, 'load_nonce_block')); add_action('litespeed_esi_load-esi', array($this, 'load_esi_shortcode')); add_action('litespeed_esi_load-' . self::COMBO, array($this, 'load_combo')); } /** * Hooked to the template_include action. * Selects the esi template file when the post type is a LiteSpeed ESI page. * * @since 1.1.3 * @access public * @param string $template The template path filtered. * @return string The new template path. */ public function esi_template($template) { // Check if is an ESI request if (defined('LSCACHE_IS_ESI')) { self::debug('calling ESI template'); return LSCWP_DIR . 'tpl/esi.tpl.php'; } self::debug('calling default template'); $this->_register_not_esi_actions(); return $template; } /** * Register all of the hooks related to the esi logic of the plugin. * Specifically when the page is NOT an esi page. * * @since 1.1.3 * @access private */ private function _register_not_esi_actions() { do_action('litespeed_tpl_normal'); if (!Control::is_cacheable()) { return; } if (Router::is_ajax()) { return; } add_filter('widget_display_callback', array($this, 'sub_widget_block'), 0, 3); // Add admin_bar esi if (Router::is_logged_in()) { remove_action('wp_body_open', 'wp_admin_bar_render', 0); // Remove default Admin bar. Fix https://github.com/elementor/elementor/issues/25198 remove_action('wp_footer', 'wp_admin_bar_render', 1000); add_action('wp_footer', array($this, 'sub_admin_bar_block'), 1000); } // Add comment forum esi for logged-in user or commenter if (!Router::is_ajax() && Vary::has_vary()) { add_filter('comment_form_defaults', array($this, 'register_comment_form_actions')); } } /** * Set an ESI to be combine='sub' * * @since 3.4.2 */ public static function combine($block_id) { if (!isset($_SERVER['X-LSCACHE']) || strpos($_SERVER['X-LSCACHE'], 'combine') === false) { return; } if (in_array($block_id, self::$_combine_ids)) { return; } self::$_combine_ids[] = $block_id; } /** * Load combined ESI * * @since 3.4.2 */ public function load_combo() { Control::set_nocache('ESI combine request'); if (empty($_POST['esi_include'])) { return; } self::set_has_esi(); Debug2::debug('[ESI] 🍔 Load combo', $_POST['esi_include']); $output = ''; foreach ($_POST['esi_include'] as $url) { $qs = parse_url(htmlspecialchars_decode($url), PHP_URL_QUERY); parse_str($qs, $qs); if (empty($qs[self::QS_ACTION])) { continue; } $esi_id = $qs[self::QS_ACTION]; $esi_param = !empty($qs[self::QS_PARAMS]) ? $this->_parse_esi_param($qs[self::QS_PARAMS]) : false; $inline_param = apply_filters('litespeed_esi_inline-' . $esi_id, array(), $esi_param); // Returned array need to be [ val, control, tag ] if ($inline_param) { $output .= self::_build_inline($url, $inline_param); } } echo $output; } /** * Build a whole inline segment * * @since 3.4.2 */ private static function _build_inline($url, $inline_param) { if (!$url || empty($inline_param['val']) || empty($inline_param['control']) || empty($inline_param['tag'])) { return ''; } $url = esc_attr($url); $control = esc_attr($inline_param['control']); $tag = esc_attr($inline_param['tag']); return "<esi:inline name='$url' cache-control='" . $control . "' cache-tag='" . $tag . "'>" . $inline_param['val'] . '</esi:inline>'; } /** * Build the esi url. This method will build the html comment wrapper as well as serialize and encode the parameter array. * * The block_id parameter should contain alphanumeric and '-_' only. * * @since 1.1.3 * @access private * @param string $block_id The id to use to display the correct esi block. * @param string $wrapper The wrapper for the esi comments. * @param array $params The esi parameters. * @param string $control The cache control attribute if any. * @param bool $silence If generate wrapper comment or not * @param bool $preserved If this ESI block is used in any filter, need to temporarily convert it to a string to avoid the HTML tag being removed/filtered. * @param bool $svar If store the value in memory or not, in memory will be faster * @param array $inline_val If show the current value for current request( this can avoid multiple esi requests in first time cache generating process ) */ public function sub_esi_block( $block_id, $wrapper, $params = array(), $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_param = array() ) { if (empty($block_id) || !is_array($params) || preg_match('/[^\w-]/', $block_id)) { return false; } if (defined('LITESPEED_ESI_OFF')) { Debug2::debug('[ESI] ESI OFF so force loading [block_id] ' . $block_id); do_action('litespeed_esi_load-' . $block_id, $params); return; } if ($silence) { // Don't add comment to esi block ( original for nonce used in tag property data-nonce='esi_block' ) $params['_ls_silence'] = true; } if ($this->cls('REST')->is_rest() || $this->cls('REST')->is_internal_rest()) { $params['is_json'] = 1; } $params = apply_filters('litespeed_esi_params', $params, $block_id); $control = apply_filters('litespeed_esi_control', $control, $block_id); if (!is_array($params) || !is_string($control)) { defined('LSCWP_LOG') && Debug2::debug("[ESI] 🛑 Sub hooks returned Params: \n" . var_export($params, true) . "\ncache control: \n" . var_export($control, true)); return false; } // Build params for URL $appended_params = array( self::QS_ACTION => $block_id, ); if (!empty($control)) { $appended_params['_control'] = $control; } if ($params) { $appended_params[self::QS_PARAMS] = base64_encode(\json_encode($params)); Debug2::debug2('[ESI] param ', $params); } // Append hash $appended_params['_hash'] = $this->_gen_esi_md5($appended_params); /** * Escape potential chars * @since 2.9.4 */ $appended_params = array_map('urlencode', $appended_params); // Generate ESI URL $url = add_query_arg($appended_params, trailingslashit(wp_make_link_relative(home_url()))); $output = ''; if ($inline_param) { $output .= self::_build_inline($url, $inline_param); } $output .= "<esi:include src='$url'"; if (!empty($control)) { $control = esc_attr($control); $output .= " cache-control='$control'"; } if ($svar) { $output .= " as-var='1'"; } if (in_array($block_id, self::$_combine_ids)) { $output .= " combine='sub'"; } if ($block_id == self::COMBO && isset($_SERVER['X-LSCACHE']) && strpos($_SERVER['X-LSCACHE'], 'combine') !== false) { $output .= " combine='main'"; } $output .= ' />'; if (!$silence) { $output = "<!-- lscwp $wrapper -->$output<!-- lscwp $wrapper esi end -->"; } self::debug("💕 [BLock_ID] $block_id \t[wrapper] $wrapper \t\t[Control] $control"); self::debug2($output); self::set_has_esi(); // Convert to string to avoid html chars filter when using // Will reverse the buffer when output in self::finalize() if ($preserved) { $hash = md5($output); $this->_esi_preserve_list[$hash] = $output; self::debug("Preserved to $hash"); return $hash; } return $output; } /** * Generate ESI hash md5 * * @since 2.9.6 * @access private */ private function _gen_esi_md5($params) { $keys = array(self::QS_ACTION, '_control', self::QS_PARAMS); $str = ''; foreach ($keys as $v) { if (isset($params[$v]) && is_string($params[$v])) { $str .= $params[$v]; } } Debug2::debug2('[ESI] md5_string=' . $str); return md5($this->conf(Base::HASH) . $str); } /** * Parses the request parameters on an ESI request * * @since 1.1.3 * @access private */ private function _parse_esi_param($qs_params = false) { $req_params = false; if ($qs_params) { $req_params = $qs_params; } elseif (isset($_REQUEST[self::QS_PARAMS])) { $req_params = $_REQUEST[self::QS_PARAMS]; } if (!$req_params) { return false; } $unencrypted = base64_decode($req_params); if ($unencrypted === false) { return false; } Debug2::debug2('[ESI] params', $unencrypted); // $unencoded = urldecode($unencrypted); no need to do this as $_GET is already parsed $params = \json_decode($unencrypted, true); return $params; } /** * Select the correct esi output based on the parameters in an ESI request. * * @since 1.1.3 * @access public */ public function load_esi_block() { /** * Validate if is a legal ESI req * @since 2.9.6 */ if (empty($_GET['_hash']) || $this->_gen_esi_md5($_GET) != $_GET['_hash']) { Debug2::debug('[ESI] ❌ Failed to validate _hash'); return; } $params = $this->_parse_esi_param(); if (defined('LSCWP_LOG')) { $logInfo = '[ESI] ⭕ '; if (!empty($params[self::PARAM_NAME])) { $logInfo .= ' Name: ' . $params[self::PARAM_NAME] . ' ----- '; } $logInfo .= ' [ID] ' . LSCACHE_IS_ESI; Debug2::debug($logInfo); } if (!empty($params['_ls_silence'])) { !defined('LSCACHE_ESI_SILENCE') && define('LSCACHE_ESI_SILENCE', true); } /** * Buffer needs to be JSON format * @since 2.9.4 */ if (!empty($params['is_json'])) { add_filter('litespeed_is_json', '__return_true'); } Tag::add(rtrim(Tag::TYPE_ESI, '.')); Tag::add(Tag::TYPE_ESI . LSCACHE_IS_ESI); // Debug2::debug(var_export($params, true )); /** * Handle default cache control 'private,no-vary' for sub_esi_block() @ticket #923505 * * @since 2.2.3 */ if (!empty($_GET['_control'])) { $control = explode(',', $_GET['_control']); if (in_array('private', $control)) { Control::set_private(); } if (in_array('no-vary', $control)) { Control::set_no_vary(); } } do_action('litespeed_esi_load-' . LSCACHE_IS_ESI, $params); } // The *_sub_* functions are helpers for the sub_* functions. // The *_load_* functions are helpers for the load_* functions. /** * Loads the default options for default WordPress widgets. * * @since 1.1.3 * @access public */ public static function widget_default_options($options, $widget) { if (!is_array($options)) { return $options; } $widget_name = get_class($widget); switch ($widget_name) { case 'WP_Widget_Recent_Posts': case 'WP_Widget_Recent_Comments': $options[self::WIDGET_O_ESIENABLE] = Base::VAL_OFF; $options[self::WIDGET_O_TTL] = 86400; break; default: break; } return $options; } /** * Hooked to the widget_display_callback filter. * If the admin configured the widget to display via esi, this function * will set up the esi request and cancel the widget display. * * @since 1.1.3 * @access public * @param array $instance Parameter used to build the widget. * @param WP_Widget $widget The widget to build. * @param array $args Parameter used to build the widget. * @return mixed Return false if display through esi, instance otherwise. */ public function sub_widget_block($instance, $widget, $args) { // #210407 if (!is_array($instance)) { return $instance; } $name = get_class($widget); if (!isset($instance[Base::OPTION_NAME])) { return $instance; } $options = $instance[Base::OPTION_NAME]; if (!isset($options) || !$options[self::WIDGET_O_ESIENABLE]) { defined('LSCWP_LOG') && Debug2::debug('ESI 0 ' . $name . ': ' . (!isset($options) ? 'not set' : 'set off')); return $instance; } $esi_private = $options[self::WIDGET_O_ESIENABLE] == Base::VAL_ON2 ? 'private,' : ''; $params = array( self::PARAM_NAME => $name, self::PARAM_ID => $widget->id, self::PARAM_INSTANCE => $instance, self::PARAM_ARGS => $args, ); echo $this->sub_esi_block('widget', 'widget ' . $name, $params, $esi_private . 'no-vary'); return false; } /** * Hooked to the wp_footer action. * Sets up the ESI request for the admin bar. * * @access public * @since 1.1.3 * @global type $wp_admin_bar */ public function sub_admin_bar_block() { global $wp_admin_bar; if (!is_admin_bar_showing() || !is_object($wp_admin_bar)) { return; } // To make each admin bar ESI request different for `Edit` button different link $params = array( 'ref' => $_SERVER['REQUEST_URI'], ); echo $this->sub_esi_block('admin-bar', 'adminbar', $params); } /** * Parses the esi input parameters and generates the widget for esi display. * * @access public * @since 1.1.3 * @global $wp_widget_factory * @param array $params Input parameters needed to correctly display widget */ public function load_widget_block($params) { // global $wp_widget_factory; // $widget = $wp_widget_factory->widgets[ $params[ self::PARAM_NAME ] ]; $option = $params[self::PARAM_INSTANCE]; $option = $option[Base::OPTION_NAME]; // Since we only reach here via esi, safe to assume setting exists. $ttl = $option[self::WIDGET_O_TTL]; defined('LSCWP_LOG') && Debug2::debug('ESI widget render: name ' . $params[self::PARAM_NAME] . ', id ' . $params[self::PARAM_ID] . ', ttl ' . $ttl); if ($ttl == 0) { Control::set_nocache('ESI Widget time to live set to 0'); } else { Control::set_custom_ttl($ttl); if ($option[self::WIDGET_O_ESIENABLE] == Base::VAL_ON2) { Control::set_private(); } Control::set_no_vary(); Tag::add(Tag::TYPE_WIDGET . $params[self::PARAM_ID]); } the_widget($params[self::PARAM_NAME], $params[self::PARAM_INSTANCE], $params[self::PARAM_ARGS]); } /** * Generates the admin bar for esi display. * * @access public * @since 1.1.3 */ public function load_admin_bar_block($params) { if (!empty($params['ref'])) { $ref_qs = parse_url($params['ref'], PHP_URL_QUERY); if (!empty($ref_qs)) { parse_str($ref_qs, $ref_qs_arr); if (!empty($ref_qs_arr)) { foreach ($ref_qs_arr as $k => $v) { $_GET[$k] = $v; } } } } // Needed when permalink structure is "Plain" wp(); wp_admin_bar_render(); if (!$this->conf(Base::O_ESI_CACHE_ADMBAR)) { Control::set_nocache('build-in set to not cacheable'); } else { Control::set_private(); Control::set_no_vary(); } defined('LSCWP_LOG') && Debug2::debug('ESI: adminbar ref: ' . $_SERVER['REQUEST_URI']); } /** * Parses the esi input parameters and generates the comment form for esi display. * * @access public * @since 1.1.3 * @param array $params Input parameters needed to correctly display comment form */ public function load_comment_form_block($params) { comment_form($params[self::PARAM_ARGS], $params[self::PARAM_ID]); if (!$this->conf(Base::O_ESI_CACHE_COMMFORM)) { Control::set_nocache('build-in set to not cacheable'); } else { // by default comment form is public if (Vary::has_vary()) { Control::set_private(); Control::set_no_vary(); } } } /** * Generate nonce for certain action * * @access public * @since 2.6 */ public function load_nonce_block($params) { $action = $params['action']; Debug2::debug('[ESI] load_nonce_block [action] ' . $action); // set nonce TTL to half day Control::set_custom_ttl(43200); if (Router::is_logged_in()) { Control::set_private(); } if (function_exists('wp_create_nonce_litespeed_esi')) { echo wp_create_nonce_litespeed_esi($action); } else { echo wp_create_nonce($action); } } /** * Show original shortcode * * @access public * @since 2.8 */ public function load_esi_shortcode($params) { if (isset($params['ttl'])) { if (!$params['ttl']) { Control::set_nocache('ESI shortcode att ttl=0'); } else { Control::set_custom_ttl($params['ttl']); } unset($params['ttl']); } // Replace to original shortcode $shortcode = $params[0]; $atts_ori = array(); foreach ($params as $k => $v) { if ($k === 0) { continue; } $atts_ori[] = is_string($k) ? "$k='" . addslashes($v) . "'" : $v; } Tag::add(Tag::TYPE_ESI . "esi.$shortcode"); // Output original shortcode final content echo do_shortcode("[$shortcode " . implode(' ', $atts_ori) . ' ]'); } /** * Hooked to the comment_form_defaults filter. * Stores the default comment form settings. * If sub_comment_form_block is triggered, the output buffer is cleared and an esi block is added. The remaining comment form is also buffered and cleared. * Else there is no need to make the comment form ESI. * * @since 1.1.3 * @access public */ public function register_comment_form_actions($defaults) { $this->esi_args = $defaults; echo GUI::clean_wrapper_begin(); add_filter('comment_form_submit_button', array($this, 'sub_comment_form_btn'), 1000, 2); // To save the params passed in add_action('comment_form', array($this, 'sub_comment_form_block'), 1000); return $defaults; } /** * Store the args passed in comment_form for the ESI comment param usage in `$this->sub_comment_form_block()` * * @since 3.4 * @access public */ public function sub_comment_form_btn($unused, $args) { if (empty($args) || empty($this->esi_args)) { Debug2::debug('comment form args empty?'); return $unused; } $esi_args = array(); // compare current args with default ones foreach ($args as $k => $v) { if (!isset($this->esi_args[$k])) { $esi_args[$k] = $v; } elseif (is_array($v)) { $diff = array_diff_assoc($v, $this->esi_args[$k]); if (!empty($diff)) { $esi_args[$k] = $diff; } } elseif ($v !== $this->esi_args[$k]) { $esi_args[$k] = $v; } } $this->esi_args = $esi_args; return $unused; } /** * Hooked to the comment_form_submit_button filter. * * This method will compare the used comment form args against the default args. The difference will be passed to the esi request. * * @access public * @since 1.1.3 */ public function sub_comment_form_block($post_id) { echo GUI::clean_wrapper_end(); $params = array( self::PARAM_ID => $post_id, self::PARAM_ARGS => $this->esi_args, ); echo $this->sub_esi_block('comment-form', 'comment form', $params); echo GUI::clean_wrapper_begin(); add_action('comment_form_after', array($this, 'comment_form_sub_clean')); } /** * Hooked to the comment_form_after action. * Cleans up the remaining comment form output. * * @since 1.1.3 * @access public */ public function comment_form_sub_clean() { echo GUI::clean_wrapper_end(); } /** * Replace preserved blocks * * @since 2.6 * @access public */ public function finalize($buffer) { // Prepend combo esi block if (self::$_combine_ids) { Debug2::debug('[ESI] 🍔 Enabled combo'); $esi_block = $this->sub_esi_block(self::COMBO, '__COMBINE_MAIN__', array(), 'no-cache', true); $buffer = $esi_block . $buffer; } // Bypass if no preserved list to be replaced if (!$this->_esi_preserve_list) { return $buffer; } $keys = array_keys($this->_esi_preserve_list); Debug2::debug('[ESI] replacing preserved blocks', $keys); $buffer = str_replace($keys, $this->_esi_preserve_list, $buffer); return $buffer; } /** * Check if the content contains preserved list or not * * @since 3.3 */ public function contain_preserve_esi($content) { $hit_list = array(); foreach ($this->_esi_preserve_list as $k => $v) { if (strpos($content, '"' . $k . '"') !== false) { $hit_list[] = '"' . $k . '"'; } if (strpos($content, "'" . $k . "'") !== false) { $hit_list[] = "'" . $k . "'"; } } return $hit_list; } } src/tag.cls.php 0000644 00000021633 15162130552 0007404 0 ustar 00 <?php /** * The plugin cache-tag class for X-LiteSpeed-Tag * * @since 1.1.3 * @since 1.5 Moved into /inc */ namespace LiteSpeed; defined('WPINC') || exit(); class Tag extends Root { const TYPE_FEED = 'FD'; const TYPE_FRONTPAGE = 'F'; const TYPE_HOME = 'H'; const TYPE_PAGES = 'PGS'; const TYPE_PAGES_WITH_RECENT_POSTS = 'PGSRP'; const TYPE_HTTP = 'HTTP.'; const TYPE_POST = 'Po.'; // Post. Cannot use P, reserved for litemage. const TYPE_ARCHIVE_POSTTYPE = 'PT.'; const TYPE_ARCHIVE_TERM = 'T.'; //for is_category|is_tag|is_tax const TYPE_AUTHOR = 'A.'; const TYPE_ARCHIVE_DATE = 'D.'; const TYPE_BLOG = 'B.'; const TYPE_LOGIN = 'L'; const TYPE_URL = 'URL.'; const TYPE_WIDGET = 'W.'; const TYPE_ESI = 'ESI.'; const TYPE_REST = 'REST'; const TYPE_AJAX = 'AJAX.'; const TYPE_LIST = 'LIST'; const TYPE_MIN = 'MIN'; const TYPE_LOCALRES = 'LOCALRES'; const X_HEADER = 'X-LiteSpeed-Tag'; private static $_tags = array(); private static $_tags_priv = array('tag_priv'); public static $error_code_tags = array(403, 404, 500); /** * Initialize * * @since 4.0 */ public function init() { // register recent posts widget tag before theme renders it to make it work add_filter('widget_posts_args', array($this, 'add_widget_recent_posts')); } /** * Check if the login page is cacheable. * If not, unset the cacheable member variable. * * NOTE: This is checked separately because login page doesn't go through WP logic. * * @since 1.0.0 * @access public */ public function check_login_cacheable() { if (!$this->conf(Base::O_CACHE_PAGE_LOGIN)) { return; } if (Control::isset_notcacheable()) { return; } if (!empty($_GET)) { Control::set_nocache('has GET request'); return; } $this->cls('Control')->set_cacheable(); self::add(self::TYPE_LOGIN); // we need to send lsc-cookie manually to make it be sent to all other users when is cacheable $list = headers_list(); if (empty($list)) { return; } foreach ($list as $hdr) { if (strncasecmp($hdr, 'set-cookie:', 11) == 0) { $cookie = substr($hdr, 12); @header('lsc-cookie: ' . $cookie, false); } } } /** * Register purge tag for pages with recent posts widget * of the plugin. * * @since 1.0.15 * @access public * @param array $params [wordpress params for widget_posts_args] */ public function add_widget_recent_posts($params) { self::add(self::TYPE_PAGES_WITH_RECENT_POSTS); return $params; } /** * Adds cache tags to the list of cache tags for the current page. * * @since 1.0.5 * @access public * @param mixed $tags A string or array of cache tags to add to the current list. */ public static function add($tags) { if (!is_array($tags)) { $tags = array($tags); } Debug2::debug('💰 [Tag] Add ', $tags); self::$_tags = array_merge(self::$_tags, $tags); // Send purge header immediately $tag_header = self::cls()->output(true); @header($tag_header); } /** * Add a post id to cache tag * * @since 3.0 * @access public */ public static function add_post($pid) { self::add(self::TYPE_POST . $pid); } /** * Add a widget id to cache tag * * @since 3.0 * @access public */ public static function add_widget($id) { self::add(self::TYPE_WIDGET . $id); } /** * Add a private ESI to cache tag * * @since 3.0 * @access public */ public static function add_private_esi($tag) { self::add_private(self::TYPE_ESI . $tag); } /** * Adds private cache tags to the list of cache tags for the current page. * * @since 1.6.3 * @access public * @param mixed $tags A string or array of cache tags to add to the current list. */ public static function add_private($tags) { if (!is_array($tags)) { $tags = array($tags); } self::$_tags_priv = array_merge(self::$_tags_priv, $tags); } /** * Return tags for Admin QS * * @since 1.1.3 * @access public */ public static function output_tags() { return self::$_tags; } /** * Will get a hash of the URI. Removes query string and appends a '/' if it is missing. * * @since 1.0.12 * @access public * @param string $uri The uri to get the hash of. * @param boolean $ori Return the original url or not * @return bool|string False on input error, hash otherwise. */ public static function get_uri_tag($uri, $ori = false) { $no_qs = strtok($uri, '?'); if (empty($no_qs)) { return false; } $slashed = trailingslashit($no_qs); // If only needs uri tag if ($ori) { return $slashed; } if (defined('LSCWP_LOG')) { return self::TYPE_URL . $slashed; } return self::TYPE_URL . md5($slashed); } /** * Get the unique tag based on self url. * * @since 1.1.3 * @access public * @param boolean $ori Return the original url or not */ public static function build_uri_tag($ori = false) { return self::get_uri_tag(urldecode($_SERVER['REQUEST_URI']), $ori); } /** * Gets the cache tags to set for the page. * * This includes site wide post types (e.g. front page) as well as * any third party plugin specific cache tags. * * @since 1.0.0 * @access private * @return array The list of cache tags to set. */ private static function _build_type_tags() { $tags = array(); $tags[] = Utility::page_type(); $tags[] = self::build_uri_tag(); if (is_front_page()) { $tags[] = self::TYPE_FRONTPAGE; } elseif (is_home()) { $tags[] = self::TYPE_HOME; } global $wp_query; if (isset($wp_query)) { $queried_obj_id = get_queried_object_id(); if (is_archive()) { //An Archive is a Category, Tag, Author, Date, Custom Post Type or Custom Taxonomy based pages. if (is_category() || is_tag() || is_tax()) { $tags[] = self::TYPE_ARCHIVE_TERM . $queried_obj_id; } elseif (is_post_type_archive() && ($post_type = get_post_type())) { $tags[] = self::TYPE_ARCHIVE_POSTTYPE . $post_type; } elseif (is_author()) { $tags[] = self::TYPE_AUTHOR . $queried_obj_id; } elseif (is_date()) { global $post; if ($post && isset($post->post_date)) { $date = $post->post_date; $date = strtotime($date); if (is_day()) { $tags[] = self::TYPE_ARCHIVE_DATE . date('Ymd', $date); } elseif (is_month()) { $tags[] = self::TYPE_ARCHIVE_DATE . date('Ym', $date); } elseif (is_year()) { $tags[] = self::TYPE_ARCHIVE_DATE . date('Y', $date); } } } } elseif (is_singular()) { //$this->is_singular = $this->is_single || $this->is_page || $this->is_attachment; $tags[] = self::TYPE_POST . $queried_obj_id; if (is_page()) { $tags[] = self::TYPE_PAGES; } } elseif (is_feed()) { $tags[] = self::TYPE_FEED; } } // Check REST API if (REST::cls()->is_rest()) { $tags[] = self::TYPE_REST; $path = !empty($_SERVER['SCRIPT_URL']) ? $_SERVER['SCRIPT_URL'] : false; if ($path) { // posts collections tag if (substr($path, -6) == '/posts') { $tags[] = self::TYPE_LIST; // Not used for purge yet } // single post tag global $post; if (!empty($post->ID) && substr($path, -strlen($post->ID) - 1) === '/' . $post->ID) { $tags[] = self::TYPE_POST . $post->ID; } // pages collections & single page tag if (stripos($path, '/pages') !== false) { $tags[] = self::TYPE_PAGES; } } } // Append AJAX action tag if (Router::is_ajax() && !empty($_REQUEST['action'])) { $tags[] = self::TYPE_AJAX . $_REQUEST['action']; } return $tags; } /** * Generate all cache tags before output * * @access private * @since 1.1.3 */ private static function _finalize() { // run 3rdparty hooks to tag do_action('litespeed_tag_finalize'); // generate wp tags if (!defined('LSCACHE_IS_ESI')) { $type_tags = self::_build_type_tags(); self::$_tags = array_merge(self::$_tags, $type_tags); } if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { self::$_tags[] = 'guest'; } // append blog main tag self::$_tags[] = ''; // removed duplicates self::$_tags = array_unique(self::$_tags); } /** * Sets up the Cache Tags header. * ONLY need to run this if is cacheable * * @since 1.1.3 * @access public * @return string empty string if empty, otherwise the cache tags header. */ public function output($no_finalize = false) { if (defined('LSCACHE_NO_CACHE') && LSCACHE_NO_CACHE) { return; } if (!$no_finalize) { self::_finalize(); } $prefix_tags = array(); /** * Only append blog_id when is multisite * @since 2.9.3 */ $prefix = LSWCP_TAG_PREFIX . (is_multisite() ? get_current_blog_id() : '') . '_'; // If is_private and has private tags, append them first, then specify prefix to `public` for public tags if (Control::is_private()) { foreach (self::$_tags_priv as $priv_tag) { $prefix_tags[] = $prefix . $priv_tag; } $prefix = 'public:' . $prefix; } foreach (self::$_tags as $tag) { $prefix_tags[] = $prefix . $tag; } $hdr = self::X_HEADER . ': ' . implode(',', $prefix_tags); return $hdr; } } src/debug2.cls.php 0000644 00000032126 15162130554 0010002 0 ustar 00 <?php /** * The plugin logging class. */ namespace LiteSpeed; defined('WPINC') || exit(); class Debug2 extends Root { private static $log_path; private static $log_path_prefix; private static $_prefix; const TYPE_CLEAR_LOG = 'clear_log'; const TYPE_BETA_TEST = 'beta_test'; const BETA_TEST_URL = 'beta_test_url'; const BETA_TEST_URL_WP = 'https://downloads.wordpress.org/plugin/litespeed-cache.zip'; /** * Log class Confructor * * NOTE: in this process, until last step ( define const LSCWP_LOG = true ), any usage to WP filter will not be logged to prevent infinite loop with log_filters() * * @since 1.1.2 * @access public */ public function __construct() { self::$log_path_prefix = LITESPEED_STATIC_DIR . '/debug/'; // Maybe move legacy log files $this->_maybe_init_folder(); self::$log_path = $this->path('debug'); if (!empty($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'lscache_') === 0) { self::$log_path = $this->path('crawler'); } !defined('LSCWP_LOG_TAG') && define('LSCWP_LOG_TAG', get_current_blog_id()); if ($this->conf(Base::O_DEBUG_LEVEL)) { !defined('LSCWP_LOG_MORE') && define('LSCWP_LOG_MORE', true); } defined('LSCWP_DEBUG_EXC_STRINGS') || define('LSCWP_DEBUG_EXC_STRINGS', $this->conf(Base::O_DEBUG_EXC_STRINGS)); } /** * Try moving legacy logs into /litespeed/debug/ folder * * @since 6.5 */ private function _maybe_init_folder() { if (file_exists(self::$log_path_prefix . 'index.php')) { return; } file::save(self::$log_path_prefix . 'index.php', '<?php // Silence is golden.', true); $logs = array('debug', 'debug.purge', 'crawler'); foreach ($logs as $log) { if (file_exists(LSCWP_CONTENT_DIR . '/' . $log . '.log') && !file_exists($this->path($log))) { rename(LSCWP_CONTENT_DIR . '/' . $log . '.log', $this->path($log)); } } } /** * Generate log file path * * @since 6.5 */ public function path($type) { return self::$log_path_prefix . self::FilePath($type); } /** * Generate the fixed log filename * * @since 6.5 */ public static function FilePath($type) { if ($type == 'debug.purge') { $type = 'purge'; } $key = defined('AUTH_KEY') ? AUTH_KEY : md5(__FILE__); $rand = substr(md5(substr($key, -16)), -16); return $type . $rand . '.log'; } /** * End call of one request process * @since 4.7 * @access public */ public static function ended() { $headers = headers_list(); foreach ($headers as $key => $header) { if (stripos($header, 'Set-Cookie') === 0) { unset($headers[$key]); } } self::debug('Response headers', $headers); $elapsed_time = number_format((microtime(true) - LSCWP_TS_0) * 1000, 2); self::debug("End response\n--------------------------------------------------Duration: " . $elapsed_time . " ms------------------------------\n"); } /** * Beta test upgrade * * @since 2.9.5 * @access public */ public function beta_test($zip = false) { if (!$zip) { if (empty($_REQUEST[self::BETA_TEST_URL])) { return; } $zip = $_REQUEST[self::BETA_TEST_URL]; if ($zip !== Debug2::BETA_TEST_URL_WP) { if ($zip === 'latest') { $zip = Debug2::BETA_TEST_URL_WP; } else { // Generate zip url $zip = $this->_package_zip($zip); } } } if (!$zip) { Debug2::debug('[Debug2] ❌ No ZIP file'); return; } Debug2::debug('[Debug2] ZIP file ' . $zip); $update_plugins = get_site_transient('update_plugins'); if (!is_object($update_plugins)) { $update_plugins = new \stdClass(); } $plugin_info = new \stdClass(); $plugin_info->new_version = Core::VER; $plugin_info->slug = Core::PLUGIN_NAME; $plugin_info->plugin = Core::PLUGIN_FILE; $plugin_info->package = $zip; $plugin_info->url = 'https://wordpress.org/plugins/litespeed-cache/'; $update_plugins->response[Core::PLUGIN_FILE] = $plugin_info; set_site_transient('update_plugins', $update_plugins); // Run upgrade Activation::cls()->upgrade(); } /** * Git package refresh * * @since 2.9.5 * @access private */ private function _package_zip($commit) { $data = array( 'commit' => $commit, ); $res = Cloud::get(Cloud::API_BETA_TEST, $data); if (empty($res['zip'])) { return false; } return $res['zip']; } /** * Log Purge headers separately * * @since 2.7 * @access public */ public static function log_purge($purge_header) { // Check if debug is ON if (!defined('LSCWP_LOG') && !defined('LSCWP_LOG_BYPASS_NOTADMIN')) { return; } $purge_file = self::cls()->path('purge'); self::cls()->_init_request($purge_file); $msg = $purge_header . self::_backtrace_info(6); File::append($purge_file, self::format_message($msg)); } /** * Enable debug log * * @since 1.1.0 * @access public */ public function init() { $debug = $this->conf(Base::O_DEBUG); if ($debug == Base::VAL_ON2) { if (!$this->cls('Router')->is_admin_ip()) { defined('LSCWP_LOG_BYPASS_NOTADMIN') || define('LSCWP_LOG_BYPASS_NOTADMIN', true); return; } } /** * Check if hit URI includes/excludes * This is after LSCWP_LOG_BYPASS_NOTADMIN to make `log_purge()` still work * @since 3.0 */ $list = $this->conf(Base::O_DEBUG_INC); if ($list) { $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $list); if (!$result) { return; } } $list = $this->conf(Base::O_DEBUG_EXC); if ($list) { $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $list); if ($result) { return; } } if (!defined('LSCWP_LOG')) { // If not initialized, do it now $this->_init_request(); define('LSCWP_LOG', true); } } /** * Create the initial log messages with the request parameters. * * @since 1.0.12 * @access private */ private function _init_request($log_file = null) { if (!$log_file) { $log_file = self::$log_path; } // Check log file size $log_file_size = $this->conf(Base::O_DEBUG_FILESIZE); if (file_exists($log_file) && filesize($log_file) > $log_file_size * 1000000) { File::save($log_file, ''); } // For more than 2s's requests, add more break if (file_exists($log_file) && time() - filemtime($log_file) > 2) { File::append($log_file, "\n\n\n\n"); } if (PHP_SAPI == 'cli') { return; } $servervars = array( 'Query String' => '', 'HTTP_ACCEPT' => '', 'HTTP_USER_AGENT' => '', 'HTTP_ACCEPT_ENCODING' => '', 'HTTP_COOKIE' => '', 'REQUEST_METHOD' => '', 'SERVER_PROTOCOL' => '', 'X-LSCACHE' => '', 'LSCACHE_VARY_COOKIE' => '', 'LSCACHE_VARY_VALUE' => '', 'ESI_CONTENT_TYPE' => '', ); $server = array_merge($servervars, $_SERVER); $params = array(); if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { $server['SERVER_PROTOCOL'] .= ' (HTTPS) '; } $param = sprintf('💓 ------%s %s %s', $server['REQUEST_METHOD'], $server['SERVER_PROTOCOL'], strtok($server['REQUEST_URI'], '?')); $qs = !empty($server['QUERY_STRING']) ? $server['QUERY_STRING'] : ''; if ($this->conf(Base::O_DEBUG_COLLAPSE_QS)) { $qs = $this->_omit_long_message($qs); if ($qs) { $param .= ' ? ' . $qs; } $params[] = $param; } else { $params[] = $param; $params[] = 'Query String: ' . $qs; } if (!empty($_SERVER['HTTP_REFERER'])) { $params[] = 'HTTP_REFERER: ' . $this->_omit_long_message($server['HTTP_REFERER']); } if (defined('LSCWP_LOG_MORE')) { $params[] = 'User Agent: ' . $this->_omit_long_message($server['HTTP_USER_AGENT']); $params[] = 'Accept: ' . $server['HTTP_ACCEPT']; $params[] = 'Accept Encoding: ' . $server['HTTP_ACCEPT_ENCODING']; } // $params[] = 'Cookie: ' . $server['HTTP_COOKIE']; if (isset($_COOKIE['_lscache_vary'])) { $params[] = 'Cookie _lscache_vary: ' . $_COOKIE['_lscache_vary']; } if (defined('LSCWP_LOG_MORE')) { $params[] = 'X-LSCACHE: ' . (!empty($server['X-LSCACHE']) ? 'true' : 'false'); } if ($server['LSCACHE_VARY_COOKIE']) { $params[] = 'LSCACHE_VARY_COOKIE: ' . $server['LSCACHE_VARY_COOKIE']; } if ($server['LSCACHE_VARY_VALUE']) { $params[] = 'LSCACHE_VARY_VALUE: ' . $server['LSCACHE_VARY_VALUE']; } if ($server['ESI_CONTENT_TYPE']) { $params[] = 'ESI_CONTENT_TYPE: ' . $server['ESI_CONTENT_TYPE']; } $request = array_map(__CLASS__ . '::format_message', $params); File::append($log_file, $request); } /** * Trim long msg to keep log neat * @since 6.3 */ private function _omit_long_message($msg) { if (strlen($msg) > 53) { $msg = substr($msg, 0, 53) . '...'; } return $msg; } /** * Formats the log message with a consistent prefix. * * @since 1.0.12 * @access private * @param string $msg The log message to write. * @return string The formatted log message. */ private static function format_message($msg) { // If call here without calling get_enabled() first, improve compatibility if (!defined('LSCWP_LOG_TAG')) { return $msg . "\n"; } if (!isset(self::$_prefix)) { // address if (PHP_SAPI == 'cli') { $addr = '=CLI='; if (isset($_SERVER['USER'])) { $addr .= $_SERVER['USER']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $addr .= $_SERVER['HTTP_X_FORWARDED_FOR']; } } else { $addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; $port = isset($_SERVER['REMOTE_PORT']) ? $_SERVER['REMOTE_PORT'] : ''; $addr = "$addr:$port"; } // Generate a unique string per request self::$_prefix = sprintf(' [%s %s %s] ', $addr, LSCWP_LOG_TAG, Str::rrand(3)); } list($usec, $sec) = explode(' ', microtime()); return date('m/d/y H:i:s', $sec + LITESPEED_TIME_OFFSET) . substr($usec, 1, 4) . self::$_prefix . $msg . "\n"; } /** * Direct call to log a debug message. * * @since 1.1.3 * @access public */ public static function debug($msg, $backtrace_limit = false) { if (!defined('LSCWP_LOG')) { return; } if (defined('LSCWP_DEBUG_EXC_STRINGS') && Utility::str_hit_array($msg, LSCWP_DEBUG_EXC_STRINGS)) { return; } if ($backtrace_limit !== false) { if (!is_numeric($backtrace_limit)) { $backtrace_limit = self::trim_longtext($backtrace_limit); if (is_array($backtrace_limit) && count($backtrace_limit) == 1 && !empty($backtrace_limit[0])) { $msg .= ' --- ' . $backtrace_limit[0]; } else { $msg .= ' --- ' . var_export($backtrace_limit, true); } self::push($msg); return; } self::push($msg, $backtrace_limit + 1); return; } self::push($msg); } /** * Trim long string before array dump * @since 3.3 */ public static function trim_longtext($backtrace_limit) { if (is_array($backtrace_limit)) { $backtrace_limit = array_map(__CLASS__ . '::trim_longtext', $backtrace_limit); } if (is_string($backtrace_limit) && strlen($backtrace_limit) > 500) { $backtrace_limit = substr($backtrace_limit, 0, 1000) . '...'; } return $backtrace_limit; } /** * Direct call to log an advanced debug message. * * @since 1.2.0 * @access public */ public static function debug2($msg, $backtrace_limit = false) { if (!defined('LSCWP_LOG_MORE')) { return; } self::debug($msg, $backtrace_limit); } /** * Logs a debug message. * * @since 1.1.0 * @access private * @param string $msg The debug message. * @param int $backtrace_limit Backtrace depth. */ private static function push($msg, $backtrace_limit = false) { // backtrace handler if (defined('LSCWP_LOG_MORE') && $backtrace_limit !== false) { $msg .= self::_backtrace_info($backtrace_limit); } File::append(self::$log_path, self::format_message($msg)); } /** * Backtrace info * * @since 2.7 */ private static function _backtrace_info($backtrace_limit) { $msg = ''; $trace = version_compare(PHP_VERSION, '5.4.0', '<') ? debug_backtrace() : debug_backtrace(false, $backtrace_limit + 3); for ($i = 2; $i <= $backtrace_limit + 2; $i++) { // 0st => _backtrace_info(), 1st => push() if (empty($trace[$i]['class'])) { if (empty($trace[$i]['file'])) { break; } $log = "\n" . $trace[$i]['file']; } else { if ($trace[$i]['class'] == __CLASS__) { continue; } $args = ''; if (!empty($trace[$i]['args'])) { foreach ($trace[$i]['args'] as $v) { if (is_array($v)) { $v = 'ARRAY'; } if (is_string($v) || is_numeric($v)) { $args .= $v . ','; } } $args = substr($args, 0, strlen($args) > 100 ? 100 : -1); } $log = str_replace('Core', 'LSC', $trace[$i]['class']) . $trace[$i]['type'] . $trace[$i]['function'] . '(' . $args . ')'; } if (!empty($trace[$i - 1]['line'])) { $log .= '@' . $trace[$i - 1]['line']; } $msg .= " => $log"; } return $msg; } /** * Clear log file * * @since 1.6.6 * @access private */ private function _clear_log() { $logs = array('debug', 'purge', 'crawler'); foreach ($logs as $log) { File::save($this->path($log), ''); } } /** * Handle all request actions from main cls * * @since 1.6.6 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_CLEAR_LOG: $this->_clear_log(); break; case self::TYPE_BETA_TEST: $this->beta_test(); break; default: break; } Admin::redirect(); } } src/object-cache.cls.php 0000644 00000037530 15162130557 0011150 0 ustar 00 <?php /** * The object cache class * * @since 1.8 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); require_once dirname(__DIR__) . '/autoload.php'; class Object_Cache extends Root { const O_DEBUG = 'debug'; const O_OBJECT = 'object'; const O_OBJECT_KIND = 'object-kind'; const O_OBJECT_HOST = 'object-host'; const O_OBJECT_PORT = 'object-port'; const O_OBJECT_LIFE = 'object-life'; const O_OBJECT_PERSISTENT = 'object-persistent'; const O_OBJECT_ADMIN = 'object-admin'; const O_OBJECT_TRANSIENTS = 'object-transients'; const O_OBJECT_DB_ID = 'object-db_id'; const O_OBJECT_USER = 'object-user'; const O_OBJECT_PSWD = 'object-pswd'; const O_OBJECT_GLOBAL_GROUPS = 'object-global_groups'; const O_OBJECT_NON_PERSISTENT_GROUPS = 'object-non_persistent_groups'; private $_conn; private $_cfg_debug; private $_cfg_enabled; private $_cfg_method; private $_cfg_host; private $_cfg_port; private $_cfg_life; private $_cfg_persistent; private $_cfg_admin; private $_cfg_transients; private $_cfg_db; private $_cfg_user; private $_cfg_pswd; private $_default_life = 360; private $_oc_driver = 'Memcached'; // Redis or Memcached private $_global_groups = array(); private $_non_persistent_groups = array(); /** * Init * * NOTE: this class may be included without initialized core * * @since 1.8 */ public function __construct($cfg = false) { if ($cfg) { if (!is_array($cfg[Base::O_OBJECT_GLOBAL_GROUPS])) { $cfg[Base::O_OBJECT_GLOBAL_GROUPS] = explode("\n", $cfg[Base::O_OBJECT_GLOBAL_GROUPS]); } if (!is_array($cfg[Base::O_OBJECT_NON_PERSISTENT_GROUPS])) { $cfg[Base::O_OBJECT_NON_PERSISTENT_GROUPS] = explode("\n", $cfg[Base::O_OBJECT_NON_PERSISTENT_GROUPS]); } $this->_cfg_debug = $cfg[Base::O_DEBUG] ? $cfg[Base::O_DEBUG] : false; $this->_cfg_method = $cfg[Base::O_OBJECT_KIND] ? true : false; $this->_cfg_host = $cfg[Base::O_OBJECT_HOST]; $this->_cfg_port = $cfg[Base::O_OBJECT_PORT]; $this->_cfg_life = $cfg[Base::O_OBJECT_LIFE]; $this->_cfg_persistent = $cfg[Base::O_OBJECT_PERSISTENT]; $this->_cfg_admin = $cfg[Base::O_OBJECT_ADMIN]; $this->_cfg_transients = $cfg[Base::O_OBJECT_TRANSIENTS]; $this->_cfg_db = $cfg[Base::O_OBJECT_DB_ID]; $this->_cfg_user = $cfg[Base::O_OBJECT_USER]; $this->_cfg_pswd = $cfg[Base::O_OBJECT_PSWD]; $this->_global_groups = $cfg[Base::O_OBJECT_GLOBAL_GROUPS]; $this->_non_persistent_groups = $cfg[Base::O_OBJECT_NON_PERSISTENT_GROUPS]; if ($this->_cfg_method) { $this->_oc_driver = 'Redis'; } $this->_cfg_enabled = $cfg[Base::O_OBJECT] && class_exists($this->_oc_driver) && $this->_cfg_host; } // If OC is OFF, will hit here to init OC after conf initialized elseif (defined('LITESPEED_CONF_LOADED')) { $this->_cfg_debug = $this->conf(Base::O_DEBUG) ? $this->conf(Base::O_DEBUG) : false; $this->_cfg_method = $this->conf(Base::O_OBJECT_KIND) ? true : false; $this->_cfg_host = $this->conf(Base::O_OBJECT_HOST); $this->_cfg_port = $this->conf(Base::O_OBJECT_PORT); $this->_cfg_life = $this->conf(Base::O_OBJECT_LIFE); $this->_cfg_persistent = $this->conf(Base::O_OBJECT_PERSISTENT); $this->_cfg_admin = $this->conf(Base::O_OBJECT_ADMIN); $this->_cfg_transients = $this->conf(Base::O_OBJECT_TRANSIENTS); $this->_cfg_db = $this->conf(Base::O_OBJECT_DB_ID); $this->_cfg_user = $this->conf(Base::O_OBJECT_USER); $this->_cfg_pswd = $this->conf(Base::O_OBJECT_PSWD); $this->_global_groups = $this->conf(Base::O_OBJECT_GLOBAL_GROUPS); $this->_non_persistent_groups = $this->conf(Base::O_OBJECT_NON_PERSISTENT_GROUPS); if ($this->_cfg_method) { $this->_oc_driver = 'Redis'; } $this->_cfg_enabled = $this->conf(Base::O_OBJECT) && class_exists($this->_oc_driver) && $this->_cfg_host; } elseif (defined('self::CONF_FILE') && file_exists(WP_CONTENT_DIR . '/' . self::CONF_FILE)) { // Get cfg from _data_file // Use self::const to avoid loading more classes $cfg = \json_decode(file_get_contents(WP_CONTENT_DIR . '/' . self::CONF_FILE), true); if (!empty($cfg[self::O_OBJECT_HOST])) { $this->_cfg_debug = !empty($cfg[Base::O_DEBUG]) ? $cfg[Base::O_DEBUG] : false; $this->_cfg_method = !empty($cfg[self::O_OBJECT_KIND]) ? $cfg[self::O_OBJECT_KIND] : false; $this->_cfg_host = $cfg[self::O_OBJECT_HOST]; $this->_cfg_port = $cfg[self::O_OBJECT_PORT]; $this->_cfg_life = !empty($cfg[self::O_OBJECT_LIFE]) ? $cfg[self::O_OBJECT_LIFE] : $this->_default_life; $this->_cfg_persistent = !empty($cfg[self::O_OBJECT_PERSISTENT]) ? $cfg[self::O_OBJECT_PERSISTENT] : false; $this->_cfg_admin = !empty($cfg[self::O_OBJECT_ADMIN]) ? $cfg[self::O_OBJECT_ADMIN] : false; $this->_cfg_transients = !empty($cfg[self::O_OBJECT_TRANSIENTS]) ? $cfg[self::O_OBJECT_TRANSIENTS] : false; $this->_cfg_db = !empty($cfg[self::O_OBJECT_DB_ID]) ? $cfg[self::O_OBJECT_DB_ID] : 0; $this->_cfg_user = !empty($cfg[self::O_OBJECT_USER]) ? $cfg[self::O_OBJECT_USER] : ''; $this->_cfg_pswd = !empty($cfg[self::O_OBJECT_PSWD]) ? $cfg[self::O_OBJECT_PSWD] : ''; $this->_global_groups = !empty($cfg[self::O_OBJECT_GLOBAL_GROUPS]) ? $cfg[self::O_OBJECT_GLOBAL_GROUPS] : array(); $this->_non_persistent_groups = !empty($cfg[self::O_OBJECT_NON_PERSISTENT_GROUPS]) ? $cfg[self::O_OBJECT_NON_PERSISTENT_GROUPS] : array(); if ($this->_cfg_method) { $this->_oc_driver = 'Redis'; } $this->_cfg_enabled = class_exists($this->_oc_driver) && $this->_cfg_host; } else { $this->_cfg_enabled = false; } } else { $this->_cfg_enabled = false; } } /** * Add debug. * * @since 6.3 * @access private */ private function debug_oc($text, $show_error = false) { if (defined('LSCWP_LOG')) { Debug2::debug($text); return; } if (!$show_error && $this->_cfg_debug != BASE::VAL_ON2) { return; } $LITESPEED_DATA_FOLDER = defined('LITESPEED_DATA_FOLDER') ? LITESPEED_DATA_FOLDER : 'litespeed'; $LSCWP_CONTENT_DIR = defined('LSCWP_CONTENT_DIR') ? LSCWP_CONTENT_DIR : WP_CONTENT_DIR; $LITESPEED_STATIC_DIR = $LSCWP_CONTENT_DIR . '/' . $LITESPEED_DATA_FOLDER; $log_path_prefix = $LITESPEED_STATIC_DIR . '/debug/'; $log_file = $log_path_prefix . Debug2::FilePath('debug'); if (file_exists($log_path_prefix . 'index.php') && file_exists($log_file)) { error_log(gmdate('m/d/y H:i:s') . ' - OC - ' . $text . PHP_EOL, 3, $log_file); } } /** * Get `Store Transients` setting value * * @since 1.8.3 * @access public */ public function store_transients($group) { return $this->_cfg_transients && $this->_is_transients_group($group); } /** * Check if the group belongs to transients or not * * @since 1.8.3 * @access private */ private function _is_transients_group($group) { return in_array($group, array('transient', 'site-transient')); } /** * Update WP object cache file config * * @since 1.8 * @access public */ public function update_file($options) { $changed = false; // NOTE: When included in oc.php, `LSCWP_DIR` will show undefined, so this must be assigned/generated when used $_oc_ori_file = LSCWP_DIR . 'lib/object-cache.php'; $_oc_wp_file = WP_CONTENT_DIR . '/object-cache.php'; // Update cls file if (!file_exists($_oc_wp_file) || md5_file($_oc_wp_file) !== md5_file($_oc_ori_file)) { $this->debug_oc('copying object-cache.php file to ' . $_oc_wp_file); copy($_oc_ori_file, $_oc_wp_file); $changed = true; } /** * Clear object cache */ if ($changed) { $this->_reconnect($options); } } /** * Remove object cache file * * @since 1.8.2 * @access public */ public function del_file() { // NOTE: When included in oc.php, `LSCWP_DIR` will show undefined, so this must be assigned/generated when used $_oc_ori_file = LSCWP_DIR . 'lib/object-cache.php'; $_oc_wp_file = WP_CONTENT_DIR . '/object-cache.php'; if (file_exists($_oc_wp_file) && md5_file($_oc_wp_file) === md5_file($_oc_ori_file)) { $this->debug_oc('removing ' . $_oc_wp_file); unlink($_oc_wp_file); } } /** * Try to build connection * * @since 1.8 * @access public */ public function test_connection() { return $this->_connect(); } /** * Force to connect with this setting * * @since 1.8 * @access private */ private function _reconnect($cfg) { $this->debug_oc('Reconnecting'); if (isset($this->_conn)) { // error_log( 'Object: Quitting existing connection!' ); $this->debug_oc('Quitting existing connection'); $this->flush(); $this->_conn = null; $this->cls(false, true); } $cls = $this->cls(false, false, $cfg); $cls->_connect(); if (isset($cls->_conn)) { $cls->flush(); } } /** * Connect to Memcached/Redis server * * @since 1.8 * @access private */ private function _connect() { if (isset($this->_conn)) { // error_log( 'Object: _connected' ); return true; } if (!class_exists($this->_oc_driver) || !$this->_cfg_host) { return null; } if (defined('LITESPEED_OC_FAILURE')) { return false; } $this->debug_oc('Init ' . $this->_oc_driver . ' connection to ' . $this->_cfg_host . ':' . $this->_cfg_port); $failed = false; /** * Connect to Redis * * @since 1.8.1 * @see https://github.com/phpredis/phpredis/#example-1 */ if ($this->_oc_driver == 'Redis') { set_error_handler('litespeed_exception_handler'); try { $this->_conn = new \Redis(); // error_log( 'Object: _connect Redis' ); if ($this->_cfg_persistent) { if ($this->_cfg_port) { $this->_conn->pconnect($this->_cfg_host, $this->_cfg_port); } else { $this->_conn->pconnect($this->_cfg_host); } } else { if ($this->_cfg_port) { $this->_conn->connect($this->_cfg_host, $this->_cfg_port); } else { $this->_conn->connect($this->_cfg_host); } } if ($this->_cfg_pswd) { if ($this->_cfg_user) { $this->_conn->auth(array($this->_cfg_user, $this->_cfg_pswd)); } else { $this->_conn->auth($this->_cfg_pswd); } } if ($this->_cfg_db) { $this->_conn->select($this->_cfg_db); } $res = $this->_conn->ping(); if ($res != '+PONG') { $failed = true; } } catch (\Exception $e) { $this->debug_oc('Redis connect exception: ' . $e->getMessage(), true); $failed = true; } catch (\ErrorException $e) { $this->debug_oc('Redis connect error: ' . $e->getMessage(), true); $failed = true; } restore_error_handler(); } else { // Connect to Memcached if ($this->_cfg_persistent) { $this->_conn = new \Memcached($this->_get_mem_id()); // Check memcached persistent connection if ($this->_validate_mem_server()) { // error_log( 'Object: _validate_mem_server' ); $this->debug_oc('Got persistent ' . $this->_oc_driver . ' connection'); return true; } $this->debug_oc('No persistent ' . $this->_oc_driver . ' server list!'); } else { // error_log( 'Object: new memcached!' ); $this->_conn = new \Memcached(); } $this->_conn->addServer($this->_cfg_host, (int) $this->_cfg_port); /** * Add SASL auth * @since 1.8.1 * @since 2.9.6 Fixed SASL connection @see https://www.litespeedtech.com/support/wiki/doku.php/litespeed_wiki:lsmcd:new_sasl */ if ($this->_cfg_user && $this->_cfg_pswd && method_exists($this->_conn, 'setSaslAuthData')) { $this->_conn->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); $this->_conn->setOption(\Memcached::OPT_COMPRESSION, false); $this->_conn->setSaslAuthData($this->_cfg_user, $this->_cfg_pswd); } // Check connection if (!$this->_validate_mem_server()) { $failed = true; } } // If failed to connect if ($failed) { $this->debug_oc('❌ Failed to connect ' . $this->_oc_driver . ' server!', true); $this->_conn = null; $this->_cfg_enabled = false; !defined('LITESPEED_OC_FAILURE') && define('LITESPEED_OC_FAILURE', true); // error_log( 'Object: false!' ); return false; } $this->debug_oc('Connected'); return true; } /** * Check if the connected memcached host is the one in cfg * * @since 1.8 * @access private */ private function _validate_mem_server() { $mem_list = $this->_conn->getStats(); if (empty($mem_list)) { return false; } foreach ($mem_list as $k => $v) { if (substr($k, 0, strlen($this->_cfg_host)) != $this->_cfg_host) { continue; } if (!empty($v['pid']) || !empty($v['curr_connections'])) { return true; } } return false; } /** * Get memcached unique id to be used for connecting * * @since 1.8 * @access private */ private function _get_mem_id() { $mem_id = 'litespeed'; if (is_multisite()) { $mem_id .= '_' . get_current_blog_id(); } return $mem_id; } /** * Get cache * * @since 1.8 * @access public */ public function get($key) { if (!$this->_cfg_enabled) { return null; } if (!$this->_can_cache()) { return null; } if (!$this->_connect()) { return null; } $res = $this->_conn->get($key); return $res; } /** * Set cache * * @since 1.8 * @access public */ public function set($key, $data, $expire) { if (!$this->_cfg_enabled) { return null; } /** * To fix the Cloud callback cached as its frontend call but the hash is generated in backend * Bug found by Stan at Jan/10/2020 */ // if ( ! $this->_can_cache() ) { // return null; // } if (!$this->_connect()) { return null; } $ttl = $expire ?: $this->_cfg_life; if ($this->_oc_driver == 'Redis') { try { $res = $this->_conn->setEx($key, $ttl, $data); } catch (\RedisException $ex) { $res = false; $msg = sprintf(__('Redis encountered a fatal error: %s (code: %d)', 'litespeed-cache'), $ex->getMessage(), $ex->getCode()); $this->debug_oc($msg); Admin_Display::error($msg); } } else { $res = $this->_conn->set($key, $data, $ttl); } return $res; } /** * Check if can cache or not * * @since 1.8 * @access private */ private function _can_cache() { if (!$this->_cfg_admin && defined('WP_ADMIN')) { return false; } return true; } /** * Delete cache * * @since 1.8 * @access public */ public function delete($key) { if (!$this->_cfg_enabled) { return null; } if (!$this->_connect()) { return null; } if ($this->_oc_driver == 'Redis') { $res = $this->_conn->del($key); } else { $res = $this->_conn->delete($key); } return (bool) $res; } /** * Clear all cache * * @since 1.8 * @access public */ public function flush() { if (!$this->_cfg_enabled) { $this->debug_oc('bypass flushing'); return null; } if (!$this->_connect()) { return null; } $this->debug_oc('flush!'); if ($this->_oc_driver == 'Redis') { $res = $this->_conn->flushDb(); } else { $res = $this->_conn->flush(); $this->_conn->resetServerList(); } return $res; } /** * Add global groups * * @since 1.8 * @access public */ public function add_global_groups($groups) { if (!is_array($groups)) { $groups = array($groups); } $this->_global_groups = array_merge($this->_global_groups, $groups); $this->_global_groups = array_unique($this->_global_groups); } /** * Check if is in global groups or not * * @since 1.8 * @access public */ public function is_global($group) { return in_array($group, $this->_global_groups); } /** * Add non persistent groups * * @since 1.8 * @access public */ public function add_non_persistent_groups($groups) { if (!is_array($groups)) { $groups = array($groups); } $this->_non_persistent_groups = array_merge($this->_non_persistent_groups, $groups); $this->_non_persistent_groups = array_unique($this->_non_persistent_groups); } /** * Check if is in non persistent groups or not * * @since 1.8 * @access public */ public function is_non_persistent($group) { return in_array($group, $this->_non_persistent_groups); } } src/base.cls.php 0000644 00000075165 15162130561 0007554 0 ustar 00 <?php /** * The base consts * * @since 3.7 */ namespace LiteSpeed; defined('WPINC') || exit(); class Base extends Root { // This is redundant since v3.0 // New conf items are `litespeed.key` const OPTION_NAME = 'litespeed-cache-conf'; const _CACHE = '_cache'; // final cache status from setting ## -------------------------------------------------- ## ## -------------- General ----------------- ## ## -------------------------------------------------- ## const _VER = '_version'; // Not set-able const HASH = 'hash'; // Not set-able const O_AUTO_UPGRADE = 'auto_upgrade'; const O_API_KEY = 'api_key'; // Deprecated since v6.4. TODO: Will drop after v6.5 const O_SERVER_IP = 'server_ip'; const O_GUEST = 'guest'; const O_GUEST_OPTM = 'guest_optm'; const O_NEWS = 'news'; const O_GUEST_UAS = 'guest_uas'; const O_GUEST_IPS = 'guest_ips'; ## -------------------------------------------------- ## ## -------------- Cache ----------------- ## ## -------------------------------------------------- ## const O_CACHE = 'cache'; const O_CACHE_PRIV = 'cache-priv'; const O_CACHE_COMMENTER = 'cache-commenter'; const O_CACHE_REST = 'cache-rest'; const O_CACHE_PAGE_LOGIN = 'cache-page_login'; const O_CACHE_FAVICON = 'cache-favicon'; // Deprecated since v6.2. TODO: Will drop after v6.5 const O_CACHE_RES = 'cache-resources'; const O_CACHE_MOBILE = 'cache-mobile'; const O_CACHE_MOBILE_RULES = 'cache-mobile_rules'; const O_CACHE_BROWSER = 'cache-browser'; const O_CACHE_EXC_USERAGENTS = 'cache-exc_useragents'; const O_CACHE_EXC_COOKIES = 'cache-exc_cookies'; const O_CACHE_EXC_QS = 'cache-exc_qs'; const O_CACHE_EXC_CAT = 'cache-exc_cat'; const O_CACHE_EXC_TAG = 'cache-exc_tag'; const O_CACHE_FORCE_URI = 'cache-force_uri'; const O_CACHE_FORCE_PUB_URI = 'cache-force_pub_uri'; const O_CACHE_PRIV_URI = 'cache-priv_uri'; const O_CACHE_EXC = 'cache-exc'; const O_CACHE_EXC_ROLES = 'cache-exc_roles'; const O_CACHE_DROP_QS = 'cache-drop_qs'; const O_CACHE_TTL_PUB = 'cache-ttl_pub'; const O_CACHE_TTL_PRIV = 'cache-ttl_priv'; const O_CACHE_TTL_FRONTPAGE = 'cache-ttl_frontpage'; const O_CACHE_TTL_FEED = 'cache-ttl_feed'; const O_CACHE_TTL_REST = 'cache-ttl_rest'; const O_CACHE_TTL_STATUS = 'cache-ttl_status'; const O_CACHE_TTL_BROWSER = 'cache-ttl_browser'; const O_CACHE_AJAX_TTL = 'cache-ajax_ttl'; const O_CACHE_LOGIN_COOKIE = 'cache-login_cookie'; const O_CACHE_VARY_COOKIES = 'cache-vary_cookies'; const O_CACHE_VARY_GROUP = 'cache-vary_group'; ## -------------------------------------------------- ## ## -------------- Purge ----------------- ## ## -------------------------------------------------- ## const O_PURGE_ON_UPGRADE = 'purge-upgrade'; const O_PURGE_STALE = 'purge-stale'; const O_PURGE_POST_ALL = 'purge-post_all'; const O_PURGE_POST_FRONTPAGE = 'purge-post_f'; const O_PURGE_POST_HOMEPAGE = 'purge-post_h'; const O_PURGE_POST_PAGES = 'purge-post_p'; const O_PURGE_POST_PAGES_WITH_RECENT_POSTS = 'purge-post_pwrp'; const O_PURGE_POST_AUTHOR = 'purge-post_a'; const O_PURGE_POST_YEAR = 'purge-post_y'; const O_PURGE_POST_MONTH = 'purge-post_m'; const O_PURGE_POST_DATE = 'purge-post_d'; const O_PURGE_POST_TERM = 'purge-post_t'; // include category|tag|tax const O_PURGE_POST_POSTTYPE = 'purge-post_pt'; const O_PURGE_TIMED_URLS = 'purge-timed_urls'; const O_PURGE_TIMED_URLS_TIME = 'purge-timed_urls_time'; const O_PURGE_HOOK_ALL = 'purge-hook_all'; ## -------------------------------------------------- ## ## -------------- ESI ----------------- ## ## -------------------------------------------------- ## const O_ESI = 'esi'; const O_ESI_CACHE_ADMBAR = 'esi-cache_admbar'; const O_ESI_CACHE_COMMFORM = 'esi-cache_commform'; const O_ESI_NONCE = 'esi-nonce'; ## -------------------------------------------------- ## ## -------------- Utilities ----------------- ## ## -------------------------------------------------- ## const O_UTIL_INSTANT_CLICK = 'util-instant_click'; const O_UTIL_NO_HTTPS_VARY = 'util-no_https_vary'; ## -------------------------------------------------- ## ## -------------- Debug ----------------- ## ## -------------------------------------------------- ## const O_DEBUG_DISABLE_ALL = 'debug-disable_all'; const O_DEBUG = 'debug'; const O_DEBUG_IPS = 'debug-ips'; const O_DEBUG_LEVEL = 'debug-level'; const O_DEBUG_FILESIZE = 'debug-filesize'; const O_DEBUG_COOKIE = 'debug-cookie'; // For backwards compatibility, will drop after v7.0 const O_DEBUG_COLLAPSE_QS = 'debug-collapse_qs'; const O_DEBUG_COLLAPS_QS = 'debug-collapse_qs'; // For backwards compatibility, will drop after v6.5 const O_DEBUG_INC = 'debug-inc'; const O_DEBUG_EXC = 'debug-exc'; const O_DEBUG_EXC_STRINGS = 'debug-exc_strings'; ## -------------------------------------------------- ## ## -------------- DB Optm ----------------- ## ## -------------------------------------------------- ## const O_DB_OPTM_REVISIONS_MAX = 'db_optm-revisions_max'; const O_DB_OPTM_REVISIONS_AGE = 'db_optm-revisions_age'; ## -------------------------------------------------- ## ## -------------- HTML Optm ----------------- ## ## -------------------------------------------------- ## const O_OPTM_CSS_MIN = 'optm-css_min'; const O_OPTM_CSS_COMB = 'optm-css_comb'; const O_OPTM_CSS_COMB_EXT_INL = 'optm-css_comb_ext_inl'; const O_OPTM_UCSS = 'optm-ucss'; const O_OPTM_UCSS_INLINE = 'optm-ucss_inline'; const O_OPTM_UCSS_SELECTOR_WHITELIST = 'optm-ucss_whitelist'; const O_OPTM_UCSS_FILE_EXC_INLINE = 'optm-ucss_file_exc_inline'; const O_OPTM_UCSS_EXC = 'optm-ucss_exc'; const O_OPTM_CSS_EXC = 'optm-css_exc'; const O_OPTM_JS_MIN = 'optm-js_min'; const O_OPTM_JS_COMB = 'optm-js_comb'; const O_OPTM_JS_COMB_EXT_INL = 'optm-js_comb_ext_inl'; const O_OPTM_JS_DELAY_INC = 'optm-js_delay_inc'; const O_OPTM_JS_EXC = 'optm-js_exc'; const O_OPTM_HTML_MIN = 'optm-html_min'; const O_OPTM_HTML_LAZY = 'optm-html_lazy'; const O_OPTM_HTML_SKIP_COMMENTS = 'optm-html_skip_comment'; const O_OPTM_QS_RM = 'optm-qs_rm'; const O_OPTM_GGFONTS_RM = 'optm-ggfonts_rm'; const O_OPTM_CSS_ASYNC = 'optm-css_async'; const O_OPTM_CCSS_PER_URL = 'optm-ccss_per_url'; const O_OPTM_CCSS_SEP_POSTTYPE = 'optm-ccss_sep_posttype'; const O_OPTM_CCSS_SEP_URI = 'optm-ccss_sep_uri'; const O_OPTM_CCSS_SELECTOR_WHITELIST = 'optm-ccss_whitelist'; const O_OPTM_CSS_ASYNC_INLINE = 'optm-css_async_inline'; const O_OPTM_CSS_FONT_DISPLAY = 'optm-css_font_display'; const O_OPTM_JS_DEFER = 'optm-js_defer'; const O_OPTM_LOCALIZE = 'optm-localize'; const O_OPTM_LOCALIZE_DOMAINS = 'optm-localize_domains'; const O_OPTM_EMOJI_RM = 'optm-emoji_rm'; const O_OPTM_NOSCRIPT_RM = 'optm-noscript_rm'; const O_OPTM_GGFONTS_ASYNC = 'optm-ggfonts_async'; const O_OPTM_EXC_ROLES = 'optm-exc_roles'; const O_OPTM_CCSS_CON = 'optm-ccss_con'; const O_OPTM_JS_DEFER_EXC = 'optm-js_defer_exc'; const O_OPTM_GM_JS_EXC = 'optm-gm_js_exc'; const O_OPTM_DNS_PREFETCH = 'optm-dns_prefetch'; const O_OPTM_DNS_PREFETCH_CTRL = 'optm-dns_prefetch_ctrl'; const O_OPTM_DNS_PRECONNECT = 'optm-dns_preconnect'; const O_OPTM_EXC = 'optm-exc'; const O_OPTM_GUEST_ONLY = 'optm-guest_only'; ## -------------------------------------------------- ## ## -------------- Object Cache ----------------- ## ## -------------------------------------------------- ## const O_OBJECT = 'object'; const O_OBJECT_KIND = 'object-kind'; const O_OBJECT_HOST = 'object-host'; const O_OBJECT_PORT = 'object-port'; const O_OBJECT_LIFE = 'object-life'; const O_OBJECT_PERSISTENT = 'object-persistent'; const O_OBJECT_ADMIN = 'object-admin'; const O_OBJECT_TRANSIENTS = 'object-transients'; const O_OBJECT_DB_ID = 'object-db_id'; const O_OBJECT_USER = 'object-user'; const O_OBJECT_PSWD = 'object-pswd'; const O_OBJECT_GLOBAL_GROUPS = 'object-global_groups'; const O_OBJECT_NON_PERSISTENT_GROUPS = 'object-non_persistent_groups'; ## -------------------------------------------------- ## ## -------------- Discussion ----------------- ## ## -------------------------------------------------- ## const O_DISCUSS_AVATAR_CACHE = 'discuss-avatar_cache'; const O_DISCUSS_AVATAR_CRON = 'discuss-avatar_cron'; const O_DISCUSS_AVATAR_CACHE_TTL = 'discuss-avatar_cache_ttl'; ## -------------------------------------------------- ## ## -------------- Media ----------------- ## ## -------------------------------------------------- ## const O_MEDIA_PRELOAD_FEATURED = 'media-preload_featured'; // Deprecated since v6.2. TODO: Will drop after v6.5 const O_MEDIA_LAZY = 'media-lazy'; const O_MEDIA_LAZY_PLACEHOLDER = 'media-lazy_placeholder'; const O_MEDIA_PLACEHOLDER_RESP = 'media-placeholder_resp'; const O_MEDIA_PLACEHOLDER_RESP_COLOR = 'media-placeholder_resp_color'; const O_MEDIA_PLACEHOLDER_RESP_SVG = 'media-placeholder_resp_svg'; const O_MEDIA_LQIP = 'media-lqip'; const O_MEDIA_LQIP_QUAL = 'media-lqip_qual'; const O_MEDIA_LQIP_MIN_W = 'media-lqip_min_w'; const O_MEDIA_LQIP_MIN_H = 'media-lqip_min_h'; const O_MEDIA_PLACEHOLDER_RESP_ASYNC = 'media-placeholder_resp_async'; const O_MEDIA_IFRAME_LAZY = 'media-iframe_lazy'; const O_MEDIA_ADD_MISSING_SIZES = 'media-add_missing_sizes'; const O_MEDIA_LAZY_EXC = 'media-lazy_exc'; const O_MEDIA_LAZY_CLS_EXC = 'media-lazy_cls_exc'; const O_MEDIA_LAZY_PARENT_CLS_EXC = 'media-lazy_parent_cls_exc'; const O_MEDIA_IFRAME_LAZY_CLS_EXC = 'media-iframe_lazy_cls_exc'; const O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC = 'media-iframe_lazy_parent_cls_exc'; const O_MEDIA_LAZY_URI_EXC = 'media-lazy_uri_exc'; const O_MEDIA_LQIP_EXC = 'media-lqip_exc'; const O_MEDIA_VPI = 'media-vpi'; const O_MEDIA_VPI_CRON = 'media-vpi_cron'; const O_IMG_OPTM_JPG_QUALITY = 'img_optm-jpg_quality'; ## -------------------------------------------------- ## ## -------------- Image Optm ----------------- ## ## -------------------------------------------------- ## const O_IMG_OPTM_AUTO = 'img_optm-auto'; const O_IMG_OPTM_CRON = 'img_optm-cron'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_IMG_OPTM_ORI = 'img_optm-ori'; const O_IMG_OPTM_RM_BKUP = 'img_optm-rm_bkup'; const O_IMG_OPTM_WEBP = 'img_optm-webp'; const O_IMG_OPTM_LOSSLESS = 'img_optm-lossless'; const O_IMG_OPTM_EXIF = 'img_optm-exif'; const O_IMG_OPTM_WEBP_ATTR = 'img_optm-webp_attr'; const O_IMG_OPTM_WEBP_REPLACE_SRCSET = 'img_optm-webp_replace_srcset'; ## -------------------------------------------------- ## ## -------------- Crawler ----------------- ## ## -------------------------------------------------- ## const O_CRAWLER = 'crawler'; const O_CRAWLER_USLEEP = 'crawler-usleep'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_RUN_DURATION = 'crawler-run_duration'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_RUN_INTERVAL = 'crawler-run_interval'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_CRAWL_INTERVAL = 'crawler-crawl_interval'; const O_CRAWLER_THREADS = 'crawler-threads'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_TIMEOUT = 'crawler-timeout'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_LOAD_LIMIT = 'crawler-load_limit'; const O_CRAWLER_SITEMAP = 'crawler-sitemap'; const O_CRAWLER_DROP_DOMAIN = 'crawler-drop_domain'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_MAP_TIMEOUT = 'crawler-map_timeout'; // @Deprecated since v7.0 TODO: remove after v7.5 const O_CRAWLER_ROLES = 'crawler-roles'; const O_CRAWLER_COOKIES = 'crawler-cookies'; ## -------------------------------------------------- ## ## -------------- Misc ----------------- ## ## -------------------------------------------------- ## const O_MISC_HEARTBEAT_FRONT = 'misc-heartbeat_front'; const O_MISC_HEARTBEAT_FRONT_TTL = 'misc-heartbeat_front_ttl'; const O_MISC_HEARTBEAT_BACK = 'misc-heartbeat_back'; const O_MISC_HEARTBEAT_BACK_TTL = 'misc-heartbeat_back_ttl'; const O_MISC_HEARTBEAT_EDITOR = 'misc-heartbeat_editor'; const O_MISC_HEARTBEAT_EDITOR_TTL = 'misc-heartbeat_editor_ttl'; ## -------------------------------------------------- ## ## -------------- CDN ----------------- ## ## -------------------------------------------------- ## const O_CDN = 'cdn'; const O_CDN_ORI = 'cdn-ori'; const O_CDN_ORI_DIR = 'cdn-ori_dir'; const O_CDN_EXC = 'cdn-exc'; const O_CDN_QUIC = 'cdn-quic'; // No more a visible setting since v7 const O_CDN_CLOUDFLARE = 'cdn-cloudflare'; const O_CDN_CLOUDFLARE_EMAIL = 'cdn-cloudflare_email'; const O_CDN_CLOUDFLARE_KEY = 'cdn-cloudflare_key'; const O_CDN_CLOUDFLARE_NAME = 'cdn-cloudflare_name'; const O_CDN_CLOUDFLARE_ZONE = 'cdn-cloudflare_zone'; const O_CDN_MAPPING = 'cdn-mapping'; const O_CDN_ATTR = 'cdn-attr'; const O_QC_NAMESERVERS = 'qc-nameservers'; const O_QC_CNAME = 'qc-cname'; const NETWORK_O_USE_PRIMARY = 'use_primary_settings'; /*** Other consts ***/ const O_GUIDE = 'litespeed-guide'; // Array of each guidance tag as key, step as val //xx todo: may need to remove // Server variables const ENV_CRAWLER_USLEEP = 'CRAWLER_USLEEP'; const ENV_CRAWLER_LOAD_LIMIT = 'CRAWLER_LOAD_LIMIT'; const ENV_CRAWLER_LOAD_LIMIT_ENFORCE = 'CRAWLER_LOAD_LIMIT_ENFORCE'; const CRWL_COOKIE_NAME = 'name'; const CRWL_COOKIE_VALS = 'vals'; const CDN_MAPPING_URL = 'url'; const CDN_MAPPING_INC_IMG = 'inc_img'; const CDN_MAPPING_INC_CSS = 'inc_css'; const CDN_MAPPING_INC_JS = 'inc_js'; const CDN_MAPPING_FILETYPE = 'filetype'; const VAL_OFF = 0; const VAL_ON = 1; const VAL_ON2 = 2; /* This is for API hook usage */ const IMG_OPTM_BM_ORI = 1; // @Deprecated since v7.0 const IMG_OPTM_BM_WEBP = 2; // @Deprecated since v7.0 const IMG_OPTM_BM_LOSSLESS = 4; // @Deprecated since v7.0 const IMG_OPTM_BM_EXIF = 8; // @Deprecated since v7.0 const IMG_OPTM_BM_AVIF = 16; // @Deprecated since v7.0 /* Site related options (Will not overwrite other sites' config) */ protected static $SINGLE_SITE_OPTIONS = array( self::O_CRAWLER, self::O_CRAWLER_SITEMAP, self::O_CDN, self::O_CDN_ORI, self::O_CDN_ORI_DIR, self::O_CDN_EXC, self::O_CDN_CLOUDFLARE, self::O_CDN_CLOUDFLARE_EMAIL, self::O_CDN_CLOUDFLARE_KEY, self::O_CDN_CLOUDFLARE_NAME, self::O_CDN_CLOUDFLARE_ZONE, self::O_CDN_MAPPING, self::O_CDN_ATTR, self::O_QC_NAMESERVERS, self::O_QC_CNAME, ); protected static $_default_options = array( self::_VER => '', self::HASH => '', self::O_API_KEY => '', self::O_AUTO_UPGRADE => false, self::O_SERVER_IP => '', self::O_GUEST => false, self::O_GUEST_OPTM => false, self::O_NEWS => false, self::O_GUEST_UAS => array(), self::O_GUEST_IPS => array(), // Cache self::O_CACHE => false, self::O_CACHE_PRIV => false, self::O_CACHE_COMMENTER => false, self::O_CACHE_REST => false, self::O_CACHE_PAGE_LOGIN => false, self::O_CACHE_RES => false, self::O_CACHE_MOBILE => false, self::O_CACHE_MOBILE_RULES => array(), self::O_CACHE_BROWSER => false, self::O_CACHE_EXC_USERAGENTS => array(), self::O_CACHE_EXC_COOKIES => array(), self::O_CACHE_EXC_QS => array(), self::O_CACHE_EXC_CAT => array(), self::O_CACHE_EXC_TAG => array(), self::O_CACHE_FORCE_URI => array(), self::O_CACHE_FORCE_PUB_URI => array(), self::O_CACHE_PRIV_URI => array(), self::O_CACHE_EXC => array(), self::O_CACHE_EXC_ROLES => array(), self::O_CACHE_DROP_QS => array(), self::O_CACHE_TTL_PUB => 0, self::O_CACHE_TTL_PRIV => 0, self::O_CACHE_TTL_FRONTPAGE => 0, self::O_CACHE_TTL_FEED => 0, self::O_CACHE_TTL_REST => 0, self::O_CACHE_TTL_BROWSER => 0, self::O_CACHE_TTL_STATUS => array(), self::O_CACHE_LOGIN_COOKIE => '', self::O_CACHE_AJAX_TTL => array(), self::O_CACHE_VARY_COOKIES => array(), self::O_CACHE_VARY_GROUP => array(), // Purge self::O_PURGE_ON_UPGRADE => false, self::O_PURGE_STALE => false, self::O_PURGE_POST_ALL => false, self::O_PURGE_POST_FRONTPAGE => false, self::O_PURGE_POST_HOMEPAGE => false, self::O_PURGE_POST_PAGES => false, self::O_PURGE_POST_PAGES_WITH_RECENT_POSTS => false, self::O_PURGE_POST_AUTHOR => false, self::O_PURGE_POST_YEAR => false, self::O_PURGE_POST_MONTH => false, self::O_PURGE_POST_DATE => false, self::O_PURGE_POST_TERM => false, self::O_PURGE_POST_POSTTYPE => false, self::O_PURGE_TIMED_URLS => array(), self::O_PURGE_TIMED_URLS_TIME => '', self::O_PURGE_HOOK_ALL => array(), // ESI self::O_ESI => false, self::O_ESI_CACHE_ADMBAR => false, self::O_ESI_CACHE_COMMFORM => false, self::O_ESI_NONCE => array(), // Util self::O_UTIL_INSTANT_CLICK => false, self::O_UTIL_NO_HTTPS_VARY => false, // Debug self::O_DEBUG_DISABLE_ALL => false, self::O_DEBUG => false, self::O_DEBUG_IPS => array(), self::O_DEBUG_LEVEL => false, self::O_DEBUG_FILESIZE => 0, self::O_DEBUG_COLLAPSE_QS => false, self::O_DEBUG_INC => array(), self::O_DEBUG_EXC => array(), self::O_DEBUG_EXC_STRINGS => array(), // DB Optm self::O_DB_OPTM_REVISIONS_MAX => 0, self::O_DB_OPTM_REVISIONS_AGE => 0, // HTML Optm self::O_OPTM_CSS_MIN => false, self::O_OPTM_CSS_COMB => false, self::O_OPTM_CSS_COMB_EXT_INL => false, self::O_OPTM_UCSS => false, self::O_OPTM_UCSS_INLINE => false, self::O_OPTM_UCSS_SELECTOR_WHITELIST => array(), self::O_OPTM_UCSS_FILE_EXC_INLINE => array(), self::O_OPTM_UCSS_EXC => array(), self::O_OPTM_CSS_EXC => array(), self::O_OPTM_JS_MIN => false, self::O_OPTM_JS_COMB => false, self::O_OPTM_JS_COMB_EXT_INL => false, self::O_OPTM_JS_DELAY_INC => array(), self::O_OPTM_JS_EXC => array(), self::O_OPTM_HTML_MIN => false, self::O_OPTM_HTML_LAZY => array(), self::O_OPTM_HTML_SKIP_COMMENTS => array(), self::O_OPTM_QS_RM => false, self::O_OPTM_GGFONTS_RM => false, self::O_OPTM_CSS_ASYNC => false, self::O_OPTM_CCSS_PER_URL => false, self::O_OPTM_CCSS_SEP_POSTTYPE => array(), self::O_OPTM_CCSS_SEP_URI => array(), self::O_OPTM_CCSS_SELECTOR_WHITELIST => array(), self::O_OPTM_CSS_ASYNC_INLINE => false, self::O_OPTM_CSS_FONT_DISPLAY => false, self::O_OPTM_JS_DEFER => false, self::O_OPTM_EMOJI_RM => false, self::O_OPTM_NOSCRIPT_RM => false, self::O_OPTM_GGFONTS_ASYNC => false, self::O_OPTM_EXC_ROLES => array(), self::O_OPTM_CCSS_CON => '', self::O_OPTM_JS_DEFER_EXC => array(), self::O_OPTM_GM_JS_EXC => array(), self::O_OPTM_DNS_PREFETCH => array(), self::O_OPTM_DNS_PREFETCH_CTRL => false, self::O_OPTM_DNS_PRECONNECT => array(), self::O_OPTM_EXC => array(), self::O_OPTM_GUEST_ONLY => false, // Object self::O_OBJECT => false, self::O_OBJECT_KIND => false, self::O_OBJECT_HOST => '', self::O_OBJECT_PORT => 0, self::O_OBJECT_LIFE => 0, self::O_OBJECT_PERSISTENT => false, self::O_OBJECT_ADMIN => false, self::O_OBJECT_TRANSIENTS => false, self::O_OBJECT_DB_ID => 0, self::O_OBJECT_USER => '', self::O_OBJECT_PSWD => '', self::O_OBJECT_GLOBAL_GROUPS => array(), self::O_OBJECT_NON_PERSISTENT_GROUPS => array(), // Discuss self::O_DISCUSS_AVATAR_CACHE => false, self::O_DISCUSS_AVATAR_CRON => false, self::O_DISCUSS_AVATAR_CACHE_TTL => 0, self::O_OPTM_LOCALIZE => false, self::O_OPTM_LOCALIZE_DOMAINS => array(), // Media self::O_MEDIA_LAZY => false, self::O_MEDIA_LAZY_PLACEHOLDER => '', self::O_MEDIA_PLACEHOLDER_RESP => false, self::O_MEDIA_PLACEHOLDER_RESP_COLOR => '', self::O_MEDIA_PLACEHOLDER_RESP_SVG => '', self::O_MEDIA_LQIP => false, self::O_MEDIA_LQIP_QUAL => 0, self::O_MEDIA_LQIP_MIN_W => 0, self::O_MEDIA_LQIP_MIN_H => 0, self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => false, self::O_MEDIA_IFRAME_LAZY => false, self::O_MEDIA_ADD_MISSING_SIZES => false, self::O_MEDIA_LAZY_EXC => array(), self::O_MEDIA_LAZY_CLS_EXC => array(), self::O_MEDIA_LAZY_PARENT_CLS_EXC => array(), self::O_MEDIA_IFRAME_LAZY_CLS_EXC => array(), self::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC => array(), self::O_MEDIA_LAZY_URI_EXC => array(), self::O_MEDIA_LQIP_EXC => array(), self::O_MEDIA_VPI => false, self::O_MEDIA_VPI_CRON => false, // Image Optm self::O_IMG_OPTM_AUTO => false, self::O_IMG_OPTM_ORI => false, self::O_IMG_OPTM_RM_BKUP => false, self::O_IMG_OPTM_WEBP => false, self::O_IMG_OPTM_LOSSLESS => false, self::O_IMG_OPTM_EXIF => false, self::O_IMG_OPTM_WEBP_ATTR => array(), self::O_IMG_OPTM_WEBP_REPLACE_SRCSET => false, self::O_IMG_OPTM_JPG_QUALITY => 0, // Crawler self::O_CRAWLER => false, self::O_CRAWLER_CRAWL_INTERVAL => 0, self::O_CRAWLER_LOAD_LIMIT => 0, self::O_CRAWLER_SITEMAP => '', self::O_CRAWLER_ROLES => array(), self::O_CRAWLER_COOKIES => array(), // Misc self::O_MISC_HEARTBEAT_FRONT => false, self::O_MISC_HEARTBEAT_FRONT_TTL => 0, self::O_MISC_HEARTBEAT_BACK => false, self::O_MISC_HEARTBEAT_BACK_TTL => 0, self::O_MISC_HEARTBEAT_EDITOR => false, self::O_MISC_HEARTBEAT_EDITOR_TTL => 0, // CDN self::O_CDN => false, self::O_CDN_ORI => array(), self::O_CDN_ORI_DIR => array(), self::O_CDN_EXC => array(), self::O_CDN_QUIC => false, self::O_CDN_CLOUDFLARE => false, self::O_CDN_CLOUDFLARE_EMAIL => '', self::O_CDN_CLOUDFLARE_KEY => '', self::O_CDN_CLOUDFLARE_NAME => '', self::O_CDN_CLOUDFLARE_ZONE => '', self::O_CDN_MAPPING => array(), self::O_CDN_ATTR => array(), self::O_QC_NAMESERVERS => '', self::O_QC_CNAME => '', ); protected static $_default_site_options = array( self::_VER => '', self::O_CACHE => false, self::NETWORK_O_USE_PRIMARY => false, self::O_AUTO_UPGRADE => false, self::O_GUEST => false, self::O_CACHE_RES => false, self::O_CACHE_BROWSER => false, self::O_CACHE_MOBILE => false, self::O_CACHE_MOBILE_RULES => array(), self::O_CACHE_LOGIN_COOKIE => '', self::O_CACHE_VARY_COOKIES => array(), self::O_CACHE_EXC_COOKIES => array(), self::O_CACHE_EXC_USERAGENTS => array(), self::O_CACHE_TTL_BROWSER => 0, self::O_PURGE_ON_UPGRADE => false, self::O_OBJECT => false, self::O_OBJECT_KIND => false, self::O_OBJECT_HOST => '', self::O_OBJECT_PORT => 0, self::O_OBJECT_LIFE => 0, self::O_OBJECT_PERSISTENT => false, self::O_OBJECT_ADMIN => false, self::O_OBJECT_TRANSIENTS => false, self::O_OBJECT_DB_ID => 0, self::O_OBJECT_USER => '', self::O_OBJECT_PSWD => '', self::O_OBJECT_GLOBAL_GROUPS => array(), self::O_OBJECT_NON_PERSISTENT_GROUPS => array(), // Debug self::O_DEBUG_DISABLE_ALL => false, self::O_DEBUG => false, self::O_DEBUG_IPS => array(), self::O_DEBUG_LEVEL => false, self::O_DEBUG_FILESIZE => 0, self::O_DEBUG_COLLAPSE_QS => false, self::O_DEBUG_INC => array(), self::O_DEBUG_EXC => array(), self::O_DEBUG_EXC_STRINGS => array(), self::O_IMG_OPTM_WEBP => false, ); // NOTE: all the val of following items will be int while not bool protected static $_multi_switch_list = array( self::O_DEBUG => 2, self::O_OPTM_JS_DEFER => 2, self::O_IMG_OPTM_WEBP => 2, ); /** * Correct the option type * * TODO: add similar network func * * @since 3.0.3 */ protected function type_casting($val, $id, $is_site_conf = false) { $default_v = !$is_site_conf ? self::$_default_options[$id] : self::$_default_site_options[$id]; if (is_bool($default_v)) { if ($val === 'true') { $val = true; } if ($val === 'false') { $val = false; } $max = $this->_conf_multi_switch($id); if ($max) { $val = (int) $val; $val %= $max + 1; } else { $val = (bool) $val; } } elseif (is_array($default_v)) { // from textarea input if (!is_array($val)) { $val = Utility::sanitize_lines($val, $this->_conf_filter($id)); } } elseif (!is_string($default_v)) { $val = (int) $val; } else { // Check if the string has a limit set $val = $this->_conf_string_val($id, $val); } return $val; } /** * Load default network settings from data.ini * * @since 3.0 */ public function load_default_site_vals() { // Load network_default.json if (file_exists(LSCWP_DIR . 'data/const.network_default.json')) { $default_ini_cfg = json_decode(File::read(LSCWP_DIR . 'data/const.network_default.json'), true); foreach (self::$_default_site_options as $k => $v) { if (!array_key_exists($k, $default_ini_cfg)) { continue; } // Parse value in ini file $ini_v = $this->type_casting($default_ini_cfg[$k], $k, true); if ($ini_v == $v) { continue; } self::$_default_site_options[$k] = $ini_v; } } self::$_default_site_options[self::_VER] = Core::VER; return self::$_default_site_options; } /** * Load default values from default.json * * @since 3.0 * @access public */ public function load_default_vals() { // Load default.json if (file_exists(LSCWP_DIR . 'data/const.default.json')) { $default_ini_cfg = json_decode(File::read(LSCWP_DIR . 'data/const.default.json'), true); foreach (self::$_default_options as $k => $v) { if (!array_key_exists($k, $default_ini_cfg)) { continue; } // Parse value in ini file $ini_v = $this->type_casting($default_ini_cfg[$k], $k); // NOTE: Multiple lines value must be stored as array /** * Special handler for CDN_mapping * * format in .ini: * [cdn-mapping] * url[0] = 'https://example.com/' * inc_js[0] = true * filetype[0] = '.css * .js * .jpg' * * format out: * [0] = [ 'url' => 'https://example.com', 'inc_js' => true, 'filetype' => [ '.css', '.js', '.jpg' ] ] */ if ($k == self::O_CDN_MAPPING) { $mapping_fields = array( self::CDN_MAPPING_URL, self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS, self::CDN_MAPPING_FILETYPE, // Array ); $ini_v2 = array(); foreach ($ini_v[self::CDN_MAPPING_URL] as $k2 => $v2) { // $k2 is numeric $this_row = array(); foreach ($mapping_fields as $v3) { $this_v = !empty($ini_v[$v3][$k2]) ? $ini_v[$v3][$k2] : false; if ($v3 == self::CDN_MAPPING_URL) { $this_v = $this_v ?: ''; } if ($v3 == self::CDN_MAPPING_FILETYPE) { $this_v = $this_v ? Utility::sanitize_lines($this_v) : array(); // Note: Since v3.0 its already an array } $this_row[$v3] = $this_v; } $ini_v2[$k2] = $this_row; } $ini_v = $ini_v2; } if ($ini_v == $v) { continue; } self::$_default_options[$k] = $ini_v; } } // Load internal default vals // Setting the default bool to int is also to avoid type casting override it back to bool self::$_default_options[self::O_CACHE] = is_multisite() ? self::VAL_ON2 : self::VAL_ON; //For multi site, default is 2 (Use Network Admin Settings). For single site, default is 1 (Enabled). // Load default vals containing variables if (!self::$_default_options[self::O_CDN_ORI_DIR]) { self::$_default_options[self::O_CDN_ORI_DIR] = LSCWP_CONTENT_FOLDER . "\nwp-includes"; self::$_default_options[self::O_CDN_ORI_DIR] = explode("\n", self::$_default_options[self::O_CDN_ORI_DIR]); self::$_default_options[self::O_CDN_ORI_DIR] = array_map('trim', self::$_default_options[self::O_CDN_ORI_DIR]); } // Set security key if not initialized yet if (!self::$_default_options[self::HASH]) { self::$_default_options[self::HASH] = Str::rrand(32); } self::$_default_options[self::_VER] = Core::VER; return self::$_default_options; } /** * Format the string value * * @since 3.0 */ protected function _conf_string_val($id, $val) { return $val; } /** * If the switch setting is a triple value or not * * @since 3.0 */ protected function _conf_multi_switch($id) { if (!empty(self::$_multi_switch_list[$id])) { return self::$_multi_switch_list[$id]; } if ($id == self::O_CACHE && is_multisite()) { return self::VAL_ON2; } return false; } /** * Append a new multi switch max limit for the bool option * * @since 3.0 */ public static function set_multi_switch($id, $v) { self::$_multi_switch_list[$id] = $v; } /** * Generate const name based on $id * * @since 3.0 */ public static function conf_const($id) { return 'LITESPEED_CONF__' . strtoupper(str_replace('-', '__', $id)); } /** * Filter to be used when saving setting * * @since 3.0 */ protected function _conf_filter($id) { $filters = array( self::O_MEDIA_LAZY_EXC => 'uri', self::O_DEBUG_INC => 'relative', self::O_DEBUG_EXC => 'relative', self::O_MEDIA_LAZY_URI_EXC => 'relative', self::O_CACHE_PRIV_URI => 'relative', self::O_PURGE_TIMED_URLS => 'relative', self::O_CACHE_FORCE_URI => 'relative', self::O_CACHE_FORCE_PUB_URI => 'relative', self::O_CACHE_EXC => 'relative', // self::O_OPTM_CSS_EXC => 'uri', // Need to comment out for inline & external CSS // self::O_OPTM_JS_EXC => 'uri', self::O_OPTM_EXC => 'relative', self::O_OPTM_CCSS_SEP_URI => 'uri', // self::O_OPTM_JS_DEFER_EXC => 'uri', self::O_OPTM_DNS_PREFETCH => 'domain', self::O_CDN_ORI => 'noprotocol,trailingslash', // `Original URLs` // self::O_OPTM_LOCALIZE_DOMAINS => 'noprotocol', // `Localize Resources` // self:: => '', // self:: => '', ); if (!empty($filters[$id])) { return $filters[$id]; } return false; } /** * If the setting changes worth a purge or not * * @since 3.0 */ protected function _conf_purge($id) { $check_ids = array( self::O_MEDIA_LAZY_URI_EXC, self::O_OPTM_EXC, self::O_CACHE_PRIV_URI, self::O_PURGE_TIMED_URLS, self::O_CACHE_FORCE_URI, self::O_CACHE_FORCE_PUB_URI, self::O_CACHE_EXC, ); return in_array($id, $check_ids); } /** * If the setting changes worth a purge ALL or not * * @since 3.0 */ protected function _conf_purge_all($id) { $check_ids = array(self::O_CACHE, self::O_ESI, self::O_DEBUG_DISABLE_ALL, self::NETWORK_O_USE_PRIMARY); return in_array($id, $check_ids); } /** * If the setting is a pswd or not * * @since 3.0 */ protected function _conf_pswd($id) { $check_ids = array(self::O_CDN_CLOUDFLARE_KEY, self::O_OBJECT_PSWD); return in_array($id, $check_ids); } /** * If the setting is cron related or not * * @since 3.0 */ protected function _conf_cron($id) { $check_ids = array(self::O_OPTM_CSS_ASYNC, self::O_MEDIA_PLACEHOLDER_RESP_ASYNC, self::O_DISCUSS_AVATAR_CRON, self::O_IMG_OPTM_AUTO, self::O_CRAWLER); return in_array($id, $check_ids); } /** * If the setting changes worth a purge, return the tag * * @since 3.0 */ protected function _conf_purge_tag($id) { $check_ids = array( self::O_CACHE_PAGE_LOGIN => Tag::TYPE_LOGIN, ); if (!empty($check_ids[$id])) { return $check_ids[$id]; } return false; } /** * Generate server vars * * @since 2.4.1 */ public function server_vars() { $consts = array( 'WP_SITEURL', 'WP_HOME', 'WP_CONTENT_DIR', 'SHORTINIT', 'LSCWP_CONTENT_DIR', 'LSCWP_CONTENT_FOLDER', 'LSCWP_DIR', 'LITESPEED_TIME_OFFSET', 'LITESPEED_SERVER_TYPE', 'LITESPEED_CLI', 'LITESPEED_ALLOWED', 'LITESPEED_ON', 'LSWCP_TAG_PREFIX', 'COOKIEHASH', ); $server_vars = array(); foreach ($consts as $v) { $server_vars[$v] = defined($v) ? constant($v) : null; } return $server_vars; } } src/vary.cls.php 0000644 00000050134 15162130562 0007611 0 ustar 00 <?php /** * The plugin vary class to manage X-LiteSpeed-Vary * * @since 1.1.3 */ namespace LiteSpeed; defined('WPINC') || exit(); class Vary extends Root { const LOG_TAG = '🔱'; const X_HEADER = 'X-LiteSpeed-Vary'; private static $_vary_name = '_lscache_vary'; // this default vary cookie is used for logged in status check private static $_can_change_vary = false; // Currently only AJAX used this /** * Adds the actions used for setting up cookies on log in/out. * * Also checks if the database matches the rewrite rule. * * @since 1.0.4 */ // public function init() // { // $this->_update_vary_name(); // } /** * Update the default vary name if changed * * @since 4.0 * @since 7.0 Moved to after_user_init to allow ESI no-vary no conflict w/ LSCACHE_VARY_COOKIE/O_CACHE_LOGIN_COOKIE */ private function _update_vary_name() { $db_cookie = $this->conf(Base::O_CACHE_LOGIN_COOKIE); // [3.0] todo: check if works in network's sites // If no vary set in rewrite rule if (!isset($_SERVER['LSCACHE_VARY_COOKIE'])) { if ($db_cookie) { // Check if is from ESI req or not. If from ESI no-vary, no need to set no-cache $something_wrong = true; if (!empty($_GET[ESI::QS_ACTION]) && !empty($_GET['_control'])) { // Have to manually build this checker bcoz ESI is not init yet. $control = explode(',', $_GET['_control']); if (in_array('no-vary', $control)) { self::debug('no-vary control existed, bypass vary_name update'); $something_wrong = false; self::$_vary_name = $db_cookie; } } if (defined('LITESPEED_CLI') || defined('DOING_CRON')) { $something_wrong = false; } if ($something_wrong) { // Display cookie error msg to admin if (is_multisite() ? is_network_admin() : is_admin()) { Admin_Display::show_error_cookie(); } Control::set_nocache('❌❌ vary cookie setting error'); } } return; } // If db setting does not exist, skip checking db value if (!$db_cookie) { return; } // beyond this point, need to make sure db vary setting is in $_SERVER env. $vary_arr = explode(',', $_SERVER['LSCACHE_VARY_COOKIE']); if (in_array($db_cookie, $vary_arr)) { self::$_vary_name = $db_cookie; return; } if (is_multisite() ? is_network_admin() : is_admin()) { Admin_Display::show_error_cookie(); } Control::set_nocache('vary cookie setting lost error'); } /** * Hooks after user init * * @since 4.0 */ public function after_user_init() { $this->_update_vary_name(); // logged in user if (Router::is_logged_in()) { // If not esi, check cache logged-in user setting if (!$this->cls('Router')->esi_enabled()) { // If cache logged-in, then init cacheable to private if ($this->conf(Base::O_CACHE_PRIV)) { add_action('wp_logout', __NAMESPACE__ . '\Purge::purge_on_logout'); $this->cls('Control')->init_cacheable(); Control::set_private('logged in user'); } // No cache for logged-in user else { Control::set_nocache('logged in user'); } } // ESI is on, can be public cache else { // Need to make sure vary is using group id $this->cls('Control')->init_cacheable(); } // register logout hook to clear login status add_action('clear_auth_cookie', array($this, 'remove_logged_in')); } else { // Only after vary init, can detect if is Guest mode or not // Here need `self::$_vary_name` to be set first. $this->_maybe_guest_mode(); // Set vary cookie for logging in user, otherwise the user will hit public with vary=0 (guest version) add_action('set_logged_in_cookie', array($this, 'add_logged_in'), 10, 4); add_action('wp_login', __NAMESPACE__ . '\Purge::purge_on_logout'); $this->cls('Control')->init_cacheable(); // Check `login page` cacheable setting because they don't go through main WP logic add_action('login_init', array($this->cls('Tag'), 'check_login_cacheable'), 5); if (!empty($_GET['litespeed_guest'])) { add_action('wp_loaded', array($this, 'update_guest_vary'), 20); } } // Add comment list ESI add_filter('comments_array', array($this, 'check_commenter')); // Set vary cookie for commenter. add_action('set_comment_cookies', array($this, 'append_commenter')); /** * Don't change for REST call because they don't carry on user info usually * @since 1.6.7 */ add_action('rest_api_init', function () { // this hook is fired in `init` hook self::debug('Rest API init disabled vary change'); add_filter('litespeed_can_change_vary', '__return_false'); }); } /** * Check if is Guest mode or not * * @since 4.0 */ private function _maybe_guest_mode() { if (defined('LITESPEED_GUEST')) { self::debug('👒👒 Guest mode ' . (LITESPEED_GUEST ? 'predefined' : 'turned off')); return; } if (!$this->conf(Base::O_GUEST)) { return; } // If vary is set, then not a guest if (self::has_vary()) { return; } // If has admin QS, then no guest if (!empty($_GET[Router::ACTION])) { return; } if (defined('DOING_AJAX')) { return; } if (defined('DOING_CRON')) { return; } // If is the request to update vary, then no guest // Don't need anymore as it is always ajax call // Still keep it in case some WP blocked the lightweight guest vary update script, WP can still update the vary if (!empty($_GET['litespeed_guest'])) { return; } /* @ref https://wordpress.org/support/topic/checkout-add-to-cart-executed-twice/ */ if (!empty($_GET['litespeed_guest_off'])) { return; } self::debug('👒👒 Guest mode'); !defined('LITESPEED_GUEST') && define('LITESPEED_GUEST', true); if ($this->conf(Base::O_GUEST_OPTM)) { !defined('LITESPEED_GUEST_OPTM') && define('LITESPEED_GUEST_OPTM', true); } } /** * Update Guest vary * * @since 4.0 * @deprecated 4.1 Use independent lightweight guest.vary.php as a replacement */ public function update_guest_vary() { // This process must not be cached !defined('LSCACHE_NO_CACHE') && define('LSCACHE_NO_CACHE', true); $_guest = new Lib\Guest(); if ($_guest->always_guest() || self::has_vary()) { // If contains vary already, don't reload to avoid infinite loop when parent page having browser cache !defined('LITESPEED_GUEST') && define('LITESPEED_GUEST', true); // Reuse this const to bypass set vary in vary finalize self::debug('🤠🤠 Guest'); echo '[]'; exit(); } self::debug('Will update guest vary in finalize'); // return json echo \json_encode(array('reload' => 'yes')); exit(); } /** * Hooked to the comments_array filter. * * Check if the user accessing the page has the commenter cookie. * * If the user does not want to cache commenters, just check if user is commenter. * Otherwise if the vary cookie is set, unset it. This is so that when the page is cached, the page will appear as if the user was a normal user. * Normal user is defined as not a logged in user and not a commenter. * * @since 1.0.4 * @access public * @global type $post * @param array $comments The current comments to output * @return array The comments to output. */ public function check_commenter($comments) { /** * Hook to bypass pending comment check for comment related plugins compatibility * @since 2.9.5 */ if (apply_filters('litespeed_vary_check_commenter_pending', true)) { $pending = false; foreach ($comments as $comment) { if (!$comment->comment_approved) { // current user has pending comment $pending = true; break; } } // No pending comments, don't need to add private cache if (!$pending) { self::debug('No pending comment'); $this->remove_commenter(); // Remove commenter prefilled info if exists, for public cache foreach ($_COOKIE as $cookie_name => $cookie_value) { if (strlen($cookie_name) >= 15 && strpos($cookie_name, 'comment_author_') === 0) { unset($_COOKIE[$cookie_name]); } } return $comments; } } // Current user/visitor has pending comments // set vary=2 for next time vary lookup $this->add_commenter(); if ($this->conf(Base::O_CACHE_COMMENTER)) { Control::set_private('existing commenter'); } else { Control::set_nocache('existing commenter'); } return $comments; } /** * Check if default vary has a value * * @since 1.1.3 * @access public */ public static function has_vary() { if (empty($_COOKIE[self::$_vary_name])) { return false; } return $_COOKIE[self::$_vary_name]; } /** * Append user status with logged in * * @since 1.1.3 * @since 1.6.2 Removed static referral * @access public */ public function add_logged_in($logged_in_cookie = false, $expire = false, $expiration = false, $uid = false) { self::debug('add_logged_in'); /** * NOTE: Run before `$this->_update_default_vary()` to make vary changeable * @since 2.2.2 */ self::can_ajax_vary(); // If the cookie is lost somehow, set it $this->_update_default_vary($uid, $expire); } /** * Remove user logged in status * * @since 1.1.3 * @since 1.6.2 Removed static referral * @access public */ public function remove_logged_in() { self::debug('remove_logged_in'); /** * NOTE: Run before `$this->_update_default_vary()` to make vary changeable * @since 2.2.2 */ self::can_ajax_vary(); // Force update vary to remove login status $this->_update_default_vary(-1); } /** * Allow vary can be changed for ajax calls * * @since 2.2.2 * @since 2.6 Changed to static * @access public */ public static function can_ajax_vary() { self::debug('_can_change_vary -> true'); self::$_can_change_vary = true; } /** * Check if can change default vary * * @since 1.6.2 * @access private */ private function can_change_vary() { // Don't change for ajax due to ajax not sending webp header if (Router::is_ajax()) { if (!self::$_can_change_vary) { self::debug('can_change_vary bypassed due to ajax call'); return false; } } /** * POST request can set vary to fix #820789 login "loop" guest cache issue * @since 1.6.5 */ if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'POST') { self::debug('can_change_vary bypassed due to method not get/post'); return false; } /** * Disable vary change if is from crawler * @since 2.9.8 To enable woocommerce cart not empty warm up (@Taba) */ if (!empty($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], Crawler::FAST_USER_AGENT) === 0) { self::debug('can_change_vary bypassed due to crawler'); return false; } if (!apply_filters('litespeed_can_change_vary', true)) { self::debug('can_change_vary bypassed due to litespeed_can_change_vary hook'); return false; } return true; } /** * Update default vary * * @since 1.6.2 * @since 1.6.6.1 Add ran check to make it only run once ( No run multiple times due to login process doesn't have valid uid ) * @access private */ private function _update_default_vary($uid = false, $expire = false) { // Make sure header output only run once if (!defined('LITESPEED_DID_' . __FUNCTION__)) { define('LITESPEED_DID_' . __FUNCTION__, true); } else { self::debug2('_update_default_vary bypassed due to run already'); return; } // ESI shouldn't change vary (Let main page do only) if (defined('LSCACHE_IS_ESI') && LSCACHE_IS_ESI) { self::debug2('_update_default_vary bypassed due to ESI'); return; } // If the cookie is lost somehow, set it $vary = $this->finalize_default_vary($uid); $current_vary = self::has_vary(); if ($current_vary !== $vary && $current_vary !== 'commenter' && $this->can_change_vary()) { // $_COOKIE[ self::$_vary_name ] = $vary; // not needed // save it if (!$expire) { $expire = time() + 2 * DAY_IN_SECONDS; } $this->_cookie($vary, $expire); // Control::set_nocache( 'changing default vary' . " $current_vary => $vary" ); } } /** * Get vary name * * @since 1.9.1 * @access public */ public function get_vary_name() { return self::$_vary_name; } /** * Check if one user role is in vary group settings * * @since 1.2.0 * @since 3.0 Moved here from conf.cls * @access public * @param string $role The user role * @return int The set value if already set */ public function in_vary_group($role) { $group = 0; $vary_groups = $this->conf(Base::O_CACHE_VARY_GROUP); $roles = explode(',', $role); if ($found = array_intersect($roles, array_keys($vary_groups))) { $groups = array(); foreach ($found as $curr_role) { $groups[] = $vary_groups[$curr_role]; } $group = implode(',', array_unique($groups)); } elseif (in_array('administrator', $roles)) { $group = 99; } if ($group) { self::debug2('role in vary_group [group] ' . $group); } return $group; } /** * Finalize default Vary Cookie * * Get user vary tag based on admin_bar & role * * NOTE: Login process will also call this because it does not call wp hook as normal page loading * * @since 1.6.2 * @access public */ public function finalize_default_vary($uid = false) { // Must check this to bypass vary generation for guests // Must check this to avoid Guest page's CSS/JS/CCSS/UCSS get non-guest vary filename if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { return false; } $vary = array(); if ($this->conf(Base::O_GUEST)) { $vary['guest_mode'] = 1; } if (!$uid) { $uid = get_current_user_id(); } else { self::debug('uid: ' . $uid); } // get user's group id $role = Router::get_role($uid); if ($uid > 0 && $role) { $vary['logged-in'] = 1; // parse role group from settings if ($role_group = $this->in_vary_group($role)) { $vary['role'] = $role_group; } // Get admin bar set // see @_get_admin_bar_pref() $pref = get_user_option('show_admin_bar_front', $uid); self::debug2('show_admin_bar_front: ' . $pref); $admin_bar = $pref === false || $pref === 'true'; if ($admin_bar) { $vary['admin_bar'] = 1; self::debug2('admin bar : true'); } } else { // Guest user self::debug('role id: failed, guest'); } /** * Add filter * @since 1.6 Added for Role Excludes for optimization cls * @since 1.6.2 Hooked to webp (checked in v4, no webp anymore) * @since 3.0 Used by 3rd hooks too */ $vary = apply_filters('litespeed_vary', $vary); if (!$vary) { return false; } ksort($vary); $res = array(); foreach ($vary as $key => $val) { $res[] = $key . ':' . $val; } $res = implode(';', $res); if (defined('LSCWP_LOG')) { return $res; } // Encrypt in production return md5($this->conf(Base::HASH) . $res); } /** * Get the hash of all vary related values * * @since 4.0 */ public function finalize_full_varies() { $vary = $this->_finalize_curr_vary_cookies(true); $vary .= $this->finalize_default_vary(get_current_user_id()); $vary .= $this->get_env_vary(); return $vary; } /** * Get request environment Vary * * @since 4.0 */ public function get_env_vary() { $env_vary = isset($_SERVER['LSCACHE_VARY_VALUE']) ? $_SERVER['LSCACHE_VARY_VALUE'] : false; if (!$env_vary) { $env_vary = isset($_SERVER['HTTP_X_LSCACHE_VARY_VALUE']) ? $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] : false; } return $env_vary; } /** * Append user status with commenter * * This is ONLY used when submit a comment * * @since 1.1.6 * @access public */ public function append_commenter() { $this->add_commenter(true); } /** * Correct user status with commenter * * @since 1.1.3 * @access private * @param boolean $from_redirect If the request is from redirect page or not */ private function add_commenter($from_redirect = false) { // If the cookie is lost somehow, set it if (self::has_vary() !== 'commenter') { self::debug('Add commenter'); // $_COOKIE[ self::$_vary_name ] = 'commenter'; // not needed // save it // only set commenter status for current domain path $this->_cookie('commenter', time() + apply_filters('comment_cookie_lifetime', 30000000), self::_relative_path($from_redirect)); // Control::set_nocache( 'adding commenter status' ); } } /** * Remove user commenter status * * @since 1.1.3 * @access private */ private function remove_commenter() { if (self::has_vary() === 'commenter') { self::debug('Remove commenter'); // remove logged in status from global var // unset( $_COOKIE[ self::$_vary_name ] ); // not needed // save it $this->_cookie(false, false, self::_relative_path()); // Control::set_nocache( 'removing commenter status' ); } } /** * Generate relative path for cookie * * @since 1.1.3 * @access private * @param boolean $from_redirect If the request is from redirect page or not */ private static function _relative_path($from_redirect = false) { $path = false; $tag = $from_redirect ? 'HTTP_REFERER' : 'SCRIPT_URL'; if (!empty($_SERVER[$tag])) { $path = parse_url($_SERVER[$tag]); $path = !empty($path['path']) ? $path['path'] : false; self::debug('Cookie Vary path: ' . $path); } return $path; } /** * Builds the vary header. * * NOTE: Non caccheable page can still set vary ( for logged in process ) * * Currently, this only checks post passwords and 3rd party. * * @since 1.0.13 * @access public * @global $post * @return mixed false if the user has the postpass cookie. Empty string if the post is not password protected. Vary header otherwise. */ public function finalize() { // Finalize default vary if (!defined('LITESPEED_GUEST') || !LITESPEED_GUEST) { $this->_update_default_vary(); } $tp_cookies = $this->_finalize_curr_vary_cookies(); if (!$tp_cookies) { self::debug2('no custimzed vary'); return; } self::debug('finalized 3rd party cookies', $tp_cookies); return self::X_HEADER . ': ' . implode(',', $tp_cookies); } /** * Gets vary cookies or their values unique hash that are already added for the current page. * * @since 1.0.13 * @access private * @return array List of all vary cookies currently added. */ private function _finalize_curr_vary_cookies($values_json = false) { global $post; $cookies = array(); // No need to append default vary cookie name if (!empty($post->post_password)) { $postpass_key = 'wp-postpass_' . COOKIEHASH; if ($this->_get_cookie_val($postpass_key)) { self::debug('finalize bypassed due to password protected vary '); // If user has password cookie, do not cache & ignore existing vary cookies Control::set_nocache('password protected vary'); return false; } $cookies[] = $values_json ? $this->_get_cookie_val($postpass_key) : $postpass_key; } $cookies = apply_filters('litespeed_vary_curr_cookies', $cookies); if ($cookies) { $cookies = array_filter(array_unique($cookies)); self::debug('vary cookies changed by filter litespeed_vary_curr_cookies', $cookies); } if (!$cookies) { return false; } // Format cookie name data or value data sort($cookies); // This is to maintain the cookie val orders for $values_json=true case. foreach ($cookies as $k => $v) { $cookies[$k] = $values_json ? $this->_get_cookie_val($v) : 'cookie=' . $v; } return $values_json ? \json_encode($cookies) : $cookies; } /** * Get one vary cookie value * * @since 4.0 */ private function _get_cookie_val($key) { if (!empty($_COOKIE[$key])) { return $_COOKIE[$key]; } return false; } /** * Set the vary cookie. * * If vary cookie changed, must set non cacheable. * * @since 1.0.4 * @access private * @param integer $val The value to update. * @param integer $expire Expire time. * @param boolean $path False if use wp root path as cookie path */ private function _cookie($val = false, $expire = false, $path = false) { if (!$val) { $expire = 1; } /** * Add HTTPS bypass in case clients use both HTTP and HTTPS version of site * @since 1.7 */ $is_ssl = $this->conf(Base::O_UTIL_NO_HTTPS_VARY) ? false : is_ssl(); setcookie(self::$_vary_name, $val, $expire, $path ?: COOKIEPATH, COOKIE_DOMAIN, $is_ssl, true); self::debug('set_cookie ---> [k] ' . self::$_vary_name . " [v] $val [ttl] " . ($expire - time())); } } src/control.cls.php 0000644 00000053211 15162130563 0010310 0 ustar 00 <?php /** * The plugin cache-control class for X-Litespeed-Cache-Control * * @since 1.1.3 * @package LiteSpeed * @subpackage LiteSpeed/inc * @author LiteSpeed Technologies <info@litespeedtech.com> */ namespace LiteSpeed; defined('WPINC') || exit(); class Control extends Root { const LOG_TAG = '💵'; const BM_CACHEABLE = 1; const BM_PRIVATE = 2; const BM_SHARED = 4; const BM_NO_VARY = 8; const BM_FORCED_CACHEABLE = 32; const BM_PUBLIC_FORCED = 64; const BM_STALE = 128; const BM_NOTCACHEABLE = 256; const X_HEADER = 'X-LiteSpeed-Cache-Control'; protected static $_control = 0; protected static $_custom_ttl = 0; private $_response_header_ttls = array(); /** * Init cache control * * @since 1.6.2 */ public function init() { /** * Add vary filter for Role Excludes * @since 1.6.2 */ add_filter('litespeed_vary', array($this, 'vary_add_role_exclude')); // 301 redirect hook add_filter('wp_redirect', array($this, 'check_redirect'), 10, 2); // Load response header conf $this->_response_header_ttls = $this->conf(Base::O_CACHE_TTL_STATUS); foreach ($this->_response_header_ttls as $k => $v) { $v = explode(' ', $v); if (empty($v[0]) || empty($v[1])) { continue; } $this->_response_header_ttls[$v[0]] = $v[1]; } if ($this->conf(Base::O_PURGE_STALE)) { $this->set_stale(); } } /** * Exclude role from optimization filter * * @since 1.6.2 * @access public */ public function vary_add_role_exclude($vary) { if ($this->in_cache_exc_roles()) { $vary['role_exclude_cache'] = 1; } return $vary; } /** * Check if one user role is in exclude cache group settings * * @since 1.6.2 * @since 3.0 Moved here from conf.cls * @access public * @param string $role The user role * @return int The set value if already set */ public function in_cache_exc_roles($role = null) { // Get user role if ($role === null) { $role = Router::get_role(); } if (!$role) { return false; } $roles = explode(',', $role); $found = array_intersect($roles, $this->conf(Base::O_CACHE_EXC_ROLES)); return $found ? implode(',', $found) : false; } /** * 1. Initialize cacheable status for `wp` hook * 2. Hook error page tags for cacheable pages * * @since 1.1.3 * @access public */ public function init_cacheable() { // Hook `wp` to mark default cacheable status // NOTE: Any process that does NOT run into `wp` hook will not get cacheable by default add_action('wp', array($this, 'set_cacheable'), 5); // Hook WP REST to be cacheable if ($this->conf(Base::O_CACHE_REST)) { add_action('rest_api_init', array($this, 'set_cacheable'), 5); } // Cache resources // NOTE: If any strange resource doesn't use normal WP logic `wp_loaded` hook, rewrite rule can handle it $cache_res = $this->conf(Base::O_CACHE_RES); if ($cache_res) { $uri = esc_url($_SERVER['REQUEST_URI']); // todo: check if need esc_url() $pattern = '!' . LSCWP_CONTENT_FOLDER . Htaccess::RW_PATTERN_RES . '!'; if (preg_match($pattern, $uri)) { add_action('wp_loaded', array($this, 'set_cacheable'), 5); } } // AJAX cache $ajax_cache = $this->conf(Base::O_CACHE_AJAX_TTL); foreach ($ajax_cache as $v) { $v = explode(' ', $v); if (empty($v[0]) || empty($v[1])) { continue; } // self::debug("Initializing cacheable status for wp_ajax_nopriv_" . $v[0]); add_action( 'wp_ajax_nopriv_' . $v[0], function () use ($v) { self::set_custom_ttl($v[1]); self::force_cacheable('ajax Cache setting for action ' . $v[0]); }, 4 ); } // Check error page add_filter('status_header', array($this, 'check_error_codes'), 10, 2); } /** * Check if the page returns any error code. * * @since 1.0.13.1 * @access public * @param $status_header * @param $code * @return $error_status */ public function check_error_codes($status_header, $code) { if (array_key_exists($code, $this->_response_header_ttls)) { if (self::is_cacheable() && !$this->_response_header_ttls[$code]) { self::set_nocache('[Ctrl] TTL is set to no cache [status_header] ' . $code); } // Set TTL self::set_custom_ttl($this->_response_header_ttls[$code]); } elseif (self::is_cacheable()) { if (substr($code, 0, 1) == 4 || substr($code, 0, 1) == 5) { self::set_nocache('[Ctrl] 4xx/5xx default to no cache [status_header] ' . $code); } } // Set cache tag if (in_array($code, Tag::$error_code_tags)) { Tag::add(Tag::TYPE_HTTP . $code); } // Give the default status_header back return $status_header; } /** * Set no vary setting * * @access public * @since 1.1.3 */ public static function set_no_vary() { if (self::is_no_vary()) { return; } self::$_control |= self::BM_NO_VARY; self::debug('X Cache_control -> no-vary', 3); } /** * Get no vary setting * * @access public * @since 1.1.3 */ public static function is_no_vary() { return self::$_control & self::BM_NO_VARY; } /** * Set stale * * @access public * @since 1.1.3 */ public function set_stale() { if (self::is_stale()) { return; } self::$_control |= self::BM_STALE; self::debug('X Cache_control -> stale'); } /** * Get stale * * @access public * @since 1.1.3 */ public static function is_stale() { return self::$_control & self::BM_STALE; } /** * Set cache control to shared private * * @access public * @since 1.1.3 * @param string $reason The reason to no cache */ public static function set_shared($reason = false) { if (self::is_shared()) { return; } self::$_control |= self::BM_SHARED; self::set_private(); if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = "( $reason )"; } self::debug('X Cache_control -> shared ' . $reason); } /** * Check if is shared private * * @access public * @since 1.1.3 */ public static function is_shared() { return self::$_control & self::BM_SHARED && self::is_private(); } /** * Set cache control to forced public * * @access public * @since 1.7.1 */ public static function set_public_forced($reason = false) { if (self::is_public_forced()) { return; } self::$_control |= self::BM_PUBLIC_FORCED; if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = "( $reason )"; } self::debug('X Cache_control -> public forced ' . $reason); } /** * Check if is public forced * * @access public * @since 1.7.1 */ public static function is_public_forced() { return self::$_control & self::BM_PUBLIC_FORCED; } /** * Set cache control to private * * @access public * @since 1.1.3 * @param string $reason The reason to no cache */ public static function set_private($reason = false) { if (self::is_private()) { return; } self::$_control |= self::BM_PRIVATE; if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = "( $reason )"; } self::debug('X Cache_control -> private ' . $reason); } /** * Check if is private * * @access public * @since 1.1.3 */ public static function is_private() { if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { // return false; } return self::$_control & self::BM_PRIVATE && !self::is_public_forced(); } /** * Initialize cacheable status in `wp` hook, if not call this, by default it will be non-cacheable * * @access public * @since 1.1.3 */ public function set_cacheable($reason = false) { self::$_control |= self::BM_CACHEABLE; if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = ' [reason] ' . $reason; } self::debug('Cache_control init on' . $reason); } /** * This will disable non-cacheable BM * * @access public * @since 2.2 */ public static function force_cacheable($reason = false) { self::$_control |= self::BM_FORCED_CACHEABLE; if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = ' [reason] ' . $reason; } self::debug('Forced cacheable' . $reason); } /** * Switch to nocacheable status * * @access public * @since 1.1.3 * @param string $reason The reason to no cache */ public static function set_nocache($reason = false) { self::$_control |= self::BM_NOTCACHEABLE; if (!is_string($reason)) { $reason = false; } if ($reason) { $reason = "( $reason )"; } self::debug('X Cache_control -> no Cache ' . $reason, 5); } /** * Check current notcacheable bit set * * @access public * @since 1.1.3 * @return bool True if notcacheable bit is set, otherwise false. */ public static function isset_notcacheable() { return self::$_control & self::BM_NOTCACHEABLE; } /** * Check current force cacheable bit set * * @access public * @since 2.2 */ public static function is_forced_cacheable() { return self::$_control & self::BM_FORCED_CACHEABLE; } /** * Check current cacheable status * * @access public * @since 1.1.3 * @return bool True if is still cacheable, otherwise false. */ public static function is_cacheable() { if (defined('LSCACHE_NO_CACHE') && LSCACHE_NO_CACHE) { self::debug('LSCACHE_NO_CACHE constant defined'); return false; } // Guest mode always cacheable if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { // return true; } // If its forced public cacheable if (self::is_public_forced()) { return true; } // If its forced cacheable if (self::is_forced_cacheable()) { return true; } return !self::isset_notcacheable() && self::$_control & self::BM_CACHEABLE; } /** * Set a custom TTL to use with the request if needed. * * @access public * @since 1.1.3 * @param mixed $ttl An integer or string to use as the TTL. Must be numeric. */ public static function set_custom_ttl($ttl, $reason = false) { if (is_numeric($ttl)) { self::$_custom_ttl = $ttl; self::debug('X Cache_control TTL -> ' . $ttl . ($reason ? ' [reason] ' . $ttl : '')); } } /** * Generate final TTL. * * @access public * @since 1.1.3 */ public function get_ttl() { if (self::$_custom_ttl != 0) { return self::$_custom_ttl; } // Check if is in timed url list or not $timed_urls = Utility::wildcard2regex($this->conf(Base::O_PURGE_TIMED_URLS)); $timed_urls_time = $this->conf(Base::O_PURGE_TIMED_URLS_TIME); if ($timed_urls && $timed_urls_time) { $current_url = Tag::build_uri_tag(true); // Use time limit ttl $scheduled_time = strtotime($timed_urls_time); $ttl = $scheduled_time - time(); if ($ttl < 0) { $ttl += 86400; // add one day } foreach ($timed_urls as $v) { if (strpos($v, '*') !== false) { if (preg_match('#' . $v . '#iU', $current_url)) { self::debug('X Cache_control TTL is limited to ' . $ttl . ' due to scheduled purge regex ' . $v); return $ttl; } } else { if ($v == $current_url) { self::debug('X Cache_control TTL is limited to ' . $ttl . ' due to scheduled purge rule ' . $v); return $ttl; } } } } // Private cache uses private ttl setting if (self::is_private()) { return $this->conf(Base::O_CACHE_TTL_PRIV); } if (is_front_page()) { return $this->conf(Base::O_CACHE_TTL_FRONTPAGE); } $feed_ttl = $this->conf(Base::O_CACHE_TTL_FEED); if (is_feed() && $feed_ttl > 0) { return $feed_ttl; } if ($this->cls('REST')->is_rest() || $this->cls('REST')->is_internal_rest()) { return $this->conf(Base::O_CACHE_TTL_REST); } return $this->conf(Base::O_CACHE_TTL_PUB); } /** * Check if need to set no cache status for redirection or not * * @access public * @since 1.1.3 */ public function check_redirect($location, $status) { // TODO: some env don't have SCRIPT_URI but only REQUEST_URI, need to be compatible if (!empty($_SERVER['SCRIPT_URI'])) { // dont check $status == '301' anymore self::debug('301 from ' . $_SERVER['SCRIPT_URI']); self::debug("301 to $location"); $to_check = array(PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PATH, PHP_URL_QUERY); $is_same_redirect = true; foreach ($to_check as $v) { $url_parsed = $v == PHP_URL_QUERY ? $_SERVER['QUERY_STRING'] : parse_url($_SERVER['SCRIPT_URI'], $v); $target = parse_url($location, $v); self::debug("Compare [from] $url_parsed [to] $target"); if ($v == PHP_URL_QUERY) { $url_parsed = $url_parsed ? urldecode($url_parsed) : ''; $target = $target ? urldecode($target) : ''; if (substr($url_parsed, -1) == '&') { $url_parsed = substr($url_parsed, 0, -1); } } if ($url_parsed != $target) { $is_same_redirect = false; self::debug('301 different redirection'); break; } } if ($is_same_redirect) { self::set_nocache('301 to same url'); } } return $location; } /** * Sets up the Cache Control header. * * @since 1.1.3 * @access public * @return string empty string if empty, otherwise the cache control header. */ public function output() { $esi_hdr = ''; if (ESI::has_esi()) { $esi_hdr = ',esi=on'; } $hdr = self::X_HEADER . ': '; if (defined('DONOTCACHEPAGE') && apply_filters('litespeed_const_DONOTCACHEPAGE', DONOTCACHEPAGE)) { self::debug('❌ forced no cache [reason] DONOTCACHEPAGE const'); $hdr .= 'no-cache' . $esi_hdr; return $hdr; } // Guest mode directly return cacheable result // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { // // If is POST, no cache // if ( defined( 'LSCACHE_NO_CACHE' ) && LSCACHE_NO_CACHE ) { // self::debug( "[Ctrl] ❌ forced no cache [reason] LSCACHE_NO_CACHE const" ); // $hdr .= 'no-cache'; // } // else if( $_SERVER[ 'REQUEST_METHOD' ] !== 'GET' ) { // self::debug( "[Ctrl] ❌ forced no cache [reason] req not GET" ); // $hdr .= 'no-cache'; // } // else { // $hdr .= 'public'; // $hdr .= ',max-age=' . $this->get_ttl(); // } // $hdr .= $esi_hdr; // return $hdr; // } // Fix cli `uninstall --deactivate` fatal err if (!self::is_cacheable()) { $hdr .= 'no-cache' . $esi_hdr; return $hdr; } if (self::is_shared()) { $hdr .= 'shared,private'; } elseif (self::is_private()) { $hdr .= 'private'; } else { $hdr .= 'public'; } if (self::is_no_vary()) { $hdr .= ',no-vary'; } $hdr .= ',max-age=' . $this->get_ttl() . $esi_hdr; return $hdr; } /** * Generate all `control` tags before output * * @access public * @since 1.1.3 */ public function finalize() { if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { // return; } if (is_preview()) { self::set_nocache('preview page'); return; } // Check if has metabox non-cacheable setting or not if (file_exists(LSCWP_DIR . 'src/metabox.cls.php') && $this->cls('Metabox')->setting('litespeed_no_cache')) { self::set_nocache('per post metabox setting'); return; } // Check if URI is forced public cache $excludes = $this->conf(Base::O_CACHE_FORCE_PUB_URI); $hit = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes, true); if ($hit) { list($result, $this_ttl) = $hit; self::set_public_forced('Setting: ' . $result); self::debug('Forced public cacheable due to setting: ' . $result); if ($this_ttl) { self::set_custom_ttl($this_ttl); } } if (self::is_public_forced()) { return; } // Check if URI is forced cache $excludes = $this->conf(Base::O_CACHE_FORCE_URI); $hit = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes, true); if ($hit) { list($result, $this_ttl) = $hit; self::force_cacheable(); self::debug('Forced cacheable due to setting: ' . $result); if ($this_ttl) { self::set_custom_ttl($this_ttl); } } // if is not cacheable, terminate check // Even no need to run 3rd party hook if (!self::is_cacheable()) { self::debug('not cacheable before ctrl finalize'); return; } // Apply 3rd party filter // NOTE: Hook always needs to run asap because some 3rd party set is_mobile in this hook do_action('litespeed_control_finalize', defined('LSCACHE_IS_ESI') ? LSCACHE_IS_ESI : false); // Pass ESI block id // if is not cacheable, terminate check if (!self::is_cacheable()) { self::debug('not cacheable after api_control'); return; } // Check litespeed setting to set cacheable status if (!$this->_setting_cacheable()) { self::set_nocache(); return; } // If user has password cookie, do not cache (moved from vary) global $post; if (!empty($post->post_password) && isset($_COOKIE['wp-postpass_' . COOKIEHASH])) { // If user has password cookie, do not cache self::set_nocache('pswd cookie'); return; } // The following check to the end is ONLY for mobile $is_mobile = apply_filters('litespeed_is_mobile', false); if (!$this->conf(Base::O_CACHE_MOBILE)) { if ($is_mobile) { self::set_nocache('mobile'); } return; } $env_vary = isset($_SERVER['LSCACHE_VARY_VALUE']) ? $_SERVER['LSCACHE_VARY_VALUE'] : false; if (!$env_vary) { $env_vary = isset($_SERVER['HTTP_X_LSCACHE_VARY_VALUE']) ? $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] : false; } if ($env_vary && strpos($env_vary, 'ismobile') !== false) { if (!wp_is_mobile() && !$is_mobile) { self::set_nocache('is not mobile'); // todo: no need to uncache, it will correct vary value in vary finalize anyways return; } } elseif (wp_is_mobile() || $is_mobile) { self::set_nocache('is mobile'); return; } } /** * Check if is mobile for filter `litespeed_is_mobile` in API * * @since 3.0 * @access public */ public static function is_mobile() { return wp_is_mobile(); } /** * Get request method w/ compatibility to X-Http-Method-Override * * @since 6.2 */ private function _get_req_method() { if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { self::debug('X-Http-Method-Override -> ' . $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); defined('LITESPEED_X_HTTP_METHOD_OVERRIDE') || define('LITESPEED_X_HTTP_METHOD_OVERRIDE', true); return $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; } if (isset($_SERVER['REQUEST_METHOD'])) { return $_SERVER['REQUEST_METHOD']; } return 'unknown'; } /** * Check if a page is cacheable based on litespeed setting. * * @since 1.0.0 * @access private * @return boolean True if cacheable, false otherwise. */ private function _setting_cacheable() { // logged_in users already excluded, no hook added if (!empty($_REQUEST[Router::ACTION])) { return $this->_no_cache_for('Query String Action'); } $method = $this->_get_req_method(); if (defined('LITESPEED_X_HTTP_METHOD_OVERRIDE') && LITESPEED_X_HTTP_METHOD_OVERRIDE && $method == 'HEAD') { return $this->_no_cache_for('HEAD method from override'); } if ('GET' !== $method && 'HEAD' !== $method) { return $this->_no_cache_for('Not GET method: ' . $method); } if (is_feed() && $this->conf(Base::O_CACHE_TTL_FEED) == 0) { return $this->_no_cache_for('feed'); } if (is_trackback()) { return $this->_no_cache_for('trackback'); } if (is_search()) { return $this->_no_cache_for('search'); } // if ( !defined('WP_USE_THEMES') || !WP_USE_THEMES ) { // return $this->_no_cache_for('no theme used'); // } // Check private cache URI setting $excludes = $this->conf(Base::O_CACHE_PRIV_URI); $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes); if ($result) { self::set_private('Admin cfg Private Cached URI: ' . $result); } if (!self::is_forced_cacheable()) { // Check if URI is excluded from cache $excludes = $this->cls('Data')->load_cache_nocacheable($this->conf(Base::O_CACHE_EXC)); $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes); if ($result) { return $this->_no_cache_for('Admin configured URI Do not cache: ' . $result); } // Check QS excluded setting $excludes = $this->conf(Base::O_CACHE_EXC_QS); if (!empty($excludes) && ($qs = $this->_is_qs_excluded($excludes))) { return $this->_no_cache_for('Admin configured QS Do not cache: ' . $qs); } $excludes = $this->conf(Base::O_CACHE_EXC_CAT); if (!empty($excludes) && has_category($excludes)) { return $this->_no_cache_for('Admin configured Category Do not cache.'); } $excludes = $this->conf(Base::O_CACHE_EXC_TAG); if (!empty($excludes) && has_tag($excludes)) { return $this->_no_cache_for('Admin configured Tag Do not cache.'); } $excludes = $this->conf(Base::O_CACHE_EXC_COOKIES); if (!empty($excludes) && !empty($_COOKIE)) { $cookie_hit = array_intersect(array_keys($_COOKIE), $excludes); if ($cookie_hit) { return $this->_no_cache_for('Admin configured Cookie Do not cache.'); } } $excludes = $this->conf(Base::O_CACHE_EXC_USERAGENTS); if (!empty($excludes) && isset($_SERVER['HTTP_USER_AGENT'])) { $nummatches = preg_match(Utility::arr2regex($excludes), $_SERVER['HTTP_USER_AGENT']); if ($nummatches) { return $this->_no_cache_for('Admin configured User Agent Do not cache.'); } } // Check if is exclude roles ( Need to set Vary too ) if ($result = $this->in_cache_exc_roles()) { return $this->_no_cache_for('Role Excludes setting ' . $result); } } return true; } /** * Write a debug message for if a page is not cacheable. * * @since 1.0.0 * @access private * @param string $reason An explanation for why the page is not cacheable. * @return boolean Return false. */ private function _no_cache_for($reason) { self::debug('X Cache_control off - ' . $reason); return false; } /** * Check if current request has qs excluded setting * * @since 1.3 * @access private * @param array $excludes QS excludes setting * @return boolean|string False if not excluded, otherwise the hit qs list */ private function _is_qs_excluded($excludes) { if (!empty($_GET) && ($intersect = array_intersect(array_keys($_GET), $excludes))) { return implode(',', $intersect); } return false; } } src/object.lib.php 0000644 00000103740 15162130565 0010070 0 ustar 00 <?php /** * LiteSpeed Object Cache Library * * @since 1.8 */ defined('WPINC') || exit(); /** * Handle exception */ if (!function_exists('litespeed_exception_handler')) { function litespeed_exception_handler($errno, $errstr, $errfile, $errline) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } } require_once __DIR__ . '/object-cache.cls.php'; /** * Sets up Object Cache Global and assigns it. * * @since 1.8 * * @global WP_Object_Cache $wp_object_cache */ function wp_cache_init() { $GLOBALS['wp_object_cache'] = WP_Object_Cache::get_instance(); } /** * Adds data to the cache, if the cache key doesn't already exist. * * @since 1.8 * * @see WP_Object_Cache::add() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The cache key to use for retrieval later. * @param mixed $data The data to add to the cache. * @param string $group Optional. The group to add the cache to. Enables the same key * to be used across groups. Default empty. * @param int $expire Optional. When the cache data should expire, in seconds. * Default 0 (no expiration). * @return bool True on success, false if cache key and group already exist. */ function wp_cache_add($key, $data, $group = '', $expire = 0) { global $wp_object_cache; return $wp_object_cache->add($key, $data, $group, (int) $expire); } /** * Adds multiple values to the cache in one call. * * @since 5.4 * * @see WP_Object_Cache::add_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $data Array of keys and values to be set. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if cache key and group already exist. */ function wp_cache_add_multiple(array $data, $group = '', $expire = 0) { global $wp_object_cache; return $wp_object_cache->add_multiple($data, $group, $expire); } /** * Replaces the contents of the cache with new data. * * @since 1.8 * * @see WP_Object_Cache::replace() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The key for the cache data that should be replaced. * @param mixed $data The new data to store in the cache. * @param string $group Optional. The group for the cache data that should be replaced. * Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True if contents were replaced, false if original value does not exist. */ function wp_cache_replace($key, $data, $group = '', $expire = 0) { global $wp_object_cache; return $wp_object_cache->replace($key, $data, $group, (int) $expire); } /** * Saves the data to the cache. * * Differs from wp_cache_add() and wp_cache_replace() in that it will always write data. * * @since 1.8 * * @see WP_Object_Cache::set() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The cache key to use for retrieval later. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Enables the same key * to be used across groups. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True on success, false on failure. */ function wp_cache_set($key, $data, $group = '', $expire = 0) { global $wp_object_cache; return $wp_object_cache->set($key, $data, $group, (int) $expire); } /** * Sets multiple values to the cache in one call. * * @since 5.4 * * @see WP_Object_Cache::set_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $data Array of keys and values to be set. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false on failure. */ function wp_cache_set_multiple(array $data, $group = '', $expire = 0) { global $wp_object_cache; return $wp_object_cache->set_multiple($data, $group, $expire); } /** * Retrieves the cache contents from the cache by key and group. * * @since 1.8 * * @see WP_Object_Cache::get() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The key under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param bool $force Optional. Whether to force an update of the local cache * from the persistent cache. Default false. * @param bool $found Optional. Whether the key was found in the cache (passed by reference). * Disambiguates a return of false, a storable value. Default null. * @return mixed|false The cache contents on success, false on failure to retrieve contents. */ function wp_cache_get($key, $group = '', $force = false, &$found = null) { global $wp_object_cache; return $wp_object_cache->get($key, $group, $force, $found); } /** * Retrieves multiple values from the cache in one call. * * @since 5.4 * * @see WP_Object_Cache::get_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $keys Array of keys under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param bool $force Optional. Whether to force an update of the local cache * from the persistent cache. Default false. * @return array Array of return values, grouped by key. Each value is either * the cache contents on success, or false on failure. */ function wp_cache_get_multiple($keys, $group = '', $force = false) { global $wp_object_cache; return $wp_object_cache->get_multiple($keys, $group, $force); } /** * Removes the cache contents matching key and group. * * @since 1.8 * * @see WP_Object_Cache::delete() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key What the contents in the cache are called. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @return bool True on successful removal, false on failure. */ function wp_cache_delete($key, $group = '') { global $wp_object_cache; return $wp_object_cache->delete($key, $group); } /** * Deletes multiple values from the cache in one call. * * @since 5.4 * * @see WP_Object_Cache::delete_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $keys Array of keys under which the cache to deleted. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if the contents were not deleted. */ function wp_cache_delete_multiple(array $keys, $group = '') { global $wp_object_cache; return $wp_object_cache->delete_multiple($keys, $group); } /** * Increments numeric cache item's value. * * @since 1.8 * * @see WP_Object_Cache::incr() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The key for the cache contents that should be incremented. * @param int $offset Optional. The amount by which to increment the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default empty. * @return int|false The item's new value on success, false on failure. */ function wp_cache_incr($key, $offset = 1, $group = '') { global $wp_object_cache; return $wp_object_cache->incr($key, $offset, $group); } /** * Decrements numeric cache item's value. * * @since 1.8 * * @see WP_Object_Cache::decr() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The cache key to decrement. * @param int $offset Optional. The amount by which to decrement the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default empty. * @return int|false The item's new value on success, false on failure. */ function wp_cache_decr($key, $offset = 1, $group = '') { global $wp_object_cache; return $wp_object_cache->decr($key, $offset, $group); } /** * Removes all cache items. * * @since 1.8 * * @see WP_Object_Cache::flush() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @return bool True on success, false on failure. */ function wp_cache_flush() { global $wp_object_cache; return $wp_object_cache->flush(); } /** * Removes all cache items from the in-memory runtime cache. * * @since 5.4 * * @see WP_Object_Cache::flush_runtime() * * @return bool True on success, false on failure. */ function wp_cache_flush_runtime() { global $wp_object_cache; return $wp_object_cache->flush_runtime(); } /** * Removes all cache items in a group, if the object cache implementation supports it. * * Before calling this function, always check for group flushing support using the * `wp_cache_supports( 'flush_group' )` function. * * @since 5.4 * * @see WP_Object_Cache::flush_group() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param string $group Name of group to remove from cache. * @return bool True if group was flushed, false otherwise. */ function wp_cache_flush_group($group) { global $wp_object_cache; return $wp_object_cache->flush_group($group); } /** * Determines whether the object cache implementation supports a particular feature. * * @since 5.4 * * @param string $feature Name of the feature to check for. Possible values include: * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple', * 'flush_runtime', 'flush_group'. * @return bool True if the feature is supported, false otherwise. */ function wp_cache_supports($feature) { switch ($feature) { case 'add_multiple': case 'set_multiple': case 'get_multiple': case 'delete_multiple': case 'flush_runtime': return true; case 'flush_group': default: return false; } } /** * Closes the cache. * * This function has ceased to do anything since WordPress 2.5. The * functionality was removed along with the rest of the persistent cache. * * This does not mean that plugins can't implement this function when they need * to make sure that the cache is cleaned up after WordPress no longer needs it. * * @since 1.8 * * @return true Always returns true. */ function wp_cache_close() { return true; } /** * Adds a group or set of groups to the list of global groups. * * @since 1.8 * * @see WP_Object_Cache::add_global_groups() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param string|string[] $groups A group or an array of groups to add. */ function wp_cache_add_global_groups($groups) { global $wp_object_cache; $wp_object_cache->add_global_groups($groups); } /** * Adds a group or set of groups to the list of non-persistent groups. * * @since 1.8 * * @param string|string[] $groups A group or an array of groups to add. */ function wp_cache_add_non_persistent_groups($groups) { global $wp_object_cache; $wp_object_cache->add_non_persistent_groups($groups); } /** * Switches the internal blog ID. * * This changes the blog id used to create keys in blog specific groups. * * @since 1.8 * * @see WP_Object_Cache::switch_to_blog() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int $blog_id Site ID. */ function wp_cache_switch_to_blog($blog_id) { global $wp_object_cache; $wp_object_cache->switch_to_blog($blog_id); } class WP_Object_Cache { protected static $_instance; private $_object_cache; private $_cache = array(); private $_cache_404 = array(); private $cache_total = 0; private $count_hit_incall = 0; private $count_hit = 0; private $count_miss_incall = 0; private $count_miss = 0; private $count_set = 0; protected $global_groups = array(); private $blog_prefix; private $multisite; /** * Init. * * @since 1.8 */ public function __construct() { $this->_object_cache = \LiteSpeed\Object_Cache::cls(); $this->multisite = is_multisite(); $this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : ''; /** * Fix multiple instance using same oc issue * @since 1.8.2 */ !defined('LSOC_PREFIX') && define('LSOC_PREFIX', substr(md5(__FILE__), -5)); } /** * Makes private properties readable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to get. * @return mixed Property. */ public function __get($name) { return $this->$name; } /** * Makes private properties settable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to set. * @param mixed $value Property value. * @return mixed Newly-set property. */ public function __set($name, $value) { return $this->$name = $value; } /** * Makes private properties checkable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to check if set. * @return bool Whether the property is set. */ public function __isset($name) { return isset($this->$name); } /** * Makes private properties un-settable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to unset. */ public function __unset($name) { unset($this->$name); } /** * Serves as a utility function to determine whether a key is valid. * * @since 5.4 * @access protected * * @param int|string $key Cache key to check for validity. * @return bool Whether the key is valid. */ protected function is_valid_key($key) { if (is_int($key)) { return true; } if (is_string($key) && trim($key) !== '') { return true; } $type = gettype($key); if (!function_exists('__')) { wp_load_translations_early(); } $message = is_string($key) ? __('Cache key must not be an empty string.') : /* translators: %s: The type of the given cache key. */ sprintf(__('Cache key must be integer or non-empty string, %s given.'), $type); _doing_it_wrong(sprintf('%s::%s', __CLASS__, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function']), $message, '6.1.0'); return false; } /** * Get the final key. * * @since 1.8 * @access private */ private function _key($key, $group = 'default') { if (empty($group)) { $group = 'default'; } $prefix = $this->_object_cache->is_global($group) ? '' : $this->blog_prefix; return LSOC_PREFIX . $prefix . $group . '.' . $key; } /** * Output debug info. * * @since 1.8 * @access public */ public function debug() { return ' [total] ' . $this->cache_total . ' [hit_incall] ' . $this->count_hit_incall . ' [hit] ' . $this->count_hit . ' [miss_incall] ' . $this->count_miss_incall . ' [miss] ' . $this->count_miss . ' [set] ' . $this->count_set; } /** * Adds data to the cache if it doesn't already exist. * * @since 1.8 * @access public * * @uses WP_Object_Cache::_exists() Checks to see if the cache already has data. * @uses WP_Object_Cache::set() Sets the data after the checking the cache * contents existence. * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True on success, false if cache key and group already exist. */ public function add($key, $data, $group = 'default', $expire = 0) { if (wp_suspend_cache_addition()) { return false; } if (!$this->is_valid_key($key)) { return false; } if (empty($group)) { $group = 'default'; } $id = $this->_key($key, $group); if (array_key_exists($id, $this->_cache)) { return false; } return $this->set($key, $data, $group, (int) $expire); } /** * Adds multiple values to the cache in one call. * * @since 5.4 * @access public * * @param array $data Array of keys and values to be added. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if cache key and group already exist. */ public function add_multiple(array $data, $group = '', $expire = 0) { $values = array(); foreach ($data as $key => $value) { $values[$key] = $this->add($key, $value, $group, $expire); } return $values; } /** * Replaces the contents in the cache, if contents already exist. * * @since 1.8 * @access public * * @see WP_Object_Cache::set() * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True if contents were replaced, false if original value does not exist. */ public function replace($key, $data, $group = 'default', $expire = 0) { if (!$this->is_valid_key($key)) { return false; } if (empty($group)) { $group = 'default'; } $id = $this->_key($key, $group); if (!array_key_exists($id, $this->_cache)) { return false; } return $this->set($key, $data, $group, (int) $expire); } /** * Sets the data contents into the cache. * * The cache contents are grouped by the $group parameter followed by the * $key. This allows for duplicate IDs in unique groups. Therefore, naming of * the group should be used with care and should follow normal function * naming guidelines outside of core WordPress usage. * * The $expire parameter is not used, because the cache will automatically * expire for each time a page is accessed and PHP finishes. The method is * more for cache plugins which use files. * * @since 1.8 * @since 5.4 Returns false if cache key is invalid. * @access public * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True if contents were set, false if key is invalid. */ public function set($key, $data, $group = 'default', $expire = 0) { if (!$this->is_valid_key($key)) { return false; } if (empty($group)) { $group = 'default'; } $id = $this->_key($key, $group); if (is_object($data)) { $data = clone $data; } // error_log("oc: set \t\t\t[key] " . $id ); $this->_cache[$id] = $data; if (array_key_exists($id, $this->_cache_404)) { // error_log("oc: unset404\t\t\t[key] " . $id ); unset($this->_cache_404[$id]); } if (!$this->_object_cache->is_non_persistent($group)) { $this->_object_cache->set($id, serialize(array('data' => $data)), (int) $expire); $this->count_set++; } if ($this->_object_cache->store_transients($group)) { $this->_transient_set($key, $data, $group, (int) $expire); } return true; } /** * Sets multiple values to the cache in one call. * * @since 5.4 * @access public * * @param array $data Array of key and value to be set. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is always true. */ public function set_multiple(array $data, $group = '', $expire = 0) { $values = array(); foreach ($data as $key => $value) { $values[$key] = $this->set($key, $value, $group, $expire); } return $values; } /** * Retrieves the cache contents, if it exists. * * The contents will be first attempted to be retrieved by searching by the * key in the cache group. If the cache is hit (success) then the contents * are returned. * * On failure, the number of cache misses will be incremented. * * @since 1.8 * @access public * * @param int|string $key The key under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @param bool $force Optional. Unused. Whether to force an update of the local cache * from the persistent cache. Default false. * @param bool $found Optional. Whether the key was found in the cache (passed by reference). * Disambiguates a return of false, a storable value. Default null. * @return mixed|false The cache contents on success, false on failure to retrieve contents. */ public function get($key, $group = 'default', $force = false, &$found = null) { if (!$this->is_valid_key($key)) { return false; } if (empty($group)) { $group = 'default'; } $id = $this->_key($key, $group); // error_log(''); // error_log("oc: get \t\t\t[key] " . $id . ( $force ? "\t\t\t [forced] " : '' ) ); $found = false; $found_in_oc = false; $cache_val = false; if (array_key_exists($id, $this->_cache) && !$force) { $found = true; $cache_val = $this->_cache[$id]; $this->count_hit_incall++; } elseif (!array_key_exists($id, $this->_cache_404) && !$this->_object_cache->is_non_persistent($group)) { $v = $this->_object_cache->get($id); if ($v !== null) { $v = @maybe_unserialize($v); } // To be compatible with false val if (is_array($v) && array_key_exists('data', $v)) { $this->count_hit++; $found = true; $found_in_oc = true; $cache_val = $v['data']; } else { // Can't find key, cache it to 404 // error_log("oc: add404\t\t\t[key] " . $id ); $this->_cache_404[$id] = 1; $this->count_miss++; } } else { $this->count_miss_incall++; } if (is_object($cache_val)) { $cache_val = clone $cache_val; } // If not found but has `Store Transients` cfg on, still need to follow WP's get_transient() logic if (!$found && $this->_object_cache->store_transients($group)) { $cache_val = $this->_transient_get($key, $group); if ($cache_val) { $found = true; // $found not used for now (v1.8.3) } } if ($found_in_oc) { $this->_cache[$id] = $cache_val; } $this->cache_total++; return $cache_val; } /** * Retrieves multiple values from the cache in one call. * * @since 5.4 * @access public * * @param array $keys Array of keys under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @param bool $force Optional. Whether to force an update of the local cache * from the persistent cache. Default false. * @return array Array of return values, grouped by key. Each value is either * the cache contents on success, or false on failure. */ public function get_multiple($keys, $group = 'default', $force = false) { $values = array(); foreach ($keys as $key) { $values[$key] = $this->get($key, $group, $force); } return $values; } /** * Removes the contents of the cache key in the group. * * If the cache key does not exist in the group, then nothing will happen. * * @since 1.8 * @access public * * @param int|string $key What the contents in the cache are called. * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @param bool $deprecated Optional. Unused. Default false. * @return bool True on success, false if the contents were not deleted. */ public function delete($key, $group = 'default', $deprecated = false) { if (!$this->is_valid_key($key)) { return false; } if (empty($group)) { $group = 'default'; } $id = $this->_key($key, $group); if ($this->_object_cache->store_transients($group)) { $this->_transient_del($key, $group); } if (array_key_exists($id, $this->_cache)) { unset($this->_cache[$id]); } // error_log("oc: delete \t\t\t[key] " . $id ); if ($this->_object_cache->is_non_persistent($group)) { return false; } return $this->_object_cache->delete($id); } /** * Deletes multiple values from the cache in one call. * * @since 5.4 * @access public * * @param array $keys Array of keys to be deleted. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if the contents were not deleted. */ public function delete_multiple(array $keys, $group = '') { $values = array(); foreach ($keys as $key) { $values[$key] = $this->delete($key, $group); } return $values; } /** * Increments numeric cache item's value. * * @since 5.4 * * @param int|string $key The cache key to increment. * @param int $offset Optional. The amount by which to increment the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default 'default'. * @return int|false The item's new value on success, false on failure. */ public function incr($key, $offset = 1, $group = 'default') { return $this->incr_desr($key, $offset, $group, true); } /** * Decrements numeric cache item's value. * * @since 5.4 * * @param int|string $key The cache key to decrement. * @param int $offset Optional. The amount by which to decrement the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default 'default'. * @return int|false The item's new value on success, false on failure. */ public function decr($key, $offset = 1, $group = 'default') { return $this->incr_desr($key, $offset, $group, false); } /** * Increments or decrements numeric cache item's value. * * @since 1.8 * @access public */ public function incr_desr($key, $offset = 1, $group = 'default', $incr = true) { if (!$this->is_valid_key($key)) { return false; } if (empty($group)) { $group = 'default'; } $cache_val = $this->get($key, $group); if (false === $cache_val) { return false; } if (!is_numeric($cache_val)) { $cache_val = 0; } $offset = (int) $offset; if ($incr) { $cache_val += $offset; } else { $cache_val -= $offset; } if ($cache_val < 0) { $cache_val = 0; } $this->set($key, $cache_val, $group); return $cache_val; } /** * Clears the object cache of all data. * * @since 1.8 * @access public * * @return true Always returns true. */ public function flush() { $this->flush_runtime(); $this->_object_cache->flush(); return true; } /** * Removes all cache items from the in-memory runtime cache. * * @since 5.4 * @access public * * @return true Always returns true. */ public function flush_runtime() { $this->_cache = array(); $this->_cache_404 = array(); return true; } /** * Removes all cache items in a group. * * @since 5.4 * @access public * * @param string $group Name of group to remove from cache. * @return true Always returns true. */ public function flush_group($group) { // unset( $this->cache[ $group ] ); return true; } /** * Sets the list of global cache groups. * * @since 1.8 * @access public * * @param string|string[] $groups List of groups that are global. */ public function add_global_groups($groups) { $groups = (array) $groups; $this->_object_cache->add_global_groups($groups); } /** * Sets the list of non-persistent cache groups. * * @since 1.8 * @access public */ public function add_non_persistent_groups($groups) { $groups = (array) $groups; $this->_object_cache->add_non_persistent_groups($groups); } /** * Switches the internal blog ID. * * This changes the blog ID used to create keys in blog specific groups. * * @since 1.8 * @access public * * @param int $blog_id Blog ID. */ public function switch_to_blog($blog_id) { $blog_id = (int) $blog_id; $this->blog_prefix = $this->multisite ? $blog_id . ':' : ''; } /** * Get transient from wp table * * @since 1.8.3 * @access private * @see `wp-includes/option.php` function `get_transient`/`set_site_transient` */ private function _transient_get($transient, $group) { if ($group == 'transient') { /**** Ori WP func start ****/ $transient_option = '_transient_' . $transient; if (!wp_installing()) { // If option is not in alloptions, it is not autoloaded and thus has a timeout $alloptions = wp_load_alloptions(); if (!isset($alloptions[$transient_option])) { $transient_timeout = '_transient_timeout_' . $transient; $timeout = get_option($transient_timeout); if (false !== $timeout && $timeout < time()) { delete_option($transient_option); delete_option($transient_timeout); $value = false; } } } if (!isset($value)) { $value = get_option($transient_option); } /**** Ori WP func end ****/ } elseif ($group == 'site-transient') { /**** Ori WP func start ****/ $no_timeout = array('update_core', 'update_plugins', 'update_themes'); $transient_option = '_site_transient_' . $transient; if (!in_array($transient, $no_timeout)) { $transient_timeout = '_site_transient_timeout_' . $transient; $timeout = get_site_option($transient_timeout); if (false !== $timeout && $timeout < time()) { delete_site_option($transient_option); delete_site_option($transient_timeout); $value = false; } } if (!isset($value)) { $value = get_site_option($transient_option); } /**** Ori WP func end ****/ } else { $value = false; } return $value; } /** * Set transient to WP table * * @since 1.8.3 * @access private * @see `wp-includes/option.php` function `set_transient`/`set_site_transient` */ private function _transient_set($transient, $value, $group, $expiration) { if ($group == 'transient') { /**** Ori WP func start ****/ $transient_timeout = '_transient_timeout_' . $transient; $transient_option = '_transient_' . $transient; if (false === get_option($transient_option)) { $autoload = 'yes'; if ((int) $expiration) { $autoload = 'no'; add_option($transient_timeout, time() + (int) $expiration, '', 'no'); } $result = add_option($transient_option, $value, '', $autoload); } else { // If expiration is requested, but the transient has no timeout option, // delete, then re-create transient rather than update. $update = true; if ((int) $expiration) { if (false === get_option($transient_timeout)) { delete_option($transient_option); add_option($transient_timeout, time() + (int) $expiration, '', 'no'); $result = add_option($transient_option, $value, '', 'no'); $update = false; } else { update_option($transient_timeout, time() + (int) $expiration); } } if ($update) { $result = update_option($transient_option, $value); } } /**** Ori WP func end ****/ } elseif ($group == 'site-transient') { /**** Ori WP func start ****/ $transient_timeout = '_site_transient_timeout_' . $transient; $option = '_site_transient_' . $transient; if (false === get_site_option($option)) { if ((int) $expiration) { add_site_option($transient_timeout, time() + (int) $expiration); } $result = add_site_option($option, $value); } else { if ((int) $expiration) { update_site_option($transient_timeout, time() + (int) $expiration); } $result = update_site_option($option, $value); } /**** Ori WP func end ****/ } else { $result = null; } return $result; } /** * Delete transient from WP table * * @since 1.8.3 * @access private * @see `wp-includes/option.php` function `delete_transient`/`delete_site_transient` */ private function _transient_del($transient, $group) { if ($group == 'transient') { /**** Ori WP func start ****/ $option_timeout = '_transient_timeout_' . $transient; $option = '_transient_' . $transient; $result = delete_option($option); if ($result) { delete_option($option_timeout); } /**** Ori WP func end ****/ } elseif ($group == 'site-transient') { /**** Ori WP func start ****/ $option_timeout = '_site_transient_timeout_' . $transient; $option = '_site_transient_' . $transient; $result = delete_site_option($option); if ($result) { delete_site_option($option_timeout); } /**** Ori WP func end ****/ } } /** * Get the current instance object. * * @since 1.8 * @access public */ public static function get_instance() { if (!isset(self::$_instance)) { self::$_instance = new self(); } return self::$_instance; } } src/task.cls.php 0000644 00000013661 15162130566 0007602 0 ustar 00 <?php /** * The cron task class. * * @since 1.1.3 * @since 1.5 Moved into /inc */ namespace LiteSpeed; defined('WPINC') || exit(); class Task extends Root { const LOG_TAG = '⏰'; private static $_triggers = array( Base::O_IMG_OPTM_CRON => array('name' => 'litespeed_task_imgoptm_pull', 'hook' => 'LiteSpeed\Img_Optm::start_async_cron'), // always fetch immediately Base::O_OPTM_CSS_ASYNC => array('name' => 'litespeed_task_ccss', 'hook' => 'LiteSpeed\CSS::cron_ccss'), Base::O_OPTM_UCSS => array('name' => 'litespeed_task_ucss', 'hook' => 'LiteSpeed\UCSS::cron'), Base::O_MEDIA_VPI_CRON => array('name' => 'litespeed_task_vpi', 'hook' => 'LiteSpeed\VPI::cron'), Base::O_MEDIA_PLACEHOLDER_RESP_ASYNC => array('name' => 'litespeed_task_lqip', 'hook' => 'LiteSpeed\Placeholder::cron'), Base::O_DISCUSS_AVATAR_CRON => array('name' => 'litespeed_task_avatar', 'hook' => 'LiteSpeed\Avatar::cron'), Base::O_IMG_OPTM_AUTO => array('name' => 'litespeed_task_imgoptm_req', 'hook' => 'LiteSpeed\Img_Optm::cron_auto_request'), Base::O_CRAWLER => array('name' => 'litespeed_task_crawler', 'hook' => 'LiteSpeed\Crawler::start_async_cron'), // Set crawler to last one to use above results ); private static $_guest_options = array(Base::O_OPTM_CSS_ASYNC, Base::O_OPTM_UCSS, Base::O_MEDIA_VPI); const FILTER_CRAWLER = 'litespeed_crawl_filter'; const FILTER = 'litespeed_filter'; /** * Keep all tasks in cron * * @since 3.0 * @access public */ public function init() { self::debug2('Init'); add_filter('cron_schedules', array($this, 'lscache_cron_filter')); $guest_optm = $this->conf(Base::O_GUEST) && $this->conf(Base::O_GUEST_OPTM); foreach (self::$_triggers as $id => $trigger) { if ($id != Base::O_IMG_OPTM_CRON && !$this->conf($id)) { if (!$guest_optm || !in_array($id, self::$_guest_options)) { continue; } } // Special check for crawler if ($id == Base::O_CRAWLER) { if (!Router::can_crawl()) { continue; } add_filter('cron_schedules', array($this, 'lscache_cron_filter_crawler')); } if (!wp_next_scheduled($trigger['name'])) { self::debug('Cron hook register [name] ' . $trigger['name']); wp_schedule_event(time(), $id == Base::O_CRAWLER ? self::FILTER_CRAWLER : self::FILTER, $trigger['name']); } add_action($trigger['name'], $trigger['hook']); } } /** * Handle all async noabort requests * * @since 5.5 */ public static function async_litespeed_handler() { $hash_data = self::get_option('async_call-hash', array()); if (!$hash_data || !is_array($hash_data) || empty($hash_data['hash']) || empty($hash_data['ts'])) { self::debug('async_litespeed_handler no hash data', $hash_data); return; } if (time() - $hash_data['ts'] > 120 || empty($_GET['nonce']) || $_GET['nonce'] != $hash_data['hash']) { self::debug('async_litespeed_handler nonce mismatch'); return; } self::delete_option('async_call-hash'); $type = Router::verify_type(); self::debug('type=' . $type); // Don't lock up other requests while processing session_write_close(); switch ($type) { case 'crawler': Crawler::async_handler(); break; case 'crawler_force': Crawler::async_handler(true); break; case 'imgoptm': Img_Optm::async_handler(); break; case 'imgoptm_force': Img_Optm::async_handler(true); break; default: } } /** * Async caller wrapper func * * @since 5.5 */ public static function async_call($type) { $hash = Str::rrand(32); self::update_option('async_call-hash', array('hash' => $hash, 'ts' => time())); $args = array( 'timeout' => 0.01, 'blocking' => false, 'sslverify' => false, // 'cookies' => $_COOKIE, ); $qs = array( 'action' => 'async_litespeed', 'nonce' => $hash, Router::TYPE => $type, ); $url = add_query_arg($qs, admin_url('admin-ajax.php')); self::debug('async call to ' . $url); wp_safe_remote_post(esc_url_raw($url), $args); } /** * Clean all potential existing crons * * @since 3.0 * @access public */ public static function destroy() { Utility::compatibility(); array_map('wp_clear_scheduled_hook', array_column(self::$_triggers, 'name')); } /** * Try to clean the crons if disabled * * @since 3.0 * @access public */ public function try_clean($id) { // Clean v2's leftover cron ( will remove in v3.1 ) // foreach ( wp_get_ready_cron_jobs() as $hooks ) { // foreach ( $hooks as $hook => $v ) { // if ( strpos( $hook, 'litespeed_' ) === 0 && ( substr( $hook, -8 ) === '_trigger' || strpos( $hook, 'litespeed_task_' ) !== 0 ) ) { // self::debug( 'Cron clear legacy [hook] ' . $hook ); // wp_clear_scheduled_hook( $hook ); // } // } // } if ($id && !empty(self::$_triggers[$id])) { if (!$this->conf($id) || ($id == Base::O_CRAWLER && !Router::can_crawl())) { self::debug('Cron clear [id] ' . $id . ' [hook] ' . self::$_triggers[$id]['name']); wp_clear_scheduled_hook(self::$_triggers[$id]['name']); } return; } self::debug('❌ Unknown cron [id] ' . $id); } /** * Register cron interval imgoptm * * @since 1.6.1 * @access public */ public function lscache_cron_filter($schedules) { if (!array_key_exists(self::FILTER, $schedules)) { $schedules[self::FILTER] = array( 'interval' => 60, 'display' => __('Every Minute', 'litespeed-cache'), ); } return $schedules; } /** * Register cron interval * * @since 1.1.0 * @access public */ public function lscache_cron_filter_crawler($schedules) { $CRAWLER_RUN_INTERVAL = defined('LITESPEED_CRAWLER_RUN_INTERVAL') ? LITESPEED_CRAWLER_RUN_INTERVAL : 600; // $wp_schedules = wp_get_schedules(); if (!array_key_exists(self::FILTER_CRAWLER, $schedules)) { // self::debug('Crawler cron log: cron filter '.$interval.' added'); $schedules[self::FILTER_CRAWLER] = array( 'interval' => $CRAWLER_RUN_INTERVAL, 'display' => __('LiteSpeed Crawler Cron', 'litespeed-cache'), ); } return $schedules; } } src/utility.cls.php 0000644 00000051252 15162130567 0010342 0 ustar 00 <?php /** * The utility class. * * @since 1.1.5 * @since 1.5 Moved into /inc */ namespace LiteSpeed; defined('WPINC') || exit(); class Utility extends Root { private static $_internal_domains; /** * Validate regex * * @since 1.0.9 * @since 3.0 Moved here from admin-settings.cls * @access public * @return bool True for valid rules, false otherwise. */ public static function syntax_checker($rules) { return preg_match(self::arr2regex($rules), '') !== false; } /** * Combine regex array to regex rule * * @since 3.0 */ public static function arr2regex($arr, $drop_delimiter = false) { $arr = self::sanitize_lines($arr); $new_arr = array(); foreach ($arr as $v) { $new_arr[] = preg_quote($v, '#'); } $regex = implode('|', $new_arr); $regex = str_replace(' ', '\\ ', $regex); if ($drop_delimiter) { return $regex; } return '#' . $regex . '#'; } /** * Replace wildcard to regex * * @since 3.2.2 */ public static function wildcard2regex($string) { if (is_array($string)) { return array_map(__CLASS__ . '::wildcard2regex', $string); } if (strpos($string, '*') !== false) { $string = preg_quote($string, '#'); $string = str_replace('\*', '.*', $string); } return $string; } /** * Check if an URL or current page is REST req or not * * @since 2.9.3 * @deprecated 2.9.4 Moved to REST class * @access public */ public static function is_rest($url = false) { return false; } /** * Get current page type * * @since 2.9 */ public static function page_type() { global $wp_query; $page_type = 'default'; if ($wp_query->is_page) { $page_type = is_front_page() ? 'front' : 'page'; } elseif ($wp_query->is_home) { $page_type = 'home'; } elseif ($wp_query->is_single) { // $page_type = $wp_query->is_attachment ? 'attachment' : 'single'; $page_type = get_post_type(); } elseif ($wp_query->is_category) { $page_type = 'category'; } elseif ($wp_query->is_tag) { $page_type = 'tag'; } elseif ($wp_query->is_tax) { $page_type = 'tax'; // $page_type = get_queried_object()->taxonomy; } elseif ($wp_query->is_archive) { if ($wp_query->is_day) { $page_type = 'day'; } elseif ($wp_query->is_month) { $page_type = 'month'; } elseif ($wp_query->is_year) { $page_type = 'year'; } elseif ($wp_query->is_author) { $page_type = 'author'; } else { $page_type = 'archive'; } } elseif ($wp_query->is_search) { $page_type = 'search'; } elseif ($wp_query->is_404) { $page_type = '404'; } return $page_type; // if ( is_404() ) { // $page_type = '404'; // } // elseif ( is_singular() ) { // $page_type = get_post_type(); // } // elseif ( is_home() && get_option( 'show_on_front' ) == 'page' ) { // $page_type = 'home'; // } // elseif ( is_front_page() ) { // $page_type = 'front'; // } // elseif ( is_tax() ) { // $page_type = get_queried_object()->taxonomy; // } // elseif ( is_category() ) { // $page_type = 'category'; // } // elseif ( is_tag() ) { // $page_type = 'tag'; // } // return $page_type; } /** * Get ping speed * * @since 2.9 */ public static function ping($domain) { if (strpos($domain, ':')) { $domain = parse_url($domain, PHP_URL_HOST); } $starttime = microtime(true); $file = fsockopen($domain, 443, $errno, $errstr, 10); $stoptime = microtime(true); $status = 0; if (!$file) { $status = 99999; } // Site is down else { fclose($file); $status = ($stoptime - $starttime) * 1000; $status = floor($status); } Debug2::debug("[Util] ping [Domain] $domain \t[Speed] $status"); return $status; } /** * Set seconds/timestamp to readable format * * @since 1.6.5 * @access public */ public static function readable_time($seconds_or_timestamp, $timeout = 3600, $forward = false) { if (strlen($seconds_or_timestamp) == 10) { $seconds = time() - $seconds_or_timestamp; if ($seconds > $timeout) { return date('m/d/Y H:i:s', $seconds_or_timestamp + LITESPEED_TIME_OFFSET); } } else { $seconds = $seconds_or_timestamp; } $res = ''; if ($seconds > 86400) { $num = floor($seconds / 86400); $res .= $num . 'd'; $seconds %= 86400; } if ($seconds > 3600) { if ($res) { $res .= ', '; } $num = floor($seconds / 3600); $res .= $num . 'h'; $seconds %= 3600; } if ($seconds > 60) { if ($res) { $res .= ', '; } $num = floor($seconds / 60); $res .= $num . 'm'; $seconds %= 60; } if ($seconds > 0) { if ($res) { $res .= ' '; } $res .= $seconds . 's'; } if (!$res) { return $forward ? __('right now', 'litespeed-cache') : __('just now', 'litespeed-cache'); } $res = $forward ? $res : sprintf(__(' %s ago', 'litespeed-cache'), $res); return $res; } /** * Convert array to string * * @since 1.6 * @access public */ public static function arr2str($arr) { if (!is_array($arr)) { return $arr; } return base64_encode(\json_encode($arr)); } /** * Get human readable size * * @since 1.6 * @access public */ public static function real_size($filesize, $is_1000 = false) { $unit = $is_1000 ? 1000 : 1024; if ($filesize >= pow($unit, 3)) { $filesize = round(($filesize / pow($unit, 3)) * 100) / 100 . 'G'; } elseif ($filesize >= pow($unit, 2)) { $filesize = round(($filesize / pow($unit, 2)) * 100) / 100 . 'M'; } elseif ($filesize >= $unit) { $filesize = round(($filesize / $unit) * 100) / 100 . 'K'; } else { $filesize = $filesize . 'B'; } return $filesize; } /** * Parse attributes from string * * @since 1.2.2 * @since 1.4 Moved from optimize to utility * @access private * @param string $str * @return array All the attributes */ public static function parse_attr($str) { $attrs = array(); preg_match_all('#([\w-]+)=(["\'])([^\2]*)\2#isU', $str, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs[$match[1]] = trim($match[3]); } return $attrs; } /** * Check if an array has a string * * Support $ exact match * * @since 1.3 * @access private * @param string $needle The string to search with * @param array $haystack * @return bool|string False if not found, otherwise return the matched string in haystack. */ public static function str_hit_array($needle, $haystack, $has_ttl = false) { if (!$haystack) { return false; } /** * Safety check to avoid PHP warning * @see https://github.com/litespeedtech/lscache_wp/pull/131/commits/45fc03af308c7d6b5583d1664fad68f75fb6d017 */ if (!is_array($haystack)) { Debug2::debug('[Util] ❌ bad param in str_hit_array()!'); return false; } $hit = false; $this_ttl = 0; foreach ($haystack as $item) { if (!$item) { continue; } if ($has_ttl) { $this_ttl = 0; $item = explode(' ', $item); if (!empty($item[1])) { $this_ttl = $item[1]; } $item = $item[0]; } if (substr($item, 0, 1) === '^' && substr($item, -1) === '$') { // do exact match if (substr($item, 1, -1) === $needle) { $hit = $item; break; } } elseif (substr($item, -1) === '$') { // match end if (substr($item, 0, -1) === substr($needle, -strlen($item) + 1)) { $hit = $item; break; } } elseif (substr($item, 0, 1) === '^') { // match beginning if (substr($item, 1) === substr($needle, 0, strlen($item) - 1)) { $hit = $item; break; } } else { if (strpos($needle, $item) !== false) { $hit = $item; break; } } } if ($hit) { if ($has_ttl) { return array($hit, $this_ttl); } return $hit; } return false; } /** * Improve compatibility to PHP old versions * * @since 1.2.2 * */ public static function compatibility() { require_once LSCWP_DIR . 'lib/php-compatibility.func.php'; } /** * Convert URI to URL * * @since 1.3 * @access public * @param string $uri `xx/xx.html` or `/subfolder/xx/xx.html` * @return string http://www.example.com/subfolder/xx/xx.html */ public static function uri2url($uri) { if (substr($uri, 0, 1) === '/') { self::domain_const(); $url = LSCWP_DOMAIN . $uri; } else { $url = home_url('/') . $uri; } return $url; } /** * Convert URL to basename (filename) * * @since 4.7 */ public static function basename($url) { $url = trim($url); $uri = @parse_url($url, PHP_URL_PATH); $basename = pathinfo($uri, PATHINFO_BASENAME); return $basename; } /** * Drop .webp and .avif if existed in filename * * @since 4.7 */ public static function drop_webp($filename) { if (in_array(substr($filename, -5), array('.webp', '.avif'))) { $filename = substr($filename, 0, -5); } return $filename; } /** * Convert URL to URI * * @since 1.2.2 * @since 1.6.2.1 Added 2nd param keep_qs * @access public */ public static function url2uri($url, $keep_qs = false) { $url = trim($url); $uri = @parse_url($url, PHP_URL_PATH); $qs = @parse_url($url, PHP_URL_QUERY); if (!$keep_qs || !$qs) { return $uri; } return $uri . '?' . $qs; } /** * Get attachment relative path to upload folder * * @since 3.0 * @access public * @param string `https://aa.com/bbb/wp-content/upload/2018/08/test.jpg` or `/bbb/wp-content/upload/2018/08/test.jpg` * @return string `2018/08/test.jpg` */ public static function att_short_path($url) { if (!defined('LITESPEED_UPLOAD_PATH')) { $_wp_upload_dir = wp_upload_dir(); $upload_path = self::url2uri($_wp_upload_dir['baseurl']); define('LITESPEED_UPLOAD_PATH', $upload_path); } $local_file = self::url2uri($url); $short_path = substr($local_file, strlen(LITESPEED_UPLOAD_PATH) + 1); return $short_path; } /** * Make URL to be relative * * NOTE: for subfolder home_url, will keep subfolder part (strip nothing but scheme and host) * * @param string $url * @return string Relative URL, start with / */ public static function make_relative($url) { // replace home_url if the url is full url self::domain_const(); if (strpos($url, LSCWP_DOMAIN) === 0) { $url = substr($url, strlen(LSCWP_DOMAIN)); } return trim($url); } /** * Convert URL to domain only * * @since 1.7.1 */ public static function parse_domain($url) { $url = @parse_url($url); if (empty($url['host'])) { return ''; } if (!empty($url['scheme'])) { return $url['scheme'] . '://' . $url['host']; } return '//' . $url['host']; } /** * Drop protocol `https:` from https://example.com * * @since 3.3 */ public static function noprotocol($url) { $tmp = parse_url(trim($url)); if (!empty($tmp['scheme'])) { $url = str_replace($tmp['scheme'] . ':', '', $url); } return $url; } /** * Validate ip v4 * @since 5.5 */ public static function valid_ipv4($ip) { return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); } /** * Generate domain const * * This will generate http://www.example.com even there is a subfolder in home_url setting * * Conf LSCWP_DOMAIN has NO trailing / * * @since 1.3 * @access public */ public static function domain_const() { if (defined('LSCWP_DOMAIN')) { return; } self::compatibility(); $domain = http_build_url(get_home_url(), array(), HTTP_URL_STRIP_ALL); define('LSCWP_DOMAIN', $domain); } /** * Array map one textarea to sanitize the url * * @since 1.3 * @access public * @param string $content * @param bool $type String handler type * @return string|array */ public static function sanitize_lines($arr, $type = null) { $types = $type ? explode(',', $type) : array(); if (!$arr) { if ($type === 'string') { return ''; } return array(); } if (!is_array($arr)) { $arr = explode("\n", $arr); } $arr = array_map('trim', $arr); $changed = false; if (in_array('uri', $types)) { $arr = array_map(__CLASS__ . '::url2uri', $arr); $changed = true; } if (in_array('basename', $types)) { $arr = array_map(__CLASS__ . '::basename', $arr); $changed = true; } if (in_array('drop_webp', $types)) { $arr = array_map(__CLASS__ . '::drop_webp', $arr); $changed = true; } if (in_array('relative', $types)) { $arr = array_map(__CLASS__ . '::make_relative', $arr); // Remove domain $changed = true; } if (in_array('domain', $types)) { $arr = array_map(__CLASS__ . '::parse_domain', $arr); // Only keep domain $changed = true; } if (in_array('noprotocol', $types)) { $arr = array_map(__CLASS__ . '::noprotocol', $arr); // Drop protocol, `https://example.com` -> `//example.com` $changed = true; } if (in_array('trailingslash', $types)) { $arr = array_map('trailingslashit', $arr); // Append trailing slash, `https://example.com` -> `https://example.com/` $changed = true; } if ($changed) { $arr = array_map('trim', $arr); } $arr = array_unique($arr); $arr = array_filter($arr); if (in_array('string', $types)) { return implode("\n", $arr); } return $arr; } /** * Builds an url with an action and a nonce. * * Assumes user capabilities are already checked. * * @since 1.6 Changed order of 2nd&3rd param, changed 3rd param `append_str` to 2nd `type` * @access public * @return string The built url. */ public static function build_url($action, $type = false, $is_ajax = false, $page = null, $append_arr = array()) { $prefix = '?'; if ($page === '_ori') { $page = true; $append_arr['_litespeed_ori'] = 1; } if (!$is_ajax) { if ($page) { // If use admin url if ($page === true) { $page = 'admin.php'; } else { if (strpos($page, '?') !== false) { $prefix = '&'; } } $combined = $page . $prefix . Router::ACTION . '=' . $action; } else { // Current page rebuild URL $params = $_GET; if (!empty($params)) { if (isset($params[Router::ACTION])) { unset($params[Router::ACTION]); } if (isset($params['_wpnonce'])) { unset($params['_wpnonce']); } if (!empty($params)) { $prefix .= http_build_query($params) . '&'; } } global $pagenow; $combined = $pagenow . $prefix . Router::ACTION . '=' . $action; } } else { $combined = 'admin-ajax.php?action=litespeed_ajax&' . Router::ACTION . '=' . $action; } if (is_network_admin()) { $prenonce = network_admin_url($combined); } else { $prenonce = admin_url($combined); } $url = wp_nonce_url($prenonce, $action, Router::NONCE); if ($type) { // Remove potential param `type` from url $url = parse_url(htmlspecialchars_decode($url)); parse_str($url['query'], $query); $built_arr = array_merge($query, array(Router::TYPE => $type)); if ($append_arr) { $built_arr = array_merge($built_arr, $append_arr); } $url['query'] = http_build_query($built_arr); self::compatibility(); $url = http_build_url($url); $url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); } return $url; } /** * Check if the host is the internal host * * @since 1.2.3 * */ public static function internal($host) { if (!defined('LITESPEED_FRONTEND_HOST')) { if (defined('WP_HOME')) { $home_host = WP_HOME; // Also think of `WP_SITEURL` } else { $home_host = get_option('home'); } define('LITESPEED_FRONTEND_HOST', parse_url($home_host, PHP_URL_HOST)); } if ($host === LITESPEED_FRONTEND_HOST) { return true; } /** * Filter for multiple domains * @since 2.9.4 */ if (!isset(self::$_internal_domains)) { self::$_internal_domains = apply_filters('litespeed_internal_domains', array()); } if (self::$_internal_domains) { return in_array($host, self::$_internal_domains); } return false; } /** * Check if an URL is a internal existing file * * @since 1.2.2 * @since 1.6.2 Moved here from optm.cls due to usage of media.cls * @access public * @return string|bool The real path of file OR false */ public static function is_internal_file($url, $addition_postfix = false) { if (substr($url, 0, 5) == 'data:') { Debug2::debug2('[Util] data: content not file'); return false; } $url_parsed = parse_url($url); if (isset($url_parsed['host']) && !self::internal($url_parsed['host'])) { // Check if is cdn path // Do this to avoid user hardcoded src in tpl if (!CDN::internal($url_parsed['host'])) { Debug2::debug2('[Util] external'); return false; } } if (empty($url_parsed['path'])) { return false; } // Need to replace child blog path for assets, ref: .htaccess if (is_multisite() && defined('PATH_CURRENT_SITE')) { $pattern = '#^' . PATH_CURRENT_SITE . '([_0-9a-zA-Z-]+/)(wp-(content|admin|includes))#U'; $replacement = PATH_CURRENT_SITE . '$2'; $url_parsed['path'] = preg_replace($pattern, $replacement, $url_parsed['path']); // $current_blog = (int) get_current_blog_id(); // $main_blog_id = (int) get_network()->site_id; // if ( $current_blog === $main_blog_id ) { // define( 'LITESPEED_IS_MAIN_BLOG', true ); // } // else { // define( 'LITESPEED_IS_MAIN_BLOG', false ); // } } // Parse file path /** * Trying to fix pure /.htaccess rewrite to /wordpress case * * Add `define( 'LITESPEED_WP_REALPATH', '/wordpress' );` in wp-config.php in this case * * @internal #611001 - Combine & Minify not working? * @since 1.6.3 */ if (substr($url_parsed['path'], 0, 1) === '/') { if (defined('LITESPEED_WP_REALPATH')) { $file_path_ori = $_SERVER['DOCUMENT_ROOT'] . LITESPEED_WP_REALPATH . $url_parsed['path']; } else { $file_path_ori = $_SERVER['DOCUMENT_ROOT'] . $url_parsed['path']; } } else { $file_path_ori = Router::frontend_path() . '/' . $url_parsed['path']; } /** * Added new file postfix to be check if passed in * @since 2.2.4 */ if ($addition_postfix) { $file_path_ori .= '.' . $addition_postfix; } /** * Added this filter for those plugins which overwrite the filepath * @see #101091 plugin `Hide My WordPress` * @since 2.2.3 */ $file_path_ori = apply_filters('litespeed_realpath', $file_path_ori); $file_path = realpath($file_path_ori); if (!is_file($file_path)) { Debug2::debug2('[Util] file not exist: ' . $file_path_ori); return false; } return array($file_path, filesize($file_path)); } /** * Safely parse URL for v5.3 compatibility * * @since 3.4.3 */ public static function parse_url_safe($url, $component = -1) { if (substr($url, 0, 2) == '//') { $url = 'https:' . $url; } return parse_url($url, $component); } /** * Replace url in srcset to new value * * @since 2.2.3 */ public static function srcset_replace($content, $callback) { preg_match_all('# srcset=([\'"])(.+)\g{1}#iU', $content, $matches); $srcset_ori = array(); $srcset_final = array(); foreach ($matches[2] as $k => $urls_ori) { $urls_final = explode(',', $urls_ori); $changed = false; foreach ($urls_final as $k2 => $url_info) { $url_info_arr = explode(' ', trim($url_info)); if (!($url2 = call_user_func($callback, $url_info_arr[0]))) { continue; } $changed = true; $urls_final[$k2] = str_replace($url_info_arr[0], $url2, $url_info); Debug2::debug2('[Util] - srcset replaced to ' . $url2 . (!empty($url_info_arr[1]) ? ' ' . $url_info_arr[1] : '')); } if (!$changed) { continue; } $urls_final = implode(',', $urls_final); $srcset_ori[] = $matches[0][$k]; $srcset_final[] = str_replace($urls_ori, $urls_final, $matches[0][$k]); } if ($srcset_ori) { $content = str_replace($srcset_ori, $srcset_final, $content); Debug2::debug2('[Util] - srcset replaced'); } return $content; } /** * Generate pagination * * @since 3.0 * @access public */ public static function pagination($total, $limit, $return_offset = false) { $pagenum = isset($_GET['pagenum']) ? absint($_GET['pagenum']) : 1; $offset = ($pagenum - 1) * $limit; $num_of_pages = ceil($total / $limit); if ($offset > $total) { $offset = $total - $limit; } if ($offset < 0) { $offset = 0; } if ($return_offset) { return $offset; } $page_links = paginate_links(array( 'base' => add_query_arg('pagenum', '%#%'), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $num_of_pages, 'current' => $pagenum, )); return '<div class="tablenav"><div class="tablenav-pages" style="margin: 1em 0">' . $page_links . '</div></div>'; } /** * Generate placeholder for an array to query * * @since 2.0 * @access public */ public static function chunk_placeholder($data, $fields) { $division = substr_count($fields, ',') + 1; $q = implode( ',', array_map(function ($el) { return '(' . implode(',', $el) . ')'; }, array_chunk(array_fill(0, count($data), '%s'), $division)) ); return $q; } } src/admin.cls.php 0000644 00000010704 15162130571 0007717 0 ustar 00