Admin
1 month ago
Commands
2 years ago
Db
1 year ago
Ecommerce
1 month 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
1 month 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
1 month ago
Feature.php
2 months ago
Installer.php
1 month ago
Logger.php
1 year ago
OptOut.php
1 month 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
AIBotTracking.php
232 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 LiteSpeed\ESI; |
| 13 | |
| 14 | /** |
| 15 | * Performs server side tracking for AI bots. |
| 16 | * |
| 17 | * Normal server side tracking: when no caching plugins or CDNs are being |
| 18 | * used, this class will send tracking requests in the wp_footer hook. |
| 19 | * |
| 20 | * When advanced-cache.php is used: when a caching plugin creates an |
| 21 | * advanced-cache.php file that WordPress uses, tracking must be done |
| 22 | * through the standalone script in misc/track_ai_bot.php. This script |
| 23 | * must be manually added to a user's wp-config.php file, right after |
| 24 | * ABSPATH is defined. |
| 25 | * |
| 26 | * When .htaccess is used: when a caching plugin modifies the .htaccess |
| 27 | * to serve cached files directly, AI bot tracking is not possible. |
| 28 | * |
| 29 | * When a CDN is used: when a CDN is used to serve cached content, AI |
| 30 | * bot tracking can be accomplished through the use of ESI (Edge Side Includes). |
| 31 | * In this case, this class outputs an `<esi:include>` directive that |
| 32 | * loads the misc/track_ai_bot.php script. This technique will only work |
| 33 | * for CDNs that support ESI. |
| 34 | * |
| 35 | * The misc/track_ai_bot.php script uses this class without all of WordPress loaded. |
| 36 | * It can also be loaded outside of WordPress. Because of this, it is important |
| 37 | * that the script and this class use as few total dependencies as possible. Otherwise, |
| 38 | * AI bot tracking reduce the performance of requests to cached content. |
| 39 | */ |
| 40 | class AIBotTracking { |
| 41 | |
| 42 | private static $ai_bot_tracked = false; |
| 43 | |
| 44 | private static $extensions_to_track = [ |
| 45 | '', // no extension |
| 46 | |
| 47 | 'htm', |
| 48 | 'html', |
| 49 | 'php', |
| 50 | ]; |
| 51 | |
| 52 | /** |
| 53 | * @var Settings |
| 54 | */ |
| 55 | private $settings; |
| 56 | |
| 57 | /** |
| 58 | * @var AjaxTracker |
| 59 | */ |
| 60 | private $tracker; |
| 61 | |
| 62 | /** |
| 63 | * @param Settings $settings |
| 64 | * @param ?AIBotTracking $tracker |
| 65 | */ |
| 66 | public function __construct( $settings, $tracker = null ) { |
| 67 | $this->settings = $settings; |
| 68 | $this->tracker = $tracker; |
| 69 | } |
| 70 | |
| 71 | public function register_hooks() { |
| 72 | add_action( 'litespeed_init', [ $this, 'litespeed_init' ] ); |
| 73 | add_action( 'wp_footer', [ $this, 'do_ai_bot_tracking' ], 999999 ); |
| 74 | } |
| 75 | |
| 76 | public function litespeed_init() { |
| 77 | if ( class_exists( ESI::class ) |
| 78 | && $this->settings->is_tracking_ai_bots_via_esi_includes() |
| 79 | ) { |
| 80 | ESI::set_has_esi(); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | public function do_ai_bot_tracking( $url = null ) { |
| 85 | // track AI bots only once per request |
| 86 | if ( self::$ai_bot_tracked ) { |
| 87 | return; |
| 88 | } |
| 89 | |
| 90 | self::$ai_bot_tracked = true; |
| 91 | |
| 92 | // if using ESI to track, and not within the track_ai_bot.php script, output the appropriate ESI tag |
| 93 | $is_using_esi_to_track = $this->settings->is_tracking_ai_bots_via_esi_includes(); |
| 94 | if ( |
| 95 | $is_using_esi_to_track |
| 96 | && empty( $GLOBALS['MATOMO_IN_AI_ESI'] ) |
| 97 | ) { |
| 98 | $track_script_url = plugins_url( '/misc/track_ai_bot.php', MATOMO_ANALYTICS_FILE ) . '?mtm_esi=1&mtm_url=' . rawurlencode( AjaxTracker::getCurrentUrl() ); |
| 99 | echo '<esi:include src="' . esc_attr( $track_script_url ) . '" cache-control="no-cache" />'; |
| 100 | return; |
| 101 | } |
| 102 | |
| 103 | if ( ! $this->is_doing_ai_bot_tracking_this_request() ) { |
| 104 | return; |
| 105 | } |
| 106 | |
| 107 | $response_code = http_response_code(); |
| 108 | $request_elapsed_ms = null; |
| 109 | |
| 110 | if ( |
| 111 | empty( $GLOBALS['MATOMO_IN_AI_ESI'] ) |
| 112 | && array_key_exists( 'REQUEST_TIME_FLOAT', $_SERVER ) |
| 113 | ) { |
| 114 | // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 115 | // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash |
| 116 | $request_elapsed_ms = (int) ( ( microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'] ) * 1000 ); |
| 117 | } |
| 118 | |
| 119 | if ( empty( $response_code ) ) { |
| 120 | $response_code = 200; |
| 121 | } |
| 122 | |
| 123 | // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled |
| 124 | $source = 'wordpress'; |
| 125 | |
| 126 | if ( empty( $url ) ) { |
| 127 | $url = AjaxTracker::getCurrentUrl(); |
| 128 | } |
| 129 | |
| 130 | $tracker = $this->get_tracker(); |
| 131 | $tracker->setUrl( $url ); |
| 132 | |
| 133 | // cannot count bytes echo'd so no response size tracked |
| 134 | $tracker->doTrackPageViewIfAIBot( $response_code, null, $request_elapsed_ms, $source ); |
| 135 | } |
| 136 | |
| 137 | public function should_track_current_page() { |
| 138 | if ( is_admin() ) { |
| 139 | return false; |
| 140 | } |
| 141 | |
| 142 | if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { |
| 143 | return false; |
| 144 | } |
| 145 | |
| 146 | if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { |
| 147 | return false; |
| 148 | } |
| 149 | |
| 150 | if ( |
| 151 | ! array_key_exists( 'REQUEST_URI', $_SERVER ) |
| 152 | || null === $_SERVER['REQUEST_URI'] |
| 153 | ) { |
| 154 | return false; |
| 155 | } |
| 156 | |
| 157 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 158 | $request_path = (string) wp_parse_url( wp_unslash( $_SERVER['REQUEST_URI'] ), PHP_URL_PATH ); |
| 159 | |
| 160 | if ( preg_match( '/matomo\.php$/', $request_path ) ) { |
| 161 | return false; |
| 162 | } |
| 163 | |
| 164 | if ( $this->is_request_for_file( $request_path ) ) { |
| 165 | return false; |
| 166 | } |
| 167 | |
| 168 | return true; |
| 169 | } |
| 170 | |
| 171 | private function is_request_for_file( $request_path ) { |
| 172 | if ( |
| 173 | ! empty( $_SERVER['DOCUMENT_ROOT'] ) |
| 174 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 175 | && is_dir( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) . $request_path ) |
| 176 | ) { |
| 177 | return false; |
| 178 | } |
| 179 | |
| 180 | $extension = pathinfo( $request_path, PATHINFO_EXTENSION ); |
| 181 | return ! in_array( $extension, self::$extensions_to_track, true ); |
| 182 | } |
| 183 | |
| 184 | public static function set_is_ai_bot_tracked( $is_tracked ) { |
| 185 | self::$ai_bot_tracked = $is_tracked; |
| 186 | } |
| 187 | |
| 188 | public function is_js_execution_detected() { |
| 189 | return ! empty( $_COOKIE['matomo_has_js'] ) |
| 190 | && '1' === $_COOKIE['matomo_has_js']; |
| 191 | } |
| 192 | |
| 193 | private function is_doing_ai_bot_tracking_this_request() { |
| 194 | if ( ! empty( $GLOBALS['MATOMO_IN_AI_ESI'] ) ) { |
| 195 | return true; |
| 196 | } |
| 197 | |
| 198 | if ( ! $this->should_track_current_page() ) { |
| 199 | return false; |
| 200 | } |
| 201 | |
| 202 | if ( $this->is_js_execution_detected() ) { |
| 203 | return false; |
| 204 | } |
| 205 | |
| 206 | $tracker = $this->get_tracker(); |
| 207 | |
| 208 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase |
| 209 | if ( ! AjaxTracker::isUserAgentAIBot( $tracker->userAgent ) ) { |
| 210 | return false; |
| 211 | } |
| 212 | |
| 213 | if ( |
| 214 | ! $this->settings->is_ai_bot_tracking_enabled() |
| 215 | || ! $this->settings->is_tracking_enabled() |
| 216 | ) { |
| 217 | return false; |
| 218 | } |
| 219 | |
| 220 | return true; |
| 221 | } |
| 222 | |
| 223 | private function get_tracker() { |
| 224 | if ( empty( $this->tracker ) ) { |
| 225 | $this->tracker = new AjaxTracker( $this->settings ); |
| 226 | $this->tracker->setRequestTimeout( 1 ); |
| 227 | } |
| 228 | |
| 229 | return $this->tracker; |
| 230 | } |
| 231 | } |
| 232 |