diff --git a/bootstrap.php b/bootstrap.php index c4d16d8..f5bfb9c 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -12,3 +12,5 @@ function ( $features ) { } new PerformanceFeatureHooks(); + +require_once __DIR__ . '/includes/BurstSafetyMode/init.php'; diff --git a/components/advancedSettings/JetpackBoost/SingleOption.js b/components/advancedSettings/JetpackBoost/SingleOption.js index 70c96e8..1aa09e8 100644 --- a/components/advancedSettings/JetpackBoost/SingleOption.js +++ b/components/advancedSettings/JetpackBoost/SingleOption.js @@ -83,7 +83,7 @@ const SingleOption = ( { params, isChild, methods, constants } ) => { ! NewfoldRuntime.sdk.performance .jetpack_boost_premium_is_active && ( { + const [ isEdited, setIsEdited ] = methods.useState( false ); + const [ isError, setIsError ] = methods.useState( false ); + const [ isSaved, setIsSaved ] = methods.useState( false ); + const [ currentValue, setCurrentValue ] = methods.useState( + methods.NewfoldRuntime.sdk.cacheExclusion + ); + const [ cacheExclusion, setCacheExclusion ] = methods.useState( + methods.NewfoldRuntime.sdk.cacheExclusion + ); + const apiUrl = methods.NewfoldRuntime.createApiUrl( + '/newfold-performance/v1/cache-exclusion/update' + ); + + const handleCacheExclusionChange = ( e ) => { + if ( e.target.value !== currentValue ) { + setIsEdited( true ); + } else { + setIsEdited( false ); + } + setCurrentValue( e.target.value ); + }; + + const handlingSaveButton = () => { + methods + .apiFetch( { + url: apiUrl, + method: 'POST', + data: { cacheExclusion: currentValue }, + } ) + .then( () => { + setIsSaved( true ); + setCacheExclusion( currentValue ); + setIsEdited( false ); + } ) + .catch( ( error ) => { + setIsError( error.message ); + } ); + }; + + methods.useUpdateEffect( () => { + methods.setStore( { + ...constants.store, + CacheExclusion: cacheExclusion, + } ); + + methods.makeNotice( + 'cache-exlusion-notice', + constants.text.cacheExclusionTitle, + ! isError ? constants.text.cacheExclusionSaved : isError, + ! isError ? 'success' : 'error', + 5000 + ); + }, [ isSaved, isError ] ); + + return ( + + + { isEdited && ( + + ) } + + ); +}; + +export default CacheExclusion; diff --git a/components/cacheSettings/index.js b/components/cacheSettings/index.js index f42a328..fdc35a0 100644 --- a/components/cacheSettings/index.js +++ b/components/cacheSettings/index.js @@ -1,100 +1,115 @@ -import { Container, RadioGroup } from "@newfold/ui-component-library"; +// Newfold +import { Container, RadioGroup } from '@newfold/ui-component-library'; -const CacheSettings = ({ methods, constants, Components }) => { - const [ cacheLevel, setCacheLevel ] = methods.useState(constants.store.cacheLevel); +const CacheSettings = ( { methods, constants, Components } ) => { + const [ cacheLevel, setCacheLevel ] = methods.useState( + constants.store.cacheLevel + ); - const cacheOptions = [ - { - label: constants.text.cacheLevel0Label, - description: constants.text.cacheLevel0Description + constants.text.cacheLevel0Recommendation, - value: 0, - notice: constants.text.cacheLevel0NoticeText, - }, - { - label: constants.text.cacheLevel1Label, - description: constants.text.cacheLevel1Description + constants.text.cacheLevel1Recommendation, - value: 1, - notice: constants.text.cacheLevel1NoticeText, - }, - { - label: constants.text.cacheLevel2Label, - description: constants.text.cacheLevel2Description + constants.text.cacheLevel2Recommendation, - value: 2, - notice: constants.text.cacheLevel2NoticeText, - }, - { - label: constants.text.cacheLevel3Label, - description: constants.text.cacheLevel3Description + constants.text.cacheLevel3Recommendation, - value: 3, - notice: constants.text.cacheLevel3NoticeText, - }, - ]; + const cacheOptions = [ + { + label: constants.text.cacheLevel0Label, + description: + constants.text.cacheLevel0Description + + constants.text.cacheLevel0Recommendation, + value: 0, + notice: constants.text.cacheLevel0NoticeText, + }, + { + label: constants.text.cacheLevel1Label, + description: + constants.text.cacheLevel1Description + + constants.text.cacheLevel1Recommendation, + value: 1, + notice: constants.text.cacheLevel1NoticeText, + }, + { + label: constants.text.cacheLevel2Label, + description: + constants.text.cacheLevel2Description + + constants.text.cacheLevel2Recommendation, + value: 2, + notice: constants.text.cacheLevel2NoticeText, + }, + { + label: constants.text.cacheLevel3Label, + description: + constants.text.cacheLevel3Description + + constants.text.cacheLevel3Recommendation, + value: 3, + notice: constants.text.cacheLevel3NoticeText, + }, + ]; - const getCacheLevelNoticeTitle = () => { - return constants.text.cacheLevelNoticeTitle; - }; + const getCacheLevelNoticeTitle = () => { + return constants.text.cacheLevelNoticeTitle; + }; - const getCacheLevelNoticeText = () => { - return cacheOptions[cacheLevel].notice; - }; + const getCacheLevelNoticeText = () => { + return cacheOptions[ cacheLevel ].notice; + }; - const handleCacheLevelChange = (e) => { - methods.newfoldSettingsApiFetch( - { cacheLevel: parseInt(e.target.value) }, - methods.setError, (response) => { - setCacheLevel(parseInt(e.target.value)); - } - ); - }; + const handleCacheLevelChange = ( e ) => { + methods.newfoldSettingsApiFetch( + { cacheLevel: parseInt( e.target.value ) }, + methods.setError, + () => { + setCacheLevel( parseInt( e.target.value ) ); + } + ); + }; - methods.useUpdateEffect(() => { - methods.setStore({ - ...constants.store, - cacheLevel, - }); + methods.useUpdateEffect( () => { + methods.setStore( { + ...constants.store, + cacheLevel, + } ); - methods.makeNotice( - "cache-level-change-notice", - getCacheLevelNoticeTitle(), - getCacheLevelNoticeText(), - "success", - 5000 - ); - }, [cacheLevel]); + methods.makeNotice( + 'cache-level-change-notice', + getCacheLevelNoticeTitle(), + getCacheLevelNoticeText(), + 'success', + 5000 + ); + }, [ cacheLevel ] ); - return ( - <> - - - {cacheOptions.map((option) => { - return ( - - -
- {option.description} -
-
- ); - })} -
-
- - ); -} + return ( + <> + + + { cacheOptions.map( ( option ) => { + return ( + + +
+ { option.description } +
+
+ ); + } ) } +
+
+ + ); +}; -export default CacheSettings; \ No newline at end of file +export default CacheSettings; diff --git a/components/clearCache/index.js b/components/clearCache/index.js index 793ddab..494a3fc 100644 --- a/components/clearCache/index.js +++ b/components/clearCache/index.js @@ -1,39 +1,37 @@ -import { Button, Container } from "@newfold/ui-component-library"; +import { Button, Container } from '@newfold/ui-component-library'; -const ClearCache = ({ methods, constants }) => { +const ClearCache = ( { methods, constants } ) => { + const clearCache = () => { + methods.newfoldPurgeCacheApiFetch( + {}, + methods.setError, + ( response ) => { + methods.makeNotice( + 'disable-old-posts-comments-notice', + constants.text.clearCacheNoticeTitle, + null, + 'success', + 5000 + ); + } + ); + }; - const clearCache = () => { - methods.newfoldPurgeCacheApiFetch( - {}, - methods.setError, - (response) => { - methods.makeNotice( - "disable-old-posts-comments-notice", - constants.text.clearCacheNoticeTitle, - null, - "success", - 5000 - ); - } - ); - }; + return ( + + + + ); +}; - return ( - - - - - ); -;} - -export default ClearCache; \ No newline at end of file +export default ClearCache; diff --git a/components/performance/defaultText.js b/components/performance/defaultText.js index 3260594..bba4251 100644 --- a/components/performance/defaultText.js +++ b/components/performance/defaultText.js @@ -66,7 +66,21 @@ const defaultText = { ), clearCacheNoticeTitle: __( 'Cache cleared', 'wp-module-performance' ), clearCacheTitle: __( 'Clear Cache', 'wp-module-performance' ), - + cacheExclusionTitle: __( 'Exclude from cache', 'wp-module-performance' ), + cacheExclusionDescription: __( + 'This setting controls what pages pass a “no-cache” header so that page caching and browser caching is not used.', + 'wp-module-performance' + ), + cacheExclusionSaved: __( 'Cache Exclusion saved', 'wp-module-performance' ), + cacheExclusionSaveButton: __( 'Save', 'wp-module-performance' ), + // Skip 404 + skip404Title: __( 'Skip 404', 'wp-module-performance' ), + skip404Description: __( + 'When enabled, static resources like images and fonts will use a default server 404 page and not WordPress 404 pages. Pages and posts will continue using WordPress for 404 pages. This can considerably speed up your website if a static resource like an image or font is missing.', + 'wp-module-performance' + ), + skip404NoticeTitle: __( 'Skip 404 saved', 'wp-module-performance' ), + skip404Notice: __( 'Skip 404 saved', 'wp-module-performance' ), // Image Optimization imageOptimizationSettingsTitle: __( 'Image Optimization', @@ -164,6 +178,7 @@ const defaultText = { 'Something went wrong while updating the settings. Please try again.', 'wp-module-performance' ), + linkPrefetchDescription: __( 'Asks the browser to download and cache links on the page ahead of them being clicked on, so that when they are clicked they load almost instantly. ', 'wp-module-performance' @@ -323,6 +338,7 @@ const defaultText = { ), optionSet: __( 'Option saved correctly', 'wp-module-performance' ), optionNotSet: __( 'Error saving option', 'wp-module-performance' ), + upgradeModule: __( 'Upgrade to unlock', 'wp-module-performance' ), }; export default defaultText; diff --git a/components/performance/index.js b/components/performance/index.js index dec0a39..30e842c 100644 --- a/components/performance/index.js +++ b/components/performance/index.js @@ -4,6 +4,8 @@ import { Container } from '@newfold/ui-component-library'; // Components. import { default as CacheSettings } from '../cacheSettings/'; import { default as ClearCache } from '../clearCache/'; +import { default as CacheExclusion } from '../cacheExclusion/'; +import { default as Skip404 } from '../skip404/'; import { default as AdvancedSettings } from '../advancedSettings'; import { default as defaultText } from './defaultText'; import ImageOptimizationSettings from '../imageOptimizationSettings'; @@ -56,12 +58,25 @@ const Performance = ( { methods, constants, Components, ...props } ) => { Components={ Components } /> + + + + + + { + const [ skip404, setSkip404 ] = methods.useState( + NewfoldRuntime.sdk.performance.skip404 + ); + + const getSkip404NoticeTitle = () => { + return constants.text.skip404NoticeTitle; + }; + + const handleSkip404Change = () => { + const value = ! skip404; + apiFetch( { + path: 'newfold-performance/v1/settings', + method: 'POST', + data: { + field: { + id: 'skip404', + value, + }, + }, + } ) + .then( () => { + methods.makeNotice( + 'skip404-change-notice', + getSkip404NoticeTitle(), + '', + 'success', + 5000 + ); + } ) + .catch( () => { + methods.makeNotice( + 'skip404-change-notice', + constants.text.optionNotSet, + '', + 'error', + 5000 + ); + } ); + + setSkip404( value ); + }; + + return ( + + + + ); +}; + +export default Skip404; diff --git a/includes/BurstSafetyMode/Browser.php b/includes/BurstSafetyMode/Browser.php new file mode 100644 index 0000000..6f97d3b --- /dev/null +++ b/includes/BurstSafetyMode/Browser.php @@ -0,0 +1,60 @@ +addHeader( 'X-Newfold-Cache-Level', BURST_SAFETY_CACHE_LEVEL ); + $this->addRules(); + } + + /** + * Add htaccess rules. + * + * @return void + */ + public static function addRules() { + + $file_typ_expirations = array( + 'default' => '1 week', + 'text/html' => '8 hours', + 'image/jpg' => '1 week', + 'image/jpeg' => '1 week', + 'image/gif' => '1 week', + 'image/png' => '1 week', + 'text/css' => '1 week', + 'text/javascript' => '1 week', + 'application/pdf' => '1 month', + 'image/x-icon' => '1 year', + ); + + $tab = "\t"; + + $rules[] = ''; + $rules[] = "{$tab}ExpiresActive On"; + + foreach ( $file_typ_expirations as $file_type => $expiration ) { + if ( 'default' === $file_type ) { + $rules[] = "{$tab}ExpiresDefault \"access plus {$expiration}\""; + } else { + $rules[] = "{$tab}ExpiresByType {$file_type} \"access plus {$expiration}\""; + } + } + + $rules [] = ''; + + $htaccess = new htaccess( self::MARKER ); + + return $htaccess->addContent( $rules ); + } +} diff --git a/includes/BurstSafetyMode/ResponseHeaderManager.php b/includes/BurstSafetyMode/ResponseHeaderManager.php new file mode 100644 index 0000000..3a3365c --- /dev/null +++ b/includes/BurstSafetyMode/ResponseHeaderManager.php @@ -0,0 +1,116 @@ +htaccess = new htaccess( self::MARKER ); + } + + /** + * Parse existing headers. + * + * @return array + */ + public function parseHeaders() { + + $headers = array(); + + $content = $this->htaccess->readContent(); + $lines = array_map( 'trim', convertContentToLines( $content ) ); + + array_shift( $lines ); // Remove opening IfModule + array_pop( $lines ); // Remove closing IfModule + + $pattern = '/^Header set (.*) "(.*)"$/'; + + foreach ( $lines as $line ) { + if ( preg_match( $pattern, trim( $line ), $matches ) && isset( $matches[1], $matches[2] ) ) { + $headers[ $matches[1] ] = $matches[2]; + } + } + + return $headers; + } + + /** + * Add a header. + * + * @param string $name Header name + * @param string $value Header value + */ + public function addHeader( string $name, string $value ) { + $this->setHeaders( + array_merge( + $this->parseHeaders(), + array( $name => $value ) + ) + ); + } + + /** + * Add multiple headers at once. + * + * @param string[] $headers + */ + public function addHeaders( array $headers ) { + $headers = array_merge( $this->parseHeaders(), $headers ); + $this->setHeaders( $headers ); + } + + /** + * Remove a header. + * + * @param string $name Header name + */ + public function removeHeader( $name ) { + $headers = $this->parseHeaders(); + unset( $headers[ $name ] ); + $this->setHeaders( $headers ); + } + + /** + * Remove all headers. + */ + public function removeAllHeaders() { + $this->setHeaders( array() ); + } + + /** + * Set headers. + * + * @param array $headers + */ + public function setHeaders( array $headers ) { + + if ( empty( $headers ) ) { + $this->htaccess->removeContent(); + + return; + } + + $content = '' . PHP_EOL; + foreach ( $headers as $key => $value ) { + $content .= "\t" . "Header set {$key} \"{$value}\"" . PHP_EOL; + } + $content .= ''; + + $this->htaccess->addContent( $content ); + } +} diff --git a/includes/BurstSafetyMode/Skip404.php b/includes/BurstSafetyMode/Skip404.php new file mode 100644 index 0000000..18afd04 --- /dev/null +++ b/includes/BurstSafetyMode/Skip404.php @@ -0,0 +1,46 @@ +addRules(); + } + + + /** + * Add our rules to the .htacces file. + */ + public static function addRules() { + $content = << + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} !(robots\.txt|ads\.txt|[a-z0-9_\-]*sitemap[a-z0-9_\.\-]*\.(xml|xsl|html)(\.gz)?) + RewriteCond %{REQUEST_URI} \.(css|htc|less|js|js2|js3|js4|html|htm|rtf|rtx|txt|xsd|xsl|xml|asf|asx|wax|wmv|wmx|avi|avif|avifs|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|webp|json|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|webm|mpp|otf|_otf|odb|odc|odf|odg|odp|ods|odt|ogg|ogv|pdf|png|pot|pps|ppt|pptx|ra|ram|svg|svgz|swf|tar|tif|tiff|ttf|ttc|_ttf|wav|wma|wri|woff|woff2|xla|xls|xlsx|xlt|xlw|zip)$ [NC] + RewriteRule .* - [L] + +HTACCESS; + + addContent( self::MARKER, $content ); + } + + /** + * Remove our rules from the .htaccess file. + */ + public static function removeRules() { + removeMarkers( self::MARKER ); + } +} diff --git a/includes/BurstSafetyMode/init.php b/includes/BurstSafetyMode/init.php new file mode 100644 index 0000000..2447e2e --- /dev/null +++ b/includes/BurstSafetyMode/init.php @@ -0,0 +1,55 @@ +addHeader( 'X-Newfold-Cache-Level', $newfold_cache_level ); + + delete_option( 'newfold_burst_safety_mode' ); + } +} elseif ( ! $newfold_burst_safety_mode ) { + $files_to_include = array( + 'htaccess' => BLUEHOST_PLUGIN_DIR . 'vendor/wp-forge/wp-htaccess-manager/includes/htaccess.php', + 'htaccess_functions' => BLUEHOST_PLUGIN_DIR . 'vendor/wp-forge/wp-htaccess-manager/includes/functions.php', + 'skip404' => BLUEHOST_PLUGIN_DIR . 'vendor/newfold-labs/wp-module-performance/includes/BurstSafetyMode/Skip404.php', + 'browser' => BLUEHOST_PLUGIN_DIR . 'vendor/newfold-labs/wp-module-performance/includes/BurstSafetyMode/Browser.php', + 'response_header_manager' => BLUEHOST_PLUGIN_DIR . 'vendor/newfold-labs/wp-module-performance/includes/BurstSafetyMode/ResponseHeaderManager.php', + ); + + foreach ( $files_to_include as $file_path ) { + if ( file_exists( $file_path ) ) { + require_once $file_path; + } + } + + define( 'BURST_SAFETY_CACHE_LEVEL', 3 ); + + $skip404 = new BurstSkip404(); + + if ( BURST_SAFETY_CACHE_LEVEL !== $newfold_cache_level && class_exists( BurstBrowser::class ) ) { + $browser = new BurstBrowser(); + } + + update_option( 'newfold_burst_safety_mode', true ); +} diff --git a/includes/CacheExclusion.php b/includes/CacheExclusion.php new file mode 100644 index 0000000..39d8977 --- /dev/null +++ b/includes/CacheExclusion.php @@ -0,0 +1,46 @@ +container = $container; + + add_filter( 'newfold-runtime', array( $this, 'add_to_runtime' ) ); + } + /** + * Add values to the runtime object. + * + * @param array $sdk The runtime object. + * + * @return array + */ + public function add_to_runtime( $sdk ) { + return array_merge( $sdk, array( 'cacheExclusion' => get_option( self::OPTION_CACHE_EXCLUSION, get_default_cache_exclusions() ) ) ); + } +} diff --git a/includes/CacheManager.php b/includes/CacheManager.php index 38da5eb..28cf7cf 100644 --- a/includes/CacheManager.php +++ b/includes/CacheManager.php @@ -5,9 +5,10 @@ use NewfoldLabs\WP\Module\Performance\CacheTypes\CacheBase; use NewfoldLabs\WP\ModuleLoader\Container; use WP_Forge\Collection\Collection; - +/** + * Cache Manager Class + */ class CacheManager { - /** * Dependency injection container. * @@ -18,7 +19,7 @@ class CacheManager { /** * Constructor. * - * @param string[] $supportedCacheTypes Cache types supported by the plugin + * @param Container $container the container */ public function __construct( Container $container ) { $this->container = $container; @@ -30,14 +31,14 @@ public function __construct( Container $container ) { * @return string[] */ protected function classMap() { - return [ + return array( 'browser' => __NAMESPACE__ . '\\CacheTypes\\Browser', 'cloudflare' => __NAMESPACE__ . '\\CacheTypes\\Cloudflare', 'file' => __NAMESPACE__ . '\\CacheTypes\\File', 'nginx' => __NAMESPACE__ . '\\CacheTypes\\Nginx', 'sitelock' => __NAMESPACE__ . '\\CacheTypes\\Sitelock', 'skip404' => __NAMESPACE__ . '\\CacheTypes\\Skip404', - ]; + ); } /** @@ -55,7 +56,7 @@ public function registeredCacheTypes() { * @return array */ public function enabledCacheTypes() { - $cacheTypes = []; + $cacheTypes = array(); if ( $this->container->has( 'cache_types' ) ) { $providedTypes = $this->container->get( 'cache_types' ); if ( is_array( $providedTypes ) ) { @@ -75,11 +76,13 @@ public function enabledCacheTypes() { * @return CacheBase[] */ public function getInstances() { - $instances = []; + $instances = array(); $collection = new Collection( $this->classMap() ); $map = $collection->only( $this->enabledCacheTypes() ); foreach ( $map as $type => $class ) { /** + * CacheBase instance. + * * @var CacheBase $class */ if ( $class::shouldEnable( $this->container ) ) { @@ -90,5 +93,4 @@ public function getInstances() { return $instances; } - } diff --git a/includes/CacheTypes/Browser.php b/includes/CacheTypes/Browser.php index 23fd2fb..bfd31e9 100644 --- a/includes/CacheTypes/Browser.php +++ b/includes/CacheTypes/Browser.php @@ -6,12 +6,15 @@ use NewfoldLabs\WP\Module\Performance\Performance; use NewfoldLabs\WP\ModuleLoader\Container; use WP_Forge\WP_Htaccess_Manager\htaccess; +use NewfoldLabs\WP\Module\Performance\CacheExclusion; use function NewfoldLabs\WP\Module\Performance\getCacheLevel; use function WP_Forge\WP_Htaccess_Manager\removeMarkers; +/** + * Browser cache class + */ class Browser extends CacheBase { - /** * The file marker name. * @@ -22,7 +25,7 @@ class Browser extends CacheBase { /** * Whether or not the code for this cache type should be loaded. * - * @param Container $container + * @param Container $container the container. * * @return bool */ @@ -35,9 +38,11 @@ public static function shouldEnable( Container $container ) { */ public function __construct() { - new OptionListener( Performance::OPTION_CACHE_LEVEL, [ __CLASS__, 'maybeAddRules' ] ); + new OptionListener( Performance::OPTION_CACHE_LEVEL, array( __CLASS__, 'maybeAddRules' ) ); + + new OptionListener( CacheExclusion::OPTION_CACHE_EXCLUSION, array( __CLASS__, 'exclusionChange' ) ); - add_filter( 'newfold_update_htaccess', [ $this, 'onRewrite' ] ); + add_filter( 'newfold_update_htaccess', array( $this, 'onRewrite' ) ); } /** @@ -47,6 +52,13 @@ public function onRewrite() { self::maybeAddRules( getCacheLevel() ); } + /** + * Manage on exlcusion option change. + */ + public static function exclusionChange() { + self::maybeAddRules( getCacheLevel() ); + } + /** * Determine whether to add or remove rules based on caching level. * @@ -79,20 +91,32 @@ public static function addRules( $cacheLevel ) { $rules[] = ''; $rules[] = "{$tab}ExpiresActive On"; - foreach ( $fileTypeExpirations as $fileType => $expiration ) { - if ( 'default' === $fileType ) { + foreach ( $fileTypeExpirations as $file_type => $expiration ) { + if ( 'default' === $file_type ) { $rules[] = "{$tab}ExpiresDefault \"access plus {$expiration}\""; } else { - $rules[] = "{$tab}ExpiresByType {$fileType} \"access plus {$expiration}\""; + $rules[] = "{$tab}ExpiresByType {$file_type} \"access plus {$expiration}\""; } } + $rules[] = ''; + + $cache_exclusion_parameters = array_map( 'trim', explode( ',', get_option( CacheExclusion::OPTION_CACHE_EXCLUSION ) ) ); - $rules [] = ''; + // Add the cache exclusion rules. + $rules[] = ''; + $rules[] = 'RewriteEngine On'; + foreach ( $cache_exclusion_parameters as $param ) { + if ( ! empty( $param ) ) { + $rules[] = "RewriteCond %{REQUEST_URI} !{$param} [NC]"; + } + } + $rules[] = 'RewriteRule .* - [E=Cache-Control:no-cache]'; + $rules[] = ''; + // Add the end of the rules about cache exclusion. $htaccess = new htaccess( self::MARKER ); return $htaccess->addContent( $rules ); - } /** @@ -106,7 +130,7 @@ protected static function getFileTypeExpirations( int $cacheLevel ) { switch ( $cacheLevel ) { case 3: - return [ + return array( 'default' => '1 week', 'text/html' => '8 hours', 'image/jpg' => '1 week', @@ -117,10 +141,10 @@ protected static function getFileTypeExpirations( int $cacheLevel ) { 'text/javascript' => '1 week', 'application/pdf' => '1 month', 'image/x-icon' => '1 year', - ]; + ); case 2: - return [ + return array( 'default' => '24 hours', 'text/html' => '2 hours', 'image/jpg' => '24 hours', @@ -131,10 +155,10 @@ protected static function getFileTypeExpirations( int $cacheLevel ) { 'text/javascript' => '24 hours', 'application/pdf' => '1 week', 'image/x-icon' => '1 year', - ]; + ); case 1: - return [ + return array( 'default' => '5 minutes', 'text/html' => '0 seconds', 'image/jpg' => '1 hour', @@ -145,10 +169,10 @@ protected static function getFileTypeExpirations( int $cacheLevel ) { 'text/javascript' => '1 hour', 'application/pdf' => '6 hours', 'image/x-icon' => '1 year', - ]; + ); default: - return []; + return array(); } } @@ -165,5 +189,4 @@ public static function onActivation() { public static function onDeactivation() { self::removeRules(); } - } diff --git a/includes/CacheTypes/File.php b/includes/CacheTypes/File.php index 6c955e0..60d9ef9 100644 --- a/includes/CacheTypes/File.php +++ b/includes/CacheTypes/File.php @@ -6,6 +6,7 @@ use NewfoldLabs\WP\Module\Performance\OptionListener; use NewfoldLabs\WP\Module\Performance\Performance; use NewfoldLabs\WP\ModuleLoader\Container; +use NewfoldLabs\WP\Module\Performance\CacheExclusion; use WP_Forge\WP_Htaccess_Manager\htaccess; use wpscholar\Url; @@ -14,8 +15,10 @@ use function NewfoldLabs\WP\Module\Performance\shouldCachePages; use function WP_Forge\WP_Htaccess_Manager\removeMarkers; +/** + * Page cache class + */ class File extends CacheBase implements Purgeable { - /** * The directory where cached files live. * @@ -33,7 +36,7 @@ class File extends CacheBase implements Purgeable { /** * Whether or not the code for this cache type should be loaded. * - * @param Container $container + * @param Container $container the container. * * @return bool */ @@ -46,10 +49,18 @@ public static function shouldEnable( Container $container ) { */ public function __construct() { - new OptionListener( Performance::OPTION_CACHE_LEVEL, [ __CLASS__, 'maybeAddRules' ] ); + new OptionListener( Performance::OPTION_CACHE_LEVEL, array( __CLASS__, 'maybeAddRules' ) ); + new OptionListener( CacheExclusion::OPTION_CACHE_EXCLUSION, array( __CLASS__, 'exclusionChange' ) ); + + add_action( 'init', array( $this, 'maybeGeneratePageCache' ) ); + add_action( 'newfold_update_htaccess', array( $this, 'onRewrite' ) ); + } - add_action( 'init', [ $this, 'maybeGeneratePageCache' ] ); - add_action( 'newfold_update_htaccess', [ $this, 'onRewrite' ] ); + /** + * Manage on exlcusion option change. + */ + public static function exclusionChange() { + self::maybeAddRules( getCacheLevel() ); } /** @@ -62,7 +73,7 @@ public function onRewrite() { /** * Determine whether to add or remove rules based on caching level. * - * @param int $cacheLevel The caching level. + * @param int $cacheLevel The caching level. */ public static function maybeAddRules( $cacheLevel ) { absint( $cacheLevel ) > 1 ? self::addRules() : self::removeRules(); @@ -110,7 +121,7 @@ public static function removeRules() { public function maybeGeneratePageCache() { if ( $this->isCacheable() ) { if ( $this->shouldCache() ) { - ob_start( [ $this, 'write' ] ); + ob_start( array( $this, 'write' ) ); } } else { nocache_headers(); @@ -120,7 +131,7 @@ public function maybeGeneratePageCache() { /** * Write page content to cache. * - * @param string $content Page content to be cached. + * @param string $content Page content to be cached. * * @return string */ @@ -150,7 +161,6 @@ public function write( $content ) { * @return bool */ public function isCacheable() { - // The request URI should never be empty – even for the homepage it should be '/' if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; @@ -198,7 +208,6 @@ public function isCacheable() { if ( is_404() || is_feed() ) { return false; } - } // Don't cache private pages @@ -221,11 +230,14 @@ public function shouldCache() { return false; } - // Don't cache if any string exclusions are present in the URL - $exclusions = $this->exclusions(); - foreach ( $exclusions as $exclude ) { - if ( false !== strpos( $_SERVER['REQUEST_URI'], $exclude ) ) { - return false; + // Check cache exclusion. + $cache_exclusion_parameters = $this->exclusions(); + + if ( ! empty( $cache_exclusion_parameters ) ) { + foreach ( $cache_exclusion_parameters as $param ) { + if ( stripos( $_SERVER['REQUEST_URI'], $param ) !== false ) { + return false; + } } } @@ -244,7 +256,9 @@ public function shouldCache() { * @return array */ protected function exclusions() { - return [ 'cart', 'checkout', 'wp-admin', '@', '%', ':', ';', '&', '=', '.', rest_get_url_prefix() ]; + $default = array( 'cart', 'checkout', 'wp-admin', '@', '%', ':', ';', '&', '=', '.', rest_get_url_prefix() ); + $cache_exclusion_option = array_map( 'trim', explode( ',', get_option( CacheExclusion::OPTION_CACHE_EXCLUSION ) ) ); + return array_merge( $default, $cache_exclusion_option ); } /** @@ -273,7 +287,7 @@ public function purgeAll() { /** * Purge a specific URL from the cache. * - * @param string $url + * @param string $url the url to purge. */ public function purgeUrl( $url ) { $path = $this->getStoragePathForRequest(); @@ -299,7 +313,7 @@ protected function getStoragePathForRequest() { if ( ! isset( $path ) ) { $url = new Url(); - $basePath = wp_parse_url( home_url('/'), PHP_URL_PATH ); + $basePath = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); $path = trailingslashit( self::CACHE_DIR . str_replace( $basePath, '', esc_url( $url->path ) ) ); } @@ -332,7 +346,5 @@ public static function onDeactivation() { // Remove all statically cached files removeDirectory( self::CACHE_DIR ); - } - } diff --git a/includes/Performance.php b/includes/Performance.php index 7efa05f..d395ae5 100644 --- a/includes/Performance.php +++ b/includes/Performance.php @@ -3,13 +3,18 @@ namespace NewfoldLabs\WP\Module\Performance; use NewfoldLabs\WP\ModuleLoader\Container; -use NewfoldLabs\WP\Module\Performance\Permissions; + use NewfoldLabs\WP\Module\Installer\Services\PluginInstaller; + +use NewfoldLabs\WP\Module\Performance\Permissions; use NewfoldLabs\WP\Module\Performance\Images\ImageManager; use NewfoldLabs\WP\Module\Performance\RestApi\RestApi; +use NewfoldLabs\WP\Module\Performance\Data\Constants; +use NewfoldLabs\WP\Module\Performance\CacheTypes\Browser; +use NewfoldLabs\WP\Module\Performance\CacheTypes\File; +use NewfoldLabs\WP\Module\Performance\CacheTypes\Skip404; use Automattic\Jetpack\Current_Plan; -use NewfoldLabs\WP\Module\Performance\Data\Constants; /** * Performance Class @@ -23,6 +28,7 @@ class Performance { */ const OPTION_CACHE_LEVEL = 'newfold_cache_level'; + /** * The option name where the "Skip WordPress 404 Handling for Static Files" option is stored. * @@ -82,6 +88,7 @@ public function __construct( Container $container ) { add_action( 'admin_menu', array( $this, 'add_sub_menu_page' ) ); new LinkPrefetch( $container ); + new CacheExclusion( $container ); $container->set( 'cachePurger', $cachePurger ); @@ -121,6 +128,7 @@ function () { * Add hooks. */ public function hooks() { + add_action( 'admin_init', array( $this, 'remove_epc_settings' ), 99 ); new OptionListener( self::OPTION_CACHE_LEVEL, array( $this, 'onCacheLevelChange' ) ); @@ -285,7 +293,6 @@ public function adminBarMenu( \WP_Admin_Bar $wp_admin_bar ) { ); } } - /** * Add performance menu in WP/Settings */ @@ -301,7 +308,7 @@ public function add_sub_menu_page() { ); } - /* + /** * Enqueue scripts and styles in admin */ public function enqueue_scripts() { @@ -310,7 +317,7 @@ public function enqueue_scripts() { wp_enqueue_style( 'wp-module-performance-styles' ); } - /* + /** * Add to Newfold SDK runtime. * * @param array $sdk SDK data. @@ -327,6 +334,7 @@ public function add_to_runtime( $sdk ) { 'jetpack_boost_minify_css' => get_option( 'jetpack_boost_status_minify-css', array() ), 'jetpack_boost_minify_css_excludes' => implode( ',', get_option( 'jetpack_boost_ds_minify_css_excludes', array( 'admin-bar', 'dashicons', 'elementor-app' ) ) ), 'install_token' => PluginInstaller::rest_get_plugin_install_hash(), + 'skip404' => (bool) get_option( 'newfold_skip_404_handling', false ), ); return array_merge( $sdk, array( 'performance' => $values ) ); diff --git a/includes/RestApi/CacheExclusionController.php b/includes/RestApi/CacheExclusionController.php new file mode 100644 index 0000000..ce056f4 --- /dev/null +++ b/includes/RestApi/CacheExclusionController.php @@ -0,0 +1,96 @@ +namespace, + $this->rest_base . '/settings', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_settings' ), + 'permission_callback' => array( Permissions::class, 'rest_is_authorized_admin' ), + ), + ) + ); + \register_rest_route( + $this->namespace, + $this->rest_base . '/update', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'update_settings' ), + 'permission_callback' => array( Permissions::class, 'rest_is_authorized_admin' ), + ), + ) + ); + } + + /** + * Get the settings + * + * @return \WP_REST_Response + */ + public function get_settings() { + return new \WP_REST_Response( + array( + 'cacheExclusion' => get_option( CacheExclusion::OPTION_CACHE_EXCLUSION, get_default_cache_exclusions() ), + ), + 200 + ); + } + + /** + * Update the settings + * + * @param \WP_REST_Request $request the request. + * @return \WP_REST_Response + */ + public function update_settings( \WP_REST_Request $request ) { + $cache_exclusion = $request->get_param( 'cacheExclusion' ); + if ( update_option( CacheExclusion::OPTION_CACHE_EXCLUSION, $cache_exclusion ) ) { + return new \WP_REST_Response( + array( + 'result' => true, + ), + 200 + ); + } + + return new \WP_REST_Response( + array( + 'result' => false, + ), + 400 + ); + } +} diff --git a/includes/RestApi/RestApi.php b/includes/RestApi/RestApi.php index 5351667..8eaeb70 100644 --- a/includes/RestApi/RestApi.php +++ b/includes/RestApi/RestApi.php @@ -1,5 +1,4 @@ namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'set_options' ), + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ), + ) + ); + } + + /** + * Set Jetpack options. + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response + */ + public function set_options( $request ) { + try { + $params = $request->get_params(); + + if ( ! isset( $params['field'] ) || ! is_array( $params['field'] ) ) { + return new \WP_REST_Response( + array( + 'success' => false, + 'error' => __( "The parameter 'field' is missing or invalid.", 'newfold-performance-module' ), + ), + 400 + ); + } + + $field = $params['field']; + + if ( ! isset( $field['id'], $field['value'] ) ) { + return new \WP_REST_Response( + array( + 'success' => false, + 'error' => __( "The fields 'id' and 'value' are required.", 'newfold-performance-module' ), + ), + 400 + ); + } + + switch ( $field['id'] ) { + case 'skip404': + $result = update_option( 'newfold_skip_404_handling', $field['value'] ); + break; + + default: + break; + } + + if ( false === $result ) { + return new \WP_REST_Response( + array( + 'success' => false, + 'error' => __( 'An error occurred while updating the option.', 'newfold-performance-module' ), + ), + 500 + ); + } + + // Success response. + return new \WP_REST_Response( + array( + 'success' => true, + 'updated_option' => $field['id'], + 'updated_value' => $field['value'], + ), + 200 + ); + } catch ( \Exception $e ) { + // Exceptions handling. + return new \WP_REST_Response( + array( + 'success' => false, + 'error' => __( 'An error occurred while updating the option.', 'newfold-performance-module' ) . $e->getMessage(), + ), + 500 + ); + } + } +} diff --git a/includes/burstSafetyModeFunctions.php b/includes/burstSafetyModeFunctions.php new file mode 100644 index 0000000..62ef02c --- /dev/null +++ b/includes/burstSafetyModeFunctions.php @@ -0,0 +1,39 @@ +addHeader( 'X-Newfold-Cache-Level', 3 ); + } +} elseif ( $newfold_burst_safety_mode ) { + $cache_level = get_option( 'newfold_burst_safety_mode_site_cache_level' ); + $browser = new Browser(); + $browser::maybeAddRules( $cache_level ); + if ( function_exists( 'getSkip404Option' ) && ! getSkip404Option() ) { + $skip404 = new Skip404(); + $skip404::maybeAddRules( false ); + } + $responseHeaderManager = new ResponseHeaderManager(); + $responseHeaderManager->addHeader( 'X-Newfold-Cache-Level', $cache_level ); + delete_option( 'newfold_burst_safety_mode' ); + delete_option( 'newfold_burst_safety_mode_site_cache_level' ); +} diff --git a/includes/functions.php b/includes/functions.php index 2a68c90..7b76a38 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -2,6 +2,15 @@ namespace NewfoldLabs\WP\Module\Performance; +/** + * Return defaul exclusions. + * + * @return array + */ +function get_default_cache_exclusions() { + return join( ',', array( 'cart', 'checkout', 'wp-admin', rest_get_url_prefix() ) ); +} + /** * Get the current cache level. * diff --git a/package-lock.json b/package-lock.json index 13360f7..57c29d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "@newfold-labs/wp-module-performance", "license": "GPL-2.0-or-later", "dependencies": { - "@newfold-labs/wp-module-runtime": "^1.0.0", + "@newfold-labs/wp-module-runtime": "^1.0.12", "@newfold/ui-component-library": "^1.1.0", "html-react-parser": "^5.1.18" }, diff --git a/package.json b/package.json index d1e7536..e900646 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "William Earnhardt (https://wearnhardt.com)" ], "dependencies": { - "@newfold-labs/wp-module-runtime": "^1.0.0", "@newfold/ui-component-library": "^1.1.0", + "@newfold-labs/wp-module-runtime": "^1.0.12", "html-react-parser": "^5.1.18" }, "devDependencies": { diff --git a/phpcs.xml b/phpcs.xml index df08a6b..b21082c 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,6 @@ - + diff --git a/tests/cypress/integration/performance.cy.js b/tests/cypress/integration/performance.cy.js index e12fc15..111f9f2 100644 --- a/tests/cypress/integration/performance.cy.js +++ b/tests/cypress/integration/performance.cy.js @@ -13,7 +13,7 @@ describe( 'Performance Page', function () { } ); it( 'Is Accessible', () => { - cy.wait( 1000 ); + cy.wait( 2000 ); cy.checkA11y( appClass + '-app-body' ); } );