From 71c46806976baf3ec265752ca61e2af3fa2a5965 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Mon, 13 Apr 2020 20:40:27 +0200 Subject: [PATCH] Refactor JS snippet to use WP AJAX (#109) * Refactored JS tracking to use WP AJAX The custom GET request intercepts normal page generation and might trigger other plugins' actions before Statify is loaded. It also provided an open door for lightweight malicious requests targeting the statistics. Using WP AJAX including Nonce verification reduces both problems. * Move sanitization of target and referrer out of if-else block * Reset AJAX request from jQuery back to vanilla JS with XmlHttpRequest * Remove superflous sanitization in pre-sanitized var in target filter * rework target and referrer sanitization again * Use minifed version of the snippet, set version to 1.7.0 (consistent with the PHPDoc) Co-authored-by: Patrick Robrecht --- .eslintrc.json | 1 + CHANGELOG.md | 1 + inc/class-statify-frontend.php | 93 ++++++++++++++++++++++------------ inc/class-statify.php | 8 +-- js/snippet.js | 11 ++-- statify.php | 1 + views/js-snippet.php | 21 -------- 7 files changed, 74 insertions(+), 62 deletions(-) delete mode 100644 views/js-snippet.php diff --git a/.eslintrc.json b/.eslintrc.json index ebeed34..9508d37 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,6 +6,7 @@ }, "globals": { "Chartist": "readonly", + "statify_ajax": "readonly", "statify_translations": "readonly" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index ff359e3..a4daec8 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 * Introduced separate settinge page and reduced widget backview to widget settings only * Add options to track logged in users, feeds and search requests * Add option to show total visits +* Refactored JavaScript tracking to use WP AJAX * Updated Chartist JS library for dashboard widget * Introduced new option to separate display from storage range diff --git a/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 634741a..6932211 100644 --- a/inc/class-statify-frontend.php +++ b/inc/class-statify-frontend.php @@ -22,42 +22,47 @@ class Statify_Frontend extends Statify { * Track the page view * * @since 0.1.0 - * @version 1.4.2 + * @since 1.7.0 $is_snippet parameter added. + * @version 1.7.0 * - * @return bool + * @param boolean $is_snippet Is tracking triggered via JS (default: false). + * + * @return boolean */ - public static function track_visit() { + public static function track_visit( $is_snippet = false ) { - /* Init vars */ + // Check of JS snippet is configured. $use_snippet = self::$_options['snippet']; - $is_snippet = $use_snippet && get_query_var( 'statify_target' ); - /* Set target & referrer */ - if ( $is_snippet ) { - $target = urldecode( get_query_var( 'statify_target' ) ); - $referrer = urldecode( get_query_var( 'statify_referrer' ) ); + // Set target & referrer. + $target = null; + $referrer = null; + if ( $use_snippet && $is_snippet ) { + if ( isset( $_REQUEST['statify_target'] ) ) { + $target = filter_var( wp_unslash( $_REQUEST['statify_target'] ), FILTER_SANITIZE_URL ); + } + if ( isset( $_REQUEST['statify_referrer'] ) ) { + $referrer = filter_var( wp_unslash( $_REQUEST['statify_referrer'] ), FILTER_SANITIZE_URL ); + } } elseif ( ! $use_snippet ) { - $target = filter_var( - ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/' ), - FILTER_SANITIZE_URL - ); - if ( is_null( $target ) || false === $target ) { - $target = '/'; - } else { - $target = wp_unslash( $target ); + if ( isset( $_SERVER['REQUEST_URI'] ) ) { + $target = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); } - - $referrer = filter_var( - ( isset( $_SERVER['HTTP_REFERER'] ) ? wp_unslash( $_SERVER['HTTP_REFERER'] ) : '' ), - FILTER_SANITIZE_URL - ); - if ( is_null( $referrer ) || false === $referrer ) { - $referrer = ''; + if ( isset( $_SERVER['HTTP_REFERER'] ) ) { + $referrer = filter_var( wp_unslash( $_SERVER['HTTP_REFERER'] ), FILTER_SANITIZE_URL ); } } else { return false; } + // Fallbacks for uninitialized or omitted target and referrer values. + if ( is_null( $target ) || false === $target ) { + $target = '/'; + } + if ( is_null( $referrer ) || false === $referrer ) { + $referrer = ''; + } + /* Invalid target? */ if ( empty( $target ) || ! wp_validate_redirect( $target, false ) ) { return self::_jump_out( $is_snippet ); @@ -116,6 +121,20 @@ public static function track_visit() { return self::_jump_out( $is_snippet ); } + /** + * Track the page view via AJAX. + * + * @return void + */ + public static function track_visit_ajax() { + // Check AJAX referrer. + check_ajax_referer( 'statify_track' ); + // Only do something if snippet use is actually configured. + if ( self::$_options['snippet'] ) { + self::track_visit( true ); + } + } + /** * Find the position of the first occurrence of a substring in a string about a array. * @@ -353,23 +372,31 @@ public static function query_vars( $vars ) { */ public static function wp_footer() { - /* Skip by option */ + // Skip by option. if ( ! self::$_options['snippet'] ) { return; } - /* Skip by internal rules (#84) */ + // Skip by internal rules (#84). if ( self::_is_internal() ) { return; } - /* Load template */ - load_template( - wp_normalize_path( - sprintf( - '%s/views/js-snippet.php', - STATIFY_DIR - ) + wp_enqueue_script( + 'statify-js', + plugins_url( 'js/snippet.min.js', STATIFY_FILE ), + array(), + STATIFY_VERSION, + true + ); + + // Add endpoint to script. + wp_localize_script( + 'statify-js', + 'statify_ajax', + array( + 'url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'statify_track' ), ) ); } diff --git a/inc/class-statify.php b/inc/class-statify.php index b2694f0..c43b76c 100644 --- a/inc/class-statify.php +++ b/inc/class-statify.php @@ -44,8 +44,8 @@ public static function instance() { * @version 2017-01-10 */ public function __construct() { - // Skip me! - if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { + // Nothing to do on autosave. + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } @@ -71,7 +71,9 @@ public function __construct() { ) ); - if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { // XMLRPC. + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + add_action( 'wp_ajax_nopriv_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' ) ); diff --git a/js/snippet.js b/js/snippet.js index f196654..34c355a 100644 --- a/js/snippet.js +++ b/js/snippet.js @@ -2,13 +2,14 @@ var statifyReq; try { statifyReq = new XMLHttpRequest(); - statifyReq.open( - 'GET', - document.getElementById( 'statify-js-snippet' ).getAttribute( 'data-home-url' ) + - '?statify_referrer=' + encodeURIComponent( document.referrer ) + + statifyReq.open( 'POST', statify_ajax.url, true ); + statifyReq.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded;' ); + statifyReq.send( + '_ajax_nonce=' + statify_ajax.nonce + + '&action=statify_track' + + '&statify_referrer=' + encodeURIComponent( document.referrer ) + '&statify_target=' + encodeURIComponent( location.pathname + location.search ) ); - statifyReq.send( null ); } catch ( e ) { } }() ); diff --git a/statify.php b/statify.php index 16d7329..875222e 100644 --- a/statify.php +++ b/statify.php @@ -20,6 +20,7 @@ define( 'STATIFY_FILE', __FILE__ ); define( 'STATIFY_DIR', dirname( __FILE__ ) ); define( 'STATIFY_BASE', plugin_basename( __FILE__ ) ); +define( 'STATIFY_VERSION', '1.7.0' ); /* Hooks */ diff --git a/views/js-snippet.php b/views/js-snippet.php deleted file mode 100644 index 5f927a3..0000000 --- a/views/js-snippet.php +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - -