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);
}