lthn.io/app/Core/Front/Services/DeviceDetectionService.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

396 lines
10 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Services;
/**
* Device and in-app browser detection.
*
* Detects device type, OS, browser, and crucially - in-app browsers
* from social media platforms. Essential for creators who need to
* handle strict content platform traffic differently.
*/
class DeviceDetectionService
{
/**
* In-app browser detection patterns.
*
* @var array<string, array<string>>
*/
protected array $inAppPatterns = [
'instagram' => ['Instagram'],
'facebook' => ['FBAN', 'FBAV', 'FB_IAB', 'FBIOS', 'FBSS'],
'tiktok' => ['BytedanceWebview', 'musical_ly', 'TikTok'],
'twitter' => ['Twitter'],
'linkedin' => ['LinkedInApp'],
'snapchat' => ['Snapchat'],
'threads' => ['Barcelona'], // Meta's internal codename
'pinterest' => ['Pinterest'],
'reddit' => ['Reddit'],
'wechat' => ['MicroMessenger'],
'line' => ['Line/'],
'telegram' => ['TelegramBot', 'Telegram'],
'discord' => ['Discord'],
'whatsapp' => ['WhatsApp'],
];
/**
* Platforms with strict content policies.
*
* @var array<string>
*/
protected array $strictPlatforms = [
'instagram',
'facebook',
'threads',
'tiktok',
'twitter',
'snapchat',
'linkedin',
];
/**
* Meta-owned platforms.
*
* @var array<string>
*/
protected array $metaPlatforms = [
'instagram',
'facebook',
'threads',
];
/**
* Platform display names.
*
* @var array<string, string>
*/
protected array $displayNames = [
'instagram' => 'Instagram',
'facebook' => 'Facebook',
'tiktok' => 'TikTok',
'twitter' => 'X (Twitter)',
'linkedin' => 'LinkedIn',
'snapchat' => 'Snapchat',
'threads' => 'Threads',
'pinterest' => 'Pinterest',
'reddit' => 'Reddit',
'wechat' => 'WeChat',
'line' => 'LINE',
'telegram' => 'Telegram',
'discord' => 'Discord',
'whatsapp' => 'WhatsApp',
];
/**
* Parse full device info from User-Agent.
*
* @return array{device_type: string, os_name: ?string, browser_name: ?string, in_app_browser: ?string, is_in_app: bool}
*/
public function parse(?string $userAgent): array
{
$userAgent ??= '';
return [
'device_type' => $this->detectDeviceType($userAgent),
'os_name' => $this->detectOS($userAgent),
'browser_name' => $this->detectBrowser($userAgent),
'in_app_browser' => $this->detectInAppBrowser($userAgent),
'is_in_app' => $this->isInAppBrowser($userAgent),
];
}
// -------------------------------------------------------------------------
// In-App Browser Detection
// -------------------------------------------------------------------------
/**
* Detect which in-app browser (if any).
*/
public function detectInAppBrowser(?string $userAgent): ?string
{
if (empty($userAgent)) {
return null;
}
foreach ($this->inAppPatterns as $platform => $patterns) {
foreach ($patterns as $pattern) {
if (stripos($userAgent, $pattern) !== false) {
return $platform;
}
}
}
// Generic WebView detection
if ($this->isGenericWebView($userAgent)) {
return 'webview';
}
return null;
}
/**
* Check if ANY in-app browser.
*/
public function isInAppBrowser(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) !== null;
}
/**
* Check for generic WebView markers.
*/
protected function isGenericWebView(?string $userAgent): bool
{
if (empty($userAgent)) {
return false;
}
// Android WebView marker
if (preg_match('/\bwv\b/', $userAgent)) {
return true;
}
// iOS WebView markers
if (stripos($userAgent, 'AppleWebKit') !== false
&& stripos($userAgent, 'Safari') === false) {
return true;
}
return false;
}
// -------------------------------------------------------------------------
// Platform-Specific Checks
// -------------------------------------------------------------------------
public function isInstagram(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'instagram';
}
public function isFacebook(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'facebook';
}
public function isTikTok(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'tiktok';
}
public function isTwitter(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'twitter';
}
public function isSnapchat(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'snapchat';
}
public function isLinkedIn(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'linkedin';
}
public function isThreads(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'threads';
}
public function isPinterest(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'pinterest';
}
public function isReddit(?string $userAgent): bool
{
return $this->detectInAppBrowser($userAgent) === 'reddit';
}
// -------------------------------------------------------------------------
// Grouped Platform Checks
// -------------------------------------------------------------------------
/**
* Check if from a strict content platform.
*
* These platforms actively enforce content policies and may
* deplatform users who link to adult/restricted content.
*/
public function isStrictContentPlatform(?string $userAgent): bool
{
$platform = $this->detectInAppBrowser($userAgent);
return $platform !== null && in_array($platform, $this->strictPlatforms, true);
}
/**
* Check if from any Meta-owned platform.
*/
public function isMetaPlatform(?string $userAgent): bool
{
$platform = $this->detectInAppBrowser($userAgent);
return $platform !== null && in_array($platform, $this->metaPlatforms, true);
}
/**
* Get human-readable platform name.
*/
public function getPlatformDisplayName(?string $userAgent): ?string
{
$platform = $this->detectInAppBrowser($userAgent);
if ($platform === null) {
return null;
}
return $this->displayNames[$platform] ?? ucfirst($platform);
}
// -------------------------------------------------------------------------
// Device Detection
// -------------------------------------------------------------------------
/**
* Detect device type: mobile, tablet, or desktop.
*/
public function detectDeviceType(?string $userAgent): string
{
if (empty($userAgent)) {
return 'desktop';
}
// Tablets first (before mobile check catches them)
if (preg_match('/iPad|Android(?!.*Mobile)|Tablet/i', $userAgent)) {
return 'tablet';
}
// Mobile devices
if (preg_match('/Mobile|iPhone|iPod|Android|webOS|BlackBerry|IEMobile|Opera Mini/i', $userAgent)) {
return 'mobile';
}
return 'desktop';
}
/**
* Detect operating system.
*/
public function detectOS(?string $userAgent): ?string
{
if (empty($userAgent)) {
return null;
}
$patterns = [
'iOS' => '/iPhone|iPad|iPod/',
'Android' => '/Android/',
'Windows' => '/Windows NT/',
'macOS' => '/Macintosh|Mac OS X/',
'Linux' => '/Linux/',
'ChromeOS' => '/CrOS/',
];
foreach ($patterns as $os => $pattern) {
if (preg_match($pattern, $userAgent)) {
return $os;
}
}
return null;
}
/**
* Detect browser (when not in-app).
*/
public function detectBrowser(?string $userAgent): ?string
{
if (empty($userAgent)) {
return null;
}
// In-app browsers often don't have standard browser identification
if ($this->isInAppBrowser($userAgent)) {
return null;
}
// Order matters - check specific before generic
$patterns = [
'Edge' => '/Edg\//',
'Opera' => '/OPR\/|Opera/',
'Chrome' => '/Chrome\//',
'Firefox' => '/Firefox\//',
'Safari' => '/Safari\//',
'IE' => '/MSIE|Trident/',
];
foreach ($patterns as $browser => $pattern) {
if (preg_match($pattern, $userAgent)) {
return $browser;
}
}
return null;
}
// -------------------------------------------------------------------------
// Bot Detection
// -------------------------------------------------------------------------
/**
* Check if User-Agent appears to be a bot/crawler.
*/
public function isBot(?string $userAgent): bool
{
if (empty($userAgent)) {
return false;
}
$botPatterns = [
'bot', 'crawl', 'spider', 'slurp', 'mediapartners',
'Googlebot', 'Bingbot', 'Baiduspider', 'YandexBot',
'DuckDuckBot', 'facebookexternalhit', 'Twitterbot',
'LinkedInBot', 'WhatsApp', 'TelegramBot', 'Discordbot',
'Applebot', 'AhrefsBot', 'SemrushBot', 'MJ12bot',
];
foreach ($botPatterns as $pattern) {
if (stripos($userAgent, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* Get list of strict content platforms.
*
* @return array<string>
*/
public function getStrictPlatforms(): array
{
return $this->strictPlatforms;
}
/**
* Get list of Meta platforms.
*
* @return array<string>
*/
public function getMetaPlatforms(): array
{
return $this->metaPlatforms;
}
}