_constants = $constants; $this->_helper_functions = $helper_functions; $main_plugin->add_to_all_plugin_models( $this ); $main_plugin->add_to_public_models( $this ); } /** * Ensure that only one instance of this class is loaded or can be loaded ( Singleton Pattern ). * * @since 3.0.0 * @access public * * @param Abstract_Main_Plugin_Class $main_plugin Main plugin object. * @param Plugin_Constants $constants Plugin constants object. * @param Helper_Functions $helper_functions Helper functions object. * @return Redirection */ public static function get_instance( Abstract_Main_Plugin_Class $main_plugin , Plugin_Constants $constants , Helper_Functions $helper_functions ) { if ( !self::$_instance instanceof self ) self::$_instance = new self( $main_plugin , $constants , $helper_functions ); return self::$_instance; } /** * Update $_browser_zone_str class property value. * * @since 3.3.3 * @access public * * @param string $timezone Timezone set on browser */ public function set_browser_zone_str( $timezone ) { if ( in_array( $timezone , timezone_identifiers_list() ) ) $this->_browser_zone_str = $timezone; } /** * Register admin interfaces. * * @since 3.3.2 * @access public * * @param array $interfaces List of admin interfaces. * @return array Filtered list of admin interfaces. */ public function register_admin_interfaces( $interfaces ) { $interfaces[ 'thirstylink_page_thirsty-reports' ] = apply_filters( 'ta_reports_admin_interface' , array( 'link_performance' => 'manage_options' ) ); return $interfaces; } /** * Register admin interfaces. * * @since 3.3.2 * @access public * * @param array $interfaces List of menu items. * @return array Filtered list of menu items. */ public function register_admin_menu_items( $menu_items ) { $menu_items[ 'thirsty-reports' ] = 'manage_options'; return $menu_items; } /* |-------------------------------------------------------------------------- | Data saving |-------------------------------------------------------------------------- */ /** * Save link click data to the database. * * @since 3.0.0 * @since 3.1.0 Set to save additional information: $cloaked_url , $redirect_url and $redirect_type. * @since 3.2.0 Set to save additonal information: keyword. * @since 3.4.0 Add tracking for browser/device. * @access private * * @global wpdb $wpdb Object that contains a set of functions used to interact with a database. * * @param Affiliate_Link $thirstylink Affiliate link object. * @param string $http_referer HTTP Referrer value. * @param string $cloaked_url Affiliate link cloaked url. * @param string $redirect_url Link to where user is redirected to. * @param string $redirect_type Redirect type (301,302, etc.) * @param string $keyword Affiliate link keyword. */ private function save_click_data( $thirstylink , $http_referer , $cloaked_url , $redirect_url , $redirect_type , $keyword = '' ) { global $wpdb; if ( apply_filters( 'ta_filter_before_save_click' , false , $thirstylink , $http_referer ) ) return; $link_click_db = $wpdb->prefix . Plugin_Constants::LINK_CLICK_DB; $link_click_meta_db = $wpdb->prefix . Plugin_Constants::LINK_CLICK_META_DB; // insert click entry $wpdb->insert( $link_click_db, array( 'link_id' => $thirstylink->get_id(), 'date_clicked' => current_time( 'mysql' , true ) ) ); // save click meta data if ( $click_id = $wpdb->insert_id ) { $meta_data = apply_filters( 'ta_save_click_data' , array( 'user_ip_address' => $this->_helper_functions->get_user_ip_address(), 'http_referer' => $http_referer, 'cloaked_url' => $cloaked_url, 'redirect_url' => $redirect_url, 'redirect_type' => $redirect_type, 'keyword' => $keyword, 'browser_device' => $this->_helper_functions->get_visitor_browser_device() ), $thirstylink ); foreach ( $meta_data as $key => $value ) { // make sure there is a key and a value before saving. if ( ! $key || ! $value ) continue; $wpdb->insert( $link_click_meta_db, array( 'click_id' => $click_id, 'meta_key' => $key, 'meta_value' => $value ) ); } } } /** * Save click data on redirect * * @since 3.0.0 * @since 3.1.0 Passed additional 2 parameters: $redirect_url and $redirect type. Updated save_click_data function call to include new required arguments. * @since 3.3.0 If enhanced javascript redirect in frontend is enabled, then all server redirects are allowed (with this, stats links clicked via "open new tab" will be saved). * @access public * * @param Affiliate_Link $thirstylink Affiliate link object. * @param string $redirect_url Link to where user is redirected to. * @param string $redirect_type Redirect type (301,302, etc.) */ public function save_click_data_on_redirect( $thirstylink , $redirect_url , $redirect_type ) { // make sure that this is only runs on the frontend and not on the backend. if ( is_admin() ) return; $link_id = $thirstylink->get_id(); $http_referer = isset( $_SERVER[ 'HTTP_REFERER' ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ 'HTTP_REFERER' ] ) ) : ''; $query_string = isset( $_SERVER[ 'QUERY_STRING' ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ 'QUERY_STRING' ] ) ) : ''; $cloaked_url = $query_string ? $thirstylink->get_prop( 'permalink' ) . '?' . $query_string : $thirstylink->get_prop( 'permalink' ); $same_site = $http_referer && strrpos( 'x' . $http_referer , home_url() ); $admin_referrer = $http_referer && strrpos( 'x' . $http_referer , admin_url() ); $js_redirect_enabled = get_option( 'ta_enable_javascript_frontend_redirect' ) == 'yes'; // NOTE: this fixes a bug reported on TA-250 if ( $http_referer == $cloaked_url || $admin_referrer ) return; // if the refferer is from an external site, then record stat. if ( ( $same_site && $js_redirect_enabled ) || ! $same_site || $thirstylink->get_prop( 'thirstypay_link' ) == '1' ) $this->save_click_data( $thirstylink , $http_referer , $cloaked_url , $redirect_url , $redirect_type ); } /** * AJAX save click data on redirect * * @since 3.0.0 * @since 3.1.0 Updated save_click_data function call to include new required arguments. * @since 3.2.0 Updated save_click_data function call to include keyword argument. * @since 3.3.0 print actual affiliate link redirect url for ehanced javascript redirect support. * @since 3.4.0 Add query string as parameter to support passing query strings in enhanced JS redirect. * @access public */ public function ajax_save_click_data_on_redirect() { if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) wp_die(); $link_id = isset( $_REQUEST[ 'link_id' ] ) ? (int) sanitize_text_field( wp_unslash( $_REQUEST[ 'link_id' ] ) ) : 0; $http_referer = isset( $_REQUEST[ 'page' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'page' ] ) ) : ''; $cloaked_url = isset( $_REQUEST[ 'href' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'href' ] ) ) : ''; $keyword = isset( $_REQUEST[ 'keyword' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'keyword' ] ) ) : ''; $query_string = isset( $_REQUEST[ 'qs' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'qs' ] ) ) : ''; if ( ! $link_id ) $link_id = url_to_postid( $cloaked_url ); if ( $link_id ) { $thirstylink = new Affiliate_Link( $link_id ); if ( $thirstylink->get_prop( 'thirstypay_link' ) == '1' ) { wp_die(); } // Only process published affiliate links: unauthenticated link_id must not disclose destination_url or write stats. if ( ! $thirstylink->get_id() || get_post_type( $link_id ) !== Plugin_Constants::AFFILIATE_LINKS_CPT || get_post_status( $link_id ) !== 'publish' ) { wp_die(); } $redirect_url = apply_filters( 'ta_filter_redirect_url' , $thirstylink->get_prop( 'destination_url' ) , $thirstylink , $query_string ); $redirect_type = $thirstylink->get_redirect_type(); $this->save_click_data( $thirstylink , $http_referer , $cloaked_url , $redirect_url , $redirect_type , $keyword ); do_action( 'ta_before_link_redirect_ajax', $thirstylink, $redirect_url, $redirect_type ); // print actual affiliate link redirect url for enhanced javascript redirect support. if ( get_option( 'ta_enable_javascript_frontend_redirect' ) == 'yes' ) echo esc_url_raw( $redirect_url ); } wp_die(); } /* |-------------------------------------------------------------------------- | Fetch Report Data |-------------------------------------------------------------------------- */ /** * Fetch link performance data by date range. * * @since 3.0.0 * @access public * * @global wpdb $wpdb Object that contains a set of functions used to interact with a database. * * @param string $start_date Report start date. Format: YYYY-MM-DD hh:mm:ss * @param string $end_date Report end date. Format: YYYY-MM-DD hh:mm:ss * @param array $link_ids Affiliate Link post ID * @return string/array Link click meta data value. */ public function get_link_performance_data( $start_date , $end_date , $link_ids ) { global $wpdb; if ( ! is_array( $link_ids ) || empty( $link_ids ) ) return array(); $link_clicks_db = $wpdb->prefix . Plugin_Constants::LINK_CLICK_DB; $link_ids_str = implode( ',', array_map('intval', $link_ids) ); return $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT * FROM $link_clicks_db WHERE link_id IN ({$link_ids_str}) AND date_clicked between %s and %s", $start_date, $end_date ) ); } /** * Get link click meta by id and key. * * @since 3.0.0 * @access public * * @param int $click_id Link click ID. * @param string $meta_key Meta key column value. * @param boolean $single Return single result or array. * @return array Link performance data. */ private function get_click_meta( $click_id , $meta_key , $single = false ) { global $wpdb; $links_click_meta_db = $wpdb->prefix . Plugin_Constants::LINK_CLICK_META_DB; if ( $single ){ $meta = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT meta_value FROM $links_click_meta_db WHERE click_id = %d and meta_key = %s", $click_id, $meta_key ), ARRAY_A ); return array_shift( $meta ); } else { $meta = array(); $raw_data = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT meta_value FROM $links_click_meta_db WHERE click_id = %d and meta_key = %s", $click_id, $meta_key ), ARRAY_N ); foreach ( $raw_data as $data ) $meta[] = array_shift( $data ); return $meta; } } /** * AJAX fetch report by linkid. * * @since 3.0.0 * @since 3.1.2 Add total clicks count on the response. * @access public */ public function ajax_fetch_report_by_linkid() { if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'Invalid AJAX call' , 'thirstyaffiliates' ) ); elseif ( ! current_user_can( $this->_helper_functions->get_capability_for_interface( 'reports', 'manage_options' ) ) ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'You do not have permission to do this' , 'thirstyaffiliates' ) ); elseif ( ! isset( $_POST[ 'link_id' ] ) ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'Missing required post data' , 'thirstyaffiliates' ) ); else { // save timezone to use $timezone = isset( $_POST[ 'timezone' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'timezone' ] ) ) : ''; $this->set_browser_zone_str( $timezone ); $link_id = isset( $_POST[ 'link_id' ] ) ? (int) sanitize_text_field( wp_unslash( $_POST[ 'link_id' ] ) ) : 0; $thirstylink = new Affiliate_Link( $link_id ); $range_txt = isset( $_POST[ 'range' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'range' ] ) ) : ''; $start_date = isset( $_POST[ 'start_date' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'start_date' ] ) ) : ''; $end_date = isset( $_POST[ 'end_date' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'end_date' ] ) ) : ''; if ( ! $thirstylink->get_id() ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'Selected affiliate link is invalid' , 'thirstyaffiliates' ) ); else { $range = $this->get_report_range_details( $range_txt , $start_date , $end_date ); $data = $this->prepare_data_for_flot( $range , array( $link_id ) ); $response = array( 'status' => 'success', 'label' => $thirstylink->get_prop( 'name' ), 'slug' => $thirstylink->get_prop( 'slug' ), 'report_data' => $data, 'total_clicks' => $this->count_total_clicks_from_flot_data( $data ) ); } } @header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) ); echo wp_json_encode( $response ); wp_die(); } /** * AJAX init first report. * * @since 3.2.2 * @access public */ public function ajax_init_first_report() { if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'Invalid AJAX call' , 'thirstyaffiliates' ) ); elseif ( ! current_user_can( $this->_helper_functions->get_capability_for_interface( 'reports', 'manage_options' ) ) ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'You do not have permission to do this' , 'thirstyaffiliates' ) ); elseif ( ! isset( $_POST[ 'timezone' ] ) ) $response = array( 'status' => 'fail' , 'error_msg' => __( 'Missing required post data' , 'thirstyaffiliates' ) ); else { // save timezone to use $timezone = isset( $_POST[ 'timezone' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'timezone' ] ) ) : ''; $this->set_browser_zone_str( $timezone ); $cpt_slug = Plugin_Constants::AFFILIATE_LINKS_CPT; $current_range = isset( $_POST[ 'range' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'range' ] ) ) : '7day'; $start_date = isset( $_POST[ 'start_date' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'start_date' ] ) ) : ''; $end_date = isset( $_POST[ 'end_date' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'end_date' ] ) ) : ''; $range = $this->get_report_range_details( $current_range , $start_date , $end_date ); // get all published affiliate link ids $query = new \WP_Query( array( 'post_type' => $cpt_slug, 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1 ) ); $data = $this->prepare_data_for_flot( $range , $query->posts ); $total_clicks = $this->count_total_clicks_from_flot_data( $data ); $response = array( 'status' => 'success', 'flot_data' => $data, 'total_clicks' => $total_clicks, ); } @header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) ); echo wp_json_encode( $response ); wp_die(); } /* |-------------------------------------------------------------------------- | Reports Structure |-------------------------------------------------------------------------- */ /** * Get all registered reports. * * @since 3.0.0 * @access public * * @return array Settings sections. */ public function get_all_reports() { $all_reports = apply_filters( 'ta_register_reports' , array() ); if( ! empty( $all_reports ) ) { foreach( $all_reports as $tab_index => $report ) { if( ! isset( $report[ 'content' ] ) || ! is_callable( $report[ 'content' ] ) ) { unset( $all_reports[ $tab_index ] ); } } } return $all_reports; } /** * Get current loaded report. * * @since 3.0.0 * @access public * * @param string $tab Current report tab. * @return array Current loaded report. */ public function get_current_report( $tab = '' ) { if ( ! $tab ) $tab = isset( $_GET[ 'tab' ] ) ? sanitize_text_field( wp_unslash( $_GET[ 'tab' ] ) ) : 'link_performance'; // get all registered sections and fields $reports = $this->get_all_reports(); return isset( $reports[ $tab ] ) ? $reports[ $tab ] : array(); } /** * Register link performance report. * * @since 3.0.0 * @access public * * @param array $reports Array list of all registered reports. * @return array Array list of all registered reports. */ public function register_link_performance_report( $reports ) { $reports[ 'link_performance' ] = array( 'id' => 'ta_link_performance_report', 'tab' => 'link_performance', 'name' => __( 'Link Overview' , 'thirstyaffiliates' ), 'title' => __( 'Link Overview Report' , 'thirstyaffiliates' ), 'desc' => __( 'Total clicks on affiliate links over a given period.' , 'thirstyaffiliates' ), 'content' => function() { $this->get_link_performance_report_content(); } ); return $reports; } /* |-------------------------------------------------------------------------- | Display Report |-------------------------------------------------------------------------- */ /** * Register reports menu page. * * @since 3.0.0 * @since 3.2.2 Access to the settings page will now be controlled by the plugin. see Bootstrap::admin_interface_visibility. * render_reports() also checks the active tab's capability before rendering. * * @access public */ public function add_reports_submenu() { if ( ! current_user_can( 'edit_posts' ) ) return; add_submenu_page( 'edit.php?post_type=thirstylink', __( 'ThirstyAffiliates Reports' , 'thirstyaffiliates' ), __( 'Reports' , 'thirstyaffiliates' ), 'read', 'thirsty-reports', array( $this, 'render_reports' ) ); } /** * Render reports page. * * @since 3.0.0 * @access public */ public function render_reports() { // Fail closed: check the capability of the report tab that is actually rendered. $active_tab = isset( $_GET[ 'tab' ] ) ? sanitize_text_field( wp_unslash( $_GET[ 'tab' ] ) ) : 'link_performance'; $interfaces = apply_filters( 'ta_admin_interfaces' , array() ); $tab_caps = isset( $interfaces[ 'thirstylink_page_thirsty-reports' ] ) ? $interfaces[ 'thirstylink_page_thirsty-reports' ] : array(); $capability = isset( $tab_caps[ $active_tab ] ) ? $tab_caps[ $active_tab ] : 'manage_options'; if ( ! current_user_can( $capability ) ) wp_die( esc_html( apply_filters( 'ta_admin_interface_error_message' , __( "Sorry, you are not allowed to access this page." , 'thirstyaffiliates' ) , 'thirstylink_page_thirsty-reports' , $active_tab , $capability , $tab_caps ) ) ); // fetch current section $current_report = $this->get_current_report(); if ( isset( $current_report[ 'content' ] ) ) { ?>