diff --git a/.distignore b/.distignore index f3aedc0b..f4469f1b 100644 --- a/.distignore +++ b/.distignore @@ -2,18 +2,26 @@ /.git /.github /.wordpress-org +/bin /node_modules +/tests /vendor # Files /.distignore /.editorconfig +/.eslintrc.json /.gitattributes /.gitignore +/.phpunit.result.cache +/.sonarcloud.properties /.stylelintrc.json /composer.json /composer.lock /package.json /package-lock.json /phpcs.xml +/phpunit.xml +/phpunit.coverage.xml +/phpunit.report.xml /README.md diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..c7405275 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": "plugin:@wordpress/eslint-plugin/es5", + "env": { + "browser": true + }, + "rules": { + "camelcase": 0, + "vars-on-top": 0 + } +} diff --git a/.gitattributes b/.gitattributes index 325d49c3..588211e2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,7 @@ # Files /.distignore export-ignore /.editorconfig export-ignore +/.eslintrc.json export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.stylelintrc.json export-ignore diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7515f29e..f1fbc86c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,19 +1,60 @@ -name: Coding Standards -on: push +name: Tests +on: [push, pull_request] jobs: + unit: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - php: '5.6' + wordpress: '4.7' + - php: '7.4' + wordpress: '5.9' + - php: '8.0' + wordpress: '6.0' + - php: '8.2' + wordpress: '6.4' + - php: '8.3' + wordpress: 'latest' + - php: '8.3' + wordpress: 'nightly' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{matrix.php}} + tools: composer + - name: Install + run: composer install --no-interaction + - name: Build + run: composer build + - name: Setup DB + uses: shogo82148/actions-setup-mysql@v1 + with: + mysql-version: 'mysql-5.7' + root-password: "root" + - name: Setup WP + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 "${{ matrix.wordpress }}" + - name: PHP unit tests + run: composer test + quality: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.2' tools: composer - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '20' - name: Code style checks for PHP and CSS run: | composer install diff --git a/.github/workflows/wordpress-plugin-asset-update.yml b/.github/workflows/wordpress-plugin-asset-update.yml index 4e9828a5..a32fc4a8 100644 --- a/.github/workflows/wordpress-plugin-asset-update.yml +++ b/.github/workflows/wordpress-plugin-asset-update.yml @@ -8,11 +8,11 @@ jobs: name: Push to stable runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.2' tools: composer - name: Build run: composer install diff --git a/.github/workflows/wordpress-plugin-deploy.yml b/.github/workflows/wordpress-plugin-deploy.yml index 64136cc0..f74cfc47 100755 --- a/.github/workflows/wordpress-plugin-deploy.yml +++ b/.github/workflows/wordpress-plugin-deploy.yml @@ -9,11 +9,11 @@ jobs: name: New tag runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.2' tools: composer - name: Build run: composer install diff --git a/.gitignore b/.gitignore index f5833602..2f0c03fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .idea/ +.phpunit.result.cache css/*.min.css +js/*.min.js vendor/ node_modules/ composer.lock package-lock.json +phpunit.*.xml +tests/coverage/ diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 00000000..67986297 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,9 @@ +# Path to sources. +sonar.sources=inc,js,css,cachify.php +sonar.exclusions=**/*.min.css,**/*.min.js +#sonar.inclusions= + +# Path to tests. +sonar.tests=tests +#sonar.test.exclusions= +#sonar.test.inclusions= diff --git a/.stylelintrc.json b/.stylelintrc.json index d15de6ea..ed7fd2f8 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,7 +1,6 @@ { "extends": "@wordpress/stylelint-config", "rules": { - "declaration-property-unit-whitelist": null, "selector-id-pattern": null } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 85555444..e1cae2e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,42 @@ # Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 2.4.0 + +Requires PHP 5.6 and WordPress 4.7 or above + +* New: introduce Redis (also KeyDB and Valkey) support using the _phpredis_ module (#253), (#252, props @newtovaux) +* New: add `cachify_modify_output` filter +* New: add `cachify_create_gzip_files` to disable creation of static GZip files (#262, props @angcl) +* New: add hooks `cachify_removed_cache_by_url` and `cachify_flushed_total_cache` for additional actions after clearing (#294, props @ouun) +* Removed: APC support (#304) +* Enhance: adjust styling for setup instructions (#215, props @timse201) +* Enhance: update hooks for Multisite initialization in WordPress 5.1 and above (#246, props @ouun) +* Enhance: rework flush hooks and add some third-party triggers for Autoptimize and WooCommerce (#225, props @timse201) +* Enhance: clean up some internal error suppressions (#256) +* Enhance: inform user on cache clear in admin bar (#257, props @angcl) +* Enhance: do not flush the cache for post revisions (#261, props @angcl) +* Enhance: prevent unnecessary cache clearing in some cases (#223) (#224, props @timse201) +* Enhance: remove empty directories when clearing the HDD cache (#289) +* Enhance: introduce common interface for caching backends (#298, props @lloc) +* Enhance: enhance examples for .htaccess and nginx configuration (#302) +* Enhance: show admin notice instead of silent fallback to DB cache, if selected backend is unavailable (#305) +* Enhance: disable gzip creation of required PHP extension is missing (#308) +* Enhance: various internal code clean ups +* Fix: invalidate cache when permalink changes (#285, #286, props @raffaelj) +* Fix: remove empty directories when pruning the HDD cache (#289) +* Fix: correctly add user-agent to robots.txt (#282) (#283) +* Fix: exclude _sitemap.xml_ from caching (#242) (#254) +* Fix: prevent cache generation of non-GET requests (#200) (#258) +* Fix: prevent cache generation of requests with status different from 200 OK (#266) (#267, props @karlkowald) +* Fix: prevent cache generation of non-HTML responses when using content negotiation (#265) (#273, props @Ancocodet) +* Fix: fix styling for various dark mode plugins (#264) (#278) +* Fix: fix SVG markup for icons in dashboard widget (#269, props @Latz) +* Fix: added missing .gz suffix in htaccess (#287) (#291, props @raffaelj) +* Fix: fix some brand names and unify spelling (#297, props @pedro-mendonca) +* Maintenance: Tested up to WordPress 6.6 + + ## 2.3.2 * Fix: enforce WordPress environment for caching modules (#221, props timse201) * Fix: Remove unnecessary build artifacts from plugin deployment (#226) @@ -54,8 +90,8 @@ All notable changes to this project will be documented in this file. This projec * Fix for the PHP notice "Call to undefined function is_plugin_active_for_network" on WordPress Multisite ## 2.2.0 -* Toolbar: Display of the "Flush the cachify cache" button on the frontend -* Toolbar: Controlling the display of the "Flush the cachify cache" button via hook +* Toolbar: Display of the "Flush the Cachify cache" button on the frontend +* Toolbar: Controlling the display of the "Flush the Cachify cache" button via hook ## 2.1.9 * Vervollständigung des Cachify-Pfades in `robots.txt`: `Disallow: /wp-content/cache/cachify/` diff --git a/README.md b/README.md index d8e9d2e2..f8d4967d 100755 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Cachify # -Smart, efficient cache solution for WordPress. Use DB, HDD, APC or Memcached for storing your blog pages. Make WordPress faster! +Smart, efficient cache solution for WordPress. Use DB, HDD, Redis or Memcached for storing your blog pages. Make WordPress faster! ## Description ## -*Cachify* optimizes your page loads by caching posts, pages and custom post types as static content. You can choose between caching via database, on the web server’s hard drive (HDD), Memcached (only on Nginx) or — thanks to APC (Alternative PHP Cache) — directly in the web server’s system cache. Whenever a page or post is loaded, it can be pulled directly from the cache. The amount of database queries and PHP requests will dramatically decrease towards zero, depending on the caching method you chose. +*Cachify* optimizes your page loads by caching posts, pages and custom post types as static content. You can choose between caching via database, on the web server’s hard drive (HDD), Memcached (only on Nginx) or Redis. Whenever a page or post is loaded, it can be pulled directly from the cache. The amount of database queries and PHP requests will dramatically decrease towards zero, depending on the caching method you chose. ### Features ### * Works with custom post types. -* Caching methods: DB, HDD, APC and Memcached. +* Caching methods: DB, HDD, Redis and Memcached. * “Flush Cache” button in the WordPress toolbar. * Ready for WordPress Multisite. * Optional compression of HTML markup. @@ -39,10 +39,10 @@ Smart, efficient cache solution for WordPress. Use DB, HDD, APC or Memcached for * If you don’t know how to install a plugin for WordPress, [here’s how](https://wordpress.org/support/article/managing-plugins/#installing-plugins). ### Requirements ### -* PHP 5.2.4 or greater -* WordPress 4.4 or greater -* APC 3.1.4 or greater (optional) +* PHP 5.6 or greater +* WordPress 4.7 or greater * Memcached in Nginx (optional) +* Redis (optional, via the phpredis module) ## Frequently Asked Questions ## diff --git a/apc/proxy.php b/apc/proxy.php index 09e382fd..f9570398 100644 --- a/apc/proxy.php +++ b/apc/proxy.php @@ -1,63 +1,9 @@ $v ) { - if ( preg_match( '/^(wp-postpass|wordpress_logged_in|comment_author)_/', $k ) ) { - $_cachify_logged_in = true; - break; - } - } -} - -/** - * Determines if SSL is used. + * This file is just here to not break any remaining "auto_prepend_file" configurations. + * Will be removed with the next major update, so adjust your server's configuration, if you happen to read this notice. * - * @see is_ssl() (wp-includes/load.php). - * - * @return bool True if SSL, otherwise false. + * @package Cachify */ -function cachify_is_ssl() { - if ( isset( $_SERVER['HTTPS'] ) ) { - $https = filter_input( INPUT_SERVER, 'HTTPS', FILTER_SANITIZE_STRING ); - if ( 'on' === strtolower( $https ) || '1' === $https ) { - return true; - } - } elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' === $_SERVER['SERVER_PORT'] ) ) { - return true; - } - - return false; -} - -if ( - empty( $_cachify_logged_in ) - && extension_loaded( 'apc' ) - && ( strpos( filter_input( INPUT_SERVER, 'PHP_SELF', FILTER_SANITIZE_STRING ), '/wp-admin/' ) === false ) - && ( strpos( filter_input( INPUT_SERVER, 'HTTP_ACCEPT_ENCODING', FILTER_SANITIZE_STRING ), 'gzip' ) !== false ) -) { - $cache = apc_fetch( - md5( - ( cachify_is_ssl() ? 'https-' : '' ) . - filter_input( INPUT_SERVER, 'HTTP_HOST', FILTER_SANITIZE_STRING ) . - filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) - ) . - '.cachify' - ); - if ( $cache ) { - ini_set( 'zlib.output_compression', 'Off' ); - - header( 'Vary: Accept-Encoding' ); - header( 'X-Powered-By: Cachify' ); - header( 'Content-Encoding: gzip' ); - header( 'Content-Length: ' . strlen( $cache ) ); - header( 'Content-Type: text/html; charset=utf-8' ); - - echo $cache; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - exit; - } -} diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100644 index 00000000..c913ea5e --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-trunk + rm -rf $TMPDIR/wordpress-trunk/* + svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress + mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + rm -rf $WP_TESTS_DIR/{includes,data} + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + + # Modify the WP_UnitTestCase class to use the polyfilled version for PHPUnit cross-compatibility. + # This is a dirty "backport" of the polyfills used in WP 5.9 and might fail with future updates. + if [ ! -f "$WP_TESTS_DIR"/includes/abstract-testcase.php ]; then + local testcase_file="$WP_TESTS_DIR"/includes/testcase.php + sed $ioption 's/class WP_UnitTestCase extends PHPUnit_Framework_TestCase /class WP_UnitTestCase extends Yoast\\PHPUnitPolyfills\\TestCases\\TestCase /' "$testcase_file" + sed $ioption 's/setUpBeforeClass[(][)]/set_up_before_class()/g' "$testcase_file" + sed $ioption 's/tearDownAfterClass[(][)]/tear_down_after_class()/g' "$testcase_file" + sed $ioption 's/setUp[(][)]/set_up()/g' "$testcase_file" + sed $ioption 's/tearDown[(][)]/tear_down()/g' "$testcase_file" + fi + +} + +recreate_db() { + shopt -s nocasematch + if [[ $1 =~ ^(y|yes)$ ]] + then + mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA + create_db + echo "Recreated the database ($DB_NAME)." + else + echo "Leaving the existing database ($DB_NAME) in place." + fi + shopt -u nocasematch +} + +create_db() { + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] + then + echo "Reinstalling will delete the existing test database ($DB_NAME)" + read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB + recreate_db $DELETE_EXISTING_DB + else + create_db + fi +} + +install_wp +install_test_suite +install_db diff --git a/cachify.php b/cachify.php index 3cba0d43..545d0f7f 100644 --- a/cachify.php +++ b/cachify.php @@ -1,13 +1,13 @@ =5.2.0", - "composer/installers": "~1.0" + "php": ">=5.6", + "composer/installers": "^v1|^v2" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^v0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^v1.0", "matthiasmullie/minify": "^1.3", - "squizlabs/php_codesniffer": "^3.6", + "phpunit/phpunit": "^5|^7|^9", + "squizlabs/php_codesniffer": "^3.10", "phpcompatibility/phpcompatibility-wp": "^2.1", - "wp-coding-standards/wpcs": "^2.3" + "wp-coding-standards/wpcs": "^3.1", + "yoast/phpunit-polyfills": "^2.0" }, "scripts": { "post-install-cmd": [ @@ -41,7 +43,8 @@ ], "build": [ "minifycss css/dashboard.css > css/dashboard.min.css", - "minifycss css/settings.css > css/settings.min.css" + "minifycss css/admin-bar-flush.css > css/admin-bar-flush.min.css", + "minifyjs js/admin-bar-flush.js > js/admin-bar-flush.min.js" ], "cs": [ "@lint-php" @@ -51,13 +54,29 @@ ], "lint-all": [ "@lint-php", - "@lint-css" + "@lint-css", + "@lint-js" ], "lint-css": [ - "npx stylelint css/dashboard.css css/settings.css" + "npx stylelint css/dashboard.css css/admin-bar-flush.css" + ], + "lint-js": [ + "npx eslint js/admin-bar-flush.js" ], "lint-php": [ "phpcs --standard=phpcs.xml -s" + ], + "test": [ + "phpunit" + ], + "coverage": [ + "php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-html tests/coverage" ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "composer/installers": true + } } } diff --git a/css/admin-bar-flush.css b/css/admin-bar-flush.css new file mode 100644 index 00000000..d1b0f024 --- /dev/null +++ b/css/admin-bar-flush.css @@ -0,0 +1,67 @@ +#wp-admin-bar-cachify .animate-pulse { + animation: cachify-dash-icon-pulse 2s infinite; + animation-timing-function: linear; +} + +#wp-admin-bar-cachify .animate-fade { + animation: cachify-dash-icon-fade-in 0.1s; + animation-timing-function: linear; +} + +@keyframes cachify-dash-icon-pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.2; + } +} + +@keyframes cachify-dash-icon-fade-in { + + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +#wp-admin-bar-cachify button.ab-item { + background: none; + border: none; + padding: 0 8px 0 7px; +} + +#wp-admin-bar-cachify button.ab-item:hover { + cursor: pointer; +} + +#wp-admin-bar-cachify button.ab-item:focus { + outline: none; +} + +#wpadminbar #wp-admin-bar-cachify button.ab-item:focus span.ab-label { + color: inherit; +} + +#wp-admin-bar-cachify { + display: list-item !important; +} + +#wp-admin-bar-cachify .ab-icon { + margin: 0 !important; +} + +#wp-admin-bar-cachify .ab-icon::before { + top: 2px; + margin: 0; +} + +#wp-admin-bar-cachify .ab-label { + margin: 0 5px; +} diff --git a/css/dashboard.css b/css/dashboard.css index 6b790189..aad8035a 100644 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -5,6 +5,10 @@ fill: #606a73; } +html.dark-mode body:not(.block-editor-page) #dashboard_right_now .cachify-icon { + fill: #bbc8d4; +} + #dashboard_right_now li a.cachify-glance::before { content: ""; padding: 0; diff --git a/css/settings.css b/css/settings.css deleted file mode 100644 index a7009d3c..00000000 --- a/css/settings.css +++ /dev/null @@ -1,28 +0,0 @@ -.wp-core-ui .button-flush { - background: #dd823b; - border-color: #c36922 #ad5d1e #ad5d1e; - color: #fff; - box-shadow: 0 1px 0 #ad5d1e; - text-shadow: 0 -1px 1px #ad5d1e, 1px 0 1px #ad5d1e, 0 1px 1px #ad5d1e, -1px 0 1px #ad5d1e; -} - -.wp-core-ui .button-flush:focus, -.wp-core-ui .button-flush:hover { - background: #df8a48; - border-color: #ad5d1e; - color: #fff; - box-shadow: 0 1px 0 #ad5d1e; -} - -.wp-core-ui .button-flush:focus { - box-shadow: inset 0 1px 0 #c36922, 0 0 2px 1px #33b3db; -} - -.wp-core-ui .button-flush.active, -.wp-core-ui .button-flush.active:focus, -.wp-core-ui .button-flush.active:hover, -.wp-core-ui .button-flush:active { - background: #c36922; - border-color: #ad5d1e; - box-shadow: inset 0 2px 0 #ad5d1e; -} diff --git a/images/symbols.svg b/images/symbols.svg index 3b55d1be..1f345223 100644 --- a/images/symbols.svg +++ b/images/symbols.svg @@ -4,16 +4,20 @@ - - - - + + + + + + + + - \ No newline at end of file + diff --git a/inc/cachify.settings.php b/inc/cachify.settings.php index 06894a64..59de26f9 100644 --- a/inc/cachify.settings.php +++ b/inc/cachify.settings.php @@ -30,13 +30,10 @@ + + - - -

