loadConfig(); $this->sessionId = $this->generateSessionId(); $this->initializeGeoIP(); } public function setAPI(CMSAPI $api): void { $this->api = $api; // Track page visit after API is available $this->trackPageVisit(); } private function loadConfig(): void { $configFile = __DIR__ . '/config.json'; $this->config = [ 'enabled' => true, 'broker_host' => 'localhost', 'broker_port' => 1883, 'client_id' => 'codepress_cms', 'username' => '', 'password' => '', 'topic_prefix' => 'codepress', 'track_visitors' => true, 'track_pages' => true, 'track_performance' => true, 'track_user_flows' => true, 'session_timeout' => 1800, 'geoip_database_path' => __DIR__ . '/GeoLite2-Country.mmdb' ]; if (file_exists($configFile)) { $jsonConfig = json_decode(file_get_contents($configFile), true); $this->config = array_merge($this->config, $jsonConfig); } } private function initializeGeoIP(): void { $geoipPath = $this->config['geoip_database_path'] ?? __DIR__ . '/GeoLite2-Country.mmdb'; // For now, disable GeoIP2 until properly configured $this->logMessage('info', 'GeoIP2 temporarily disabled - will be enabled when database is available'); $this->geoipReader = null; } private function generateSessionId(): string { if (isset($_COOKIE['cms_session_id'])) { return $_COOKIE['cms_session_id']; } $sessionId = uniqid('cms_', true); setcookie('cms_session_id', $sessionId, time() + $this->config['session_timeout'], '/'); return $sessionId; } private function trackPageVisit(): void { if (!$this->config['enabled'] || !$this->config['track_pages']) { return; } // Track user flow before updating current page $this->trackUserFlow(); // Format URL nicely: ?page=foo/bar -> /page/foo/bar $pageUrl = $_SERVER['REQUEST_URI'] ?? ''; if (isset($_GET['page'])) { $pageUrl = '/page/' . $_GET['page']; // Append other relevant params if needed, e.g., language if (isset($_GET['lang'])) { $pageUrl .= '?lang=' . $_GET['lang']; } } $clientIp = $this->getClientIp(); $geoData = $this->getGeoData($clientIp); $pageData = [ 'timestamp' => date('c'), 'session_id' => $this->sessionId, 'page_url' => $pageUrl, 'page_title' => $this->api ? $this->api->getCurrentPageTitle() : '', 'referrer' => $_SERVER['HTTP_REFERER'] ?? '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'ip_address' => $clientIp, 'country' => $geoData['country'], 'country_code' => $geoData['country_code'], 'city' => $geoData['city'], 'language' => $this->api ? $this->api->getCurrentLanguage() : 'nl', 'layout' => $this->api ? $this->getPageLayout() : 'unknown', 'device_type' => $this->getDeviceType(), 'browser' => $this->getBrowser(), 'os' => $this->getOS() ]; // Update tracking cookies setcookie('cms_previous_page', $pageUrl, time() + $this->config['session_timeout'], '/'); setcookie('cms_page_timestamp', time(), time() + $this->config['session_timeout'], '/'); $this->publishMessage('page_visit', $pageData); } private function getPageLayout(): string { if (!$this->api) return 'unknown'; $page = $this->api->getCurrentPage(); return $page['layout'] ?? 'sidebar-content'; } private function trackUserFlow(): void { if (!$this->config['track_user_flows'] ?? true) { return; } $previousPage = $_COOKIE['cms_previous_page'] ?? null; $currentPage = $this->getCurrentPageUrl(); $previousTimestamp = $_COOKIE['cms_page_timestamp'] ?? time(); if ($previousPage && $previousPage !== $currentPage) { $flowData = [ 'timestamp' => date('c'), 'session_id' => $this->sessionId, 'from_page' => $previousPage, 'to_page' => $currentPage, 'flow_duration' => time() - $previousTimestamp, 'ip_address' => $this->getClientIp() ]; $this->publishMessage('user_flow', $flowData); } } private function getCurrentPageUrl(): string { $pageUrl = $_SERVER['REQUEST_URI'] ?? ''; if (isset($_GET['page'])) { $pageUrl = '/page/' . $_GET['page']; if (isset($_GET['lang'])) { $pageUrl .= '?lang=' . $_GET['lang']; } } return $pageUrl; } private function getGeoData(string $ip): array { // Simplified geolocation - will be enhanced later return [ 'country' => 'Unknown', 'country_code' => 'XX', 'city' => 'Unknown' ]; } private function getDeviceType(): string { $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (preg_match('/Mobile|Android|iPhone|iPad|iPod/', $userAgent)) { return preg_match('/iPad/', $userAgent) ? 'tablet' : 'mobile'; } return 'desktop'; } private function getBrowser(): string { $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (preg_match('/Chrome/', $userAgent)) return 'Chrome'; if (preg_match('/Firefox/', $userAgent)) return 'Firefox'; if (preg_match('/Safari/', $userAgent)) return 'Safari'; if (preg_match('/Edge/', $userAgent)) return 'Edge'; if (preg_match('/Opera/', $userAgent)) return 'Opera'; return 'Unknown'; } private function getOS(): string { $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (preg_match('/Windows/', $userAgent)) return 'Windows'; if (preg_match('/Mac/', $userAgent)) return 'macOS'; if (preg_match('/Linux/', $userAgent)) return 'Linux'; if (preg_match('/Android/', $userAgent)) return 'Android'; if (preg_match('/iOS|iPhone|iPad/', $userAgent)) return 'iOS'; return 'Unknown'; } private function getClientIp(): string { // Check Cloudflare header first if present if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) { return $_SERVER['HTTP_CF_CONNECTING_IP']; } $ipKeys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR']; foreach ($ipKeys as $key) { if (!empty($_SERVER[$key])) { $ips = explode(',', $_SERVER[$key]); // Return the first IP in the list (client IP) return trim($ips[0]); } } return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; } private function publishMessage(string $topic, array $data): void { if (!class_exists('PhpMqtt\Client\MqttClient')) { $this->logMessage('error', 'MQTT client library not installed. Run: composer require php-mqtt/client'); return; } try { $server = new \PhpMqtt\Client\MqttClient( $this->config['broker_host'], $this->config['broker_port'], $this->config['client_id'] ); $connectionSettings = new \PhpMqtt\Client\ConnectionSettings(); if (!empty($this->config['username'])) { $connectionSettings->setUsername($this->config['username']) ->setPassword($this->config['password']); } $server->connect($connectionSettings, true); // Topic format: prefix/action $topic = $this->config['topic_prefix'] . '/' . $topic; $payload = json_encode($data); $server->publish($topic, $payload, 0); $server->disconnect(); $this->logMessage('published', $topic . ' - ' . $payload); } catch (Exception $e) { $this->logMessage('error', 'MQTT publish failed: ' . $e->getMessage()); } } private function logMessage(string $topic, string $payload): void { $logFile = __DIR__ . '/mqtt_tracker.log'; $logEntry = date('Y-m-d H:i:s') . " [{$topic}] {$payload}\n"; file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX); } public function getSidebarContent(): string { // MQTT Tracker is een functionele plugin zonder UI return ''; } public function getConfig(): array { return $this->config; } public function updateConfig(array $newConfig): void { $this->config = array_merge($this->config, $newConfig); $configFile = __DIR__ . '/config.json'; file_put_contents($configFile, json_encode($this->config, JSON_PRETTY_PRINT)); } }