From f8003576f7fc21a610e9cf07c7f227d24496f5dc Mon Sep 17 00:00:00 2001 From: Justin Kostka <128225253+jujoko7CF@users.noreply.github.com> Date: Sun, 19 Mar 2023 21:44:54 +0100 Subject: [PATCH] Implement tracking of page title User has a feature request to show the page title instead of just the target url. After some long discussions we decided to introduce a new statify meta table. In this table any additional tracking data can be saved. With this PR we save the wp_document_title in this meta table. --- inc/class-statify-frontend.php | 136 ++++++++++++++++++++++++-------- inc/class-statify-install.php | 5 +- inc/class-statify-schema.php | 126 +++++++++++++++++++++++++++++ inc/class-statify-table.php | 90 --------------------- inc/class-statify-uninstall.php | 8 +- inc/class-statify.php | 24 +++++- js/snippet.js | 31 ++++---- statify.php | 2 +- tests/test-frontend.php | 2 +- 9 files changed, 277 insertions(+), 147 deletions(-) create mode 100644 inc/class-statify-schema.php delete mode 100644 inc/class-statify-table.php diff --git a/inc/class-statify-frontend.php b/inc/class-statify-frontend.php index 633105b..7c87071 100644 --- a/inc/class-statify-frontend.php +++ b/inc/class-statify-frontend.php @@ -18,6 +18,47 @@ */ class Statify_Frontend extends Statify { + /** + * Statify meta fields for tracking + * + * @var array + */ + private static $tracking_meta = array(); + + /** + * Default statify tracking data + * + * @var array + */ + private static $tracking_data = array(); + + /** + * Initialization of tracking data + * + * @return void + */ + public static function init_tracking_data() { + self::$tracking_data['target'] = isset( $_SERVER['REQUEST_URI'] ) + ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ) + : '/'; + + self::$tracking_data['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) + ? filter_var( wp_unslash( $_SERVER['HTTP_REFERER'] ), FILTER_SANITIZE_URL ) + : ''; + + self::$tracking_data = apply_filters( 'statify__tracking_data', self::$tracking_data ); + + self::$tracking_meta = array( + array( + 'meta_key' => 'title', + 'meta_value' => wp_get_document_title(), + 'type' => 'text', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + self::$tracking_meta = apply_filters( 'statify__tracking_meta', self::$tracking_meta, self::$tracking_data ); + } + /** * Track the page view * @@ -30,51 +71,55 @@ class Statify_Frontend extends Statify { * @return boolean */ public static function track_visit( $is_snippet = false ) { - // Set target & referrer. - $target = null; - $referrer = null; + if ( empty( self::$tracking_data ) ) { + self::init_tracking_data(); + } + if ( self::is_javascript_tracking_enabled() ) { if ( ! $is_snippet ) { return false; } - if ( isset( $_REQUEST['statify_target'] ) ) { - $target = filter_var( wp_unslash( $_REQUEST['statify_target'] ), FILTER_SANITIZE_URL ); + $json = file_get_contents( 'php://input' ); + $raw_data = json_decode( $json, true ); + if ( ! $raw_data || ! isset( $raw_data['statify_tracking_data'] ) ) { + return false; } - if ( isset( $_REQUEST['statify_referrer'] ) ) { - $referrer = filter_var( wp_unslash( $_REQUEST['statify_referrer'] ), FILTER_SANITIZE_URL ); + + $tracking_data = array( + 'target' => + isset( $raw_data['statify_tracking_data']['target'] ) + ? filter_var( wp_unslash( $raw_data['statify_tracking_data']['target'] ), FILTER_SANITIZE_URL ) + : '/', + 'referrer' => + isset( $raw_data['statify_tracking_data']['referrer'] ) + ? filter_var( wp_unslash( $raw_data['statify_tracking_data']['referrer'] ), FILTER_SANITIZE_URL ) + : '', + ); + + $tracking_meta = array(); + if ( isset( $raw_data['statify_tracking_meta'] ) && is_array( $raw_data['statify_tracking_meta'] ) ) { + $tracking_meta = $raw_data['statify_tracking_meta']; } } else { - if ( isset( $_SERVER['REQUEST_URI'] ) ) { - $target = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); - } - if ( isset( $_SERVER['HTTP_REFERER'] ) ) { - $referrer = filter_var( wp_unslash( $_SERVER['HTTP_REFERER'] ), FILTER_SANITIZE_URL ); - } + $tracking_data = self::$tracking_data; + $tracking_meta = wp_list_pluck( self::$tracking_meta, 'meta_value', 'meta_key' ); } - // 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 ) ) { + // Invalid target. + if ( ! wp_validate_redirect( $tracking_data['target'], false ) ) { return self::_jump_out( $is_snippet ); } - /* Check whether tracking should be skipped for this view. */ + // Check whether tracking should be skipped for this view. if ( self::_skip_tracking() ) { return self::_jump_out( $is_snippet ); } - /* Global vars */ + // Global vars. global $wpdb, $wp_rewrite; - /* Init rows */ + // Init rows. $data = array( 'created' => '', 'referrer' => '', @@ -87,24 +132,49 @@ public static function track_visit( $is_snippet = false ) { $needles = array( home_url(), network_admin_url() ); // Sanitize referrer url. - if ( ! empty( $referrer ) && self::strposa( $referrer, $needles ) === false ) { - $data['referrer'] = esc_url_raw( $referrer, array( 'http', 'https' ) ); + if ( self::strposa( $tracking_data['referrer'], $needles ) === false ) { + $data['referrer'] = filter_var( $tracking_data['referrer'], FILTER_SANITIZE_URL ); + $data['referrer'] = esc_url_raw( $data['referrer'], array( 'http', 'https' ) ); } - /* Relative target url */ - $data['target'] = user_trailingslashit( str_replace( home_url( '/', 'relative' ), '/', $target ) ); + // Relative target url. + $data['target'] = filter_var( $tracking_data['target'], FILTER_SANITIZE_URL ); + $data['target'] = user_trailingslashit( str_replace( home_url( '/', 'relative' ), '/', $data['target'] ) ); // Trim target url. if ( $wp_rewrite->permalink_structure ) { $data['target'] = wp_parse_url( $data['target'], PHP_URL_PATH ); } - // Sanitize target url. + // Escaping target url. $data['target'] = esc_url_raw( $data['target'] ); // Insert. $wpdb->insert( $wpdb->statify, $data ); + $statify_id = $wpdb->insert_id; + + foreach ( self::$tracking_meta as $meta_field ) { + if ( array_key_exists( $meta_field['meta_key'], $tracking_meta ) ) { + $meta_value = $tracking_meta[ $meta_field['meta_key'] ]; + + $sanitize_function = isset( $meta_field['sanitize_callback'] ) && is_callable( $meta_field['sanitize_callback'] ) + ? $meta_field['sanitize_callback'] + : 'sanitize_text_field'; + + $meta_value = call_user_func( $sanitize_function, $meta_value ); + + // Init rows. + $data = array( + 'statify_id' => $statify_id, + 'meta_key' => $meta_field['meta_key'], + 'meta_value' => $meta_value, + ); + + $wpdb->insert( $wpdb->statifymeta, $data ); + } + } + /** * Fires after a visit was stored in the database * @@ -374,13 +444,15 @@ public static function wp_footer() { true ); - // Add endpoint to script. + // Add endpoint and tracking_information to script. wp_localize_script( 'statify-js', 'statify_ajax', array( 'url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'statify_track' ), + 'tracking_data' => self::$tracking_data, + 'tracking_meta' => wp_list_pluck( self::$tracking_meta, 'meta_value', 'meta_key' ), ) ); } diff --git a/inc/class-statify-install.php b/inc/class-statify-install.php index ee7ecf4..c685cdd 100644 --- a/inc/class-statify-install.php +++ b/inc/class-statify-install.php @@ -82,8 +82,7 @@ private static function _apply() { ); } - // Create the actual tables. - Statify_Table::init(); - Statify_Table::create(); + // Initialize the database schema. + Statify_Schema::init(); } } diff --git a/inc/class-statify-schema.php b/inc/class-statify-schema.php new file mode 100644 index 0000000..02aeb79 --- /dev/null +++ b/inc/class-statify-schema.php @@ -0,0 +1,126 @@ +tables[] = $table; + $wpdb->$table = $wpdb->get_blog_prefix() . $table; + } + + self::maybe_create_tables(); + } + + /** + * Create the tables. + * + * @since 2.0.0 + * @version 2.0.0 + */ + public static function maybe_create_tables() { + $current_db_version = get_option( 'statify_db_version', '1.8.4' ); + if ( $current_db_version === self::$db_version ) { + return; + } + + // Global. + global $wpdb, $charset_collate; + + /** + * Use same index length like the WordPress core + * + * @see wp_get_db_schema() + */ + $max_index_length = 191; + + // Include. + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + // Create statify table. + dbDelta( + "CREATE TABLE {$wpdb->statify} ( + id bigint(20) unsigned NOT NULL auto_increment, + created date NOT NULL default '0000-00-00', + referrer varchar(255) NOT NULL default '', + target varchar(255) NOT NULL default '', + PRIMARY KEY (id), + KEY referrer (referrer), + KEY target (target), + KEY created (created) + ) {$charset_collate};" + ); + + // Create statifymeta table. + dbDelta( + "CREATE TABLE {$wpdb->statifymeta} ( + meta_id bigint(20) unsigned NOT NULL auto_increment, + statify_id bigint(20) unsigned NOT NULL default 0, + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY statify_id (statify_id), + KEY meta_key (meta_key({$max_index_length})) + ) {$charset_collate};" + ); + + update_option( 'statify_db_version', self::$db_version ); + } + + /** + * Remove the custom tables. + * + * @since 2.0.0 + * @version 2.0.0 + */ + public static function drop_tables() { + global $wpdb; + + // Remove. + foreach ( static::$tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + delete_option( 'statify_db_version' ); + } +} diff --git a/inc/class-statify-table.php b/inc/class-statify-table.php deleted file mode 100644 index 05b375c..0000000 --- a/inc/class-statify-table.php +++ /dev/null @@ -1,90 +0,0 @@ -tables[] = $table; - - // With prefix. - $wpdb->$table = $wpdb->get_blog_prefix() . $table; - } - - - /** - * Create the table. - * - * @since 0.6.0 - * @version 1.2.4 - */ - public static function create() { - - global $wpdb; - - // If existent. - if ( $wpdb->get_var( "SHOW TABLES LIKE '$wpdb->statify'" ) === $wpdb->statify ) { - return; - } - - // Include. - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - - // Create. - dbDelta( - "CREATE TABLE `$wpdb->statify` ( - `id` bigint(20) unsigned NOT NULL auto_increment, - `created` date NOT NULL default '0000-00-00', - `referrer` varchar(255) NOT NULL default '', - `target` varchar(255) NOT NULL default '', - PRIMARY KEY (`id`), - KEY `referrer` (`referrer`), - KEY `target` (`target`), - KEY `created` (`created`) - );" - ); - } - - - /** - * Remove the custom table. - * - * @since 0.6.0 - * @version 1.2.4 - */ - public static function drop() { - - global $wpdb; - - // Remove. - $wpdb->query( "DROP TABLE IF EXISTS `$wpdb->statify`" ); - } -} diff --git a/inc/class-statify-uninstall.php b/inc/class-statify-uninstall.php index f9f1cb3..493dd6f 100644 --- a/inc/class-statify-uninstall.php +++ b/inc/class-statify-uninstall.php @@ -70,10 +70,10 @@ private static function _apply() { // Delete options. delete_option( 'statify' ); - // Init table. - Statify_Table::init(); + // Initialize the database schema. + Statify_Schema::init(); - // Delete table. - Statify_Table::drop(); + // Delete tables. + Statify_Schema::drop_tables(); } } diff --git a/inc/class-statify.php b/inc/class-statify.php index 3044090..eeb0b92 100644 --- a/inc/class-statify.php +++ b/inc/class-statify.php @@ -40,8 +40,8 @@ public static function init() { return; } - // Table init. - Statify_Table::init(); + // Initialize the database schema. + Statify_Schema::init(); // Plugin options. self::$_options = wp_parse_args( @@ -78,6 +78,7 @@ public static function init() { add_action( 'admin_menu', array( 'Statify_Settings', 'add_admin_menu' ) ); add_action( 'update_option_statify', array( 'Statify_Settings', 'action_update_options' ), 10, 2 ); } else { // Frontend. + add_action( 'template_redirect', array( 'Statify_Frontend', 'init_tracking_data' ), 9 ); 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' ) ); @@ -123,4 +124,23 @@ public static function is_javascript_tracking_enabled() { true ); } + + /** + * Retrieves statify metadata for the given statify ID. + * + * @param int $statify_id Statify ID. + * @param string $meta_key Optional. The meta key to retrieve. By default, + * returns data for all keys. Default empty. + * @param bool $single Optional. Whether to return a single value. + * This parameter has no effect if `$key` is not specified. + * Default false. + * + * @return mixed An array of values if `$single` is false. + * The value of the meta field if `$single` is true. + * False for an invalid `$statify_id` (non-numeric, zero, or negative value). + * An empty string if a valid but non-existing statify ID is passed. + */ + public static function get_meta( $statify_id, $meta_key = '', $single = false ) { + return get_metadata( 'statify', $statify_id, $meta_key, $single ); + } } diff --git a/js/snippet.js b/js/snippet.js index 34c355a..248d273 100644 --- a/js/snippet.js +++ b/js/snippet.js @@ -1,15 +1,18 @@ -( function() { - var statifyReq; - try { - statifyReq = new XMLHttpRequest(); - 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 ) +(function () { + var statifyReq; + try { + statifyReq = new XMLHttpRequest(); + statifyReq.open('POST', statify_ajax.url + '?_ajax_nonce=' + statify_ajax.nonce + + '&action=statify_track', true); + statifyReq.setRequestHeader('Content-Type', 'application/json;'); + statifyReq.send( + JSON.stringify( + { + 'statify_tracking_data': statify_ajax.tracking_data, + 'statify_tracking_meta': statify_ajax.tracking_meta, + } + ) ); - } catch ( e ) { - } -}() ); + } catch (e) { + } +}()); diff --git a/statify.php b/statify.php index 81e787d..45401c7 100644 --- a/statify.php +++ b/statify.php @@ -75,7 +75,7 @@ function statify_autoload( $class ) { 'Statify_Uninstall', 'Statify_Deactivate', 'Statify_Settings', - 'Statify_Table', + 'Statify_Schema', 'Statify_XMLRPC', 'Statify_Cron', ); diff --git a/tests/test-frontend.php b/tests/test-frontend.php index 189e35b..efae39b 100644 --- a/tests/test-frontend.php +++ b/tests/test-frontend.php @@ -40,7 +40,7 @@ public function test_wp_footer() { $script_data = wp_scripts()->registered['statify-js']->extra['data']; $this->assertNotNull( $script_data, 'Statify script not localized' ); $this->assertMatchesRegularExpression( - '/^var statify_ajax = {"url":"[^"]+","nonce":"[^"]+"};$/', + '/^var statify_ajax = {"url":"[^"]+","nonce":"[^"]+","tracking_data":.*,"tracking_meta":.*};$/', $script_data, 'unexpected JS localization values' );