- - - +

@@ -44,7 +41,7 @@ printf( /* translators: Placeholder is the trash icon itself as dashicon */ esc_html__( 'Flush the cache by clicking the button below or the %1$s icon in the admin bar.', 'cachify' ), - '"' . esc_html__( 'Flush the cachify cache', 'cachify' ) . '"' + '"' . esc_html__( 'Flush the Cachify cache', 'cachify' ) . '"' ); ?>

@@ -54,7 +51,7 @@ ?>

- +

@@ -140,6 +137,16 @@ + + + + + + + /> + + + diff --git a/inc/class-cachify-apc.php b/inc/class-cachify-apc.php deleted file mode 100644 index 39761f4f..00000000 --- a/inc/class-cachify-apc.php +++ /dev/null @@ -1,156 +0,0 @@ -", - 'Cachify | https://cachify.pluginkollektiv.org', - ( $detail ? 'APC Cache' : __( 'Generated', 'cachify' ) ), - date_i18n( - 'd.m.Y H:i:s', - current_time( 'timestamp' ) - ) - ); - } -} diff --git a/inc/class-cachify-backend.php b/inc/class-cachify-backend.php new file mode 100644 index 00000000..b20b765c --- /dev/null +++ b/inc/class-cachify-backend.php @@ -0,0 +1,69 @@ + 'Flush site cache', 'synopsis' => array( array( - 'type' => 'flag', - 'name' => 'all-methods', - 'description' => 'Flush all caching methods', - 'optional' => true, + 'type' => 'flag', + 'name' => 'all-methods', + 'description' => 'Flush all caching methods', + 'optional' => true, ), ), ) @@ -97,14 +94,13 @@ public static function add_commands() { 'shortdesc' => 'Get the size of the cache in bytes', 'synopsis' => array( array( - 'type' => 'flag', - 'name' => 'raw', - 'description' => 'Raw size output in bytes', - 'optional' => true, + 'type' => 'flag', + 'name' => 'raw', + 'description' => 'Raw size output in bytes', + 'optional' => true, ), ), ) ); - } } diff --git a/inc/class-cachify-db.php b/inc/class-cachify-db.php index 86011462..627de8c0 100644 --- a/inc/class-cachify-db.php +++ b/inc/class-cachify-db.php @@ -11,15 +11,14 @@ /** * Cachify_DB */ -final class Cachify_DB { +final class Cachify_DB implements Cachify_Backend { /** * Availability check * - * @since 2.0.7 - * @change 2.0.7 + * @return bool TRUE when installed * - * @return boolean true/false TRUE when installed + * @since 2.0.7 */ public static function is_available() { return true; @@ -28,10 +27,9 @@ public static function is_available() { /** * Caching method as string * - * @since 2.1.2 - * @change 2.1.2 + * @return string Caching method * - * @return string Caching method + * @since 2.1.2 */ public static function stringify_method() { return 'DB'; @@ -40,17 +38,18 @@ public static function stringify_method() { /** * Store item in cache * - * @since 2.0 - * @change 2.0 + * @param string $hash Hash of the entry. + * @param string $data Content of the entry. + * @param int $lifetime Lifetime of the entry. + * @param bool $sig_detail Show details in signature. * - * @param string $hash Hash of the entry. - * @param string $data Content of the entry. - * @param integer $lifetime Lifetime of the entry. + * @since 2.0 */ - public static function store_item( $hash, $data, $lifetime ) { + public static function store_item( $hash, $data, $lifetime, $sig_detail ) { /* Do not store empty data. */ if ( empty( $data ) ) { trigger_error( __METHOD__ . ': Empty input.', E_USER_WARNING ); + return; } @@ -73,11 +72,11 @@ public static function store_item( $hash, $data, $lifetime ) { /** * Read item from cache * - * @since 2.0 - * @change 2.0 + * @param string $hash Hash of the entry. + * + * @return mixed Content of the entry * - * @param string $hash Hash of the entry. - * @return mixed Content of the entry + * @since 2.0 */ public static function get_item( $hash ) { return get_transient( $hash ); @@ -86,11 +85,10 @@ public static function get_item( $hash ) { /** * Delete item from cache * - * @since 2.0 - * @change 2.0 + * @param string $hash Hash of the entry. + * @param string $url URL of the entry [optional]. * - * @param string $hash Hash of the entry. - * @param string $url URL of the entry [optional]. + * @since 2.0 */ public static function delete_item( $hash, $url = '' ) { delete_transient( $hash ); @@ -99,8 +97,7 @@ public static function delete_item( $hash, $url = '' ) { /** * Clear the cache * - * @since 2.0 - * @change 2.0 + * @since 2.0 */ public static function clear_cache() { /* Init */ @@ -113,11 +110,11 @@ public static function clear_cache() { /** * Print the cache * - * @since 2.0 - * @change 2.3.0 + * @param bool $sig_detail Show details in signature. + * @param array $cache Array of cache values. * - * @param bool $sig_detail Show details in signature. - * @param array $cache Array of cache values. + * @since 2.0 + * @since 2.3.0 added $sig_detail parameter */ public static function print_cache( $sig_detail, $cache ) { /* No array? */ @@ -141,35 +138,37 @@ public static function print_cache( $sig_detail, $cache ) { /** * Get the cache size * - * @since 2.0 - * @change 2.0 + * @return int Column size * - * @return integer Column size + * @since 2.0 */ public static function get_stats() { /* Init */ global $wpdb; /* Read */ - return $wpdb->get_var( - 'SELECT SUM( CHAR_LENGTH(option_value) ) FROM `' . $wpdb->options . "` WHERE `option_name` LIKE ('\_transient%.cachify')" + return intval( + $wpdb->get_var( + 'SELECT SUM( CHAR_LENGTH(option_value) ) FROM `' . $wpdb->options . "` WHERE `option_name` LIKE ('\_transient%.cachify')" + ) ); } /** * Generate signature * - * @since 2.0 - * @change 2.3.0 + * @param bool $detail Show details in signature. + * @param array $meta Content of metadata. * - * @param bool $detail Show details in signature. - * @param array $meta Content of metadata. - * @return string Signature string + * @return string Signature string + * + * @since 2.0 + * @since 2.3.0 added $detail parameter */ private static function _cache_signature( $detail, $meta ) { /* No array? */ if ( ! is_array( $meta ) ) { - return; + return ''; } if ( $detail ) { @@ -210,10 +209,9 @@ private static function _cache_signature( $detail, $meta ) { /** * Return query count * - * @since 0.1 - * @change 2.0 + * @return int Number of queries * - * @return integer Number of queries + * @since 0.1 */ private static function _page_queries() { return $GLOBALS['wpdb']->num_queries; @@ -222,10 +220,9 @@ private static function _page_queries() { /** * Return execution time * - * @since 0.1 - * @change 2.0 + * @return string Execution time in seconds * - * @return integer Execution time in seconds + * @since 0.1 */ private static function _page_timer() { return timer_stop( 0, 2 ); @@ -234,10 +231,9 @@ private static function _page_timer() { /** * Return memory consumption * - * @since 0.7 - * @change 2.0 + * @return string Formatted memory size * - * @return string Formatted memory size + * @since 0.7 */ private static function _page_memory() { return ( function_exists( 'memory_get_usage' ) ? size_format( memory_get_usage(), 2 ) : 0 ); diff --git a/inc/class-cachify-hdd.php b/inc/class-cachify-hdd.php index 49f4713f..a82c90e2 100644 --- a/inc/class-cachify-hdd.php +++ b/inc/class-cachify-hdd.php @@ -11,28 +11,47 @@ /** * Cachify_HDD */ -final class Cachify_HDD { +final class Cachify_HDD implements Cachify_Backend { /** * Availability check * - * @since 2.0.7 - * @change 2.0.7 + * @return bool TRUE when installed * - * @return boolean true/false TRUE when installed + * @since 2.0.7 */ public static function is_available() { $option = get_option( 'permalink_structure' ); return ! empty( $option ); } + /** + * Returns if gzip file creation is enabled + * + * @return bool + * + * @since 2.4.0 + */ + public static function is_gzip_enabled() { + if ( ! function_exists( 'gzencode' ) ) { + // GZip is not available on the system. + return false; + } + + /** + * Filter that allows to enable/disable gzip file creation + * + * @param bool $create_gzip_files Whether to create gzip files. Default is `true` + */ + return apply_filters( 'cachify_create_gzip_files', true ); + } + /** * Caching method as string * - * @since 2.1.2 - * @change 2.1.2 + * @return string Caching method * - * @return string Caching method + * @since 2.1.2 */ public static function stringify_method() { return 'HDD'; @@ -41,13 +60,13 @@ public static function stringify_method() { /** * Store item in cache * - * @since 2.0 - * @change 2.3.0 + * @param string $hash Hash of the entry [ignored]. + * @param string $data Content of the entry. + * @param int $lifetime Lifetime of the entry [ignored]. + * @param bool $sig_detail Show details in signature. * - * @param string $hash Hash of the entry [ignored]. - * @param string $data Content of the entry. - * @param integer $lifetime Lifetime of the entry [ignored]. - * @param bool $sig_detail Show details in signature. + * @since 2.0 + * @since 2.3.0 added $sig_details parameter */ public static function store_item( $hash, $data, $lifetime, $sig_detail ) { /* Do not store empty data. */ @@ -65,12 +84,12 @@ public static function store_item( $hash, $data, $lifetime, $sig_detail ) { /** * Read item from cache * - * @since 2.0 - * @change 2.0 + * @param string $hash Hash of the entry. + * @return bool True if cache is present. * - * @return boolean True if cache is present. + * @since 2.0 */ - public static function get_item() { + public static function get_item( $hash ) { return is_readable( self::_file_html() ); @@ -79,11 +98,10 @@ public static function get_item() { /** * Delete item from cache * - * @since 2.0 - * @change 2.0 + * @param string $hash Hash of the entry [ignored]. + * @param string $url URL of the entry. * - * @param string $hash Hash of the entry [ignored]. - * @param string $url URL of the entry. + * @since 2.0 */ public static function delete_item( $hash, $url ) { self::_clear_dir( @@ -94,8 +112,7 @@ public static function delete_item( $hash, $url ) { /** * Clear the cache * - * @since 2.0 - * @change 2.0 + * @since 2.0 */ public static function clear_cache() { self::_clear_dir( @@ -107,12 +124,15 @@ public static function clear_cache() { /** * Print the cache * - * @since 2.0 - * @change 2.3 + * @param bool $sig_detail Show details in signature. + * @param array $cache Array of cache values. + * + * @since 2.0 */ - public static function print_cache() { + public static function print_cache( $sig_detail, $cache ) { $filename = self::_file_html(); - $size = is_readable( $filename ) ? readfile( $filename ) : false; + $size = is_readable( $filename ) ? readfile( $filename ) : false; + if ( ! empty( $size ) ) { /* Ok, cache file has been sent to output. */ exit; @@ -122,10 +142,9 @@ public static function print_cache() { /** * Get the cache size * - * @since 2.0 - * @change 2.0 + * @return int Directory size * - * @return integer Directory size + * @since 2.0 */ public static function get_stats() { return self::_dir_size( CACHIFY_CACHE_DIR ); @@ -134,11 +153,12 @@ public static function get_stats() { /** * Generate signature * - * @since 2.0 - * @change 2.3.0 + * @param bool $detail Show details in signature. + * + * @return string Signature string * - * @param bool $detail Show details in signature. - * @return string Signature string + * @since 2.0 + * @since 2.3.0 added $detail parameter */ private static function _cache_signature( $detail ) { return sprintf( @@ -155,49 +175,54 @@ private static function _cache_signature( $detail ) { /** * Initialize caching process * - * @since 2.0 - * @change 2.0 + * @param string $data Cache content. * - * @param string $data Cache content. + * @since 2.0 */ private static function _create_files( $data ) { $file_path = self::_file_path(); /* Create directory */ if ( ! wp_mkdir_p( $file_path ) ) { - trigger_error( esc_html( __METHOD__ . ": Unable to create directory {$file_path}.", E_USER_WARNING ) ); + trigger_error( esc_html( __METHOD__ . ": Unable to create directory {$file_path}." ), E_USER_WARNING ); return; } - /* Write to file */ self::_create_file( self::_file_html( $file_path ), $data ); - self::_create_file( self::_file_gzip( $file_path ), gzencode( $data, 9 ) ); + + /** + * Filter that allows to enable/disable gzip file creation + * + * @param bool $create_gzip_files Whether to create gzip files. Default is `true` + */ + if ( self::is_gzip_enabled() ) { + self::_create_file( self::_file_gzip( $file_path ), gzencode( $data, 9 ) ); + } } /** * Create cache file * - * @since 2.0 - * @change 2.0 + * @param string $file Path to cache file. + * @param string $data Cache content. * - * @param string $file Path to cache file. - * @param string $data Cache content. + * @since 2.0 */ private static function _create_file( $file, $data ) { /* Writable? */ $handle = @fopen( $file, 'wb' ); if ( ! $handle ) { - trigger_error( esc_html( __METHOD__ . ": Could not write file {$file}.", E_USER_WARNING ) ); + trigger_error( esc_html( __METHOD__ . ": Could not write file {$file}." ), E_USER_WARNING ); return; } /* Write */ - @fwrite( $handle, $data ); + fwrite( $handle, $data ); fclose( $handle ); clearstatcache(); /* Permissions */ - $stat = @stat( dirname( $file ) ); + $stat = @stat( dirname( $file ) ); $perms = $stat['mode'] & 0007777; $perms = $perms & 0000666; @chmod( $file, $perms ); @@ -207,71 +232,67 @@ private static function _create_file( $file, $data ) { /** * Clear directory * - * @since 2.0 - * @change 2.0.5 + * @param string $dir Directory path. + * @param bool $recursive true for clearing subdirectories as well. * - * @param string $dir Directory path. - * @param boolean $recursive true for clearing subdirectories as well. + * @since 2.0 */ private static function _clear_dir( $dir, $recursive = false ) { - /* Remote training slash */ + // Remove trailing slash. $dir = untrailingslashit( $dir ); - /* Is directory? */ + // Is directory? if ( ! is_dir( $dir ) ) { return; } - /* Read */ + // List directory contents. $objects = array_diff( scandir( $dir ), array( '..', '.' ) ); - /* Empty? */ - if ( empty( $objects ) ) { - return; - } - - /* Loop over items */ + // Loop over items. foreach ( $objects as $object ) { - /* Expand path */ + // Expand path. $object = $dir . DIRECTORY_SEPARATOR . $object; - /* Directory or file */ - if ( is_dir( $object ) && $recursive ) { - self::_clear_dir( $object, $recursive ); - } else { - if ( self::_user_can_delete( $object ) ) { - unlink( $object ); + if ( is_dir( $object ) ) { + if ( $recursive ) { + // Recursively clear the directory. + self::_clear_dir( $object, $recursive ); + } elseif ( self::_user_can_delete( $object ) && 0 === count( glob( trailingslashit( $object ) . '*' ) ) ) { + // Delete the directory, if empty. + @rmdir( $object ); } + } elseif ( self::_user_can_delete( $object ) ) { + // Delete the file. + unlink( $object ); } } - /* Remove directory */ - if ( $recursive ) { - if ( self::_user_can_delete( $dir ) && 0 === count( glob( trailingslashit( $dir ) . '*' ) ) ) { - @rmdir( $dir ); - } + // Remove directory, if empty. + if ( self::_user_can_delete( $dir ) && 0 === count( glob( trailingslashit( $dir ) . '*' ) ) ) { + @rmdir( $dir ); } - /* Clean up */ + // Clean up. clearstatcache(); } /** * Get directory size * - * @since 2.0 - * @change 2.0 + * @param string $dir Directory path. * - * @param string $dir Directory path. - * @return mixed Directory size + * @return int|false Directory size + * + * @since 2.0 */ public static function _dir_size( $dir = '.' ) { /* Is directory? */ if ( ! is_dir( $dir ) ) { - return; + return false; } /* Read */ @@ -282,7 +303,7 @@ public static function _dir_size( $dir = '.' ) { /* Empty? */ if ( empty( $objects ) ) { - return; + return false; } /* Init */ @@ -307,11 +328,11 @@ public static function _dir_size( $dir = '.' ) { /** * Path to cache file * - * @since 2.0 - * @change 2.0 + * @param string $path Request URI or permalink [optional]. * - * @param string $path Request URI or permalink [optional]. - * @return string Path to cache file + * @return string Path to cache file + * + * @since 2.0 */ private static function _file_path( $path = null ) { $prefix = is_ssl() ? 'https-' : ''; @@ -339,11 +360,11 @@ private static function _file_path( $path = null ) { /** * Path to HTML file * - * @since 2.0 - * @change 2.3.0 + * @param string $file_path File path [optional]. + * + * @return string Path to HTML file * - * @param string $file_path File path [optional]. - * @return string Path to HTML file + * @since 2.0 */ private static function _file_html( $file_path = '' ) { return ( empty( $file_path ) ? self::_file_path() : $file_path ) . 'index.html'; @@ -352,11 +373,11 @@ private static function _file_html( $file_path = '' ) { /** * Path to GZIP file * - * @since 2.0 - * @change 2.3.0 + * @param string $file_path File path [optional]. * - * @param string $file_path File path [optional]. - * @return string Path to GZIP file + * @return string Path to GZIP file + * + * @since 2.0 */ private static function _file_gzip( $file_path = '' ) { return ( empty( $file_path ) ? self::_file_path() : $file_path ) . 'index.html.gz'; @@ -388,9 +409,9 @@ private static function _user_can_delete( $file ) { $file = trailingslashit( $file ); } - $ssl_prefix = is_ssl() ? 'https-' : ''; + $ssl_prefix = is_ssl() ? 'https-' : ''; $current_blog = get_blog_details( get_current_blog_id() ); - $blog_path = CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . $ssl_prefix . $current_blog->domain . $current_blog->path; + $blog_path = CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . $ssl_prefix . $current_blog->domain . $current_blog->path; if ( 0 !== strpos( $file, $blog_path ) ) { return false; @@ -424,6 +445,5 @@ private static function _user_can_delete( $file ) { } return true; - } } diff --git a/inc/class-cachify-memcached.php b/inc/class-cachify-memcached.php index 3bb495d7..32db7d61 100644 --- a/inc/class-cachify-memcached.php +++ b/inc/class-cachify-memcached.php @@ -11,37 +11,36 @@ /** * Cachify_MEMCACHED */ -final class Cachify_MEMCACHED { +final class Cachify_MEMCACHED implements Cachify_Backend { /** * Memcached-Object * - * @since 2.0.7 - * @var object + * @var object + * + * @since 2.0.7 */ private static $_memcached; /** * Availability check * - * @since 2.0.7 - * @change 2.0.7 + * @return bool TRUE when installed * - * @return boolean true/false TRUE when installed + * @since 2.0.7 */ public static function is_available() { return class_exists( 'Memcached' ) - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - && isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( strtolower( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ), 'nginx' ) !== false; + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + && isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( strtolower( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ), 'nginx' ) !== false; } /** * Caching method as string * - * @since 2.1.2 - * @change 2.1.2 + * @return string Caching method * - * @return string Caching method + * @since 2.1.2 */ public static function stringify_method() { return 'Memcached'; @@ -50,13 +49,13 @@ public static function stringify_method() { /** * Store item in cache * - * @param string $hash Hash of the entry [ignored]. - * @param string $data Content of the entry. - * @param integer $lifetime Lifetime of the entry. - * @param bool $sig_detail Show details in signature. + * @param string $hash Hash of the entry [ignored]. + * @param string $data Content of the entry. + * @param int $lifetime Lifetime of the entry. + * @param bool $sig_detail Show details in signature. * - * @since 2.0.7 - * @change 2.3.0 + * @since 2.0.7 + * @since 2.3.0 added $sig_detail parameter */ public static function store_item( $hash, $data, $lifetime, $sig_detail ) { /* Do not store empty data. */ @@ -81,16 +80,16 @@ public static function store_item( $hash, $data, $lifetime, $sig_detail ) { /** * Read item from cache * - * @since 2.0.7 - * @change 2.0.7 + * @param string $hash Hash of the entry. + * + * @return mixed Content of the entry * - * @param string $hash Hash of the entry. - * @return mixed Content of the entry + * @since 2.0.7 */ public static function get_item( $hash ) { /* Server connect */ if ( ! self::_connect_server() ) { - return; + return null; } /* Get item */ @@ -102,11 +101,10 @@ public static function get_item( $hash ) { /** * Delete item from cache * - * @since 2.0.7 - * @change 2.0.7 + * @param string $hash Hash of the entry. + * @param string $url URL of the entry [optional]. * - * @param string $hash Hash of the entry. - * @param string $url URL of the entry [optional]. + * @since 2.0.7 */ public static function delete_item( $hash, $url = '' ) { /* Server connect */ @@ -123,8 +121,7 @@ public static function delete_item( $hash, $url = '' ) { /** * Clear the cache * - * @since 2.0.7 - * @change 2.0.7 + * @since 2.0.7 */ public static function clear_cache() { /* Server connect */ @@ -132,27 +129,32 @@ public static function clear_cache() { return; } + if ( ! self::$_memcached instanceof Memcached ) { + return; + } + /* Flush */ - @self::$_memcached->flush(); + self::$_memcached->flush(); } /** * Print the cache * - * @since 2.0.7 - * @change 2.0.7 + * @param bool $sig_detail Show details in signature. + * @param array $cache Array of cache values. + * + * @since 2.0.7 */ - public static function print_cache() { - return; + public static function print_cache( $sig_detail, $cache ) { + // Not supported. } /** * Get the cache size * - * @since 2.0.7 - * @change 2.0.7 + * @return mixed Cache size * - * @return mixed Cache size + * @since 2.0.7 */ public static function get_stats() { /* Server connect */ @@ -182,11 +184,12 @@ public static function get_stats() { /** * Generate signature * - * @since 2.0.7 - * @change 2.3.0 + * @param bool $detail Show details in signature. * - * @param bool $detail Show details in signature. - * @return string Signature string + * @return string Signature string + * + * @since 2.0.7 + * @since 2.3.0 added $detail parameter */ private static function _cache_signature( $detail ) { return sprintf( @@ -203,11 +206,11 @@ private static function _cache_signature( $detail ) { /** * Path of cache file * - * @since 2.0.7 - * @change 2.0.7 + * @param string $path Request URI or permalink [optional]. + * + * @return string Path to cache file * - * @param string $path Request URI or permalink [optional]. - * @return string Path to cache file + * @since 2.0.7 */ private static function _file_path( $path = null ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated @@ -226,12 +229,11 @@ private static function _file_path( $path = null ) { /** * Connect to Memcached server * - * @since 2.0.7 - * @change 2.1.8 + * @hook array cachify_memcached_servers Array with memcached servers * - * @hook array cachify_memcached_servers Array with memcached servers + * @return bool TRUE on success * - * @return boolean true/false TRUE on success + * @since 2.0.7 */ private static function _connect_server() { /* Not enabled? */ @@ -255,8 +257,8 @@ private static function _connect_server() { } else { self::$_memcached->setOptions( array( - Memcached::OPT_COMPRESSION => false, - Memcached::OPT_BUFFER_WRITES => true, + Memcached::OPT_COMPRESSION => false, + Memcached::OPT_BUFFER_WRITES => true, Memcached::OPT_BINARY_PROTOCOL => true, ) ); diff --git a/inc/class-cachify-noop.php b/inc/class-cachify-noop.php new file mode 100644 index 00000000..62634f98 --- /dev/null +++ b/inc/class-cachify-noop.php @@ -0,0 +1,112 @@ +unavailable_method = $unavailable_method; + } + + /** + * Availability check + * + * @return bool TRUE when installed + */ + public static function is_available() { + return true; + } + + /** + * Caching method as string + * + * @return string Caching method + */ + public static function stringify_method() { + return 'NOOP'; + } + + /** + * Store item in cache + * + * @param string $hash Hash of the entry. + * @param string $data Content of the entry. + * @param int $lifetime Lifetime of the entry. + * @param bool $sig_detail Show details in signature. + */ + public static function store_item( $hash, $data, $lifetime, $sig_detail ) { + // NOOP. + } + + /** + * Read item from cache + * + * @param string $hash Hash of the entry. + * + * @return false No content + */ + public static function get_item( $hash ) { + return false; + } + + /** + * Delete item from cache + * + * @param string $hash Hash of the entry. + * @param string $url URL of the entry [optional]. + */ + public static function delete_item( $hash, $url = '' ) { + // NOOP. + } + + /** + * Clear the cache + */ + public static function clear_cache() { + // NOOP. + } + + /** + * Print the cache + * + * @param bool $sig_detail Show details in signature. + * @param array $cache Array of cache values. + */ + public static function print_cache( $sig_detail, $cache ) { + // NOOP. + } + + /** + * Get the cache size + * + * @return int Column size + */ + public static function get_stats() { + return 0; + } +} diff --git a/inc/class-cachify-redis.php b/inc/class-cachify-redis.php new file mode 100644 index 00000000..28b1e00a --- /dev/null +++ b/inc/class-cachify-redis.php @@ -0,0 +1,286 @@ +set( + self::_file_path(), + $data . self::_cache_signature( $sig_detail ), + $lifetime + ); + } + + /** + * Read item from cache + * + * @param string $hash Hash of the entry. + * @return mixed Content of the entry + */ + public static function get_item( $hash ) { + /* Server connect */ + if ( ! self::_connect_server() ) { + return null; + } + + /* Get item */ + return self::$_redis->get( + self::_file_path() + ); + } + + /** + * Delete item from cache + * + * @param string $hash Hash of the entry [ignored]. + * @param string $url URL of the entry. + */ + public static function delete_item( $hash, $url ) { + /* Server connect */ + if ( ! self::_connect_server() ) { + return; + } + + /* Delete */ + self::$_redis->del( + self::_file_path( $url ) + ); + } + + /** + * Clear the cache + * + * @return void + */ + public static function clear_cache() { + /* Server connect */ + if ( ! self::_connect_server() ) { + return; + } + + /* Flush */ + @self::$_redis->flushAll(); + } + + /** + * Print the cache + * + * @param bool $sig_detail Show details in signature. + * @param string $cache Cached content. + */ + public static function print_cache( $sig_detail, $cache ) { + echo $cache; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Get the cache size + * + * @return integer Directory size + */ + public static function get_stats() { + /* Server connect */ + if ( ! self::_connect_server() ) { + return null; + } + + /* Info */ + $data = self::$_redis->info( 'MEMORY' ); + + /* No stats? */ + if ( empty( $data ) ) { + return null; + } + + /* Empty */ + if ( empty( $data['used_memory_dataset'] ) ) { + return null; + } + + return $data['used_memory_dataset']; + } + + /** + * Generate signature + * + * @param bool $detail Show details in signature. + * @return string Signature string + */ + private static function _cache_signature( $detail ) { + return sprintf( + "\n\n", + 'Cachify | https://cachify.pluginkollektiv.org', + ( $detail ? 'Redis Cache' : __( 'Generated', 'cachify' ) ), + date_i18n( + 'd.m.Y H:i:s', + current_time( 'timestamp' ) + ) + ); + } + + /** + * Path of cache file + * + * @param string $path Request URI or permalink [optional]. + * @return string Path to cache file + */ + private static function _file_path( $path = null ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + $path_parts = wp_parse_url( $path ? $path : wp_unslash( $_SERVER['REQUEST_URI'] ) ); + + return trailingslashit( + sprintf( + '%s%s', + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + wp_unslash( $_SERVER['HTTP_HOST'] ), + $path_parts['path'] + ) + ); + } + + /** + * Connect to Redis server + * + * @return boolean true/false TRUE on success + */ + private static function _connect_server() { + /* Not enabled? */ + if ( ! self::is_available() ) { + return false; + } + + /* Have object and it thinks it's connected to a server */ + if ( is_object( self::$_redis ) && self::$_redis->isConnected() ) { + return true; + } + + /* Init */ + self::$_redis = new Redis(); + + /** + * Filter hook to adjust Redis connection parameters + * + * @param array $redis_server Redis connection parameters. + * + * @see Redis::connect() For supported parameters. + * + * @since 2.4.0 + */ + $con = apply_filters( 'cachify_redis_servers', array( 'localhost' ) ); + $con = self::sanitize_con_parameters( $con ); + + if ( false === $con ) { + return false; + } + + // Establish connection. + try { + self::$_redis->connect( ...$con ); + + if ( ! self::$_redis->isConnected() ) { + return false; + } + } catch ( Exception $e ) { + return false; + } + + return true; + } + + /** + * Sanitize Redis connection parameters. + * + * @param mixed $con Connection parameters (from hook). + * + * @return array|false Array of connection arguments or FALSE, if invalid. + */ + private static function sanitize_con_parameters( $con ) { + if ( is_string( $con ) ) { + return array( $con ); + } elseif ( is_array( $con ) && ! empty( $con ) ) { + $con[0] = strval( $con[0] ); // Host or socket path. + if ( count( $con ) > 1 ) { + $con[1] = intval( $con[1] ); // Port number. + } + if ( count( $con ) > 2 ) { + $con[2] = floatval( $con[2] ); // Socket timeout in seconds. + } + if ( count( $con ) > 3 && ! is_null( $con[3] ) ) { + $con[3] = strval( $con[3] ); // Persistent connection ID. + } + if ( count( $con ) > 4 ) { + $con[4] = intval( $con[4] ); // Retry interval in milliseconds. + } + if ( count( $con ) > 5 ) { + $con[5] = floatval( $con[5] ); // Read timeout in seconds. + } + if ( count( $con ) > 6 && ! is_null( $con[6] ) && is_array( $con[6] ) ) { + return false; // Context parameters, e.g. authentication (since PhpRedis 5.3). + } + if ( count( $con ) > 7 ) { + $con = array_slice( $con, 0, 7 ); // Trim excessive parameters. + } + + return $con; + } else { + return false; + } + } +} diff --git a/inc/class-cachify.php b/inc/class-cachify.php index 2c9fbd2f..debe51cd 100644 --- a/inc/class-cachify.php +++ b/inc/class-cachify.php @@ -16,53 +16,66 @@ final class Cachify { /** * Plugin options * - * @since 2.0 - * @var array + * @var array + * + * @since 2.0 */ private static $options; /** * Caching method * - * @since 2.0 - * @var object + * @var object + * + * @since 2.0 */ private static $method; /** - * Whether we are on an Nginx server or not. + * Whether we are on a Nginx server or not. + * + * @var bool * * @since 2.2.5 - * @var boolean */ private static $is_nginx; /** * Method settings * - * @since 2.0.9 - * @var integer + * @var int + * + * @since 2.0.9 */ - const METHOD_DB = 0; - const METHOD_APC = 1; + const METHOD_DB = 0; + const METHOD_APC = 1; // No longer available. const METHOD_HDD = 2; const METHOD_MMC = 3; + const METHOD_REDIS = 4; /** * Minify settings * - * @since 2.0.9 - * @var integer + * @var int + * + * @since 2.0.9 */ - const MINIFY_DISABLED = 0; + const MINIFY_DISABLED = 0; const MINIFY_HTML_ONLY = 1; - const MINIFY_HTML_JS = 2; + const MINIFY_HTML_JS = 2; + + /** + * REST endpoints + * + * @var string + */ + const REST_NAMESPACE = 'cachify/v1'; + const REST_ROUTE_FLUSH = 'flush'; /** * Pseudo constructor * - * @since 2.0.5 - * @change 2.0.5 + * @since 2.0.5 */ public static function instance() { new self(); @@ -71,10 +84,7 @@ public static function instance() { /** * Constructor * - * @since 1.0.0 - * @change 2.2.2 - * - * @return void + * @since 1.0 */ public function __construct() { /* Set defaults */ @@ -82,202 +92,105 @@ public function __construct() { self::$is_nginx = $GLOBALS['is_nginx']; - /* Publish hooks */ - add_action( - 'init', - array( - __CLASS__, - 'register_publish_hooks', - ), - 99 - ); - /* Flush Hooks */ add_action( 'init', array( __CLASS__, 'register_flush_cache_hooks' ), 10, 0 ); + add_action( 'post_updated', array( __CLASS__, 'save_update_trash_post' ), 10, 3 ); + add_action( 'pre_post_update', array( __CLASS__, 'post_update' ), 10, 2 ); + add_action( 'cachify_remove_post_cache', array( __CLASS__, 'remove_page_cache_by_post_id' ) ); + add_action( 'comment_post', array( __CLASS__, 'new_comment' ), 99, 2 ); + add_action( 'edit_comment', array( __CLASS__, 'comment_edit' ), 10, 2 ); + add_action( 'transition_comment_status', array( __CLASS__, 'comment_status' ), 10, 3 ); - add_action( - 'cachify_remove_post_cache', - array( - __CLASS__, - 'remove_page_cache_by_post_id', - ) - ); + /* Flush Hooks - third party */ + add_action( 'woocommerce_product_set_stock', array( __CLASS__, 'flush_woocommerce' ) ); + add_action( 'woocommerce_variation_set_stock', array( __CLASS__, 'flush_woocommerce' ) ); + add_action( 'woocommerce_product_set_stock_status', array( __CLASS__, 'flush_woocommerce' ) ); + add_action( 'woocommerce_variation_set_stock_status', array( __CLASS__, 'flush_woocommerce' ) ); + + /* Register scripts */ + add_action( 'init', array( __CLASS__, 'register_scripts' ) ); + + /* Register styles */ + add_action( 'init', array( __CLASS__, 'register_styles' ) ); /* Flush icon */ - add_action( - 'admin_bar_menu', - array( - __CLASS__, - 'add_flush_icon', - ), - 90 - ); + add_action( 'admin_bar_menu', array( __CLASS__, 'add_flush_icon' ), 90 ); - add_action( - 'init', - array( - __CLASS__, - 'process_flush_request', - ) - ); + /* Flush icon script */ + add_action( 'admin_bar_menu', array( __CLASS__, 'add_flush_icon_script' ), 90 ); - /* Flush (post) cache if comment is made from frontend or backend */ - add_action( - 'pre_comment_approved', - array( - __CLASS__, - 'pre_comment', - ), - 99, - 2 - ); + /* Flush REST endpoint */ + add_action( 'rest_api_init', array( __CLASS__, 'add_flush_rest_endpoint' ) ); - /* Backend */ - if ( is_admin() ) { - add_action( - 'wpmu_new_blog', - array( - __CLASS__, - 'install_later', - ) - ); + add_action( 'init', array( __CLASS__, 'process_flush_request' ) ); - add_action( - 'delete_blog', - array( - __CLASS__, - 'uninstall_later', - ) - ); + /* Add Cron for clearing the HDD Cache */ + if ( self::METHOD_HDD === self::$options['use_apc'] ) { + add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_cache_expiration' ) ); - add_action( - 'admin_init', - array( - __CLASS__, - 'register_textdomain', - ) - ); + $timestamp = wp_next_scheduled( 'hdd_cache_cron' ); + if ( false === $timestamp ) { + wp_schedule_event( time(), 'cachify_cache_expire', 'hdd_cache_cron' ); + } - add_action( - 'admin_init', - array( - __CLASS__, - 'register_settings', - ) - ); + add_action( 'hdd_cache_cron', array( __CLASS__, 'run_hdd_cache_cron' ) ); + } - add_action( - 'admin_menu', - array( - __CLASS__, - 'add_page', - ) - ); + if ( is_admin() ) { + /* Backend */ + if ( version_compare( get_bloginfo( 'version' ), '5.1', '<' ) ) { + // The following hooks are deprecated since WP 5.1 (#246). + add_action( 'wpmu_new_blog', array( __CLASS__, 'install_later' ) ); + add_action( 'delete_blog', array( __CLASS__, 'uninstall_later' ) ); + } else { + add_action( 'wp_initialize_site', array( __CLASS__, 'install_later' ) ); + add_action( 'wp_delete_site', array( __CLASS__, 'uninstall_later' ) ); + } - add_action( - 'admin_enqueue_scripts', - array( - __CLASS__, - 'add_admin_resources', - ) - ); + add_action( 'admin_init', array( __CLASS__, 'register_textdomain' ) ); - add_action( - 'admin_head', - array( - __CLASS__, - 'admin_dashboard_styles', - ) - ); + add_action( 'admin_init', array( __CLASS__, 'register_settings' ) ); - add_action( - 'doing_dark_mode', - array( - __CLASS__, - 'admin_dashboard_dark_mode_styles', - ) - ); + add_action( 'admin_menu', array( __CLASS__, 'add_page' ) ); - add_action( - 'transition_comment_status', - array( - __CLASS__, - 'touch_comment', - ), - 10, - 3 - ); + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'add_admin_resources' ) ); - add_action( - 'edit_comment', - array( - __CLASS__, - 'edit_comment', - ) - ); + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_dashboard_styles' ) ); - add_filter( - 'dashboard_glance_items', - array( - __CLASS__, - 'add_dashboard_count', - ) - ); + add_filter( 'dashboard_glance_items', array( __CLASS__, 'add_dashboard_count' ) ); - add_filter( - 'plugin_row_meta', - array( - __CLASS__, - 'row_meta', - ), - 10, - 2 - ); + add_filter( 'plugin_row_meta', array( __CLASS__, 'row_meta' ), 10, 2 ); - add_filter( - 'plugin_action_links_' . CACHIFY_BASE, - array( - __CLASS__, - 'action_links', - ) - ); + add_filter( 'plugin_action_links_' . CACHIFY_BASE, array( __CLASS__, 'action_links' ) ); - /* Frontend */ } else { - add_action( - 'template_redirect', - array( - __CLASS__, - 'manage_cache', - ), - 0 - ); - - add_action( - 'robots_txt', - array( - __CLASS__, - 'robots_txt', - ) - ); - }// End if(). + /* Frontend */ + add_action( 'template_redirect', array( __CLASS__, 'manage_cache' ), 0 ); + add_filter( 'robots_txt', array( __CLASS__, 'robots_txt' ) ); + } } /** * Deactivation hook * - * @since 2.1.0 - * @change 2.1.0 + * @since 2.1.0 */ public static function on_deactivation() { + /* Remove hdd cache cron when hdd is selected */ + if ( self::METHOD_HDD === self::$options['use_apc'] ) { + $timestamp = wp_next_scheduled( 'hdd_cache_cron' ); + if ( false !== $timestamp ) { + wp_unschedule_event( $timestamp, 'hdd_cache_cron' ); + } + } + self::flush_total_cache( true ); } /** * Activation hook * - * @since 1.0 - * @change 2.1.0 + * @since 1.0 */ public static function on_activation() { /* Multisite & Network */ @@ -300,21 +213,21 @@ public static function on_activation() { } /** - * Plugin installation on new MU blog. + * Plugin installation on new WPMS site. * - * @since 1.0 - * @change 1.0 + * @param int|WP_Site $new_site New site ID or object. * - * @param integer $id Blog ID. + * @since 1.0 + * @since 2.4.0 supports WP_Site argument */ - public static function install_later( $id ) { + public static function install_later( $new_site ) { /* No network plugin */ if ( ! is_plugin_active_for_network( CACHIFY_BASE ) ) { return; } /* Switch to blog */ - switch_to_blog( $id ); + switch_to_blog( is_int( $new_site ) ? $new_site : $new_site->blog_id ); /* Install */ self::_install_backend(); @@ -326,8 +239,7 @@ public static function install_later( $id ) { /** * Actual installation of the options * - * @since 1.0 - * @change 2.0 + * @since 1.0 */ private static function _install_backend() { add_option( @@ -342,8 +254,7 @@ private static function _install_backend() { /** * Uninstalling of the plugin per MU blog. * - * @since 1.0 - * @change 2.1.0 + * @since 1.0 */ public static function on_uninstall() { /* Global */ @@ -371,21 +282,21 @@ public static function on_uninstall() { } /** - * Uninstalling of the plugin for MU and network. + * Uninstalling of the plugin for WPMS site. * - * @since 1.0 - * @change 1.0 + * @param int|WP_Site $old_site Old site ID or object. * - * @param integer $id Blog ID. + * @since 1.0 + * @since 2.4.0 supports WP_Site argument */ - public static function uninstall_later( $id ) { + public static function uninstall_later( $old_site ) { /* No network plugin */ if ( ! is_plugin_active_for_network( CACHIFY_BASE ) ) { return; } /* Switch to blog */ - switch_to_blog( $id ); + switch_to_blog( is_int( $old_site ) ? $old_site : $old_site->blog_id ); /* Install */ self::_uninstall_backend(); @@ -397,8 +308,7 @@ public static function uninstall_later( $id ) { /** * Actual uninstalling of the plugin * - * @since 1.0 - * @change 1.0 + * @since 1.0 */ private static function _uninstall_backend() { /* Option */ @@ -411,10 +321,9 @@ private static function _uninstall_backend() { /** * Get IDs of installed blogs * - * @since 1.0 - * @change 1.0 + * @return array Blog IDs * - * @return array Blog IDs + * @since 1.0 */ private static function _get_blog_ids() { /* Global */ @@ -423,11 +332,49 @@ private static function _get_blog_ids() { return $wpdb->get_col( "SELECT blog_id FROM `$wpdb->blogs`" ); } + /** + * Register the styles + * + * @since 2.4.0 + */ + public static function register_styles() { + /* Register dashboard CSS */ + wp_register_style( + 'cachify-dashboard', + plugins_url( 'css/dashboard.min.css', CACHIFY_FILE ), + array(), + filemtime( plugin_dir_path( CACHIFY_FILE ) . 'css/dashboard.min.css' ) + ); + + /* Register admin bar flush CSS */ + wp_register_style( + 'cachify-admin-bar-flush', + plugins_url( 'css/admin-bar-flush.min.css', CACHIFY_FILE ), + array(), + filemtime( plugin_dir_path( CACHIFY_FILE ) . 'css/admin-bar-flush.min.css' ) + ); + } + + /** + * Register the scripts + * + * @since 2.4.0 + */ + public static function register_scripts() { + /* Register admin bar flush script */ + wp_register_script( + 'cachify-admin-bar-flush', + plugins_url( 'js/admin-bar-flush.min.js', CACHIFY_FILE ), + array(), + filemtime( plugin_dir_path( CACHIFY_FILE ) . 'js/admin-bar-flush.min.js' ), + true + ); + } + /** * Register the language file * - * @since 2.1.3 - * @change 2.3.2 + * @since 2.1.3 */ public static function register_textdomain() { load_plugin_textdomain( 'cachify' ); @@ -436,38 +383,86 @@ public static function register_textdomain() { /** * Set default options * - * @since 2.0 - * @change 2.0.7 + * @since 2.0 */ private static function _set_default_vars() { /* Options */ self::$options = self::_get_options(); - /* APC */ - if ( self::METHOD_APC === self::$options['use_apc'] && Cachify_APC::is_available() ) { - self::$method = new Cachify_APC(); - + if ( self::METHOD_APC === self::$options['use_apc'] ) { + /* APC */ + add_action( 'admin_notices', array( __CLASS__, 'admin_notice_unavailable' ) ); + self::$method = new Cachify_NOOP( 'APC' ); + } elseif ( self::METHOD_HDD === self::$options['use_apc'] ) { /* HDD */ - } elseif ( self::METHOD_HDD === self::$options['use_apc'] && Cachify_HDD::is_available() ) { - self::$method = new Cachify_HDD(); - - /* MEMCACHED */ - } elseif ( self::METHOD_MMC === self::$options['use_apc'] && Cachify_MEMCACHED::is_available() ) { - self::$method = new Cachify_MEMCACHED(); - - /* DB */ + if ( Cachify_HDD::is_available() ) { + self::$method = new Cachify_HDD(); + } else { + add_action( 'admin_notices', array( __CLASS__, 'admin_notice_unavailable' ) ); + self::$method = new Cachify_NOOP( Cachify_HDD::stringify_method() ); + } + } elseif ( self::METHOD_MMC === self::$options['use_apc'] ) { + /* Memcached */ + if ( Cachify_MEMCACHED::is_available() ) { + self::$method = new Cachify_MEMCACHED(); + } else { + add_action( 'admin_notices', array( __CLASS__, 'admin_notice_unavailable' ) ); + self::$method = new Cachify_NOOP( Cachify_MEMCACHED::stringify_method() ); + } + } elseif ( self::METHOD_REDIS === self::$options['use_apc'] ) { + /* Redis */ + if ( Cachify_REDIS::is_available() ) { + self::$method = new Cachify_REDIS(); + } else { + add_action( 'admin_notices', array( __CLASS__, 'admin_notice_unavailable' ) ); + self::$method = new Cachify_NOOP( Cachify_REDIS::stringify_method() ); + } } else { + /* Database */ self::$method = new Cachify_DB(); } } + /** + * Show admin notice if caching backend is unavailable. + * + * @since 2.4.0 + */ + public static function admin_notice_unavailable() { + if ( current_user_can( 'manage_options' ) ) { + $unavailable_method = '-'; + if ( self::$method instanceof Cachify_NOOP ) { + $unavailable_method = self::$method->unavailable_method; + } + + printf( + '

