Browser Fingerprint Viewer — PHP source
See exactly what websites can learn about your browser without cookies — screen, GPU, fonts, timezone, language, and more. Educational, not tracking.
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* browser-fingerprint — PHP polyglot showcase port (pure analysis layer).
*
* Ported from the TypeScript reference at src/lib/browser-fingerprint.ts.
* Covers the portable core: the 18-signal registry with privacy-risk
* classifications, canonical category grouping, risk counts, and the stable
* SHA-256 fingerprint hash. The browser-API collection layer (canvas / WebGL
* / font probes) is DOM-bound by design and does not port. Requires PHP 8.1+
* (backed enums, readonly properties).
*
* Display source — part of CosmoDev's polyglot tool pages.
*/
declare(strict_types=1);
namespace CosmoDev\BrowserFingerprint;
/** Privacy-risk classification. */
enum RiskLevel: string
{
/** Near-unique on its own or part of a rare combination. */
case High = 'high';
/** Narrows you to a sizable-but-specific population. */
case Medium = 'medium';
/** Coarse; shared by millions of browsers. */
case Low = 'low';
}
/** Groups related signals. */
enum FingerprintCategory: string
{
case Hardware = 'Hardware';
case Graphics = 'Graphics';
case Network = 'Network';
case Browser = 'Browser';
case Input = 'Input';
}
/** One collected, classified signal. */
final class FingerprintSignal
{
public function __construct(
public readonly string $id,
public readonly string $label,
public readonly string $value,
public readonly RiskLevel $risk,
public readonly FingerprintCategory $category,
) {
}
}
/** Classifies one signal id. */
final class SignalDef
{
public function __construct(
public readonly string $label,
public readonly RiskLevel $risk,
public readonly FingerprintCategory $category,
) {
}
}
/** One category and its signals. */
final class CategoryGroup
{
/** @param list<FingerprintSignal> $signals */
public function __construct(
public readonly FingerprintCategory $category,
public readonly array $signals,
) {
}
}
/** Signals per risk level (drives the summary line). */
final class RiskCounts
{
public function __construct(
public int $low = 0,
public int $medium = 0,
public int $high = 0,
) {
}
}
/** Static registry + pure helpers — the portable core of the tool. */
final class BrowserFingerprint
{
/**
* Static registry of every signal the tool collects, with its
* privacy-risk classification. Single source of truth: buildSignal()
* derives from it, so a signal can never be collected without being
* classified.
*/
private const SIGNAL_DEFINITIONS = [
// High risk
'canvas' => ['label' => 'Canvas fingerprint', 'risk' => RiskLevel::High, 'category' => FingerprintCategory::Graphics],
'webgl-renderer' => ['label' => 'WebGL renderer', 'risk' => RiskLevel::High, 'category' => FingerprintCategory::Graphics],
'webgl-vendor' => ['label' => 'WebGL vendor', 'risk' => RiskLevel::High, 'category' => FingerprintCategory::Graphics],
'fonts' => ['label' => 'Installed fonts', 'risk' => RiskLevel::High, 'category' => FingerprintCategory::Browser],
'timezone' => ['label' => 'Timezone', 'risk' => RiskLevel::High, 'category' => FingerprintCategory::Browser],
// Medium risk
'screen' => ['label' => 'Screen & color depth', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Hardware],
'device-pixel-ratio' => ['label' => 'Device pixel ratio', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Hardware],
'hardware-concurrency' => ['label' => 'CPU cores', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Hardware],
'device-memory' => ['label' => 'Device Memory', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Hardware],
'platform' => ['label' => 'Platform', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Browser],
'languages' => ['label' => 'Languages', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Browser],
'touch' => ['label' => 'Touch support', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Input],
'connection' => ['label' => 'Connection type', 'risk' => RiskLevel::Medium, 'category' => FingerprintCategory::Network],
// Low risk
'user-agent' => ['label' => 'User agent', 'risk' => RiskLevel::Low, 'category' => FingerprintCategory::Browser],
'do-not-track' => ['label' => 'Do Not Track', 'risk' => RiskLevel::Low, 'category' => FingerprintCategory::Browser],
'cookies-enabled' => ['label' => 'Cookies enabled', 'risk' => RiskLevel::Low, 'category' => FingerprintCategory::Browser],
'online' => ['label' => 'Online status', 'risk' => RiskLevel::Low, 'category' => FingerprintCategory::Network],
'pdf-viewer' => ['label' => 'PDF viewer', 'risk' => RiskLevel::Low, 'category' => FingerprintCategory::Browser],
];
/** Canonical category order (card grid, top to bottom). */
private const FINGERPRINT_CATEGORIES = [
FingerprintCategory::Hardware,
FingerprintCategory::Graphics,
FingerprintCategory::Network,
FingerprintCategory::Browser,
FingerprintCategory::Input,
];
/** Risk level for a known signal id; null for unknown ids. */
public static function classifySignalRisk(string $id): ?RiskLevel
{
return self::SIGNAL_DEFINITIONS[$id]['risk'] ?? null;
}
/**
* Build a signal from its id and collected value. Throws on unknown ids
* so a typo'd id fails loudly instead of silently rendering an
* unclassified row.
*/
public static function buildSignal(string $id, string $value): FingerprintSignal
{
$def = self::SIGNAL_DEFINITIONS[$id] ?? null;
if ($def === null) {
throw new \InvalidArgumentException("Unknown fingerprint signal id: {$id}");
}
return new FingerprintSignal($id, $def['label'], $value, $def['risk'], $def['category']);
}
/** Group signals by category in canonical order, omitting empty categories. */
public static function groupByCategory(array $signals): array
{
$groups = [];
foreach ($signals as $signal) {
$groups[$signal->category->value][] = $signal;
}
$out = [];
foreach (self::FINGERPRINT_CATEGORIES as $category) {
if (isset($groups[$category->value])) {
$out[] = new CategoryGroup($category, $groups[$category->value]);
}
}
return $out;
}
/** Count signals per risk level. */
public static function countByRisk(array $signals): RiskCounts
{
$counts = new RiskCounts();
foreach ($signals as $signal) {
match ($signal->risk) {
RiskLevel::Low => $counts->low++,
RiskLevel::Medium => $counts->medium++,
RiskLevel::High => $counts->high++,
};
}
return $counts;
}
/** The exact byte string the fingerprint hash is computed over. */
public static function concatSignalValues(array $signals): string
{
return implode('|', array_map(static fn (FingerprintSignal $s): string => $s->value, $signals));
}
/** SHA-256 hex digest of input. */
public static function sha256Hex(string $input): string
{
return hash('sha256', $input);
}
/**
* Stable fingerprint ID: SHA-256 over every signal value, joined in
* signal order. Same browser state -> same hash; any single changed
* value -> new hash.
*/
public static function hashFingerprint(array $signals): string
{
return self::sha256Hex(self::concatSignalValues($signals));
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →