includes/rules.php
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| Page identifiers
|--------------------------------------------------------------------------
*/
// Neutral placeholder used only when no language could be found at all
// (no lang param, no /xx/ in the URL, no /xx/ in the referrer). This is NOT
// a guessed language — 'xx' is not a real ISO 639-1 code. It mirrors
// Brivacia's existing "unknown" convention (see countryName()'s 'XX'
// handling and assets/images/flags/xx.png): the visit still counts, but the
// page shows up in the dashboard with the '?' flag instead of blending in
// silently — a visible alert, not a fix for why the language went missing.
const BRIVACIA_UNKNOWN_LANG = 'xx';
// Extracts a strict 2-letter ISO 639-1-style code from any raw language
// value: "en", "en-US", "en_US", "EN-us", "fr-CA"... all collapse to their
// primary subtag ("en", "fr"). Returns '' if nothing usable is found.
function brivaciaNormalizeLangCode(string $raw): string {
$raw = strtolower(trim($raw));
return preg_match('/^([a-z]{2})(?:[-_][a-z0-9]+)*$/', $raw, $m)
? $m[1]
: '';
}
function brivaciaPageKey(string $site, string $page, string $trackedWebsiteLang = ''): string {
$lang = brivaciaNormalizeLangCode($trackedWebsiteLang) ?: BRIVACIA_UNKNOWN_LANG;
return $lang . ':' . $page;
}
function brivaciaRawPageKey(string $key): string {
return str_contains($key, ':')
? substr($key, strpos($key, ':') + 1)
: $key;
}
/*
|--------------------------------------------------------------------------
| Tracked website language
|--------------------------------------------------------------------------
|
| Tries to infer the tracked website language from a page URL or referrer.
| This is only about the tracked website, not the Brivacia dashboard language.
|
*/
function brivaciaInferTrackedWebsiteLang(string $url, string $ref = '', string $fallback = ''): string {
$fallback = brivaciaNormalizeLangCode(substr(trim($fallback), 0, 10));
if ($fallback !== '') {
return $fallback;
}
foreach ([$url, $ref] as $source) {
$path = parse_url($source, PHP_URL_PATH);
if (!is_string($path) || $path === '') {
continue;
}
$first = trim(explode('/', trim($path, '/'))[0] ?? '');
$lang = brivaciaNormalizeLangCode($first);
if ($lang !== '' && preg_match('~^[a-z]{2}(?:-[a-z]{2})?$~i', $first)) {
return $lang;
}
}
return '';
}
/*
|--------------------------------------------------------------------------
| Page URLs
|--------------------------------------------------------------------------
*/
function brivaciaCleanStoredPageUrl(string $url): string {
$url = trim($url);
if ($url === '') {
return '';
}
$path = parse_url($url, PHP_URL_PATH);
if (is_string($path) && $path !== '') {
return '/' . ltrim($path, '/');
}
return str_starts_with($url, '/') ? '/' . ltrim($url, '/') : '';
}
function brivaciaPageUrl(string $site, string $pageKey, string $url = ''): string {
$host = siteLabel($site);
$url = brivaciaCleanStoredPageUrl($url);
if ($url !== '') {
return 'https://' . $host . '/' . ltrim($url, '/');
}
if (str_starts_with($pageKey, '/')) {
return 'https://' . $host . $pageKey;
}
return 'https://' . $host . '/' . ltrim($pageKey, '/');
}
/*
|--------------------------------------------------------------------------
| Page titles
|--------------------------------------------------------------------------
*/
function brivaciaCleanPageTitle(string $title, string $site): string {
return trim($title);
}
/*
|--------------------------------------------------------------------------
| Metadata extraction
|--------------------------------------------------------------------------
*/
function brivaciaExtractHtmlTitle(string $html): string {
if (!preg_match('~<title[^>]*>(.*?)</title>~is', $html, $m)) {
return '';
}
return html_entity_decode(
strip_tags(trim($m[1])),
ENT_QUOTES,
'UTF-8'
);
}
function brivaciaExtractCanonicalUrl(string $html): string {
if (preg_match_all('~<link\b[^>]*>~i', $html, $links)) {
foreach ($links[0] as $tag) {
if (!preg_match('~\brel=["\']([^"\']+)["\']~i', $tag, $rel)) continue;
if (!str_contains(strtolower($rel[1]), 'canonical')) continue;
if (!preg_match('~\bhref=["\']([^"\']+)["\']~i', $tag, $href)) continue;
return html_entity_decode(trim($href[1]), ENT_QUOTES, 'UTF-8');
}
}
if (preg_match_all('~<meta\b[^>]*>~i', $html, $tags)) {
foreach ($tags[0] as $tag) {
$lower = strtolower($tag);
if (!str_contains($lower, 'og:url')) continue;
if (!preg_match('~\bcontent=["\']([^"\']+)["\']~i', $tag, $m)) continue;
return html_entity_decode(trim($m[1]), ENT_QUOTES, 'UTF-8');
}
}
return '';
}
function brivaciaExtractHtmlLang(string $html): string {
if (!preg_match('~<html\b[^>]*\blang=["\']([^"\']+)["\']~i', $html, $m)) {
return '';
}
return brivaciaNormalizeLangCode(
html_entity_decode(trim($m[1]), ENT_QUOTES, 'UTF-8')
);
}
function brivaciaPathFromAbsoluteOrRelativeUrl(string $url): string {
return brivaciaCleanStoredPageUrl($url);
}
/*
|--------------------------------------------------------------------------
| Metadata fallback detection
|--------------------------------------------------------------------------
*/
function brivaciaShouldLogPageFallback(string $key, string $title, string $url): bool {
$rawKey = brivaciaRawPageKey($key);
return $title === ''
|| $title === $rawKey
|| $url === ''
|| $url === '/' . $rawKey
|| str_contains($title, $rawKey);
}
/*
|--------------------------------------------------------------------------
| Metadata resolution
|--------------------------------------------------------------------------
*/
function brivaciaResolvePageMetadata(string $site, string $pageKey, string $url = ''): array {
$candidateUrl = brivaciaPageUrl($site, $pageKey, $url);
$html = @file_get_contents($candidateUrl, false, brivaciaHttpContext(3));
if (!$html) {
return ['title' => '', 'url' => '', 'lang' => ''];
}
$title = brivaciaExtractHtmlTitle($html);
$canonical = brivaciaExtractCanonicalUrl($html);
$path = brivaciaPathFromAbsoluteOrRelativeUrl($canonical);
$lang = brivaciaExtractHtmlLang($html);
if ($path === '') {
$path = brivaciaPathFromAbsoluteOrRelativeUrl($candidateUrl);
}
if (brivaciaShouldLogPageFallback($pageKey, $title, $path)) {
return ['title' => '', 'url' => '', 'lang' => $lang];
}
return [
'title' => $title,
'url' => $path,
'lang' => $lang,
];
}