%1$s

%2$s

%3$s

', + esc_html__( 'Cachify backend not available', 'cachify' ), + esc_html( + sprintf( + /* translators: Name of the caching backend inserted for placeholder */ + __( 'The configured caching backend is not available: %s', 'cachify' ), + $unavailable_method + ) + ), + wp_kses( + sprintf( + /* translators: Link to Cachify settings page inserted at placeholder */ + __( 'Please check your server configuration and visit the settings page to chose a different method.', 'cachify' ), + add_query_arg( array( 'page' => 'cachify' ), admin_url( 'options-general.php' ) ) + ), + array( 'a' => array( 'href' => array() ) ) + ) + ); + } + } + /** * Get options * - * @since 2.0 - * @change 2.3.0 + * @return array Array of option values * - * @return array Array of option values + * @since 2.0 */ private static function _get_options() { return wp_parse_args( @@ -482,6 +477,7 @@ private static function _get_options() { 'reset_on_post' => 1, 'reset_on_comment' => 0, 'sig_detail' => 0, + 'change_robots_txt' => 1, ) ); } @@ -489,39 +485,57 @@ private static function _get_options() { /** * Modify robots.txt * - * @since 1.0 - * @change 2.1.9 + * @param string $output The robots.txt output. * - * @param string $data Original content of dynamic robots.txt. - * @return string Modified content of robots.txt. + * @since 1.0 + * @since 2.1.9 */ - public static function robots_txt( $data ) { + public static function robots_txt( $output ) { + if ( ! self::$options['change_robots_txt'] ) { + return $output; + } /* HDD only */ - if ( self::METHOD_HDD !== self::$options['use_apc'] ) { - return $data; + if ( self::METHOD_HDD === self::$options['use_apc'] ) { + $output .= "\nUser-agent: *\nDisallow: */cache/cachify/\n"; } - /* Parse site URL */ - $url_parts = wp_parse_url( site_url() ); + return $output; + } + + /** + * HDD Cache expiration cron action. + * + * @since 2.4.0 + */ + public static function run_hdd_cache_cron() { + Cachify_HDD::clear_cache(); + } - /* Output */ - $data .= sprintf( - '%2$sDisallow: %1$s/wp-content/cache/cachify/%2$s', - ( empty( $url_parts['path'] ) ? '' : $url_parts['path'] ), - PHP_EOL + /** + * Add cache expiration cron schedule. + * + * @param array $schedules Array of previously added non-default schedules. + * + * @return array Array of non-default schedules with our tasks added. + * + * @since 2.4.0 + */ + public static function add_cron_cache_expiration( $schedules ) { + $schedules['cachify_cache_expire'] = array( + 'interval' => self::$options['cache_expires'] * 3600, + 'display' => esc_html__( 'Cachify expire', 'cachify' ), ); - - return $data; + return $schedules; } /** * Add the action links * - * @since 1.0 - * @change 1.0 + * @param array $data Initial array with action links. + * + * @return array Merged array with action links. * - * @param array $data Initial array with action links. - * @return array Merged array with action links. + * @since 1.0 */ public static function action_links( $data ) { /* Permissions? */ @@ -549,12 +563,12 @@ public static function action_links( $data ) { /** * Meta links of the plugin * - * @since 0.5 - * @change 2.0.5 + * @param array $input Initial array with meta links. + * @param string $page Current page. * - * @param array $input Initial array with meta links. - * @param string $page Current page. - * @return array Merged array with meta links. + * @return array Merged array with meta links. + * + * @since 0.5 */ public static function row_meta( $input, $page ) { /* Permissions */ @@ -574,11 +588,11 @@ public static function row_meta( $input, $page ) { /** * Add cache properties to dashboard * - * @since 2.0.0 - * @change 2.2.2 + * @param array $items Initial array with dashboard items. + * + * @return array Merged array with dashboard items. * - * @param array $items Initial array with dashboard items. - * @return array Merged array with dashboard items. + * @since 2.0.0 */ public static function add_dashboard_count( $items = array() ) { /* Skip */ @@ -605,9 +619,9 @@ public static function add_dashboard_count( $items = array() ) { /* Right now item */ $items[] = sprintf( - ' + ' %s', add_query_arg( array( @@ -615,8 +629,12 @@ public static function add_dashboard_count( $items = array() ) { ), admin_url( 'options-general.php' ) ), - esc_attr( strtolower( $method ) ), - esc_html__( 'Caching method', 'cachify' ), + sprintf( + /* translators: 1: "Caching method label"; 2: Actual method. */ + esc_html__( '%1$s: %2$s', 'cachify' ), + esc_html__( 'Caching method', 'cachify' ), + esc_attr( strtolower( $method ) ) + ), esc_attr( $method ), plugins_url( 'images/symbols.svg', CACHIFY_FILE ), esc_attr( strtolower( $method ) ), @@ -631,10 +649,9 @@ public static function add_dashboard_count( $items = array() ) { /** * Get the cache size * - * @since 2.0.6 - * @change 2.0.6 + * @return int Cache size in bytes. * - * @return integer Cache size in bytes. + * @since 2.0.6 */ public static function get_cache_size() { $size = get_transient( 'cachify_cache_size' ); @@ -661,12 +678,13 @@ public static function get_cache_size() { /** * Add flush icon to admin bar menu * - * @since 1.2 - * @change 2.2.2 + * @hook mixed cachify_user_can_flush_cache * - * @hook mixed cachify_user_can_flush_cache + * @param object $wp_admin_bar Object of menu items. * - * @param object $wp_admin_bar Object of menu items. + * @since 1.2 + * @since 2.2.2 + * @since 2.4.0 Adjust icon for flush request via AJAX */ public static function add_flush_icon( $wp_admin_bar ) { /* Quit */ @@ -674,8 +692,16 @@ public static function add_flush_icon( $wp_admin_bar ) { return; } - /* Display the admin icon anytime */ - echo ''; + /* Enqueue style */ + wp_enqueue_style( 'cachify-admin-bar-flush' ); + + /* Print area for aria live updates */ + echo ''; + /* Check if the flush action was used without AJAX */ + $dashicon_class = 'dashicons-trash'; + if ( isset( $_GET['_cachify'] ) && 'flushed' === $_GET['_cachify'] ) { + $dashicon_class = self::get_dashicon_success_class(); + } /* Add menu item */ $wp_admin_bar->add_menu( @@ -683,7 +709,7 @@ public static function add_flush_icon( $wp_admin_bar ) { 'id' => 'cachify', 'href' => wp_nonce_url( add_query_arg( '_cachify', 'flush' ), '_cachify__flush_nonce' ), // esc_url in /wp-includes/class-wp-admin-bar.php#L438. 'parent' => 'top-secondary', - 'title' => '' . + 'title' => '' . '' . __( 'Flush site cache', @@ -691,21 +717,105 @@ public static function add_flush_icon( $wp_admin_bar ) { ) . '', 'meta' => array( - 'title' => esc_html__( 'Flush the cachify cache', 'cachify' ), + 'title' => esc_html__( 'Flush the Cachify cache', 'cachify' ), ), ) ); } /** - * Process plugin's meta actions + * Returns the dashicon class for the success state in admin bar flush button + * + * @return string + * + * @since 2.4.0 + */ + public static function get_dashicon_success_class() { + global $wp_version; + if ( version_compare( $wp_version, '5.2', '<' ) ) { + return 'dashicons-yes'; + } + + return 'dashicons-yes-alt'; + } + + /** + * Add a script to query the REST endpoint and animate the flush icon in admin bar menu + * + * @hook mixed cachify_user_can_flush_cache ? + * + * @param object $wp_admin_bar Object of menu items. + * + * @since 2.4.0 + */ + public static function add_flush_icon_script( $wp_admin_bar ) { + /* Quit */ + if ( ! is_admin_bar_showing() || ! apply_filters( 'cachify_user_can_flush_cache', current_user_can( 'manage_options' ) ) ) { + return; + } + + /* Enqueue script */ + wp_enqueue_script( 'cachify-admin-bar-flush' ); + + /* Localize script */ + wp_localize_script( + 'cachify-admin-bar-flush', + 'cachify_admin_bar_flush_ajax_object', + array( + 'url' => esc_url_raw( rest_url( self::REST_NAMESPACE . '/' . self::REST_ROUTE_FLUSH ) ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'flushing' => __( 'Flushing cache', 'cachify' ), + 'flushed' => __( 'Cache flushed successfully', 'cachify' ), + 'dashicon_success' => self::get_dashicon_success_class(), + ) + ); + } + + + /** + * Registers an REST endpoint for the flush operation + * + * @since 2.4.0 + */ + public static function add_flush_rest_endpoint() { + register_rest_route( + self::REST_NAMESPACE, + self::REST_ROUTE_FLUSH, + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( + __CLASS__, + 'flush_cache', + ), + 'permission_callback' => array( + __CLASS__, + 'user_can_manage_options', + ), + ) + ); + } + + /** + * Check if user can manage options * - * @since 0.5 - * @change 2.2.2 + * @return bool + * + * @since 2.4.0 + */ + public static function user_can_manage_options() { + return current_user_can( 'manage_options' ); + } + + /** + * Process plugin's meta actions * * @hook mixed cachify_user_can_flush_cache * - * @param array $data Metadata of the plugin. + * @param array $data Metadata of the plugin. + * + * @since 0.5 + * @since 2.2.2 + * @since 2.4.0 Extract cache flushing to own method and always redirect to referer with new value for `_cachify` param. */ public static function process_flush_request( $data ) { /* Skip if not a flush request */ @@ -725,9 +835,29 @@ public static function process_flush_request( $data ) { /* Load on demand */ if ( ! function_exists( 'is_plugin_active_for_network' ) ) { - require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + require_once ABSPATH . 'wp-admin/includes/plugin.php'; } + /* Flush cache */ + self::flush_cache(); + + wp_safe_redirect( + add_query_arg( + '_cachify', + 'flushed', + wp_get_referer() + ) + ); + + exit(); + } + + /** + * Flush cache + * + * @since 2.4.0 + */ + public static function flush_cache() { /* Flush cache */ if ( is_multisite() && is_network_admin() ) { /* Old blog */ @@ -747,28 +877,26 @@ public static function process_flush_request( $data ) { /* Notice */ if ( is_admin() ) { - add_action( - 'network_admin_notices', - array( - __CLASS__, - 'flush_notice', - ) - ); + add_action( 'network_admin_notices', array( __CLASS__, 'flush_notice' ) ); } } else { self::flush_total_cache(); /* Notice */ if ( is_admin() ) { - add_action( - 'admin_notices', - array( - __CLASS__, - 'flush_notice', - ) - ); + add_action( 'admin_notices', array( __CLASS__, 'flush_notice' ) ); } } + + /* Reschedule HDD Cache Cron */ + if ( self::METHOD_HDD === self::$options['use_apc'] ) { + $timestamp = wp_next_scheduled( 'hdd_cache_cron' ); + if ( false !== $timestamp ) { + wp_reschedule_event( $timestamp, 'cachify_cache_expire', 'hdd_cache_cron' ); + wp_unschedule_event( $timestamp, 'hdd_cache_cron' ); + } + } + if ( ! is_admin() ) { wp_safe_redirect( remove_query_arg( @@ -784,15 +912,15 @@ public static function process_flush_request( $data ) { /** * Notice after successful flushing of the cache * - * @since 1.2 - * @change 2.2.2 + * @hook mixed cachify_user_can_flush_cache * - * @hook mixed cachify_user_can_flush_cache + * @since 1.2 + * @since 2.2.2 */ public static function flush_notice() { /* No admin */ if ( ! is_admin_bar_showing() || ! apply_filters( 'cachify_user_can_flush_cache', current_user_can( 'manage_options' ) ) ) { - return false; + return; } printf( @@ -804,56 +932,108 @@ public static function flush_notice() { /** * Remove page from cache or flush on comment edit * - * @since 0.1.0 - * @change 2.1.2 + * @param int $id Comment ID. + * + * @since 0.1.0 + * @since 2.1.2 * - * @param integer $id Comment ID. + * @deprecated 2.4.0 Use comment_edit($id, $comment) instead. */ public static function edit_comment( $id ) { - if ( self::$options['reset_on_comment'] ) { - self::flush_total_cache(); - } else { - self::remove_page_cache_by_post_id( - get_comment( $id )->comment_post_ID - ); + self::comment_edit( $id, array( 'comment_approved' => 1 ) ); + } + + /** + * Remove page from cache or flush on comment edit. + * + * @param integer $id Comment ID. + * @param array $comment Comment data. + * + * @since 2.4.0 Replacement for edit_comment($id) with additional comment parameter. + */ + public static function comment_edit( $id, $comment ) { + $approved = (int) $comment['comment_approved']; + + /* Approved comment? */ + if ( 1 === $approved ) { + if ( self::$options['reset_on_comment'] ) { + self::flush_total_cache(); + } else { + self::remove_page_cache_by_post_id( + get_comment( $id )->comment_post_ID + ); + } } } /** * Remove page from cache or flush on new comment * - * @since 0.1.0 - * @change 2.1.2 + * @param mixed $approved Comment status. + * @param array $comment Array of properties. + * + * @return mixed Comment status. * - * @param mixed $approved Comment status. - * @param array $comment Array of properties. - * @return mixed Comment status. + * @since 0.1 + * @since 2.1.2 + * @since 2.4.0 Replacement for edit_comment($id) with additional comment parameter. */ public static function pre_comment( $approved, $comment ) { + self::new_comment( $comment['comment_ID'], $approved ); + + return $approved; + } + + /** + * Remove page from cache or flush on new comment + * + * @param integer|string $id Comment ID. + * @param integer|string $approved Comment status. + * + * @since 0.1.0 + * @since 2.1.2 + * @since 2.4.0 Renamed with ID parameter instead of comment array. + */ + public static function new_comment( $id, $approved ) { /* Approved comment? */ if ( 1 === $approved ) { if ( self::$options['reset_on_comment'] ) { self::flush_total_cache(); } else { - self::remove_page_cache_by_post_id( $comment['comment_post_ID'] ); + self::remove_page_cache_by_post_id( get_comment( $id )->comment_post_ID ); } } - - return $approved; } /** * Remove page from cache or flush on comment edit * - * @since 0.1 - * @change 2.1.2 + * @param string $new_status New status. + * @param string $old_status Old status. + * @param object $comment The comment. + * + * @since 0.1 + * @since 2.1.2 * - * @param string $new_status New status. - * @param string $old_status Old status. - * @param object $comment The comment. + * @deprecated 2.4.0 Use comment_status($new_status, $old_status, $comment) instead. */ public static function touch_comment( $new_status, $old_status, $comment ) { - if ( $new_status !== $old_status ) { + self::comment_status( $new_status, $old_status, $comment ); + } + + /** + * Remove page from cache or flush on comment edit. + * + * @param string $new_status New status. + * @param string $old_status Old status. + * @param WP_Comment $comment The comment. + * + * @since 0.1 + * @since 2.1.2 + * @since 2.4.0 Renamed from touch_comment(). + */ + public static function comment_status( $new_status, $old_status, $comment ) { + if ( 'approved' === $old_status || 'approved' === $new_status ) { if ( self::$options['reset_on_comment'] ) { self::flush_total_cache(); } else { @@ -865,10 +1045,10 @@ public static function touch_comment( $new_status, $old_status, $comment ) { /** * Generate publish hook for custom post types * - * @since 2.1.7 Make the function public - * @since 2.0.3 + * @since 2.0.3 + * @since 2.1.7 Make the function public * - * @return void + * @deprecated no longer used since 2.4 */ public static function register_publish_hooks() { /* Available post types */ @@ -885,33 +1065,20 @@ public static function register_publish_hooks() { /* Loop the post types */ foreach ( $post_types as $post_type ) { - add_action( - 'publish_' . $post_type, - array( - __CLASS__, - 'publish_post_types', - ), - 10, - 2 - ); - add_action( - 'publish_future_' . $post_type, - array( - __CLASS__, - 'flush_total_cache', - ) - ); + add_action( 'publish_' . $post_type, array( __CLASS__, 'publish_post_types' ), 10, 2 ); + add_action( 'publish_future_' . $post_type, array( __CLASS__, 'flush_total_cache' ) ); } } /** * Removes the post type cache on post updates * - * @since 2.0.3 - * @change 2.3.0 + * @param int $post_id Post ID. + * @param object $post Post object. + * + * @since 2.0.3 * - * @param integer $post_id Post ID. - * @param object $post Post object. + * @deprecated no longer used since 2.4 */ public static function publish_post_types( $post_id, $post ) { /* No post_id? */ @@ -937,13 +1104,98 @@ public static function publish_post_types( $post_id, $post ) { } } + /** + * Removes the post type cache if saved or updated + * + * @param int $id Post ID. + * @param WP_Post $post_after Post object following the update. + * @param WP_Post $post_before Post object before the update. + * + * @since 2.0.3 + * @since 2.1.7 Make the function public. + * @since 2.4.0 Renamed to save_update_trash_post and introduced parameters. + */ + public static function save_update_trash_post( $id, $post_after, $post_before ) { + $status = get_post_status( $post_before ); + + /* Post type published? */ + if ( 'publish' === $status ) { + self::flush_cache_for_posts( $id ); + } + } + + /** + * Removes the post type cache before an existing post type is updated in the db + * + * @param int $id Post ID. + * @param array $data Post data. + * + * @since 2.0.3 + * @since 2.3.0 + * @since 2.4.0 Renamed to post_update. + */ + public static function post_update( $id, $data ) { + $new_status = $data['post_status']; + $old_status = get_post_status( $id ); + + /* Was it published and is it not trashed now? */ + if ( 'trash' !== $new_status && 'publish' === $old_status ) { + self::flush_cache_for_posts( $id ); + } + } + + /** + * Clear cache when any post type has been created or updated + * + * @param int|WP_Post $post Post ID or object. + * + * @since 2.4.0 + */ + public static function flush_cache_for_posts( $post ) { + if ( is_int( $post ) ) { + $post_id = $post; + $data = get_post( $post_id ); + + if ( ! is_object( $data ) ) { + return; + } + } elseif ( is_object( $post ) ) { + $post_id = $post->ID; + } else { + return; + } + + /* Remove cache OR flush */ + if ( 1 !== self::$options['reset_on_post'] ) { + self::remove_page_cache_by_post_id( $post_id ); + } else { + self::flush_total_cache(); + } + } + + /** + * Flush post cache on WooCommerce stock changes. + * + * @param int|WC_Product $product Product ID or object. + * + * @since 2.4.0 + */ + public static function flush_woocommerce( $product ) { + if ( is_int( $product ) ) { + $id = $product; + } else { + $id = $product->get_id(); + } + + self::flush_cache_for_posts( $id ); + } + /** * Removes a page (id) from cache * - * @since 2.0.3 - * @change 2.1.3 + * @param int $post_id Post ID. * - * @param integer $post_id Post ID. + * @since 2.0.3 */ public static function remove_page_cache_by_post_id( $post_id ) { $post_id = (int) $post_id; @@ -957,10 +1209,9 @@ public static function remove_page_cache_by_post_id( $post_id ) { /** * Removes a page url from cache * - * @since 0.1 - * @change 2.1.3 + * @param string $url Page URL. * - * @param string $url Page URL. + * @since 0.1 */ public static function remove_page_cache_by_url( $url ) { $url = (string) $url; @@ -968,23 +1219,26 @@ public static function remove_page_cache_by_url( $url ) { return; } - call_user_func( - array( - self::$method, - 'delete_item', - ), - self::_cache_hash( $url ), - $url - ); + $hash = self::_cache_hash( $url ); + call_user_func( array( self::$method, 'delete_item' ), $hash, $url ); + + /** + * Call hook for further actions after cache has been flushed for a single page. + * + * @since 2.4.0 + * + * @param string $url Page URL. + * @param string $hash Cache hash for given URL. + */ + do_action( 'cachify_removed_cache_by_url', $url, $hash ); } /** * Get cache validity * - * @since 2.0.0 - * @change 2.1.7 + * @return int Validity period in seconds. * - * @return integer Validity period in seconds. + * @since 2.0.0 */ private static function _cache_expires() { return HOUR_IN_SECONDS * self::$options['cache_expires']; @@ -993,9 +1247,9 @@ private static function _cache_expires() { /** * Determine if cache details should be printed in signature * - * @since 2.3.0 + * @return bool Show details in signature. * - * @return bool Show details in signature. + * @since 2.3.0 */ private static function _signature_details() { return 1 === self::$options['sig_detail']; @@ -1004,34 +1258,36 @@ private static function _signature_details() { /** * Get hash value for caching * - * @since 0.1 - * @change 2.0 + * @param string $url URL to hash [optional]. + * + * @return string Cachify hash value. * - * @param string $url URL to hash [optional]. - * @return string Cachify hash value. + * @since 0.1 + * @since 2.0 */ private static function _cache_hash( $url = '' ) { $prefix = is_ssl() ? 'https-' : ''; - $url_parts = wp_parse_url( $url ); if ( empty( $url ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated - $hash_key = $prefix . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ); - } else { - $hash_key = $prefix . $url_parts['host'] . $url_parts['path']; + $url = '//' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ); } + $url_parts = wp_parse_url( $url ); + $hash_key = $prefix . $url_parts['host'] . $url_parts['path']; + return md5( $hash_key ) . '.cachify'; } /** * Split by comma * - * @since 0.9.1 - * @change 1.0 + * @param string $input String to split. + * + * @return array Splitted values. * - * @param string $input String to split. - * @return array Splitted values. + * @since 0.9.1 + * @since 1.0 */ private static function _preg_split( $input ) { return (array) preg_split( '/,/', $input, -1, PREG_SPLIT_NO_EMPTY ); @@ -1040,10 +1296,9 @@ private static function _preg_split( $input ) { /** * Check for index page * - * @since 0.6 - * @change 1.0 + * @return bool TRUE if index * - * @return boolean TRUE if index + * @since 0.6 */ private static function _is_index() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated @@ -1053,10 +1308,9 @@ private static function _is_index() { /** * Check for mobile devices * - * @since 0.9.1 - * @change 2.3.0 + * @return bool TRUE if mobile * - * @return boolean TRUE if mobile + * @since 0.9.1 */ private static function _is_mobile() { $templatedir = get_template_directory(); @@ -1066,10 +1320,9 @@ private static function _is_mobile() { /** * Check if user is logged in or marked * - * @since 2.0.0 - * @change 2.0.5 + * @return bool TRUE on "marked" users * - * @return boolean $diff TRUE on "marked" users + * @since 2.0.0 */ private static function _is_logged_in() { /* Logged in */ @@ -1095,58 +1348,50 @@ private static function _is_logged_in() { /** * Register all hooks to flush the total cache * - * @since 2.4.0 + * @since 2.4.0 */ public static function register_flush_cache_hooks() { /* Define all default flush cache hooks */ $flush_cache_hooks = array( - 'cachify_flush_cache' => 10, - '_core_updated_successfully' => 10, - 'switch_theme' => 10, - 'before_delete_post' => 10, - 'wp_trash_post' => 10, - 'create_term' => 10, - 'delete_term' => 10, - 'edit_terms' => 10, - 'user_register' => 10, - 'edit_user_profile_update' => 10, - 'delete_user' => 10, + 'cachify_flush_cache' => 10, + '_core_updated_successfully' => 10, + 'switch_theme' => 10, + 'before_delete_post' => 10, + 'wp_trash_post' => 10, + 'create_term' => 10, + 'delete_term' => 10, + 'edit_terms' => 10, + 'user_register' => 10, + 'edit_user_profile_update' => 10, + 'delete_user' => 10, + /* third party */ + 'autoptimize_action_cachepurged' => 10, ); $flush_cache_hooks = apply_filters( 'cachify_flush_cache_hooks', $flush_cache_hooks ); /* Loop all hooks and register actions */ foreach ( $flush_cache_hooks as $hook => $priority ) { - add_action( - $hook, - array( - 'Cachify', - 'flush_total_cache', - ), - $priority, - 0 - ); + add_action( $hook, array( 'Cachify', 'flush_total_cache' ), $priority, 0 ); } - } /** * Define exclusions for caching * - * @since 0.2 - * @change 2.3.0 + * @hook bool cachify_skip_cache * - * @return boolean TRUE on exclusion + * @return bool TRUE on exclusion * - * @hook boolean cachify_skip_cache + * @since 0.2 */ private static function _skip_cache() { /* Plugin options */ $options = self::$options; - /* Request vars */ - if ( ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + /* Skip for all request methods except GET */ + if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return true; } if ( ! empty( $_GET ) && get_option( 'permalink_structure' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing @@ -1202,19 +1447,31 @@ private static function _skip_cache() { } } + // Sitemap feature added in WP 5.5. + if ( get_query_var( 'sitemap' ) || get_query_var( 'sitemap-subtype' ) || get_query_var( 'sitemap-stylesheet' ) ) { + return true; + } + + /* Content Negotiation */ + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + if ( isset( $_SERVER['HTTP_ACCEPT'] ) && false === strpos( $_SERVER['HTTP_ACCEPT'], 'text/html' ) ) { + return true; + } + return false; } /** * Minify HTML code * - * @since 0.9.2 - * @change 2.0.9 + * @hook array cachify_minify_ignore_tags * - * @param string $data Original HTML code. - * @return string Minified code + * @param string $data Original HTML code. * - * @hook array cachify_minify_ignore_tags + * @return string Minified code + * + * @since 0.9.2 */ private static function _minify_cache( $data ) { /* Disabled? */ @@ -1273,33 +1530,42 @@ private static function _minify_cache( $data ) { /** * Flush total cache * - * @since 0.1 - * @change 2.0 + * @param bool $clear_all_methods Flush all caching methods (default: FALSE). * - * @param bool $clear_all_methods Flush all caching methods (default: FALSE). + * @since 0.1 + * @since 2.0 */ public static function flush_total_cache( $clear_all_methods = false ) { + // We do not need to flush the cache for saved post revisions. + if ( did_action( 'save_post_revision' ) ) { + return; + } + if ( $clear_all_methods ) { /* DB */ Cachify_DB::clear_cache(); - /* APC */ - Cachify_APC::clear_cache(); - /* HDD */ Cachify_HDD::clear_cache(); + /* REDIS */ + Cachify_REDIS::clear_cache(); + /* MEMCACHED */ Cachify_MEMCACHED::clear_cache(); } else { - call_user_func( - array( - self::$method, - 'clear_cache', - ) - ); + call_user_func( array( self::$method, 'clear_cache' ) ); } + /** + * Call hook for further actions after total cache has been flushed. + * + * @since 2.4.0 + * + * @param bool $clear_all_methods All available caching backends have been flushed. + */ + do_action( 'cachify_flushed_total_cache', $clear_all_methods ); + /* Transient */ delete_transient( 'cachify_cache_size' ); } @@ -1307,11 +1573,12 @@ public static function flush_total_cache( $clear_all_methods = false ) { /** * Assign the cache * - * @since 0.1 - * @change 2.0 + * @param string $data Content of the page. + * + * @return string Content of the page. * - * @param string $data Content of the page. - * @return string Content of the page. + * @since 0.1 + * @since 2.0 */ public static function set_cache( $data ) { /* Empty? */ @@ -1322,18 +1589,37 @@ public static function set_cache( $data ) { /** * Filters whether the buffered data should actually be cached * - * @since 2.3 - * * @param bool $should_cache Whether the data should be cached. * @param string $data The actual data. * @param object $method Instance of the selected caching method. * @param string $cache_hash The cache hash. * @param int $cache_expires Cache validity period. + * + * @since 2.3.0 */ - $should_cache = apply_filters( 'cachify_store_item', true, $data, self::$method, self::_cache_hash(), self::_cache_expires() ); + $should_cache = apply_filters( + 'cachify_store_item', + 200 === http_response_code(), + $data, + self::$method, + self::_cache_hash(), + self::_cache_expires() + ); /* Save? */ if ( $should_cache ) { + /** + * Filters the buffered data itself + * + * @param string $data The actual data. + * @param object $method Instance of the selected caching method. + * @param string $cache_hash The cache hash. + * @param int $cache_expires Cache validity period. + * + * @since 2.4.0 + */ + $data = apply_filters( 'cachify_modify_output', $data, self::$method, self::_cache_hash(), self::_cache_expires() ); + call_user_func( array( self::$method, @@ -1352,8 +1638,7 @@ public static function set_cache( $data ) { /** * Manage the cache. * - * @since 0.1 - * @change 2.3 + * @since 0.1 */ public static function manage_cache() { /* No caching? */ @@ -1390,10 +1675,9 @@ public static function manage_cache() { /** * Register CSS * - * @since 1.0 - * @change 2.3.0 + * @param string $hook Current hook. * - * @param string $hook Current hook. + * @since 1.0 */ public static function add_admin_resources( $hook ) { /* Hooks check */ @@ -1401,33 +1685,15 @@ public static function add_admin_resources( $hook ) { return; } - /* Plugin data */ - $plugin_data = get_plugin_data( CACHIFY_FILE ); - /* Register css */ switch ( $hook ) { case 'index.php': - wp_enqueue_style( - 'cachify-dashboard', - plugins_url( 'css/dashboard.min.css', CACHIFY_FILE ), - array(), - $plugin_data['Version'] - ); - break; - - case 'settings_page_cachify': - wp_enqueue_style( - 'cachify-settings', - plugins_url( 'css/settings.min.css', CACHIFY_FILE ), - array(), - $plugin_data['Version'] - ); + wp_enqueue_style( 'cachify-dashboard' ); break; default: break; } - } /** @@ -1439,7 +1705,7 @@ public static function admin_dashboard_styles() { $wp_version = get_bloginfo( 'version' ); if ( version_compare( $wp_version, '5.3', '<' ) ) { - echo ''; + wp_add_inline_style( 'cachify-dashboard', '#dashboard_right_now .cachify-icon use { fill: #82878c; }' ); } } @@ -1447,16 +1713,17 @@ public static function admin_dashboard_styles() { * Fixing some admin dashboard styles * * @since 2.3.0 + * + * @deprecated included in dashboard.css since 2.4 */ public static function admin_dashboard_dark_mode_styles() { - echo ''; + wp_add_inline_style( 'cachify-dashboard', '#dashboard_right_now .cachify-icon use { fill: #bbc8d4; }' ); } /** * Add options page * - * @since 1.0 - * @change 2.2.2 + * @since 1.0 */ public static function add_page() { add_options_page( @@ -1474,25 +1741,19 @@ public static function add_page() { /** * Available caching methods * - * @since 2.0.0 - * @change 2.1.3 + * @return array Array of actually available methods. * - * @return array Array of actually available methods. + * @since 2.0 */ private static function _method_select() { /* Defaults */ $methods = array( self::METHOD_DB => esc_html__( 'Database', 'cachify' ), - self::METHOD_APC => esc_html__( 'APC', 'cachify' ), self::METHOD_HDD => esc_html__( 'Hard disk', 'cachify' ), self::METHOD_MMC => esc_html__( 'Memcached', 'cachify' ), + self::METHOD_REDIS => esc_html__( 'Redis', 'cachify' ), ); - /* APC */ - if ( ! Cachify_APC::is_available() ) { - unset( $methods[1] ); - } - /* Memcached? */ if ( ! Cachify_MEMCACHED::is_available() ) { unset( $methods[3] ); @@ -1503,16 +1764,20 @@ private static function _method_select() { unset( $methods[2] ); } + /* Redis */ + if ( ! Cachify_REDIS::is_available() ) { + unset( $methods[4] ); + } + return $methods; } /** * Minify cache dropdown * - * @since 2.1.3 - * @change 2.1.3 + * @return array Key => value array * - * @return array Key => value array + * @since 2.1.3 */ private static function _minify_select() { return array( @@ -1525,8 +1790,7 @@ private static function _minify_select() { /** * Register settings * - * @since 1.0 - * @change 1.0 + * @since 1.0 */ public static function register_settings() { register_setting( @@ -1542,11 +1806,12 @@ public static function register_settings() { /** * Validate options * - * @since 1.0.0 - * @change 2.1.3 + * @param array $data Array of form values. + * + * @return array Array of validated values. * - * @param array $data Array of form values. - * @return array Array of validated values. + * @since 1.0 + * @since 2.1.3 */ public static function validate_options( $data ) { /* Empty data? */ @@ -1558,7 +1823,7 @@ public static function validate_options( $data ) { self::flush_total_cache( true ); /* Notification */ - if ( self::$options['use_apc'] !== $data['use_apc'] && $data['use_apc'] >= self::METHOD_APC ) { + if ( self::$options['use_apc'] !== $data['use_apc'] && $data['use_apc'] >= self::METHOD_HDD && self::METHOD_REDIS != $data['use_apc'] ) { add_settings_error( 'cachify_method_tip', 'cachify_method_tip', @@ -1569,28 +1834,28 @@ public static function validate_options( $data ) { /* Return */ return array( - 'only_guests' => (int) ( ! empty( $data['only_guests'] ) ), - 'compress_html' => (int) $data['compress_html'], - 'cache_expires' => (int) ( isset( $data['cache_expires'] ) ? $data['cache_expires'] : self::$options['cache_expires'] ), - 'without_ids' => (string) isset( $data['without_ids'] ) ? sanitize_text_field( $data['without_ids'] ) : '', - 'without_agents' => (string) isset( $data['without_agents'] ) ? sanitize_text_field( $data['without_agents'] ) : '', - 'use_apc' => (int) $data['use_apc'], - 'reset_on_post' => (int) ( ! empty( $data['reset_on_post'] ) ), - 'reset_on_comment' => (int) ( ! empty( $data['reset_on_comment'] ) ), - 'sig_detail' => (int) ( ! empty( $data['sig_detail'] ) ), + 'only_guests' => (int) ( ! empty( $data['only_guests'] ) ), + 'compress_html' => (int) $data['compress_html'], + 'cache_expires' => (int) ( isset( $data['cache_expires'] ) ? $data['cache_expires'] : self::$options['cache_expires'] ), + 'without_ids' => (string) isset( $data['without_ids'] ) ? sanitize_text_field( $data['without_ids'] ) : '', + 'without_agents' => (string) isset( $data['without_agents'] ) ? sanitize_text_field( $data['without_agents'] ) : '', + 'use_apc' => (int) $data['use_apc'], + 'reset_on_post' => (int) ( ! empty( $data['reset_on_post'] ) ), + 'reset_on_comment' => (int) ( ! empty( $data['reset_on_comment'] ) ), + 'sig_detail' => (int) ( ! empty( $data['sig_detail'] ) ), + 'change_robots_txt' => (int) ( ! empty( $data['change_robots_txt'] ) ), ); } /** * Display options page * - * @since 1.0 - * @change 2.3.0 + * @since 1.0 */ public static function options_page() { - $options = self::_get_options(); + $options = self::_get_options(); $cachify_tabs = self::_get_tabs( $options ); - $current_tab = isset( $_GET['cachify_tab'] ) && isset( $cachify_tabs[ $_GET['cachify_tab'] ] ) + $current_tab = isset( $_GET['cachify_tab'] ) && isset( $cachify_tabs[ $_GET['cachify_tab'] ] ) ? sanitize_text_field( wp_unslash( $_GET['cachify_tab'] ) ) : 'settings'; ?> @@ -1609,7 +1874,7 @@ public static function options_page() { esc_url( add_query_arg( array( - 'page' => 'cachify', + 'page' => 'cachify', 'cachify_tab' => $tab_key, ), admin_url( 'options-general.php' ) @@ -1634,11 +1899,11 @@ public static function options_page() { /** * Return an array with all settings tabs applicable in context of current plugin options. * - * @since 2.3.0 - * @change 2.3.0 - * * @param array $options the options. + * * @return array + * + * @since 2.3.0 */ private static function _get_tabs( $options ) { /* Settings tab is always present */ @@ -1655,12 +1920,6 @@ private static function _get_tabs( $options ) { 'name' => __( 'Setup', 'cachify' ), 'page' => 'setup/cachify.hdd.' . ( self::$is_nginx ? 'nginx' : 'htaccess' ) . '.php', ); - } elseif ( self::METHOD_APC === $options['use_apc'] ) { - /* Setup tab for APC */ - $tabs['setup'] = array( - 'name' => __( 'Setup', 'cachify' ), - 'page' => 'setup/cachify.apc.' . ( self::$is_nginx ? 'nginx' : 'htaccess' ) . '.php', - ); } elseif ( self::METHOD_MMC === $options['use_apc'] && self::$is_nginx ) { /* Setup tab for Memcached */ $tabs['setup'] = array( diff --git a/inc/setup/cachify.apc.htaccess.php b/inc/setup/cachify.apc.htaccess.php deleted file mode 100644 index c03f92d8..00000000 --- a/inc/setup/cachify.apc.htaccess.php +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - -
- - - -
- -
-
-		
-		
-
diff --git a/inc/setup/cachify.apc.nginx.php b/inc/setup/cachify.apc.nginx.php deleted file mode 100644 index a332d30e..00000000 --- a/inc/setup/cachify.apc.nginx.php +++ /dev/null @@ -1,53 +0,0 @@ -fastcgi_param PHP_VALUE auto_prepend_file='; - -$ending = '/cachify/apc/proxy.php; - - location ~ /wp-admin/ { - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_param PHP_VALUE auto_prepend_file=; - } -}'; - -?> - - - - - - -
- - - -
- -
-
-		
-		
-
- - () diff --git a/inc/setup/cachify.hdd.htaccess.php b/inc/setup/cachify.hdd.htaccess.php index 68f5cc17..ef51be87 100644 --- a/inc/setup/cachify.hdd.htaccess.php +++ b/inc/setup/cachify.hdd.htaccess.php @@ -8,9 +8,8 @@ /* Quit */ defined( 'ABSPATH' ) || exit; -$beginning = '# BEGIN CACHIFY -<IfModule mod_rewrite.c> - # ENGINE ON +$htaccess = '# BEGIN CACHIFY + RewriteEngine on # set hostname directory @@ -24,79 +23,59 @@ RewriteRule .* - [E=CACHIFY_DIR:%{REQUEST_URI}] RewriteCond %{REQUEST_URI} ^$ RewriteRule .* - [E=CACHIFY_DIR:/] +'; +if ( Cachify_HDD::is_gzip_enabled() ) { + $htaccess .= ' # gzip RewriteRule .* - [E=CACHIFY_SUFFIX:] - <IfModule mod_mime.c> + RewriteCond %{HTTP:Accept-Encoding} gzip RewriteRule .* - [E=CACHIFY_SUFFIX:.gz] AddType text/html .gz AddEncoding gzip .gz - </IfModule> + +'; +} +$htaccess .= ' # Main Rules - RewriteCond %{REQUEST_METHOD} !=POST - RewriteCond %{QUERY_STRING} ="" + RewriteCond %{HTTP_ACCEPT} .*text/html.* + RewriteCond %{REQUEST_METHOD} GET + RewriteCond %{QUERY_STRING} ^$ RewriteCond %{REQUEST_URI} !^/(wp-admin|wp-content/cache)/.* RewriteCond %{HTTP_COOKIE} !(wp-postpass|wordpress_logged_in|comment_author)_ - RewriteCond '; - -$middle = '/cache/cachify/%{ENV:CACHIFY_HOST}%{ENV:CACHIFY_DIR}index.html -f - RewriteRule ^(.*) '; - -$ending = '/cache/cachify/%{ENV:CACHIFY_HOST}%{ENV:CACHIFY_DIR}index.html%{ENV:CACHIFY_SUFFIX} [L] -</IfModule> + RewriteCond ' . WP_CONTENT_DIR . '/cache/cachify/%{ENV:CACHIFY_HOST}%{ENV:CACHIFY_DIR}index.html%{ENV:CACHIFY_SUFFIX} -f + RewriteRule ^(.*) ' . wp_make_link_relative( content_url() ) . '/cache/cachify/%{ENV:CACHIFY_HOST}%{ENV:CACHIFY_DIR}index.html%{ENV:CACHIFY_SUFFIX} [L] + # END CACHIFY'; + +// phpcs:disable Squiz.PHP.EmbeddedPhp ?> - - - - - +

+

- - - - -
- - - -
- - -
    -
  • - -
  • -
  • - -
  • -
  • -
    -
    <IfModule mod_cache.c>
    +
    +
    +

    +
      +
    1. + +
    2. +
    3. + +
    4. +
    5. +
      +
      <IfModule mod_cache.c>
         CacheDisable /
       </IfModule>
      -
    6. -
    7. -
      -
      AddDefaultCharset UTF-8
      -
    8. -
-
- -
-
-			
-		
-
+ +
  • +
    +
    AddDefaultCharset UTF-8
    +
  • + diff --git a/inc/setup/cachify.hdd.nginx.php b/inc/setup/cachify.hdd.nginx.php index e0248f11..18f56d68 100644 --- a/inc/setup/cachify.hdd.nginx.php +++ b/inc/setup/cachify.hdd.nginx.php @@ -9,43 +9,15 @@ defined( 'ABSPATH' ) || exit; ?> - - - - - +

    +

    - - - - -
    - - - -
    - - -
      -
    • - ${http_host}', - '${host}' - ); - ?> -
    • -
    -
    - -
    +
     
     ()
    +
    +

    +
      +
    1. + ${http_host}', + '${host}' + ); + ?> +
    2. +
    diff --git a/inc/setup/cachify.memcached.nginx.php b/inc/setup/cachify.memcached.nginx.php index 5ed0f633..22a7a9ff 100644 --- a/inc/setup/cachify.memcached.nginx.php +++ b/inc/setup/cachify.memcached.nginx.php @@ -9,53 +9,10 @@ defined( 'ABSPATH' ) || exit; ?> - - - - - - - - - - -
    - - - -
    - - -
      -
    • - ${http_host}', - '${host}' - ); - ?> -
    • -
    • - memcached_pass localhost:11211;', - 'memcached_pass 127.0.0.1:11211;' - ); - ?> -
    • -
    -
    - -
    -## GZIP
    -gzip_static on;
    +

    +

    + () + +

    +
      +
    1. + ${http_host}', + '${host}' + ); + ?> +
    2. +
    3. + memcached_pass localhost:11211;', + 'memcached_pass 127.0.0.1:11211;' + ); + ?> +
    4. +
    diff --git a/js/admin-bar-flush.js b/js/admin-bar-flush.js new file mode 100644 index 00000000..081b3708 --- /dev/null +++ b/js/admin-bar-flush.js @@ -0,0 +1,83 @@ +/* global cachify_admin_bar_flush_ajax_object */ +( function() { + var is_flushing = false, + admin_bar_cachify_list_item = document.getElementById( 'wp-admin-bar-cachify' ), + flush_link = admin_bar_cachify_list_item.querySelector( 'a.ab-item' ), + fallback_url = flush_link.getAttribute( 'href' ), + aria_live_area = document.querySelector( '.ab-aria-live-area' ); + + // Replacing flush link with button because with AJAX action, it is semantically not a link anymore. + var button = document.createRange().createContextualFragment( '' ); + flush_link.parentNode.replaceChild( button, flush_link ); + + var admin_bar_icon = admin_bar_cachify_list_item.querySelector( '#wp-admin-bar-cachify .ab-icon' ); + + document.querySelector( '#wp-admin-bar-cachify .ab-item' ).addEventListener( 'click', flush ); + + admin_bar_icon.addEventListener( 'animationend', function() { + admin_bar_icon.classList.remove( 'animate-fade' ); + } ); + + function flush_icon_remove_classes() { + var classes = [ + 'animate-fade', + 'animate-pulse', + 'dashicons-trash', + 'dashicons-yes', + 'dashicons-yes-alt', + 'dashicons-dismiss', + ]; + + for ( var i = 0; i < classes.length; i++ ) { + admin_bar_icon.classList.remove( classes[i] ); + } + } + + function start_flush_icon_reset_timeout() { + setTimeout( function() { + flush_icon_remove_classes(); + admin_bar_icon.classList.add( 'animate-fade' ); + admin_bar_icon.classList.add( 'dashicons-trash' ); + is_flushing = false; + aria_live_area.textContent = ''; + }, 2000 ); + } + + function flush( event ) { + event.preventDefault(); + + if ( is_flushing ) { + return; + } + is_flushing = true; + aria_live_area.textContent = cachify_admin_bar_flush_ajax_object.flushing; + + if ( admin_bar_icon !== null ) { + flush_icon_remove_classes(); + admin_bar_icon.classList.add( 'animate-pulse' ); + admin_bar_icon.classList.add( 'dashicons-trash' ); + } + + var request = new XMLHttpRequest(); + request.addEventListener( 'load', function() { + if ( this.status === 200 ) { + start_flush_icon_reset_timeout(); + flush_icon_remove_classes(); + admin_bar_icon.classList.add( 'animate-fade' ); + admin_bar_icon.classList.add( cachify_admin_bar_flush_ajax_object.dashicon_success ); + aria_live_area.textContent = cachify_admin_bar_flush_ajax_object.flushed; + return; + } + + window.location = fallback_url; + } ); + + request.addEventListener( 'error', function() { + window.location = fallback_url; + } ); + + request.open( 'DELETE', cachify_admin_bar_flush_ajax_object.url ); + request.setRequestHeader( 'X-WP-Nonce', cachify_admin_bar_flush_ajax_object.nonce ); + request.send(); + } +}() ); diff --git a/package.json b/package.json index 51d27052..84a68ad6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "private": true, "devDependencies": { - "stylelint": "^13.13.1", - "@wordpress/stylelint-config": "^19.0.5" + "stylelint": "^16.9.0", + "eslint": "^8.57.1", + "@wordpress/eslint-plugin": "^21.2.0", + "@wordpress/stylelint-config": "^23.1.0" } } diff --git a/phpcs.xml b/phpcs.xml index be0ffbf5..356fca15 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -20,8 +20,12 @@ vendor/* - - + + + + + + @@ -29,6 +33,6 @@ - + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..1fc1d59a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + cachify.php + inc + + + inc/setup/ + inc/cachify.settings.php + inc/cachify.settings-footer.php + + + + + + + + + ./tests/ + + + + + + diff --git a/readme.txt b/readme.txt index 3cbc92e5..796aaeeb 100644 --- a/readme.txt +++ b/readme.txt @@ -1,22 +1,22 @@ # Cachify # * Contributors: pluginkollektiv * Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=TD4AMD2D8EMZW -* Tags: acceleration, cache, caching, minimize, performance, apc, disk, hdd, memcached, compression, minify, speed -* Requires at least: 4.4 -* Tested up to: 5.8 -* Requires PHP: 5.2.4 -* Stable tag: 2.3.2 +* Tags: cache, caching, performance, optimize, speed +* Requires at least: 4.7 +* Tested up to: 6.6 +* Requires PHP: 5.6 +* Stable tag: 2.4.0 * License: GPLv2 or later * License URI: http://www.gnu.org/licenses/gpl-2.0.html -Smart, efficient cache solution for WordPress. Use DB, HDD, APC or Memcached for storing your blog pages. Make WordPress faster! +Smart, efficient cache solution for WordPress. Use DB, HDD, APC, Redis or Memcached for storing your blog pages. Make WordPress faster! ## Description ## -*Cachify* optimizes your page loads by caching posts, pages and custom post types as static content. You can choose between caching via database, on the web server’s hard drive (HDD), Memcached (only on Nginx) or — thanks to APC (Alternative PHP Cache) — directly in the web server’s system cache. Whenever a page or post is loaded, it can be pulled directly from the cache. The amount of database queries and PHP requests will dramatically decrease towards zero, depending on the caching method you chose. +*Cachify* optimizes your page loads by caching posts, pages and custom post types as static content. You can choose between caching via database, on the web server’s hard drive (HDD), Memcached (only on Nginx), Redis or — thanks to APC (Alternative PHP Cache) — directly in the web server’s system cache. Whenever a page or post is loaded, it can be pulled directly from the cache. The amount of database queries and PHP requests will dramatically decrease towards zero, depending on the caching method you chose. ### Features ### * Works with custom post types. -* Caching methods: DB, HDD, APC and Memcached. +* Caching methods: DB, HDD, APC, Redis and Memcached. * “Flush Cache” button in the WordPress toolbar. * Ready for WordPress Multisite. * Optional compression of HTML markup. @@ -46,10 +46,10 @@ Smart, efficient cache solution for WordPress. Use DB, HDD, APC or Memcached for * If you don’t know how to install a plugin for WordPress, [here’s how](https://wordpress.org/support/article/managing-plugins/#installing-plugins). ### Requirements ### -* PHP 5.2.4 or greater -* WordPress 3.8 or greater -* APC 3.1.4 or greater (optional) +* PHP 5.6 or greater +* WordPress 4.7 or greater * Memcached in Nginx (optional) +* Redis (optional, via the phpredis module) ## Frequently Asked Questions ## @@ -69,8 +69,6 @@ If you use Cachify to store the cache on HDD there is no PHP to run. In the case ### Cachify with CDN support? ### Currently the caching plugin for WordPress has no connection to a CDN provider. Although the Buzzword CDN (Content Delivery Network) is praised as a performance factor, CDN makes little sense for WordPress websites with a national audience. In this case, a home host could provide the requested files faster than a worldwide CDN service provider because the next node could be far away. -### PHP OPcache as a caching method? ### -Compared to APC (Alternative PHP Cache), PHP OPcache is not able to contain content with custom keys and values. Because of this Cachify can not consider the PHP OPcache as a caching method. ### When does Cachify automaticaly flush its cache? ### * After publishing new posts @@ -102,6 +100,41 @@ A complete documentation is available in the [online handbook](https://cachify.p ## Changelog ## +### 2.4.0 ### + +Requires PHP 5.6 and WordPress 4.7 or above + +* New: introduce Redis (also KeyDB and Valkey) support using the _phpredis_ module (#253), (#252, props @newtovaux) +* New: add `cachify_modify_output` filter +* New: add `cachify_create_gzip_files` to disable creation of static GZip files (#262, props @angcl) +* New: add hooks `cachify_removed_cache_by_url` and `cachify_flushed_total_cache` for additional actions after clearing (#294, props @ouun) +* Removed: APC support (#304) +* Enhance: adjust styling for setup instructions (#215, props @timse201) +* Enhance: update hooks for Multisite initialization in WordPress 5.1 and above (#246, props @ouun) +* Enhance: rework flush hooks and add some third-party triggers for Autoptimize and WooCommerce (#225, props @timse201) +* Enhance: clean up some internal error suppressions (#256) +* Enhance: inform user on cache clear in admin bar (#257, props @angcl) +* Enhance: do not flush the cache for post revisions (#261, props @angcl) +* Enhance: prevent unnecessary cache clearing in some cases (#223) (#224, props @timse201) +* Enhance: remove empty directories when clearing the HDD cache (#289) +* Enhance: introduce common interface for caching backends (#298, props @lloc) +* Enhance: enhance examples for .htaccess and nginx configuration (#302) +* Enhance: show admin notice instead of silent fallback to DB cache, if selected backend is unavailable (#305) +* Enhance: disable gzip creation of required PHP extension is missing (#308) +* Enhance: various internal code clean ups +* Fix: invalidate cache when permalink changes (#285, #286, props @raffaelj) +* Fix: remove empty directories when pruning the HDD cache (#289) +* Fix: correctly add user-agent to robots.txt (#282) (#283) +* Fix: exclude _sitemap.xml_ from caching (#242) (#254) +* Fix: prevent cache generation of non-GET requests (#200) (#258) +* Fix: prevent cache generation of requests with status different from 200 OK (#266) (#267, props @karlkowald) +* Fix: prevent cache generation of non-HTML responses when using content negotiation (#265) (#273, props @Ancocodet) +* Fix: fix styling for various dark mode plugins (#264) (#278) +* Fix: fix SVG markup for icons in dashboard widget (#269, props @Latz) +* Fix: added missing .gz suffix in htaccess (#287) (#291, props @raffaelj) +* Fix: fix some brand names and unify spelling (#297, props @pedro-mendonca) +* Maintenance: Tested up to WordPress 6.6 + ### 2.3.2 ### * Fix: enforce WordPress environment for caching modules (#221, props timse201) * Fix: Remove unnecessary build artifacts from plugin deployment (#226) @@ -154,8 +187,8 @@ A complete documentation is available in the [online handbook](https://cachify.p * Fix for the PHP notice "Call to undefined function is_plugin_active_for_network" on WordPress Multisite ### 2.2.0 ### -* Toolbar: Display of the "Flush the cachify cache" button on the frontend -* Toolbar: Controlling the display of the "Flush the cachify cache" button via hook +* Toolbar: Display of the "Flush the Cachify cache" button on the frontend +* Toolbar: Controlling the display of the "Flush the Cachify cache" button via hook For the complete changelog, check out our [GitHub repository](https://github.com/pluginkollektiv/cachify). diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..4e5991f7 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,36 @@ +Test Me

    Test Content.

    ', + 3600, + false + ); + + $cached = Cachify_DB::get_item( '965b4abf2414e45036ab90c9d3f8dbc7' ); + self::assertIsArray( $cached, 'item was not stored' ); + self::assertEquals( + 'Test Me

    Test Content.

    ', + $cached['data'], + 'unexpected data in cache' + ); + self::assertIsInt( $cached['meta']['queries'], 'number of queries not filled' ); + self::assertIsString( $cached['meta']['timer'], 'timing not filled' ); + self::assertIsString( $cached['meta']['memory'], 'memory not filled' ); + self::assertIsInt( $cached['meta']['time'], 'time not filled' ); + + // Another item. + Cachify_DB::store_item( + 'ef7e4a0540f6cde19e6eb658c69b0064', + 'Test 2

    Test Content #2.

    ', + 3600, + false + ); + self::assertIsArray( Cachify_DB::get_item( 'ef7e4a0540f6cde19e6eb658c69b0064' ), 'second item was not stored' ); + + // Delete the first item. + Cachify_DB::delete_item( '965b4abf2414e45036ab90c9d3f8dbc7' ); + self::assertFalse( Cachify_DB::get_item( '965b4abf2414e45036ab90c9d3f8dbc7' ), 'first item was not deleted' ); + self::assertIsArray( Cachify_DB::get_item( 'ef7e4a0540f6cde19e6eb658c69b0064' ), 'second item should still be present' ); + } +} diff --git a/tests/test-cachify-hdd.php b/tests/test-cachify-hdd.php new file mode 100644 index 00000000..b573020a --- /dev/null +++ b/tests/test-cachify-hdd.php @@ -0,0 +1,132 @@ +Test Me

    Test Content.

    ', + 3600, // Ignored. + false + ); + self::assertTrue( Cachify_HDD::get_item( '965b4abf2414e45036ab90c9d3f8dbc7' ) ); + self::assertTrue( is_file( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/index.html' ) ); + $cached = file_get_contents( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/index.html' ); + self::assertStringStartsWith( + 'Test Me

    Test Content.

    + +', $cached ); + + // A subpage + self::go_to( '/testme/sub' ); + Cachify_HDD::store_item( + '965b4abf2414e45036ab90c9d3f8dbc7', // Ignored. + 'Test Me

    This is a subpage.

    ', + 3600, // Ignored. + false + ); + self::assertTrue( Cachify_HDD::get_item( '965b4abf2414e45036ab90c9d3f8dbc7' ) ); + self::assertTrue( is_file( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/sub/index.html' ) ); + + // Another item. + self::go_to( '/test2/' ); + Cachify_HDD::store_item( + 'ef7e4a0540f6cde19e6eb658c69b0064', // Ignored. + 'Test 2

    Test Content #2.

    ', + 3600, // Ignored. + true + ); + self::assertTrue( is_file( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/test2/index.html' ) ); + $cached = file_get_contents( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/test2/index.html' ); + self::assertStringStartsWith( + 'Test 2

    Test Content #2.

    + +', $cached ); + + // Delete the first item. + Cachify_HDD::delete_item( '965b4abf2414e45036ab90c9d3f8dbc7', 'http://example.org/testme/' ); + self::assertFalse( is_file( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/index.html' ), 'first item was not deleted' ); + self::assertTrue( is_file( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/test2/index.html' ), 'second item should still be present' ); + self::assertTrue( is_file( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/sub/index.html' ), 'subpage should now have been deleted' ); + + // Delete the subpage. + Cachify_HDD::delete_item( '965b4abf2414e45036ab90c9d3f8dbc7', 'http://example.org/testme/sub' ); + self::assertFalse( is_dir( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/sub/index.html' ), 'subpage item was not deleted' ); + self::assertFalse( is_dir( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme/sub' ), 'empty directory was not deleted' ); + + // Clear the cache. + Cachify_HDD::clear_cache(); + self::assertFalse( is_dir( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/testme' ), 'empty directory was not deleted' ); + self::assertFalse( is_dir( CACHIFY_CACHE_DIR . DIRECTORY_SEPARATOR . 'example.org/test2' ), 'second test page was not deleted' ); + self::assertFalse( Cachify_HDD::get_item( '965b4abf2414e45036ab90c9d3f8dbc7' ) ); + } +} diff --git a/tests/test-cachify-memcached.php b/tests/test-cachify-memcached.php new file mode 100644 index 00000000..84cc0b58 --- /dev/null +++ b/tests/test-cachify-memcached.php @@ -0,0 +1,34 @@ +unavailable_method, 'unexpected name of unavailable method' ); + $noop = new Cachify_NOOP(); + self::assertSame( '', $noop->unavailable_method, 'unexpected default name of unavailable method' ); + } + + /** + * Test the actual caching. + */ + public function test_caching() { + self::go_to( '/testme/' ); + Cachify_NOOP::store_item( + '965b4abf2414e45036ab90c9d3f8dbc7', + 'Test Me

    Test Content.

    ', + 3600, + false + ); + self::assertFalse( + Cachify_NOOP::get_item('965b4abf2414e45036ab90c9d3f8dbc7'), + "item should not have been stored" + ); + + Cachify_NOOP::delete_item( '965b4abf2414e45036ab90c9d3f8dbc7' ); + self::assertFalse( + Cachify_NOOP::get_item('965b4abf2414e45036ab90c9d3f8dbc7'), + "item present after deletion" + ); + } +} diff --git a/tests/test-cachify-redis.php b/tests/test-cachify-redis.php new file mode 100644 index 00000000..89b72f43 --- /dev/null +++ b/tests/test-cachify-redis.php @@ -0,0 +1,27 @@ + 10, + 'test_2' => 20, + ); + } + ); + + // Call flush registration. + Cachify::register_flush_cache_hooks(); + + // Verify that the filter has been called. + self::assertNotNull( $original_capture, 'Filter not called' ); + self::assertEquals( 12, count( $original_capture ), 'Unexpected number of default hooks' ); + self::assertEmpty( + array_filter( + $original_capture, + function( $v ) { + return 10 !== $v; + } + ), + 'All default filters should have priority 10' + ); + + // Verify that the action has been hooked with given priority. + self::assertEquals( + 10, + has_action( 'test_1', array( Cachify::class, 'flush_total_cache' ) ), + 'Flush action not hooked as expected' + ); + self::assertEquals( + 20, + has_action( 'test_2', array( Cachify::class, 'flush_total_cache' ) ), + 'Flush action not hooked as expected' + ); + } + + /** + * Test registration of scripts. + */ + public function test_register_scripts() { + Cachify::register_scripts(); + self::assertTrue( wp_script_is( 'cachify-admin-bar-flush', 'registered' ) ); + $script = wp_scripts()->registered['cachify-admin-bar-flush']; + self::assertStringEndsWith( + '/js/admin-bar-flush.min.js', + $script->src, + 'unexpected script source' + ); + } + + /** + * Test registration of styles. + */ + public function test_register_styles() { + Cachify::register_styles(); + self::assertTrue( wp_style_is( 'cachify-dashboard', 'registered' ) ); + self::assertTrue( wp_style_is( 'cachify-admin-bar-flush', 'registered' ) ); + + $style = wp_styles()->registered['cachify-dashboard']; + self::assertStringEndsWith( + '/css/dashboard.min.css', + $style->src, + 'unexpected dashboard style source' + ); + + $style = wp_styles()->registered['cachify-admin-bar-flush']; + self::assertStringEndsWith( + '/css/admin-bar-flush.min.css', + $style->src, + 'unexpected admin bar style source' + ); + } + + /** + * Test single site plugin activation. + */ + public function test_on_activation() { + self::assertFalse( get_option( 'cachify' ), 'Cachify option should not be initialized initially' ); + Cachify::on_activation(); + self::assertEquals( array() , get_option( 'cachify' ), 'Cachify option not initialized' ); + } + + + /** + * Test hook for robots.txt customization. + */ + public function test_robots_txt() { + // Initial robots.txt content. + $robots_txt = "User-agent: *\nDisallow: /wordpress/wp-admin/\nAllow: /wordpress/wp-admin/admin-ajax.php\n"; + + // DB cache enabled. + update_option( + 'cachify', + array( + 'use_apc' => Cachify::METHOD_DB, + 'change_robots_txt' => 1, + ) + ); + new Cachify(); + + self::assertEquals( + $robots_txt, + Cachify::robots_txt( $robots_txt ), + 'robots.txt should not be modified using DB cache' + ); + + // HDD cache enabled. + update_option( + 'cachify', + array( + 'use_apc' => Cachify::METHOD_HDD, + 'change_robots_txt' => 1, + ) + ); + new Cachify(); + + self::assertEquals( + $robots_txt . "\nUser-agent: *\nDisallow: */cache/cachify/\n", + Cachify::robots_txt( $robots_txt ), + 'robots.txt should have been modified using HDD cache' + ); + + // Disable robots.txt modification. + update_option( + 'cachify', + array( + 'use_apc' => Cachify::METHOD_HDD, + 'change_robots_txt' => 0, + ) + ); + + self::assertEquals( + $robots_txt . "\nUser-agent: *\nDisallow: */cache/cachify/\n", + Cachify::robots_txt( $robots_txt ), + 'robots.txt should have been modified using HDD cache' + ); + } + + /** + * Test call of hooks after flushing the cache. + */ + public function test_flushed_total_hook() { + $flushed_total = array(); + add_action( + 'cachify_flushed_total_cache', + function( $arg ) use ( &$flushed_total ) { + $flushed_total[] = $arg; + } + ); + + Cachify::flush_total_cache( ); + Cachify::flush_total_cache( false ); + Cachify::flush_total_cache( true ); + + self::assertEquals( + array( false, false, true ), + $flushed_total, + 'unexpected hook calls after flushing the cache' + ); + } + + /** + * Test call of hooks after remove cache by URL. + */ + public function test_removed_by_url_hook() { + $flushed_single = array(); + add_action( + 'cachify_removed_cache_by_url', + function( $url, $hash ) use ( &$flushed_single ) { + $flushed_single[] = array( $url, $hash ); + }, + 10, + 2 + ); + + Cachify::remove_page_cache_by_url( 'https://example.com/foo' ); + Cachify::remove_page_cache_by_url( 'https://example.com/bar' ); + + self::assertEquals( + array( + array( 'https://example.com/foo', '45cc6f45ed67ff733a550ceb93ac2694.cachify' ), + array( 'https://example.com/bar', '7b579c8fd2d8d8685f9680e7f5fedadc.cachify' ), + ), + $flushed_single, + 'unexpected hook calls after removing by url' + ); + + } +}