api/pixel.php

<?php
declare(strict_types=1);

require_once __DIR__ . '/../includes/core.php';
require_once __DIR__ . '/../includes/backup.php';

brivaciaSecurityHeaders();

/*
|--------------------------------------------------------------------------
| Tracking pixel endpoint
|--------------------------------------------------------------------------
|
| Collects one pageview, updates daily counters, then returns a transparent
| 1x1 PNG pixel.
|
*/

/*
|--------------------------------------------------------------------------
| Ignore admin visits
|--------------------------------------------------------------------------
*/

if (($_COOKIE['brivacia_ignore'] ?? '') === '1') {
    sendPixel();
}

/*
|--------------------------------------------------------------------------
| Bootstrap
|--------------------------------------------------------------------------
*/

$db = brivaciaDb();
maybeBackupBrivaciaDb();

$day = date('Y-m-d');
$hour = (int)date('G'); // 0-23, used for the hourly counters below

/*
|--------------------------------------------------------------------------
| Input
|--------------------------------------------------------------------------
*/

$site = param('site');
$page = param('page');
$trackedWebsiteLang = strtolower(substr(param('lang'), 0, 10));
$ref = param('ref', '__missing__');
$url = brivaciaCleanStoredPageUrl(substr(param('url'), 0, 500));

if ($ref === '__missing__' || $ref === '') {
    $ref = $_SERVER['HTTP_REFERER'] ?? '';
}

// Keep the language even when the tracked site only returns a fallback /pageid URL.
// Priority: explicit pixel lang, then current URL path, then referrer path.
$trackedWebsiteLang = brivaciaInferTrackedWebsiteLang($url, $ref, $trackedWebsiteLang);

if ($site === '' || $page === '') {
    brivaciaLog(
        'pixel/invalid.log',
        'missing required parameter site=' . $site . ' page=' . $page
    );

    sendPixel();
}

$site = strtolower(substr($site, 0, 50));
$page = substr($page, 0, 300);
$pageKey = brivaciaPageKey($site, $page, $trackedWebsiteLang);

if (str_starts_with($page, '/') || str_starts_with($page, 'http://') || str_starts_with($page, 'https://')) {
    brivaciaLog(
        'pixel/legacy-page-param.log',
        'site=' . $site .
        ' page=' . brivaciaSafeLogPath($page) .
        ' key=' . brivaciaSafeLogPath($pageKey) .
        ' url=' . brivaciaSafeLogUrl($url) .
        ' ref=' . brivaciaSafeLogRef($ref)
    );
}

if (str_starts_with($pageKey, '/') || preg_match('~^[a-z]{2}/~', $pageKey)) {
    brivaciaLog(
        'pixel/bad-page-key.log',
        'site=' . $site .
        ' page=' . brivaciaSafeLogPath($page) .
        ' key=' . brivaciaSafeLogPath($pageKey) .
        ' url=' . brivaciaSafeLogUrl($url) .
        ' ref=' . brivaciaSafeLogRef($ref)
    );
}

if (!isset(brivacia_sites()[$site])) {
    brivaciaLog(
        'pixel/invalid.log',
        'unknown site=' . $site
    );

    sendPixel();
}

/*
|--------------------------------------------------------------------------
| Respond to the browser now
|--------------------------------------------------------------------------
|
| Everything below this point (country lookup, page-label/language
| auto-detection, DB writes) can involve slow local work or outbound
| HTTP requests. None of that should make the visitor's browser wait,
| so the pixel is flushed here and the rest runs after the response
| has already been sent.
*/

sendPixelNow();

/*
|--------------------------------------------------------------------------
| Visitor context
|--------------------------------------------------------------------------
*/

$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$ip = brivaciaClientIp();
$country = countryFromIp($ip);

$rateLimitKey = brivaciaPixelRateLimitKey($ip);

if (brivaciaPixelRateLimited($db, $rateLimitKey)) {
    brivaciaLog('pixel/rate-limited.log', 'prefix=' . $rateLimitKey . ' site=' . $site);
    exit;
}

