Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions src/Controllers/Rest/TrackingRestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,23 +185,22 @@ public function handle_tracking(\WP_REST_Request $request)
\SlimStat\Services\Privacy\ConsentHandler::handleBannerConsent(false, $consent_data);
}

// Handle tracking hits
$result = null;
if (function_exists('ob_start')) {
ob_start();
$maybe = Tracker::slimtrack_ajax();
$output = ob_get_clean();
$result = $maybe ?? $output;
} else {
$result = Tracker::slimtrack_ajax();
}
// Handle tracking hits - process() returns result without exit()
$result = Tracker::slimtrack_ajax();

// Normalize to string numeric id if possible
if (is_numeric($result) && (int) $result > 0) {
// Success: pure numeric ID (rare — most paths return checksum format)
if (is_numeric($result) && 0 < (int) $result) {
return rest_ensure_response((string) $result);
}

// If no numeric id detected, return a non-200 status to trigger fallback tracking methods
// Success: checksum-formatted string "<id>.<hash>" from Utils::getValueWithChecksum()
// Return the full checksum string so the JS tracker can send it back for
// subsequent requests (consent upgrade, events) where getValueWithoutChecksum() validates it.
if (is_string($result) && preg_match('/^(\d+)\.[0-9a-fA-F]+$/', $result, $matches) && 0 < (int) $matches[1]) {
return rest_ensure_response($result);
}

// If no valid tracking ID detected, return a non-200 status to trigger fallback tracking methods
return new \WP_Error(
'slimstat_tracking_failed',
esc_html__('[REST API] Tracking failed, falling back to alternative methods.', 'wp-slimstat'),
Expand Down
5 changes: 4 additions & 1 deletion src/Providers/RestApiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ public static function handleAdblockTracking(): void
\SlimStat\Services\Privacy\ConsentHandler::handleBannerConsent(false, $consent_data);
}

Tracker::slimtrack_ajax();
$result = Tracker::slimtrack_ajax();
// Output result and exit for adblock bypass requests
echo $result;
exit;
}
}
}
50 changes: 33 additions & 17 deletions src/Tracker/Ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,37 @@

class Ajax
{
/**
* Handle AJAX tracking request with exit (for admin-ajax.php).
* This wrapper calls process() and exits with the result.
*/
public static function handle()
{
$result = self::process();
exit($result);
}

/**
* Process tracking request and return result (for REST API and other contexts).
* Returns the tracking result without calling exit().
*
* @return string|int The tracking result (record ID with checksum, error code, or 0)
*/
public static function process()
{
$remote_ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
if (!empty($remote_ip)) {
$key = 'slimstat_rl_' . md5($remote_ip);
$hits_in_5s = (int) get_transient($key);
if ($hits_in_5s >= 10) {
exit(Utils::logError(429));
return Utils::logError(429);
}

set_transient($key, $hits_in_5s + 1, 5);
}

if ('on' != \wp_slimstat::$settings['is_tracking']) {
exit(Utils::logError(204));
return Utils::logError(204);
}

$id = 0;
Expand Down Expand Up @@ -67,7 +83,7 @@ public static function handle()
// Security: Validate referer format
if (false === $parsed_ref) {
// Invalid referer format - reject request
exit(Utils::logError(201));
return Utils::logError(201);
}

// Security: Validate host (if present) - allow external domains for referer
Expand All @@ -76,7 +92,7 @@ public static function handle()
// Validate host format (prevent injection)
if (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/', $parsed_ref['host'])) {
// Invalid host format - reject request
exit(Utils::logError(201));
return Utils::logError(201);
}
}

Expand All @@ -94,13 +110,13 @@ public static function handle()
if (!empty($data_js['id'])) {
$data_js['id'] = Utils::getValueWithoutChecksum($data_js['id']);
if (false === $data_js['id']) {
exit(Utils::logError(101));
return Utils::logError(101);
}

$stat['id'] = intval($data_js['id']);
if ($stat['id'] < 0) {
do_action('slimstat_track_exit_' . abs($stat['id']));
exit(Utils::getValueWithChecksum($stat['id']));
return Utils::getValueWithChecksum($stat['id']);
}

// Process IP according to consent status (cookie set only by consent upgrade handler)
Expand Down Expand Up @@ -137,7 +153,7 @@ public static function handle()
// Security: Whitelist validation - only allow current site domain
if (!$is_allowed_host($parsed_resource['host'])) {
// Invalid host - reject request
exit(Utils::logError(203));
return Utils::logError(203);
}

// Security: Validate path format (prevent path traversal attacks)
Expand All @@ -147,7 +163,7 @@ public static function handle()
// Validate path contains only safe characters
if (!preg_match('#^[/\w\-\.~!*\'();:@&=+$,?#\[\]%]*$#', $path)) {
// Invalid path format - reject request
exit(Utils::logError(203));
return Utils::logError(203);
}

// Extract path from resource URL
Expand All @@ -173,9 +189,9 @@ public static function handle()
$visitIdAssigned = Session::ensureVisitId(true);
$stat = \wp_slimstat::get_stat();

// Security: Validate visit_id exists - exit if generation failed
// Security: Validate visit_id exists - return error if generation failed
if (empty($stat['visit_id']) || $stat['visit_id'] <= 0) {
exit(Utils::logError(500));
return Utils::logError(500);
}

$stat = Utils::getClientInfo($data_js, $stat);
Expand Down Expand Up @@ -244,7 +260,7 @@ public static function handle()
$stat['id'] = intval($existing_record->id);
\wp_slimstat::set_stat($stat);
$GLOBALS['wpdb']->query('COMMIT');
exit(Utils::getValueWithChecksum($stat['id']));
return Utils::getValueWithChecksum($stat['id']);
}

$GLOBALS['wpdb']->query('COMMIT');
Expand Down Expand Up @@ -290,7 +306,7 @@ public static function handle()
$resource = Utils::base64UrlDecode($data_js['res']);
$parsed_resource = parse_url($resource ?: '');
if (false === $parsed_resource || empty($parsed_resource['host'])) {
exit(Utils::logError(203));
return Utils::logError(203);
}

if (!empty($parsed_resource['path']) && in_array(pathinfo($parsed_resource['path'], PATHINFO_EXTENSION), \wp_slimstat::string_to_array(\wp_slimstat::$settings['extensions_to_track']))) {
Expand Down Expand Up @@ -333,22 +349,22 @@ public static function handle()
if (!empty($data_js['res'])) {
$stat['resource'] = Utils::base64UrlDecode($data_js['res']);
if (false === parse_url($stat['resource'] ?: '')) {
exit(Utils::logError(203));
return Utils::logError(203);
}
}

$stat = Utils::getClientInfo($data_js, $stat);
if (!empty($data_js['ci'])) {
$data_js['ci'] = Utils::getValueWithoutChecksum($data_js['ci']);
if (false === $data_js['ci']) {
exit(Utils::logError(102));
return Utils::logError(102);
}

$decoded_ci = Utils::base64UrlDecode($data_js['ci']);
$content_info = json_decode($decoded_ci, true);
// Security: Only accept JSON-encoded content info, reject serialized data
if (empty($content_info) || !is_array($content_info)) {
exit(Utils::logError(103));
return Utils::logError(103);
}

foreach (['content_type', 'category', 'content_id', 'author'] as $a_key) {
Expand Down Expand Up @@ -378,10 +394,10 @@ public static function handle()
}

if (empty($id)) {
exit(0);
return 0;
}

do_action('slimstat_track_success');
exit(Utils::getValueWithChecksum($id));
return Utils::getValueWithChecksum($id);
}
}
8 changes: 7 additions & 1 deletion src/Tracker/Tracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@

class Tracker
{
/**
* Process AJAX tracking request.
* Returns the result for REST API and other callers.
*
* @return string|int The tracking result (record ID with checksum, error code, or 0)
*/
public static function slimtrack_ajax()
{
Ajax::handle();
return Ajax::process();
}

public static function rewrite_rule_tracker()
Expand Down
Loading