From a149aec5e4b3ce1a20ab2ee562f1e9158cd8e662 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Fri, 28 Aug 2020 21:07:35 +0200 Subject: [PATCH 01/10] add second AMP analytics filter for Standard mode compatibility Standard and Transitional mode use the "amp_analytics_entries" hook, Reader mode uses "amp_post_template_analytics". We only added a filter for the latter, so AMP compatibility was only available in Reader mode. --- CHANGELOG.md | 3 ++ inc/class-statify-frontend.php | 74 +++++++++++++++++++++++----------- inc/class-statify.php | 1 + 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2adaa2..59a1cfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # 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 compatiblity for Standard and Transitional mode (#181) (#182) + ## 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/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 6394886..19a460d 100644 --- a/inc/class-statify-frontend.php +++ b/inc/class-statify-frontend.php @@ -404,7 +404,30 @@ public static function wp_footer() { } /** - * Add amp-analytics. + * Add amp-analytics for Standard and Transitional mode. + * + * @see hAMPttps://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 +443,35 @@ 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', + ), + ), + ); + } } diff --git a/inc/class-statify.php b/inc/class-statify.php index 1fb38f5..bd48788 100644 --- a/inc/class-statify.php +++ b/inc/class-statify.php @@ -81,6 +81,7 @@ public static function init() { 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. + add_filter( 'amp_analytics_entries', array( 'Statify_Frontend', 'amp_analytics_entries' ) ); add_filter( 'amp_post_template_analytics', array( 'Statify_Frontend', 'amp_post_template_analytics' ) ); } } From a19a01a5f440bf6ecfc9e0a908b963d61c1b290f Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Sat, 29 Aug 2020 11:15:38 +0200 Subject: [PATCH 02/10] do not include JS snippet for AMP requests Scripts and inline JS get removed on pages served via AMP. While that's fine for Statify, because we do use a separate analytics hook, it raises warnings for invalid markup on every AMP page. We now detect AMP usage and skip embedding the script. Also the checks are updated to use the new amp_is_request() function introduced in AMP plugin 2.0 for future compatibility. --- CHANGELOG.md | 1 + inc/class-statify-frontend.php | 8 ++++++-- inc/class-statify.php | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a1cfa..c27421d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This projec ## 1.8.1 * Fix AMP compatiblity for Standard and Transitional mode (#181) (#182) +* JavaScript is no longer embedded if request is served by AMP (#181) (#182) ## 1.8.0 * Fix date offset in dashboard widget in WP 5.3+ environments with mixed timezones (#167) diff --git a/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 19a460d..8e0eb2b 100644 --- a/inc/class-statify-frontend.php +++ b/inc/class-statify-frontend.php @@ -374,8 +374,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; } diff --git a/inc/class-statify.php b/inc/class-statify.php index bd48788..da6a056 100644 --- a/inc/class-statify.php +++ b/inc/class-statify.php @@ -80,7 +80,8 @@ 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' ) ); } From 719593524345d80a2693e219a214eff15365c664 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Sat, 17 Oct 2020 15:37:29 +0200 Subject: [PATCH 03/10] restore transport parameter for AMP analytics hook Commonly used methods "beacon" and "xhrpost" are enabled by default, but as Statify doesn't work with "image", i.e. GET requests, we now specify the supported methods explicitly again. --- inc/class-statify-frontend.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 8e0eb2b..711187a 100644 --- a/inc/class-statify-frontend.php +++ b/inc/class-statify-frontend.php @@ -410,7 +410,7 @@ public static function wp_footer() { /** * Add amp-analytics for Standard and Transitional mode. * - * @see hAMPttps://amp-wp.org/documentation/playbooks/analytics/ + * @see https://amp-wp.org/documentation/playbooks/analytics/ * * @param array $analytics_entries Analytics entries. */ @@ -476,6 +476,11 @@ private static function make_amp_config() { 'request' => 'pageview', ), ), + 'transport' => array( + 'beacon' => true, + 'xhrpost' => true, + 'image' => false, + ), ); } } From e556f89f81783308bdbbec97e0336c4dd709f24d Mon Sep 17 00:00:00 2001 From: Patrick Robrecht Date: Tue, 20 Oct 2020 05:41:35 +0000 Subject: [PATCH 04/10] always register the action for the cleanup (#184) (#187) --- CHANGELOG.md | 1 + inc/class-statify.php | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c27421d..9bec0ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. This projec ## 1.8.1 * Fix AMP compatiblity 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) ## 1.8.0 * Fix date offset in dashboard widget in WP 5.3+ environments with mixed timezones (#167) diff --git a/inc/class-statify.php b/inc/class-statify.php index da6a056..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' ) ); From 3d35da2f00ade2c537d159e1309bd84d0bb3f4e5 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Tue, 3 Nov 2020 20:45:54 +0100 Subject: [PATCH 05/10] Introduce automated tests (#161) Add integration tests for tracking, frontend, cron and dashboard widget All initializations with custom options are triggered through the same trait method to ensure complete options in every test case. CI matrix with PHP Versions from 5.6 to 7.4, integrations tests against oldest supported WP 4.7 with PHP 5.6 and latest WP 5.5 with PHP 7.4. Co-authored-by: Florian Brinkmann --- .distignore | 3 + .gitattributes | 3 + .travis.yml | 28 +- bin/install-wp-tests.sh | 152 +++ composer.json | 9 +- composer.lock | 1493 +++++++++++++++++++++++++- phpunit.xml | 19 + tests/bootstrap.php | 35 + tests/phpcs.xml | 29 + tests/test-ajax-tracking.php | 173 +++ tests/test-cron.php | 76 ++ tests/test-dashboard.php | 260 +++++ tests/test-frontend.php | 67 ++ tests/test-tracking.php | 385 +++++++ tests/trait-statify-test-support.php | 106 ++ 15 files changed, 2788 insertions(+), 50 deletions(-) create mode 100644 bin/install-wp-tests.sh create mode 100644 phpunit.xml create mode 100644 tests/bootstrap.php create mode 100644 tests/phpcs.xml create mode 100644 tests/test-ajax-tracking.php create mode 100644 tests/test-cron.php create mode 100644 tests/test-dashboard.php create mode 100644 tests/test-frontend.php create mode 100644 tests/test-tracking.php create mode 100644 tests/trait-statify-test-support.php 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..5d03918 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.5 + - 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/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/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/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..b650a78 --- /dev/null +++ b/tests/test-tracking.php @@ -0,0 +1,385 @@ +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; + + // 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; + } + } + + /** + * 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 ); + } + } +} From ab33ee583f709efb996f6c96c5d905bfd3472ddd Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Mon, 7 Dec 2020 17:46:34 +0100 Subject: [PATCH 06/10] exclude sitemap calls from tracking (#185) (#186) Exclude sitemap and sitemap stylesheet calls with WP 5.5 core sitemaps from tracking. Including unit tests. --- CHANGELOG.md | 1 + inc/class-statify-frontend.php | 5 +++-- tests/test-tracking.php | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bec0ba..ae32d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. This projec * Fix AMP compatiblity 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) ## 1.8.0 * Fix date offset in dashboard widget in WP 5.3+ environments with mixed timezones (#167) diff --git a/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 711187a..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' ); } /** diff --git a/tests/test-tracking.php b/tests/test-tracking.php index b650a78..d514491 100644 --- a/tests/test-tracking.php +++ b/tests/test-tracking.php @@ -121,6 +121,7 @@ public function test_default_tracking() { 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(); @@ -168,6 +169,19 @@ public function test_skip_tracking() { $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.' ); + } } /** From 95a726242a0653755b4fb0daea94458251a7704d Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Tue, 8 Dec 2020 19:43:39 +0100 Subject: [PATCH 07/10] Declare compatibility with WP 5.6 (#189) --- CHANGELOG.md | 1 + readme.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae32d15..f3239f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. This projec * 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) diff --git a/readme.txt b/readme.txt index f928c6d..f53a78f 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ * 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 * License: GPLv3 or later From 53c11cd3371d0903db3c05d0f1cc4e37259be344 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Wed, 9 Dec 2020 09:28:51 +0100 Subject: [PATCH 08/10] prepare release of v1.8.1 --- CHANGELOG.md | 2 +- readme.txt | 13 ++++++++++++- statify.php | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3239f7..6add414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ 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 compatiblity for Standard and Transitional mode (#181) (#182) +* 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) diff --git a/readme.txt b/readme.txt index f53a78f..f5c5093 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ * Requires at least: 4.7 * 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 */ From 20ecdfd8c9dac037685593a63c39cb2581f5b68f Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Wed, 9 Dec 2020 11:15:04 +0100 Subject: [PATCH 09/10] clarify WordPress versions "5.5+" for sitemap exclusion in changelog --- CHANGELOG.md | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6add414..79265a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. This projec * 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) +* Exclude sitemap calls (WP 5.5+) from tracking (#185) (#186) * Tested up to WordPress 5.6 ## 1.8.0 diff --git a/readme.txt b/readme.txt index f4c58ea..54ac458 100644 --- a/readme.txt +++ b/readme.txt @@ -121,7 +121,7 @@ You can find the full changelog in [our GitHub repository](https://github.com/pl * 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) +* Exclude sitemap calls (WP 5.5+) from tracking (#185) (#186) * Tested up to WordPress 5.6 ### 1.8.0 From 1d451a69979f837cf03f75cbb2b0569bce150e4e Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Wed, 9 Dec 2020 11:58:32 +0100 Subject: [PATCH 10/10] update integration test to use WP 5.6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5d03918..229294d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: - php: 7.2 - php: 7.3 - php: 7.4 - env: WP_VERSION=5.5 + env: WP_VERSION=5.6 - php: nightly allow_failures: - php: nightly