Admin
2 months ago
Commands
2 years ago
Db
1 year ago
Ecommerce
3 months ago
Report
2 months ago
Site
2 months ago
TrackingCode
4 months ago
Updater
4 years ago
User
2 months ago
Workarounds
2 years ago
WpStatistics
5 months ago
views
4 years ago
AIBotTracking.php
4 months ago
API.php
2 months ago
Access.php
4 years ago
AjaxTracker.php
4 months ago
Annotations.php
2 months ago
Bootstrap.php
10 months ago
Capabilities.php
2 months ago
Compatibility.php
2 months ago
Email.php
2 years ago
ErrorNotice.php
2 months ago
Feature.php
2 months ago
Installer.php
5 months ago
Logger.php
1 year ago
OptOut.php
2 months ago
Paths.php
5 months ago
PluginActionLinks.php
2 months ago
PluginAdminOverrides.php
2 months ago
PluginInit.php
2 months ago
PrivacyBadge.php
4 years ago
RedirectOnActivation.php
2 months ago
Referral.php
2 months ago
Roles.php
2 months ago
ScheduledTasks.php
2 months ago
Settings.php
4 months ago
Site.php
3 years ago
TrackingCode.php
2 months ago
Uninstaller.php
5 months ago
Updater.php
10 months ago
User.php
4 years ago
AjaxTracker.php
233 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Matomo - free/libre analytics platform |
| 4 | * |
| 5 | * @link https://matomo.org |
| 6 | * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 7 | * @package matomo |
| 8 | */ |
| 9 | |
| 10 | namespace WpMatomo; |
| 11 | |
| 12 | use WpMatomo\Ecommerce\ServerSideVisitorId; |
| 13 | use WpMatomo\TrackingCode\GeneratorOptions; |
| 14 | use WpMatomo\TrackingCode\TrackingCodeGenerator; |
| 15 | |
| 16 | if ( ! defined( 'ABSPATH' ) ) { |
| 17 | exit; // if accessed directly |
| 18 | } |
| 19 | |
| 20 | if ( ! class_exists( '\PiwikTracker' ) ) { |
| 21 | include_once plugin_dir_path( MATOMO_ANALYTICS_FILE ) . 'app/vendor/matomo/matomo-php-tracker/MatomoTracker.php'; |
| 22 | } |
| 23 | |
| 24 | class AjaxTracker extends \MatomoTracker { |
| 25 | |
| 26 | const IP_ADDRESS_FORWARDING_HEADER = 'X-Matomo-Forwarded-Ip'; |
| 27 | const IP_ADDRESS_FORWARDING_HEADER_SERVER_NAME = 'HTTP_X_MATOMO_FORWARDED_IP'; |
| 28 | const IP_ADDRESS_FORWARDING_NONCE_NAME = 'matomo-track-forward-ip'; |
| 29 | |
| 30 | private $has_cookie = false; |
| 31 | private $logger; |
| 32 | |
| 33 | public function __construct( Settings $settings ) { |
| 34 | $this->logger = new Logger(); |
| 35 | |
| 36 | $site = new Site(); |
| 37 | $idsite = $site->get_current_matomo_site_id(); |
| 38 | |
| 39 | if ( ! $idsite ) { |
| 40 | return; |
| 41 | } |
| 42 | |
| 43 | $paths = new Paths(); |
| 44 | |
| 45 | if ( $settings->get_global_option( 'track_api_endpoint' ) === 'restapi' ) { |
| 46 | $api_endpoint = $paths->get_tracker_api_rest_api_endpoint(); |
| 47 | } else { |
| 48 | $api_endpoint = $paths->get_tracker_api_url_in_matomo_dir(); |
| 49 | } |
| 50 | |
| 51 | parent::__construct( $idsite, $api_endpoint ); |
| 52 | |
| 53 | $this->ip = false; |
| 54 | |
| 55 | // we are using the tracker only in ajax so the referer contains the actual url |
| 56 | $this->urlReferrer = false; |
| 57 | $this->pageUrl = ! empty( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : false; |
| 58 | |
| 59 | if ( ! $settings->get_global_option( 'disable_cookies' ) ) { |
| 60 | $tracking_code_generator = new TrackingCodeGenerator( $settings, new GeneratorOptions( $settings ) ); |
| 61 | $cookie_domain = $tracking_code_generator->get_tracking_cookie_domain(); |
| 62 | $this->enableCookies( $cookie_domain ); |
| 63 | } else { |
| 64 | $this->disableCookieSupport(); |
| 65 | } |
| 66 | |
| 67 | if ( $this->loadVisitorIdCookie() ) { |
| 68 | if ( ! empty( $this->cookieVisitorId ) ) { |
| 69 | $this->has_cookie = true; |
| 70 | $this->set_visitor_id_safe( $this->cookieVisitorId ); |
| 71 | } |
| 72 | } else if ( function_exists( 'WC' ) && isset( WC()->session ) ) { |
| 73 | $visitor_id = WC()->session->get( ServerSideVisitorId::VISITOR_ID_SESSION_VAR_NAME ); |
| 74 | if ( ! empty( $visitor_id ) ) { |
| 75 | $this->hasCookie = true; // do not set cookies for this visitor, since it would have no effect anyway |
| 76 | $this->set_visitor_id_safe( $visitor_id ); |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | public function set_visitor_id_safe( $visitor_id ) { |
| 82 | try { |
| 83 | $this->setVisitorId( $visitor_id ); |
| 84 | } catch ( \Exception $ex ) { |
| 85 | // do not fatal if the visitor ID is invalid for some reason |
| 86 | if ( ! $this->is_invalid_visitor_id_error( $ex ) ) { |
| 87 | throw $ex; |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | public function is_success_response( $response ) { |
| 93 | $gif_response = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; |
| 94 | return $response === base64_decode( $gif_response ); |
| 95 | } |
| 96 | |
| 97 | protected function setCookie( $cookieName, $cookieValue, $cookieTTL ) { |
| 98 | if ( ! $this->has_cookie ) { |
| 99 | // we only set / overwrite cookies if it is a visitor that has eg no JS enabled or ad blocker enabled etc. |
| 100 | // this way we will track all cart updates and orders into the same visitor on following requests. |
| 101 | // If we recognized the visitor before via cookie we want in our case to make sure to not overwrite |
| 102 | // any cookie |
| 103 | parent::setCookie( $cookieName, $cookieValue, $cookieTTL ); |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | protected function sendRequest( $url, $method = 'GET', $data = null, $force = false ) { |
| 108 | if ( ! $this->idSite ) { |
| 109 | $this->logger->log('ecommerce tracking could not find idSite, cannot send request'); |
| 110 | return ''; // not installed or synced yet |
| 111 | } |
| 112 | |
| 113 | if ( $this->is_prerender() ) { |
| 114 | // do not track if for some reason we are prerendering |
| 115 | return ''; |
| 116 | } |
| 117 | |
| 118 | $args = [ |
| 119 | 'method' => $method, |
| 120 | 'headers' => [ |
| 121 | 'User-Agent' => $this->userAgent, |
| 122 | ], |
| 123 | 'blocking' => false, |
| 124 | ]; |
| 125 | if ( ! empty( $data ) ) { |
| 126 | $args['body'] = $data; |
| 127 | } |
| 128 | |
| 129 | if ( ! empty( $this->ip ) ) { |
| 130 | $args['headers'][self::IP_ADDRESS_FORWARDING_HEADER] = $this->ip; |
| 131 | |
| 132 | $ip_nonce = wp_create_nonce( self::IP_ADDRESS_FORWARDING_NONCE_NAME ); |
| 133 | $url = $url . '&ip_nonce=' . rawurlencode( $ip_nonce ); |
| 134 | } |
| 135 | |
| 136 | // todo at some point we could think about including `matomo.php` here instead of doing an http request |
| 137 | // however we would need to make sure to set a custom tracker response handler to |
| 138 | // 1) Not send any response no matter what happens |
| 139 | // 2) Never exit at any point |
| 140 | |
| 141 | $url = $url . '&bots=1'; |
| 142 | |
| 143 | try { |
| 144 | $response = $this->wp_remote_request( $url, $args ); |
| 145 | } catch ( \Exception $ex ) { |
| 146 | $this->logger->log_exception( 'ajax_tracker', $ex ); |
| 147 | return ''; |
| 148 | } |
| 149 | |
| 150 | if ( is_wp_error( $response ) ) { |
| 151 | $this->logger->log_exception( 'ajax_tracker', new \Exception( $response->get_error_message() ) ); |
| 152 | return ''; |
| 153 | } |
| 154 | |
| 155 | return $response['body']; |
| 156 | } |
| 157 | |
| 158 | private function is_invalid_visitor_id_error( \Exception $ex ) { |
| 159 | return strpos( $ex->getMessage(), 'setVisitorId() expects' ) === 0; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * See https://developer.chrome.com/docs/web-platform/prerender-pages |
| 164 | * @return bool |
| 165 | */ |
| 166 | private function is_prerender() { |
| 167 | // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 168 | $purpose = strtolower( isset( $_SERVER['HTTP_SEC_PURPOSE'] ) ? wp_unslash( $_SERVER['HTTP_SEC_PURPOSE'] ) : '' ); |
| 169 | return strpos( $purpose, 'prefetch' ) !== false |
| 170 | || strpos( $purpose, 'prerender' ) !== false; |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * for tests to override |
| 175 | * @param string $url |
| 176 | * @param array $args |
| 177 | * @return array|\WP_Error |
| 178 | */ |
| 179 | protected function wp_remote_request( $url, $args ) { |
| 180 | return wp_remote_request( $url, $args ); |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * In Matomo for WordPress we want to rely entirely on JavaScript tracker |
| 185 | * for creating cookies. |
| 186 | * |
| 187 | * @return void |
| 188 | */ |
| 189 | protected function setFirstPartyCookies() { |
| 190 | // disabled |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * Enables the handling of the X-Matomo-Forwarded-Ip, if it should be for |
| 195 | * the current request. |
| 196 | * |
| 197 | * Matomo for WordPress uses a custom header to correctly track client IP addresses |
| 198 | * when doing server side tracking. We choose to use this approach instead |
| 199 | * of the `cip` tracking parameter, since that parameter requires the use of |
| 200 | * a token_auth, and creating and storing a token_auth is more complexity than |
| 201 | * we want. |
| 202 | * |
| 203 | * Instead, we use a WP nonce to check whether the current request is authorized |
| 204 | * to handle the X-Matomo-Forwarded-Ip header. |
| 205 | * |
| 206 | * If it should be handled, the X-Matomo-Forwarded-Ip header is added to Matomo's |
| 207 | * list of proxy HTTP headers to look at for IP addresses. |
| 208 | */ |
| 209 | public static function add_ip_forward_proxy_header_to_config(\Piwik\Config $config ) { |
| 210 | if ( empty( $_REQUEST['ip_nonce'] ) ) { |
| 211 | return; |
| 212 | } |
| 213 | |
| 214 | $ip_nonce = $_REQUEST['ip_nonce']; |
| 215 | if ( ! wp_verify_nonce( $ip_nonce, self::IP_ADDRESS_FORWARDING_NONCE_NAME ) ) { |
| 216 | return; |
| 217 | } |
| 218 | |
| 219 | $general = $config->General; |
| 220 | $proxy_client_headers = isset( $general['proxy_client_headers'] ) ? $general['proxy_client_headers'] : []; |
| 221 | if ( ! is_array( $proxy_client_headers ) ) { |
| 222 | $proxy_client_headers = []; |
| 223 | } |
| 224 | $proxy_client_headers[] = self::IP_ADDRESS_FORWARDING_HEADER_SERVER_NAME; |
| 225 | |
| 226 | $config->General['proxy_client_headers'] = $proxy_client_headers; |
| 227 | } |
| 228 | |
| 229 | public static function getCurrentUrl(): string { |
| 230 | return parent::getCurrentUrl(); |
| 231 | } |
| 232 | } |
| 233 |