diff --git a/.distignore b/.distignore index 5ddd085..62683f6 100644 --- a/.distignore +++ b/.distignore @@ -2,7 +2,9 @@ /.git /.github /.wordpress-org +/bin /node_modules +/tests /vendor # Files @@ -17,4 +19,5 @@ /package-lock.json /composer.lock /phpcs.xml +/phpunit.xml /README.md diff --git a/.gitattributes b/.gitattributes index 74f6667..24aae2f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,9 @@ # Directories /.github export-ignore /.wordpress-org export-ignore +/bin export-ignore /node_modules export-ignore +/tests export-ignore /vendor export-ignore # Files @@ -19,4 +21,5 @@ /package-lock.json export-ignore /composer.lock export-ignore /phpcs.xml export-ignore +/phpunit.xml export-ignore /README.md export-ignore diff --git a/.travis.yml b/.travis.yml index 4cf9df8..229294d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,19 @@ language: php -php: - - '5.6' - - '7.0' - - '7.1' - - '7.2' - - '7.3' - - '7.4' - - nightly +services: + - mysql matrix: + include: + - php: 5.6 + env: WP_VERSION=4.7 + - php: 7.0 + - php: 7.1 + - php: 7.2 + - php: 7.3 + - php: 7.4 + env: WP_VERSION=5.6 + - php: nightly allow_failures: - php: nightly @@ -19,10 +23,18 @@ before_install: before_script: - composer install - npm install + - | + if [[ ! -z "$WP_VERSION" ]] ; then + bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + fi script: - composer build - composer lint-all + - | + if [[ ! -z "$WP_VERSION" ]] ; then + composer test + fi notifications: email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index e2adaa2..79265a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.8.1 +* Fix AMP compatibility for Standard and Transitional mode (#181) (#182) +* JavaScript is no longer embedded if request is served by AMP (#181) (#182) +* Always register the action for the cleanup (#184) +* Exclude sitemap calls (WP 5.5+) from tracking (#185) (#186) +* Tested up to WordPress 5.6 + ## 1.8.0 * Fix date offset in dashboard widget in WP 5.3+ environments with mixed timezones (#167) * Allow to deactivate the nonce check during JavaScript tracking (#168) diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100644 index 0000000..878881f --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,152 @@ +#!/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]+$ ]]; 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-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/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.github.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 + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet 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/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 + +} + +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 + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 1ffe409..4833719 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "slowprog/composer-copy-file": "^0.3", "squizlabs/php_codesniffer": "^3.5", "phpcompatibility/php-compatibility": "^9.3", - "wp-coding-standards/wpcs": "^2.3" + "wp-coding-standards/wpcs": "^2.3", + "phpunit/phpunit": "^5" }, "repositories": [ { @@ -62,11 +63,17 @@ "lint-php": [ "phpcs --standard=phpcs.xml -s" ], + "lint-tests": [ + "phpcs --standard=tests/phpcs.xml tests" + ], "minify": [ "minifycss css/dashboard.css > css/dashboard.min.css", "minifyjs js/dashboard.js > js/dashboard.min.js", "minifyjs js/snippet.js > js/snippet.min.js", "minifycss vendor/npm-asset/chartist-plugin-tooltips-updated/dist/chartist-plugin-tooltip.css > css/chartist-plugin-tooltip.min.css" + ], + "test": [ + "phpunit" ] }, "extra": { diff --git a/composer.lock b/composer.lock index 7fdd628..74d4977 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c7637130c9cb589c3e2d63215d0fc883", + "content-hash": "26df26d7fc1bbe770a956bbef72d97fc", "packages": [ { "name": "npm-asset/chartist", @@ -101,6 +101,60 @@ ], "time": "2020-06-25T14:57:39+00:00" }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, { "name": "matthiasmullie/minify", "version": "1.3.63", @@ -210,6 +264,51 @@ ], "time": "2019-02-05T23:41:09+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-10-19T19:58:43+00:00" + }, { "name": "phpcompatibility/php-compatibility", "version": "9.3.5", @@ -269,34 +368,36 @@ "time": "2019-12-27T09:44:58+00:00" }, { - "name": "slowprog/composer-copy-file", - "version": "0.3.3", + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/slowprog/CopyFile.git", - "reference": "b02d55f7587577f29d355f0ce7b697f66dffc0af" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slowprog/CopyFile/zipball/b02d55f7587577f29d355f0ce7b697f66dffc0af", - "reference": "b02d55f7587577f29d355f0ce7b697f66dffc0af", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=5.5" }, "require-dev": { - "composer/composer": "1.0.*@dev", - "mikey179/vfsstream": "~1", - "php-mock/php-mock-phpunit": "~1", - "phpunit/phpunit": "5.7.27", - "symfony/filesystem": "~2.7", - "symfony/finder": "~2.7" + "phpunit/phpunit": "^4.6" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "SlowProg\\CopyFile\\": "" + "phpDocumentor\\Reflection\\": [ + "src" + ] } }, "notification-url": "https://packagist.org/downloads/", @@ -305,67 +406,1377 @@ ], "authors": [ { - "name": "Andrey Tyshev", - "email": "slowprog@gmail.com" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Composer script copying your files after install", - "homepage": "https://github.com/SlowProg/composer-copy-file", + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", "keywords": [ - "copy file" + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" ], - "time": "2020-07-02T14:44:41+00:00" + "time": "2017-09-11T18:02:19+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.5.5", + "name": "phpdocumentor/reflection-docblock", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", + "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", + "php": "^5.6 || ^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-11-10T14:09:06+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.10.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "451c3cd1418cf640de218914901e51b064abb093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-03-05T15:02:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "shasum": "" + }, + "require": { + "ext-dom": "*", "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", + "phpunit/php-token-stream": "^1.4.2 || ^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "^1.0 || ^2.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" + "suggest": { + "ext-xdebug": "^2.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2017-04-02T07:44:40+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.4.x-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { - "name": "Greg Sherwood", + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", "role": "lead" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ - "phpcs", - "standards" + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "abandoned": true, + "time": "2017-12-04T08:55:13+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "5.7.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.4", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "^1.2.4", + "sebastian/diff": "^1.4.3", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.1", + "sebastian/object-enumerator": "~2.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "^1.0.6|^2.0.1", + "symfony/yaml": "~2.1|~3.0|~4.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2018-02-01T05:50:59+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "abandoned": true, + "time": "2017-06-30T09:13:00+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29T09:50:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-05-22T07:24:03+00:00" + }, + { + "name": "sebastian/environment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-11-26T07:53:53+00:00" + }, + { + "name": "sebastian/exporter", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-11-19T08:54:04+00:00" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12T03:26:01+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-02-18T15:18:39+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-11-19T07:33:16+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "slowprog/composer-copy-file", + "version": "0.3.3", + "source": { + "type": "git", + "url": "https://github.com/slowprog/CopyFile.git", + "reference": "b02d55f7587577f29d355f0ce7b697f66dffc0af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slowprog/CopyFile/zipball/b02d55f7587577f29d355f0ce7b697f66dffc0af", + "reference": "b02d55f7587577f29d355f0ce7b697f66dffc0af", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "composer/composer": "1.0.*@dev", + "mikey179/vfsstream": "~1", + "php-mock/php-mock-phpunit": "~1", + "phpunit/phpunit": "5.7.27", + "symfony/filesystem": "~2.7", + "symfony/finder": "~2.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "SlowProg\\CopyFile\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andrey Tyshev", + "email": "slowprog@gmail.com" + } + ], + "description": "Composer script copying your files after install", + "homepage": "https://github.com/SlowProg/composer-copy-file", + "keywords": [ + "copy file" + ], + "time": "2020-07-02T14:44:41+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.6", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2020-08-10T04:50:15+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "ec3c2ac4d881a4684c1f0317d2107f1a4152bad9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ec3c2ac4d881a4684c1f0317d2107f1a4152bad9", + "reference": "ec3c2ac4d881a4684c1f0317d2107f1a4152bad9", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-18T15:58:55+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" ], - "time": "2020-04-17T01:09:41+00:00" + "time": "2020-07-08T17:02:28+00:00" }, { "name": "wp-coding-standards/wpcs", diff --git a/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 6394886..6199dad 100644 --- a/inc/class-statify-frontend.php +++ b/inc/class-statify-frontend.php @@ -252,9 +252,10 @@ private static function is_bot( $user_agent ) { * @return boolean $skip_hook TRUE if NO tracking is desired */ private static function _is_internal() { - // Skip for preview, 404 calls, feed, search and favicon access. + // Skip for preview, 404 calls, feed, search, favicon and sitemap access. return is_preview() || is_404() || is_feed() || is_search() - || ( function_exists( 'is_favicon' ) && is_favicon() ); + || ( function_exists( 'is_favicon' ) && is_favicon() ) + || '' !== get_query_var( 'sitemap' ) || '' !== get_query_var( 'sitemap-stylesheet' ); } /** @@ -374,8 +375,12 @@ public static function query_vars( $vars ) { * @version 1.4.1 */ public static function wp_footer() { - // Skip by option. - if ( ! self::is_javascript_tracking_enabled() ) { + // JS tracking disabled or AMP is used for the current request. + if ( + ! self::is_javascript_tracking_enabled() || + ( function_exists( 'amp_is_request' ) && amp_is_request() ) || + ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) + ) { return; } @@ -404,7 +409,30 @@ public static function wp_footer() { } /** - * Add amp-analytics. + * Add amp-analytics for Standard and Transitional mode. + * + * @see https://amp-wp.org/documentation/playbooks/analytics/ + * + * @param array $analytics_entries Analytics entries. + */ + public static function amp_analytics_entries( $analytics_entries ) { + if ( ! is_array( $analytics_entries ) ) { + $analytics_entries = array(); + } + + // Analytics script is only relevant, if "JS" tracking is enabled, to prevent double tracking. + if ( self::is_javascript_tracking_enabled() ) { + $analytics_entries['statify'] = array( + 'type' => '', + 'config' => wp_json_encode( self::make_amp_config() ), + ); + } + + return $analytics_entries; + } + + /** + * Add AMP-analytics for Reader mode. * * @see https://amp-wp.org/documentation/playbooks/analytics/ * @@ -420,32 +448,40 @@ public static function amp_post_template_analytics( $analytics ) { $analytics['statify'] = array( 'type' => '', 'attributes' => array(), - 'config_data' => array( - 'extraUrlParams' => array( - 'action' => 'statify_track', - '_ajax_nonce' => wp_create_nonce( 'statify_track' ), - 'statify_referrer' => '${documentReferrer}', - 'statify_target' => '${canonicalPath}amp/', - ), - 'requests' => array( - 'event' => admin_url( 'admin-ajax.php' ), - ), - 'triggers' => array( - 'trackPageview' => array( - 'on' => 'visible', - 'request' => 'event', - 'vars' => array( - 'eventId' => 'pageview', - ), - ), - ), - 'transport' => array( - 'xhrpost' => true, - ), - ), + 'config_data' => self::make_amp_config(), ); } return $analytics; } + + /** + * Generate AMP-analytics configuration. + * + * @return array Configuration array. + */ + private static function make_amp_config() { + return array( + 'requests' => array( + 'pageview' => admin_url( 'admin-ajax.php' ), + ), + 'extraUrlParams' => array( + 'action' => 'statify_track', + '_ajax_nonce' => wp_create_nonce( 'statify_track' ), + 'statify_referrer' => '${documentReferrer}', + 'statify_target' => '${canonicalPath}amp/', + ), + 'triggers' => array( + 'trackPageview' => array( + 'on' => 'visible', + 'request' => 'pageview', + ), + ), + 'transport' => array( + 'beacon' => true, + 'xhrpost' => true, + 'image' => false, + ), + ); + } } diff --git a/inc/class-statify.php b/inc/class-statify.php index 1fb38f5..3044090 100644 --- a/inc/class-statify.php +++ b/inc/class-statify.php @@ -60,13 +60,14 @@ public static function init() { ) ); + // Cron. + add_action( 'statify_cleanup', array( 'Statify_Cron', 'cleanup_data' ) ); + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { add_action( 'wp_ajax_nopriv_statify_track', array( 'Statify_Frontend', 'track_visit_ajax' ) ); add_action( 'wp_ajax_statify_track', array( 'Statify_Frontend', 'track_visit_ajax' ) ); } elseif ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { // XMLRPC. add_filter( 'xmlrpc_methods', array( 'Statify_XMLRPC', 'xmlrpc_methods' ) ); - } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) { // Cron. - add_action( 'statify_cleanup', array( 'Statify_Cron', 'cleanup_data' ) ); } elseif ( is_admin() ) { // Backend. add_action( 'wpmu_new_blog', array( 'Statify_Install', 'init_site' ) ); add_action( 'delete_blog', array( 'Statify_Uninstall', 'init_site' ) ); @@ -80,7 +81,9 @@ public static function init() { add_action( 'template_redirect', array( 'Statify_Frontend', 'track_visit' ) ); add_filter( 'query_vars', array( 'Statify_Frontend', 'query_vars' ) ); add_action( 'wp_footer', array( 'Statify_Frontend', 'wp_footer' ) ); - if ( function_exists( 'is_amp_endpoint' ) ) { // Automattic AMP plugin present. + if ( function_exists( 'amp_is_request' ) || function_exists( 'is_amp_endpoint' ) ) { + // Automattic AMP plugin present. + add_filter( 'amp_analytics_entries', array( 'Statify_Frontend', 'amp_analytics_entries' ) ); add_filter( 'amp_post_template_analytics', array( 'Statify_Frontend', 'amp_post_template_analytics' ) ); } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..cfee8b1 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + ./tests/ + + + + + + statify.php + inc + + + diff --git a/readme.txt b/readme.txt index 9ad8145..54ac458 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ * Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=TD4AMD2D8EMZW * Tags: analytics, dashboard, pageviews, privacy, statistics, stats, visits, web stats, widget * Requires at least: 4.7 -* Tested up to: 5.5 +* Tested up to: 5.6 * Requires PHP: 5.2 -* Stable tag: 1.8.0 +* Stable tag: 1.8.1 * License: GPLv3 or later * License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -65,6 +65,7 @@ If you've problems or think you’ve found a bug (e.g. you’re experiencing une * views by logged in users (unless tracking is activated via the settings page) * error pages * favicon (as of WP 5.4) +* sitemap (as of WP 5.5) This behavior can be modified with the `statify__skip_tracking` hook. @@ -116,6 +117,13 @@ has to be added to the theme's `functions.php`. The condition has modified such ## Changelog ## You can find the full changelog in [our GitHub repository](https://github.com/pluginkollektiv/statify/blob/master/CHANGELOG.md). +### 1.8.1 +* Fix AMP compatibility for Standard and Transitional mode (#181) (#182) +* JavaScript is no longer embedded if request is served by AMP (#181) (#182) +* Always register the action for the cleanup (#184) +* Exclude sitemap calls (WP 5.5+) from tracking (#185) (#186) +* Tested up to WordPress 5.6 + ### 1.8.0 * Fix date offset in dashboard widget in WP 5.3+ environments with mixed timezones (#167) * Allow to deactivate the nonce check during JavaScript tracking (#168) @@ -155,6 +163,9 @@ For the complete changelog, check out our [GitHub repository](https://github.com ## Upgrade Notice ## +### 1.8.1 ### +This is a bugfix release improving AMP compatibility and excluding native sitemaps as of WordPress 5.5. It is recommended for all users. + ### 1.8.0 ### Some minor improvements. The most important one: This version offers to deactivate the nonce check for JavaScript tracking (recommend if a caching plugin with a long caching time is used). diff --git a/statify.php b/statify.php index 96988b4..65ab105 100644 --- a/statify.php +++ b/statify.php @@ -7,7 +7,7 @@ * Author URI: https://pluginkollektiv.org * Plugin URI: https://wordpress.org/plugins/statify/ * License: GPLv3 or later - * Version: 1.8.0 + * Version: 1.8.1 * * @package WordPress */ @@ -20,7 +20,7 @@ define( 'STATIFY_FILE', __FILE__ ); define( 'STATIFY_DIR', dirname( __FILE__ ) ); define( 'STATIFY_BASE', plugin_basename( __FILE__ ) ); -define( 'STATIFY_VERSION', '1.8.0' ); +define( 'STATIFY_VERSION', '1.8.1' ); /* Hooks */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..480d06d --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,35 @@ + + + Coding standard for WordPress plugin tests + + + + + + + + + tests + + + + + + + + + + + + + diff --git a/tests/test-ajax-tracking.php b/tests/test-ajax-tracking.php new file mode 100644 index 0000000..1483703 --- /dev/null +++ b/tests/test-ajax-tracking.php @@ -0,0 +1,173 @@ +init_statify_tracking(); + + try { + $this->_handleAjax( 'nopriv_statify_track' ); + } catch ( WPAjaxDieStopException $e ) { + // Expected exception. + } + + $this->assertFalse( isset( $e ), 'AJAX should not fail for valid request without JS enabled' ); + + // Get the stats and assert emptiness. + $stats = Statify_Dashboard::get_stats(); + $this->assertNull( $stats, 'Stats should be empty, i.e. visit should not have been tracked' ); + + // Now enable JS tracking. + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_JAVASCRIPT_WITH_NONCE_CHECK ); + + try { + $_POST['_wpnonce'] = wp_create_nonce( 'statify_track' ); + $this->_handleAjax( 'nopriv_statify_track' ); + } catch ( WPAjaxDieStopException $e ) { + // Expected exception. + } + $this->assertTrue( isset( $e ), 'AJAX should have stopped' ); + $this->assertEquals( 0, $e->getCode(), 'Unexpected exit code after AJAX processing' ); + + $stats = $this->get_stats(); + $this->assertNotNull( $stats, 'Stats should be filled after tracking' ); + + $this->assertEquals( 1, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + $this->assertEquals( ( new DateTime() )->format( 'Y-m-d' ), $stats['visits'][0]['date'], 'Unexpected date of tracking' ); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Unexpected visit count' ); + + $this->assertEquals( 1, count( $stats['target'] ), 'Unexpected number of targets' ); + $this->assertEquals( '', $stats['target'][0]['url'], 'Unexpected target URL' ); + $this->assertEquals( 1, $stats['target'][0]['count'], 'Unexpected target count' ); + + $this->assertEquals( 1, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 'https://statify.pluginkollektiv.org/', $stats['referrer'][0]['url'], 'Unexpected referrer URL' ); + $this->assertEquals( 'statify.pluginkollektiv.org', $stats['referrer'][0]['host'], 'Unexpected referrer hostname' ); + $this->assertEquals( 1, $stats['referrer'][0]['count'], 'Unexpected referrer count' ); + + unset( $e ); + + // Simulate outdated, i.e. invalid nonce. + try { + $_POST['_wpnonce'] = $_POST['_wpnonce'] . '-old'; + $this->_handleAjax( 'nopriv_statify_track' ); + } catch ( WPAjaxDieStopException $e ) { + // Expected exception. + } + $this->assertTrue( isset( $e ), 'AJAX should have stopped' ); + $this->assertEquals( 0, $e->getCode(), 'Unexpected exit code after AJAX processing' ); + + // Numbers should not have been increased. + $stats = $this->get_stats(); + $this->assertEquals( 1, count( $stats['visits'] ), 'Number of days with visits should not be higher after AJAX request failed' ); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Visit count should not be higher after AJAX request failed' ); + + unset( $e ); + + // Disable nonce verification. + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_JAVASCRIPT_WITHOUT_NONCE_CHECK ); + + try { + $_POST['_wpnonce'] = $_POST['_wpnonce'] . '-old'; + $this->_handleAjax( 'nopriv_statify_track' ); + } catch ( WPAjaxDieStopException $e ) { + // Expected exception. + } + $this->assertTrue( isset( $e ), 'AJAX should have stopped' ); + $this->assertEquals( 0, $e->getCode(), 'Unexpected exit code after AJAX processing' ); + + // Numbers should not have been increased. + $stats = $this->get_stats(); + $this->assertEquals( 1, count( $stats['visits'] ), 'Number of days with visits should not be highered' ); + $this->assertEquals( 2, $stats['visits'][0]['count'], 'Visit count should be higher after successful AJAX request without nonce' ); + + unset( $e ); + + // Now we are logged in. + wp_set_current_user( 1 ); + + try { + $_POST['_wpnonce'] = wp_create_nonce( 'statify_track' ); + $this->_handleAjax( 'statify_track' ); + } catch ( WPAjaxDieStopException $e ) { + // Expected exception. + } + $this->assertTrue( isset( $e ), 'AJAX should have stopped' ); + $this->assertEquals( 0, $e->getCode(), 'Unexpected exit code after AJAX processing' ); + + $stats = $this->get_stats(); + $this->assertNotNull( $stats, 'Stats should be filled after tracking' ); + + // Numbers should not have been increased. + $this->assertEquals( 1, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + $this->assertEquals( 2, $stats['visits'][0]['count'], 'Unexpected visit count' ); + $this->assertEquals( 1, count( $stats['target'] ), 'Unexpected number of targets' ); + $this->assertEquals( 2, $stats['target'][0]['count'], 'Unexpected target count' ); + $this->assertEquals( 1, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 2, $stats['referrer'][0]['count'], 'Unexpected referrer count' ); + + unset( $e ); + + // Now we allow tracking for logged-in users. + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_JAVASCRIPT_WITH_NONCE_CHECK, true ); + + try { + $_POST['_wpnonce'] = wp_create_nonce( 'statify_track' ); + $this->_handleAjax( 'statify_track' ); + } catch ( WPAjaxDieStopException $e ) { + // Expected exception. + } + $this->assertTrue( isset( $e ), 'AJAX should have stopped' ); + $this->assertEquals( 0, $e->getCode(), 'Unexpected exit code after AJAX processing' ); + + $stats = $this->get_stats(); + $this->assertNotNull( $stats, 'Stats should be filled after tracking' ); + + // Numbers should not have been increased. + $this->assertEquals( 1, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + $this->assertEquals( 3, $stats['visits'][0]['count'], 'Unexpected visit count' ); + $this->assertEquals( 1, count( $stats['target'] ), 'Unexpected number of targets' ); + $this->assertEquals( 3, $stats['target'][0]['count'], 'Unexpected target count' ); + $this->assertEquals( 1, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 3, $stats['referrer'][0]['count'], 'Unexpected referrer count' ); + } +} diff --git a/tests/test-cron.php b/tests/test-cron.php new file mode 100644 index 0000000..e6f8885 --- /dev/null +++ b/tests/test-cron.php @@ -0,0 +1,76 @@ +init_statify_widget( 3 ); + $this->assertNotFalse( + has_action( 'statify_cleanup', array( 'Statify_Cron', 'cleanup_data' ) ), + 'Statify cleanup cron job should be registered in normal cycle (always)' + ); + + // Initialize cron cycle. + define( 'DOING_CRON', true ); + Statify::init(); + $this->assertNotFalse( + has_action( 'statify_cleanup', array( 'Statify_Cron', 'cleanup_data' ) ), + 'Statify cleanup cron job was not registered' + ); + + // Insert some test data, 2 entries over the last 5 days (including today). + $date = new DateTime(); + $dates = array(); + for ( $i = 0; $i < 5; $i ++ ) { + $dates[] = $date->format( 'Y-m-d' ); + $this->insert_test_data( $date->format( 'Y-m-d' ), '', '', 2 ); + $date->modify( '-1 days' ); + } + + // Make sure our test data is correct. + $stats = $this->get_stats(); + $this->assertEquals( 5, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + foreach ( $stats['visits'] as $v ) { + $this->assertContains( $v['date'], $dates, 'Unexpected creation date in stats' ); + $this->assertEquals( 2, $v['count'], 'Unexpected visit count' ); + } + + // Run the cron job. + Statify_Cron::cleanup_data(); + + // Verify that 2 days have been deleted. + $stats = $this->get_stats(); + $this->assertEquals( 3, count( $stats['visits'] ), 'Unexpected number of days with visits after cleanup' ); + $remaining_dates = array_slice( $dates, 0, 3 ); + foreach ( $stats['visits'] as $v ) { + $this->assertContains( $v['date'], $remaining_dates, 'Unexpected remaining date in stats' ); + $this->assertEquals( 2, $v['count'], 'Unexpected visit count' ); + } + } +} diff --git a/tests/test-dashboard.php b/tests/test-dashboard.php new file mode 100644 index 0000000..48b0aa5 --- /dev/null +++ b/tests/test-dashboard.php @@ -0,0 +1,260 @@ +assertFalse( + has_action( 'admin_print_styles', array( Statify_Dashboard::class, 'add_style' ) ), + 'Styles unexpectedly added' + ); + $this->assertFalse( + has_action( 'admin_print_scripts', array( Statify_Dashboard::class, 'add_js' ) ), + 'Scripts unexpectedly added' + ); + + // The current user gets the "edit_dashboard" capability. + wp_get_current_user()->add_cap( 'edit_dashboard' ); + wp_set_current_user( 1 ); + + Statify_Dashboard::init(); + $this->assertCount( 5, $widget_capture, 'No widget registered' ); + $this->assertEquals( 'statify_dashboard', $widget_capture['widget_id'], 'Unexpected widget ID' ); + $this->assertEquals( 'Statify', $widget_capture['widget_name'], 'Unexpected widget name' ); + $this->assertEquals( + array( Statify_Dashboard::class, 'print_frontview' ), + $widget_capture['callback'], + 'Unexpected widget callback' + ); + $this->assertEquals( + array( Statify_Dashboard::class, 'print_backview' ), + $widget_capture['control_callback'], + 'Unexpected control callback' + ); + $this->assertNotFalse( + has_action( 'admin_print_styles', array( Statify_Dashboard::class, 'add_style' ) ), + 'Styles not added' + ); + $this->assertNotFalse( + has_action( 'admin_print_scripts', array( Statify_Dashboard::class, 'add_js' ) ), + 'Scripts not added' + ); + } + + /** + * Test evaluation of the statify__user_can_see_stats hook. + */ + public function test_user_can_see_stats_hook() { + global $widget_capture; + + // Add a custom filter that captures the original result and overrides the response. + $original_capture = null; + $override = true; + add_filter( + 'statify__user_can_see_stats', + function ( $original ) use ( &$original_capture, &$override ) { + $original_capture = $original; + + return $override; + } + ); + + // No Capability, but override to TRUE. + wp_set_current_user( 0 ); + wp_get_current_user()->remove_all_caps(); + $widget_capture = array(); + $override = true; + + Statify_Dashboard::init(); + $this->assertCount( 5, $widget_capture, 'Widget should not have been registered' ); + $this->assertFalse( $original_capture, 'Expected original result to be FALSE' ); + + // With capability, but overridden to FALSE. + $widget_capture = array(); + wp_get_current_user()->add_cap( 'edit_dashboard' ); + $override = false; + + Statify_Dashboard::init(); + $this->assertEmpty( $widget_capture, 'Widget should not have been registered' ); + $this->assertTrue( $original_capture, 'Expected original result to be TRUE' ); + } + + /** + * Test prepared stats data. + */ + public function test_get_stats() { + // Initially the database is empty. + $this->assertNull( $this->get_stats(), 'Expected NULL stats for empty database' ); + + // Now insert data for the last 3 days. + $date1 = new DateTime(); + $date2 = ( new DateTime() )->modify( '-1 days' ); + $date3 = ( new DateTime() )->modify( '-2 days' ); + + $this->insert_test_data( $date1->format( 'Y-m-d' ), 'https://statify.pluginkollektiv.org/', '/', 3 ); + $this->insert_test_data( $date1->format( 'Y-m-d' ), 'https://statify.pluginkollektiv.org/', '/test/', 4 ); + $this->insert_test_data( $date1->format( 'Y-m-d' ), 'https://pluginkollektiv.org/', '', 1 ); + + $this->insert_test_data( $date2->format( 'Y-m-d' ), 'https://pluginkollektiv.org/', '/', 1 ); + $this->insert_test_data( $date2->format( 'Y-m-d' ), 'https://statify.pluginkollektiv.org/documentation/', '/test/', 2 ); + $this->insert_test_data( $date2->format( 'Y-m-d' ), 'https://wordpress.org/plugins/statify/', '', 1 ); + + $this->insert_test_data( $date3->format( 'Y-m-d' ), 'https://pluginkollektiv.org/', '', 2 ); + $this->insert_test_data( $date3->format( 'Y-m-d' ), '', '/', 1 ); + + // Initialize with default configuration, all limits greater data dimension. + Statify::init(); + $this->init_statify_widget( 14, 14, 3, false, false ); + $stats = $this->get_stats(); + + $this->assertEquals( 3, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + $this->assertEquals( $date3->format( 'Y-m-d' ), $stats['visits'][0]['date'], 'Unexpected date of tracking 2 days ago' ); + $this->assertEquals( 3, $stats['visits'][0]['count'], 'Unexpected number of visits 2 days ago' ); + $this->assertEquals( $date2->format( 'Y-m-d' ), $stats['visits'][1]['date'], 'Unexpected date of tracking yesterday' ); + $this->assertEquals( 4, $stats['visits'][1]['count'], 'Unexpected number of visits yesterday' ); + $this->assertEquals( $date1->format( 'Y-m-d' ), $stats['visits'][2]['date'], 'Unexpected date of tracking today' ); + $this->assertEquals( 8, $stats['visits'][2]['count'], 'Unexpected number of visits today' ); + + $this->assertEquals( 3, count( $stats['target'] ), 'Unexpected number of top targets' ); + $this->assertEquals( '/test/', $stats['target'][0]['url'], 'Unexpected 1st target path' ); + $this->assertEquals( 6, $stats['target'][0]['count'], 'Unexpected 1st target count' ); + $this->assertEquals( '/', $stats['target'][1]['url'], 'Unexpected 2nd target path' ); + $this->assertEquals( 5, $stats['target'][1]['count'], 'Unexpected 2nd target count' ); + $this->assertEquals( '', $stats['target'][2]['url'], 'Unexpected 3rd target path' ); + $this->assertEquals( 4, $stats['target'][2]['count'], 'Unexpected 3rd target count' ); + + $this->assertEquals( 3, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 9, $stats['referrer'][0]['count'], 'Unexpected referrer URL' ); + /* Top referrer URL is "https://statify.pluginkollektiv.org/". As we aggregate by host, the reported URL however + depends on the DB server, so it might be ".../documentation/", too. Just check the prefix here. */ + $this->assertEquals( 'https://statify.pluginkollektiv.org/', substr( $stats['referrer'][0]['url'], 0, 36 ), 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'statify.pluginkollektiv.org', $stats['referrer'][0]['host'], 'Unexpected 1st referrer hostname' ); + $this->assertEquals( 4, $stats['referrer'][1]['count'], 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'https://pluginkollektiv.org/', $stats['referrer'][1]['url'], 'Unexpected 2nd referrer URL' ); + $this->assertEquals( 'pluginkollektiv.org', $stats['referrer'][1]['host'], 'Unexpected 3rd referrer hostname' ); + $this->assertEquals( 1, $stats['referrer'][2]['count'], 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'https://wordpress.org/plugins/statify/', $stats['referrer'][2]['url'], 'Unexpected 3rd referrer URL' ); + $this->assertEquals( 'wordpress.org', $stats['referrer'][2]['host'], 'Unexpected 3rd referrer hostname' ); + + $this->assertArrayNotHasKey( 'visit_totals', $stats, 'Totals should not be provided, if not configured' ); + + // Top lists only for today. + $this->init_statify_widget( 14, 14, 3, true, false ); + $stats2 = $this->get_stats(); + + $this->assertEquals( $stats['visits'], $stats2['visits'], 'Visit counts should not be affected by "today" switch' ); + + $this->assertEquals( 3, count( $stats['target'] ), 'Unexpected number of top targets' ); + $this->assertEquals( '/test/', $stats2['target'][0]['url'], 'Unexpected 1st target path' ); + $this->assertEquals( 4, $stats2['target'][0]['count'], 'Unexpected 1st target count' ); + $this->assertEquals( '/', $stats2['target'][1]['url'], 'Unexpected 2nd target path' ); + $this->assertEquals( 3, $stats2['target'][1]['count'], 'Unexpected 2nd target count' ); + $this->assertEquals( '', $stats2['target'][2]['url'], 'Unexpected 3rd target path' ); + $this->assertEquals( 1, $stats2['target'][2]['count'], 'Unexpected 3rd target count' ); + + $this->assertEquals( 2, count( $stats2['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 7, $stats2['referrer'][0]['count'], 'Unexpected referrer URL' ); + $this->assertEquals( 'https://statify.pluginkollektiv.org/', $stats2['referrer'][0]['url'], 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'statify.pluginkollektiv.org', $stats2['referrer'][0]['host'], 'Unexpected 1st referrer hostname' ); + $this->assertEquals( 1, $stats2['referrer'][1]['count'], 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'https://pluginkollektiv.org/', $stats2['referrer'][1]['url'], 'Unexpected 2nd referrer URL' ); + $this->assertEquals( 'pluginkollektiv.org', $stats2['referrer'][1]['host'], 'Unexpected 3rd referrer hostname' ); + + $this->assertArrayNotHasKey( 'visit_totals', $stats2, 'Totals should not be provided, if not configured' ); + + // Limited display range of 2 days with total numbers. + $this->init_statify_widget( 14, 2, 3, false, true ); + $stats3 = $this->get_stats(); + + $this->assertEquals( + array_slice( $stats['visits'], 1 ), + $stats3['visits'], + 'Stats for 2 days should be equal to the slice of complete data' + ); + + $this->assertEquals( 3, count( $stats3['target'] ), 'Unexpected number of top targets' ); + $this->assertEquals( '/test/', $stats3['target'][0]['url'], 'Unexpected 1st target path' ); + $this->assertEquals( 6, $stats3['target'][0]['count'], 'Unexpected 1st target count' ); + $this->assertEquals( '/', $stats3['target'][1]['url'], 'Unexpected 2nd target path' ); + $this->assertEquals( 4, $stats3['target'][1]['count'], 'Unexpected 2nd target count' ); + $this->assertEquals( '', $stats3['target'][2]['url'], 'Unexpected 3rd target path' ); + $this->assertEquals( 2, $stats3['target'][2]['count'], 'Unexpected 3rd target count' ); + + $this->assertEquals( 3, count( $stats3['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 9, $stats3['referrer'][0]['count'], 'Unexpected referrer URL' ); + /* Top referrer URL is "https://statify.pluginkollektiv.org/". As we aggregate by host, the reported URL however + depends on the DB server, so it might be ".../documentation/", too. Just check the prefix here. */ + $this->assertEquals( 'https://statify.pluginkollektiv.org/', substr( $stats['referrer'][0]['url'], 0, 36 ), 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'statify.pluginkollektiv.org', $stats3['referrer'][0]['host'], 'Unexpected 1st referrer hostname' ); + $this->assertEquals( 2, $stats3['referrer'][1]['count'], 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'https://pluginkollektiv.org/', $stats3['referrer'][1]['url'], 'Unexpected 2nd referrer URL' ); + $this->assertEquals( 'pluginkollektiv.org', $stats3['referrer'][1]['host'], 'Unexpected 3rd referrer hostname' ); + $this->assertEquals( 1, $stats3['referrer'][2]['count'], 'Unexpected 1st referrer URL' ); + $this->assertEquals( 'https://wordpress.org/plugins/statify/', $stats3['referrer'][2]['url'], 'Unexpected 3rd referrer URL' ); + $this->assertEquals( 'wordpress.org', $stats3['referrer'][2]['host'], 'Unexpected 3rd referrer hostname' ); + + $this->assertArrayHasKey( 'visit_totals', $stats3, 'Totals should be provided, if configured' ); + $this->assertEquals( 8, $stats3['visit_totals']['today'], 'Unexpected total for today' ); + $this->assertEquals( 15, $stats3['visit_totals']['since_beginning']['count'], 'Unexpected total since beginning' ); + $this->assertEquals( $date3->format( 'Y-m-d' ), $stats3['visit_totals']['since_beginning']['date'], 'Unexpected first date' ); + + // Finally we add another entry in the database, but utilize the transient cache (4min should be enough for the test case). + $this->insert_test_data( $date1->format( 'Y-m-d' ), 'https://example.com/', '/example/', 1 ); + $stats4 = Statify_Dashboard::get_stats(); + $this->assertEquals( $stats3, $stats4, 'Stats expected to be equal, is the transient cache active?' ); + } +} diff --git a/tests/test-frontend.php b/tests/test-frontend.php new file mode 100644 index 0000000..fb1005f --- /dev/null +++ b/tests/test-frontend.php @@ -0,0 +1,67 @@ +init_statify_tracking( Statify_Frontend::TRACKING_METHOD_DEFAULT ); + $this->assertNotFalse( + has_action( 'wp_footer', array( 'Statify_Frontend', 'wp_footer' ) ), + 'Statify footer action not registered' + ); + + Statify_Frontend::wp_footer(); + $this->assertFalse( + wp_script_is( 'statify-js', 'enqueued' ), + 'Statify JS should not be enqueued if JS tracking is disabled' + ); + + // Enable JS tracking. + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_JAVASCRIPT_WITH_NONCE_CHECK ); + + Statify_Frontend::wp_footer(); + $this->assertTrue( + wp_script_is( 'statify-js', 'enqueued' ), + 'Statify JS must be equeued if JS tracking is enabled' + ); + $script_data = wp_scripts()->registered['statify-js']->extra['data']; + $this->assertNotNull( $script_data, 'Statify script not localized' ); + $this->assertRegExp( + '/^var statify_ajax = {"url":"[^"]+","nonce":"[^"]+"};$/', + $script_data, + 'unexpected JS localization values' + ); + } + + /** + * Test query_vars() integration. + */ + public function test_query_vars() { + Statify::init(); + $this->assertNotFalse( + has_action( + 'query_vars', + array( 'Statify_Frontend', 'query_vars' ) + ), + 'Statify query_vars action not registered' + ); + + $vars = Statify_Frontend::query_vars( array() ); + $this->assertCount( 2, $vars, 'Unexpected number of query vars' ); + $this->assertContains( 'statify_referrer', $vars, 'Referrer variable not declared' ); + $this->assertContains( 'statify_target', $vars, 'Target variable not declared' ); + } +} diff --git a/tests/test-tracking.php b/tests/test-tracking.php new file mode 100644 index 0000000..d514491 --- /dev/null +++ b/tests/test-tracking.php @@ -0,0 +1,399 @@ +init_statify_tracking(); + + // Check if actions are registered. + $this->assertNotFalse( + has_action( + 'template_redirect', + array( 'Statify_Frontend', 'track_visit' ) + ), + 'Statify tracking action not registered' + ); + + // Track a valid request. + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + + $this->assertNotNull( $stats, 'Stats should be filled after tracking' ); + + $this->assertEquals( 1, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + $this->assertEquals( ( new DateTime() )->format( 'Y-m-d' ), $stats['visits'][0]['date'], 'Unexpected date of tracking' ); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Unexpected visit count' ); + + $this->assertEquals( 1, count( $stats['target'] ), 'Unexpected number of targets' ); + $this->assertEquals( '', $stats['target'][0]['url'], 'Unexpected target URL' ); + $this->assertEquals( 1, $stats['target'][0]['count'], 'Unexpected target count' ); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Unexpected visit count' ); + + $this->assertEquals( 1, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 'https://statify.pluginkollektiv.org/', $stats['referrer'][0]['url'], 'Unexpected referrer URL' ); + $this->assertEquals( 'statify.pluginkollektiv.org', $stats['referrer'][0]['host'], 'Unexpected referrer hostname' ); + $this->assertEquals( 1, $stats['referrer'][0]['count'], 'Unexpected referrer count' ); + + // And a second try... + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/documentation/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0'; + Statify_Frontend::track_visit(); + + $stats = $this->get_stats(); + $this->assertNotNull( $stats, 'Stats should be filled after tracking' ); + + $this->assertEquals( 1, count( $stats['visits'] ), 'Unexpected number of days with visits' ); + $this->assertEquals( ( new DateTime() )->format( 'Y-m-d' ), $stats['visits'][0]['date'], 'Unexpected date of tracking' ); + $this->assertEquals( 2, $stats['visits'][0]['count'], 'Unexpected visit count' ); + + $this->assertEquals( 1, count( $stats['target'] ), 'Unexpected number of targets' ); + $this->assertEquals( '', $stats['target'][0]['url'], 'Unexpected target URL' ); + $this->assertEquals( 2, $stats['target'][0]['count'], 'Unexpected target count' ); + + $this->assertEquals( 1, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 'https://statify.pluginkollektiv.org/', $stats['referrer'][0]['url'], 'Unexpected referrer URL' ); + $this->assertEquals( 'statify.pluginkollektiv.org', $stats['referrer'][0]['host'], 'Unexpected referrer hostname' ); + $this->assertEquals( 2, $stats['referrer'][0]['count'], 'Unexpected referrer count' ); + + // Request to invalid target should not be tracked. + $_SERVER['REQUEST_URI'] = ''; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 2, $stats['visits'][0]['count'], 'Unexpected visit count' ); + + // Internal referrer should be cleared + check permalink with structure. + $this->set_permalink_structure( '/%postname%/' ); + $_SERVER['REQUEST_URI'] = '/?foo=bar'; + $_SERVER['HTTP_REFERER'] = home_url(); + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 3, $stats['visits'][0]['count'], 'Unexpected visit count' ); + $this->assertEquals( 2, count( $stats['target'] ), 'Unexpected number of targets' ); + $this->assertEquals( '/', $stats['target'][1]['url'], 'Unexpected target URL' ); + $this->assertEquals( 1, $stats['target'][1]['count'], 'Unexpected target count' ); + $this->assertEquals( 1, count( $stats['referrer'] ), 'Unexpected number of referrers' ); + $this->assertEquals( 2, $stats['referrer'][0]['count'], 'Unexpected referrer count' ); + $this->set_permalink_structure( '' ); + + // If JavaScript tracking is enabled, the regular request should not be tracked. + $_SERVER['REQUEST_URI'] = '/'; + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_JAVASCRIPT_WITH_NONCE_CHECK, false ); + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 3, $stats['visits'][0]['count'], 'Unexpected visit count' ); + } + + /** + * Test case for non-js tracking with built-in skip conditions (except bots and configurable features). + */ + public function test_skip_tracking() { + global $_SERVER; + global $wp_query; + global $wp_version; + + // Initialize Statify with default configuration: no JS tracking, no logged-in users. + $this->init_statify_tracking(); + + // Basically a valid request. + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; + + $wp_query->is_robots = true; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Robots should not be tracked' ); + + $wp_query->is_robots = false; + $wp_query->is_trackback = true; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Trackbacks should not be tracked.' ); + + $wp_query->is_trackback = false; + $wp_query->is_preview = true; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Previews should not be tracked.' ); + + $wp_query->is_preview = false; + $wp_query->is_404 = true; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, '404 should not be tracked.' ); + + $wp_query->is_404 = false; + $wp_query->is_feed = true; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Feeds should not be tracked.' ); + + // Favicon is available for WP 5.4 and above only. + $wp_query->is_feed = false; + if ( function_exists( 'is_favicon' ) ) { + $wp_query->is_favicon = true; + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Favicons should not be tracked.' ); + $wp_query->is_favicon = false; + } + + // Sitemap XML and XSL for WP 5.5 and above. + if ( version_compare( $wp_version, '5.5', '>=' ) ) { + set_query_var( 'sitemap', 'index' ); + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Sitemap XML should not be tracked.' ); + set_query_var( 'sitemap', null ); + set_query_var( 'sitemap-stylesheet', 'sitemap' ); + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Sitemap XSL should not be tracked.' ); + } + } + + /** + * Test tracking exclusions for bots. + */ + public function test_bot_tracking() { + global $_SERVER; + + // Initialize Statify with default configuration: no JS tracking, no logged-in users. + $this->init_statify_tracking(); + + $bot_uas = array( + // Google Bots. + 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + 'AdsBot-Google (+http://www.google.com/adsbot.html)', + 'AdsBot-Google-Mobile-Apps', + // Bing Bots. + 'Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)', + 'msnbot/2.0b (+http://search.msn.com/msnbot.htm)', + 'Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 530) like Gecko (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', + // Yahoo Slurp. + 'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)', + // DUckDuckGo Bot. + 'DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)', + // Baidu Spider. + 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)', + 'Baiduspider+(+http://www.baidu.com/search/spider.htm)', + // Yandex Bots. + 'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)', + 'Mozilla/5.0 (compatible; YandexBlogs/0.99; robot; +http://yandex.com/bots)', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B411 Safari/600.1.4 (compatible; YandexBot/3.0; +http://yandex.com/bots)', + // Sogou Spider. + 'Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)', + // Exabot. + 'Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko) (Exabot-Thumbnails)', + 'Mozilla/5.0 (compatible; Exabot/3.0; +http://www.exabot.com/go/robot)', + // Facebook. + 'facebot', + 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)', + // Alexa crawler. + 'ia_archiver (+http://www.alexa.com/site/help/webmasters; crawler@alexa.com)', + // Script clients. + 'curl/7.69.1', + 'python-requests/2.22.0', + 'Python-urllib/3.8', + 'Wget/1.20.3 (linux-gnu)', + // Monitoring tools. + 'check_http/v2.2 (monitoring-plugins 2.2)', + 'Mozilla/5.0 (compatible; PRTG Network Monitor (www.paessler.com); Windows)', + ); + + // Basically a valid request. + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + + foreach ( $bot_uas as $bot_ua ) { + $_SERVER['HTTP_USER_AGENT'] = $bot_ua; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Bot exclusion failed for user agent: ' . $bot_ua ); + } + } + + /** + * Test tracking exclusions for disallowed keys. + */ + public function test_disallowed_referer() { + global $_SERVER; + global $wp_version; + + // Define a list of disallowed keys. + update_option( + version_compare( $wp_version, '5.5', '>=' ) ? 'disallowed_keys' : 'blacklist_keys', + "example.com\nstatify.pluginkollektiv.org\nexample.net" + ); + + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_DEFAULT, false, true ); + + // Basically a valid request. + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Tracking for blacklisted referrer succeeded' ); + + $this->init_statify_tracking(); + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNotNull( $stats, 'Blacklist evaluated when not enabled' ); + } + + /** + * Test evaluation of the statify__skip_tracking hook. + */ + public function test_skip_tracking_hook() { + global $_SERVER; + global $wp_query; + + $this->init_statify_tracking(); + + // A valid request that should be tracked. + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; + + $capture = null; + $filter_result = null; + + add_filter( + 'statify__skip_tracking', + function ( $previous_result ) use ( &$capture, &$filter_result ) { + $capture = $previous_result; + + return $filter_result; + } + ); + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Filter result NULL should not affect counting' ); + $this->assertNull( $capture, 'Initial filter should receive NULL value as previous result' ); + + // Explicitly blacklist request. + $filter_result = true; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Filter result FALSE should prevent request from being tracked' ); + + // The following request sould be skipped by internal filters, let's say the request raises a 404. + $filter_result = null; + $wp_query->is_404 = true; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 1, $stats['visits'][0]['count'], 'Filter result NULL should not affect built-in filters' ); + + // We now explicitly NOT skip the request. + $filter_result = false; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertEquals( 2, $stats['visits'][0]['count'], 'Filter result TRUE should force counting' ); + } + + /** + * Test evaluation of the statify__visit_saved hook. + */ + public function test_visit_saved_hook() { + global $_SERVER; + + $this->init_statify_tracking(); + + // A valid request that should be tracked. + $_SERVER['REQUEST_URI'] = '/page/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; + + $capture = array(); + + add_filter( + 'statify__visit_saved', + function ( $data, $id ) use ( &$capture ) { + $capture['data'] = $data; + $capture['id'] = $id; + }, + 10, + 2 + ); + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNotNull( $stats['visits'][0]['count'], 'Request not tracked' ); + $this->assertNotEmpty( $capture, 'Hook stativy__visit_saved has not fired' ); + $this->assertTrue( is_numeric( $capture['id'] ) && $capture['id'] > 0, 'unexpected entry ID' ); + $this->assertCount( 3, $capture['data'], 'unexpected number of data fields' ); + $this->assertEquals( ( new DateTime() )->format( 'Y-m-d' ), $capture['data']['created'], 'unexpected creation date' ); + $this->assertEquals( 'https://statify.pluginkollektiv.org/', $capture['data']['referrer'], 'unexpected referrer' ); + $this->assertEquals( '/page', $capture['data']['target'], 'unexpected target' ); + } + + /** + * Test tracking for logged-in users. + */ + public function test_track_users() { + global $_SERVER; + global $wp_query; + + // Assume we are logged in. + wp_set_current_user( 1 ); + + // Initialize Statify with default configuration: no JS tracking, no logged-in users. + $this->init_statify_tracking(); + + // Basically a valid request. + $_SERVER['REQUEST_URI'] = '/private-page/'; + $_SERVER['HTTP_REFERER'] = 'https://statify.pluginkollektiv.org/'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNull( $stats, 'Logged-in user should not be tracked' ); + + // Re-initialize Statify, enabling logged-in user tracking. + $this->init_statify_tracking( Statify_Frontend::TRACKING_METHOD_DEFAULT, true ); + + Statify_Frontend::track_visit(); + $stats = $this->get_stats(); + $this->assertNotNull( $stats, 'Logged-in user should be tracked' ); + } +} diff --git a/tests/trait-statify-test-support.php b/tests/trait-statify-test-support.php new file mode 100644 index 0000000..43915f6 --- /dev/null +++ b/tests/trait-statify-test-support.php @@ -0,0 +1,106 @@ +init_statify( + array( + 'snippet' => $method, + 'skip' => array( + 'logged_in' => $track_logged_in ? 0 : 1, + ), + 'blacklist' => $blacklist ? 1 : 0, + ) + ); + } + + /** + * Initialize Statify with widget-relevant options. + * + * @param integer $days_store Number of days to store data. + * @param integer $days_show Number of days to show data. + * @param integer $top_limit Number of entries for top lists. + * @param boolean $today Show top list only for today. + * @param boolean $totals Show totals. + */ + protected function init_statify_widget( $days_store = 14, $days_show = 14, $top_limit = 3, $today = false, $totals = false ) { + $this->init_statify( + array( + 'days' => $days_store, + 'days_show' => $days_show, + 'limit' => $top_limit, + 'today' => $today ? 1 : 0, + 'show_totals' => $totals ? 1 : 0, + ) + ); + } + + /** + * Initialize Statify with custom options. + * + * @param array $args Custom parameters (key => value). + */ + protected function init_statify( $args = array() ) { + $options = get_option( 'statify' ); + + if ( false === $options && isset( Statify::$_options ) ) { + $options = Statify::$_options; + } + + $options = wp_parse_args( $args, $options ); + + update_option( 'statify', $options ); + + Statify::init(); + } + + /** + * Get current stats value. + * This method always gets fresh data, no cached transients. + * + * @return array|null Statify stats value. + */ + protected function get_stats() { + delete_transient( 'statify_data' ); + + return Statify_Dashboard::get_stats(); + } + + /** + * Insert datapoint(s) into database. + * + * @param string $created Date of creation ('YYYY-MM-DD'). + * @param string $referrer Referrer URL (default: empty). + * @param string $target Target path (default: empty). + * @param integer $count Number of entries to create for given data (default: 1). + */ + protected function insert_test_data( $created, $referrer = '', $target = '', $count = 1 ) { + global $wpdb; + + $data = array( + 'created' => $created, + 'referrer' => $referrer, + 'target' => $target, + ); + + for ( $i = 0; $i < $count; $i ++ ) { + $wpdb->insert( $wpdb->statify, $data ); + } + } +}