$kind = detectVisitorKind($ua);
$visitorHash = visitorHash($day, $ua, $ip);

/*
|--------------------------------------------------------------------------
| Page title
|--------------------------------------------------------------------------
*/

$title = param('title');

if ($title === '') {
    $title = $site . ' — ' . $page;
}

/*
|--------------------------------------------------------------------------
| Store hit
|--------------------------------------------------------------------------
*/

try {
    $db->beginTransaction();

    ensureDay($db, $day, $site);
    ensureHour($db, $day, $hour, $site);
    maybePurgeHourlyHits($db, $day, $hour);

    if ($kind === 'human') {
        $isNew = markSeen($db, $day, $visitorHash, $site);
        $isNewSession = markSession($db, $day, $visitorHash, $site);

        if ($isNew) {
            inc($db, 'unique_visitors', $day, $site);
            incHourly($db, 'unique_visitors', $day, $hour, $site);
        }

        if ($isNewSession) {
            incCountry($db, $day, $country, $site);

            $referrer = refSourceFast($ref);

            // Discover new referrers immediately
            if ($referrer !== BRIVACIA_UNKNOWN && $referrer !== BRIVACIA_BLOCKED && !isOwnHost($referrer)) {
                discoverReferrer($referrer);
            }

            incReferrer($db, $day, $referrer, $site);
            inc($db, 'visits', $day, $site);
            incHourly($db, 'visits', $day, $hour, $site);

            // Remembered so a later hit in this same session can upgrade
            // it if this first one turns out to be Unknown/empty — see
            // maybeUpgradeSessionReferrer().
            $db->prepare('
                UPDATE visitor_sessions SET referrer = ?
                WHERE site = ? AND day = ? AND visitor_hash = ?
            ')->execute([$referrer, $site, $day, $visitorHash]);
        } else {
            $referrer = refSourceFast($ref);

            if ($referrer !== BRIVACIA_UNKNOWN && $referrer !== BRIVACIA_BLOCKED && !isOwnHost($referrer)) {
                discoverReferrer($referrer);
            }

            maybeUpgradeSessionReferrer($db, $day, $visitorHash, $site, $referrer);
        }

        inc($db, 'pageviews', $day, $site);
        incHourly($db, 'pageviews', $day, $hour, $site);

        // pageKey always has a language prefix now (real one, or the 'xx'
        // alert marker from brivaciaPageKey() when nothing was found) — so
        // the visit is always recorded. The log below is only to help
        // track down *why* the language went missing for a real human hit;
        // it doesn't gate storage.
        if ($trackedWebsiteLang === '') {
            brivaciaLog(
                'pixel/lang-missing.log',
                'site=' . $site .
                ' page=' . brivaciaSafeLogPath($page) .
                ' url=' . brivaciaSafeLogUrl($url) .
                ' ref=' . brivaciaSafeLogRef($ref) .
                ' title=' . (param('title') === '' ? '[empty]' : '[present]')
            );
        }

        incPage(
            $db,
            $day,
            $site,
            $pageKey,
            $title,
            $url
        );
    } else {
        inc($db, 'bots', $day, $site);
        incHourly($db, 'bots', $day, $hour, $site);
    }

    $db->commit();

} catch (Throwable $e) {
    if ($db->inTransaction()) {
        $db->rollBack();
    }

    brivaciaLog(
        'pixel/failed.log',
        'site=' . $site .
        ' page=' . $page .
        ' kind=' . $kind .
        ' error=' . $e->getMessage()
    );
}

if ($kind === 'human') {
    $pageLimit = brivaciaMaintenancePageLimit($db);

    if ($pageLimit > 0) {
        refreshPageLabels($db, $pageLimit);
    }

    $langLimit = brivaciaMaintenanceLangLimit($db);

    if ($langLimit > 0) {
        refreshPageLanguages($db, $langLimit);
    }

    maybeNormalizeStoredReferrers($db);
}

Contribute