diff --git a/.gitignore b/.gitignore index d0f2f3e..cbf6a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ logs/*.log.txt logs/*.srv.txt logs/*.tck.txt +config/user-config.json php_errors.log diff --git a/action.php b/action.php index a1aab74..0535472 100644 --- a/action.php +++ b/action.php @@ -20,12 +20,19 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin { * @return void */ public function register(EventHandler $controller) { + + // insert header data into the page: $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertHeader'); + + // write to the log after the page content was displayed: + $controller->register_hook('TPL_CONTENT_DISPLAY', 'AFTER', $this, 'writeServerLog'); + } /* session information */ private $sessionId = null; private $sessionType = ''; + private $ipAddress = null; /** * Inserts tracking code to the page header @@ -41,28 +48,23 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin { $this->getSessionInfo(); // is there a user logged in? - $username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) - ? $INFO['userinfo']['name'] : ''); + $username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) ? $INFO['userinfo']['name'] : ''); // build the tracker code: $code = NL . DOKU_TAB . "document._botmon = {'t0': Date.now(), 'session': '" . json_encode($this->sessionId) . "'};" . NL; if ($username) { $code .= DOKU_TAB . 'document._botmon.user = "' . $username . '";'. NL; } - $code .= DOKU_TAB . "addEventListener('load',function(){" . NL; + // add the deferred script loader:: + $code .= DOKU_TAB . "addEventListener('load', function(){" . NL; $code .= DOKU_TAB . DOKU_TAB . "const e=document.createElement('script');" . NL; $code .= DOKU_TAB . DOKU_TAB . "e.async=true;e.defer=true;" . NL; $code .= DOKU_TAB . DOKU_TAB . "e.src='".DOKU_BASE."lib/plugins/botmon/client.js';" . NL; $code .= DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(e);" . NL; $code .= DOKU_TAB . "});" . NL . DOKU_TAB; - $event->data['script'][] = [ - '_data' => $code - ]; - - /* Write out server-side info to a server log: */ - $this->writeServerLog($username); + $event->data['script'][] = ['_data' => $code]; } /** @@ -70,38 +72,32 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin { * * @return void */ - private function writeServerLog($username) { + public function writeServerLog(Event $event, $param) { global $conf; global $INFO; + // is there a user logged in? + $username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) + ? $INFO['userinfo']['name'] : ''); + + + // clean the page ID $pageId = preg_replace('/[\x00-\x1F]/', "\u{FFFD}", $INFO['id'] ?? ''); - // collect GeoIP information (if available): - $geoIp = ( $this->sessionId == 'localhost' ? 'local' : 'ZZ' ); /* User-defined code for unknown country */ - try { - if (extension_loaded('geoip') && geoip_db_avail(GEOIP_COUNTRY_EDITION)) { - $geoIp = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']); - } else { - Logger::debug('BotMon Plugin: GeoIP module not available'); - } - } catch (Exception $e) { - Logger::error('BotMon Plugin: GeoIP Error', $e->getMessage()); - } - // create the log array: $logArr = Array( - $_SERVER['REMOTE_ADDR'] ?? '', /* remote IP */ + $this->ipAddress, /* remote IP */ $pageId, /* page ID */ $this->sessionId, /* Session ID */ $this->sessionType, /* session ID type */ - $username, + $username, /* user name */ $_SERVER['HTTP_USER_AGENT'] ?? '', /* User agent */ $_SERVER['HTTP_REFERER'] ?? '', /* HTTP Referrer */ substr($conf['lang'],0,2), /* page language */ implode(',', array_unique(array_map( function($it) { return substr($it,0,2); }, explode(',',trim($_SERVER['HTTP_ACCEPT_LANGUAGE'], " \t;,*"))))), /* accepted client languages */ - $geoIp /* GeoIP country code */ + $this->getCountryCode() /* GeoIP country code */ ); //* create the log line */ @@ -123,8 +119,31 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin { fclose($logfile); } + private function getCountryCode() { + + $country = ( $this->ipAddress == 'localhost' ? 'AA' : 'ZZ' ); // default if no geoip is available! + + $lib = $this->getConf('geoiplib'); /* which library to use? (can only be phpgeoip or disabled) */ + + try { + + // use GeoIP module? + if ($lib == 'phpgeoip' && extension_loaded('geoip') && geoip_db_avail(GEOIP_COUNTRY_EDITION)) { // Use PHP GeoIP module + $result = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']); + $country = ($result ? $result : $country); + } + } catch (Exception $e) { + Logger::error('BotMon Plugin: GeoIP Error', $e->getMessage()); + } + + return $country; + } + private function getSessionInfo() { + $this->ipAddress = $_SERVER['REMOTE_ADDR'] ?? null; + if ($this->ipAddress == '127.0.0.1' || $this->ipAddress == '::1') $this->ipAddress = 'localhost'; + // what is the session identifier? if (isset($_SESSION)) { $sesKeys = array_keys($_SESSION); /* DokuWiki Session ID preferred */ @@ -140,8 +159,8 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin { $this->sessionId = session_id(); $this->sessionType = 'php'; } - if (!$this->sessionId) { /* no PHP session ID, try IP address */ - $this->sessionId = $_SERVER['REMOTE_ADDR'] ?? ''; + if (!$this->sessionId && $this->ipAddress) { /* no PHP session ID, try IP address */ + $this->sessionId = $this->ipAddress; $this->sessionType = 'ip'; } if (!$this->sessionId) { /* if everything else fails, just us a random ID */ diff --git a/admin.php b/admin.php index aae7228..ade38a3 100644 --- a/admin.php +++ b/admin.php @@ -59,7 +59,6 @@ class admin_plugin_botmon extends AdminPlugin {
-
@@ -69,7 +68,6 @@ class admin_plugin_botmon extends AdminPlugin {
-
diff --git a/client.js b/client.js index 91b7c7b..d1f8018 100644 --- a/client.js +++ b/client.js @@ -73,7 +73,7 @@ botmon_client = { console.error(err); } finally { /* send the next heartbeat signal after x seconds: */ - setTimeout(this._onHeartbeat.bind(this, this._src.replace( this._scriptName, '/tick.php')),this._heartbeat * 1000); + // setTimeout(this._onHeartbeat.bind(this, this._src.replace( this._scriptName, '/tick.php')),this._heartbeat * 1000); } } } diff --git a/conf/default.php b/conf/default.php new file mode 100644 index 0000000..1ba3fbd --- /dev/null +++ b/conf/default.php @@ -0,0 +1,8 @@ + + */ + +$conf['geoiplib'] = 'disabled'; diff --git a/conf/metadata.php b/conf/metadata.php new file mode 100644 index 0000000..409716f --- /dev/null +++ b/conf/metadata.php @@ -0,0 +1,9 @@ + + */ + +$meta['geoiplib'] = array('multichoice', + '_choices' => array ('disabled', 'phpgeoip')); diff --git a/config/default-config.json b/config/default-config.json new file mode 100644 index 0000000..a3c1e72 --- /dev/null +++ b/config/default-config.json @@ -0,0 +1,118 @@ +{ + "threshold": 100, + "rules": [ + {"func": "fromKnownBotIP", + "id": "botIpRange", "desc": "Common Bot IP range", + "bot": 50 + }, + {"func": "matchesClient", "params": ["aol", "msie", "chromeold","oldedge"], + "id": "oldClient", "desc": "Obsolete browser version", + "bot": 40 + }, + {"func": "matchesPlatform", "params": ["winold", "macosold", "androidold"], + "id": "oldOS", "desc": "Obsolete platform version", + "bot": 40 + }, + {"func": "matchesPlatform", "params": ["winsrvr", "bsd"], + "id": "serverOS", "desc": "Server OS", + "bot": 40 + }, + {"func": "matchesPlatform", "params": ["null"], + "id": "noOS", "desc": "Unknown or missing OS information", + "bot": 40 + }, + {"func": "smallPageCount", "params": [1], + "id": "onePage", "desc": "Visiter viewed only a single page", + "bot": 30 + }, + {"func": "noRecord", "params": ["log"], + "id": "noClient", "desc": "No client-side JS log was recorded", + "bot": 40 + }, + {"func": "noRecord", "params": ["tck"], + "id": "noTicks", "desc": "No client ticks were recorded", + "bot": 10 + }, + {"func": "noReferrer", + "id": "noRefs", "desc": "No referer field", + "bot": 30 + }, + {"func": "matchLang", "params": [], + "id": "langMatch", "desc": "Client’s ‘Accept-Language’ header does not match the page language", + "bot": 30 + }, + {"func": "matchesClient", "params": ["brave"], + "id": "susClient", "desc": "Client identifier that is popular with bot networks", + "bot": 10 + }, + {"func": "combinationTest", "params": [["macos","chromeold"],["macos","msie"],["winold","edge"],["winold","brave"]], + "id": "suspPC", "desc": "Suspicious combination of platform and client", + "bot": 30 + }, + {"func": "combinationTest", "params": [["macos","msie"],["win10","safari"],["macosold","brave"]], + "id": "impPC", "desc": "Impossible combination of platform and client", + "bot": 70 + }, + {"func": "loadSpeed", "params": [3, 20], + "id": "speedRun", "desc": "Average time between page loads is less than 20 seconds", + "bot": 80 + }, + {"func": "noAcceptLang", + "id": "noAcc", "desc": "No “Accept-Language” header", + "bot": 40 + }, + {"func": "clientAccepts", "params": ["zh"], + "id": "zhLang", "desc": "Client accepts Chinese language", + "bot": 60 + }, + {"func": "matchesCountry", "params": ["BR", "CN", "RU", "US", "MX", "SG", "IN", "UY"], + "id": "isFrom", "desc": "Location is in a known bot-spamming country.", + "bot": 50 + } + ], + "ipRanges": [ + {"from": "3.0.0.0", "to": "3.255.255.254", "label": "Amazon Data Services [US]"}, + {"from": "8.127.0.0", "to": "8.223.255.254", "label": "Alibaba [CN]"}, + {"from": "24.240.0.0", "to": "24.247.255.255", "m": 13, "label": "Charter [US]"}, + {"from": "27.106.0.0", "to": "27.106.127.254", "label": "Huawei [US]"}, + {"from": "34.0.0.0", "to": "34.191.255.254", "label": "Google LLC"}, + {"from": "45.0.0.0", "to": "45.255.255.254", "label": "Various small ISPs, mostly BR"}, + {"from": "46.250.160.0", "to": "46.250.191.254", "label": "Huawei [MX]"}, + {"from": "47.200.0.0", "to": "47.203.255.255", "m": 14, "label": "Frontier Communications [US]"}, + {"from": "49.0.200.0", "to": "49.0.255.254", "label": "Huawei [SG]"}, + {"from": "66.249.64.0", "to": "66.249.95.255", "m": 19, "label": "Google LLC [US]"}, + {"from": "84.37.35.0", "to": "84.37.255.254", "label": "GTT.net [US]"}, + {"from": "94.74.64.0", "to": "94.74.127.254", "label": "Huawei [HK]"}, + {"from": "101.0.0.0", "to": "101.255.255.254", "label": "ChinaNet [CN]"}, + {"from": "110.238.80.0", "to": "110.238.127.254", "label": "Huawei [SG]"}, + {"from": "111.119.192.0", "to": "111.119.255.254", "label": "Huawei [SG]"}, + {"from": "119.0.0.0", "to": "119.207.255.254", "label": "Unicom [CN]"}, + {"from": "121.91.168.", "to": "121.91.175.254", "label": "Huawei [HK]"}, + {"from": "122.8.0.0", "to": "122.8.255.254", "label": "CN-ISP [CN]"}, + {"from": "122.9.0.0", "to": "122.9.255.254", "label": "Huawei [CN]"}, + {"from": "124.243.128.0", "to": "124.243.191.254", "label": "Huawei [SG]"}, + {"from": "142.147.128.0", "to": "1142.147.255.254", "label": "Web2Objects LLC [US]"}, + {"from": "150.40.128.0", "to": "150.40.255.254", "label": "Huawei [HK]"}, + {"from": "159.138.0.0", "to": "159.138.225.254", "label": "Huawei [TH]"}, + {"from": "162.128.0.0", "to": "162.128.255.254", "label": "Zenlayer [SG]"}, + {"from": "166.108.192.0", "to": "166.108.255.254", "label": "Huawei [SG]"}, + {"from": "177.0.0.0", "to": "177.255.255.254", "label": "BrasilNET [BR]"}, + {"from": "179.0.0.0", "to": "179.255.255.254", "label": "BrasilNET [BR]"}, + {"from": "183.87.32.0", "to": "183.87.159.254", "label": "Huawei [HK]"}, + {"from": "186.0.0.0", "to": "186.255.255.254", "label": "South-American ISPs (186.x)"}, + {"from": "187.0.0.0", "to": "187.255.255.254", "label": "South-American ISPs (187.x)"}, + {"from": "188.0.0.0", "to": "188.255.255.254", "label": "South-American ISPs (188.x)"}, + {"from": "189.0.0.0", "to": "189.255.255.254", "label": "South-American ISPs (189.x)"}, + {"from": "190.0.0.0", "to": "190.255.255.254", "label": "South-American ISPs (190.x)"}, + {"from": "192.124.170.0", "to": "192.124.182.254", "label": "Relcom [CZ]"}, + {"from": "195.37.0.0", "to": "195.37.255.255", "label": "DFN [DE]"}, + {"from": "2001:4860::::::", "to": "2001:4860:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "label": "Google LLC [US]"}, + {"from": "2001:0ee0::::::", "to": "2001:ee3:ffff:ffff:ffff:ffff:ffff:ffff", "m": 30, "label": "VNPT [VN]"}, + {"from": "2408:8210::::::", "to": "2408:8210:ffff:ffff:ffff:ffff:ffff:ffff", "m": 30, "label": "China Unicom [CN]"}, + {"from": "2600:1f00::::::", "to": "2600:1fff:ffff:ffff:ffff:ffff:ffff:ffff", "m": "Amazon Cloud [US]"}, + {"from": "2603:6010::::::", "to": "2603:6010:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "label": "Charter [US]"}, + {"from": "2603:8000::::::", "to": "2603:80ff:ffff:ffff:ffff:ffff:ffff:ffff", "m": 24, "label": "Charter [US]"}, + {"from": "2804:::::::", "to": "2804:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "m": 16, "label": "Inspire [BR]"}, + {"from": "2a0a:4cc0::::::", "to": "2a0a:4cc0:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "label": "Netcup [DE]"} + ] +} \ No newline at end of file diff --git a/conf/known-bots.json b/config/known-bots.json similarity index 91% rename from conf/known-bots.json rename to config/known-bots.json index 40ab292..646823c 100644 --- a/conf/known-bots.json +++ b/config/known-bots.json @@ -1,35 +1,41 @@ [ {"id": "bingbot", - "n": "BingBot", + "n": "BingBot", "geo": "US", "r": ["bingbot"], "rx": ["\\sbingbot\\/(\\d+\\.\\d+);"], "url": "http://www.bing.com/bingbot.htm" }, {"id": "googlebot", - "n": "GoogleBot", + "n": "GoogleBot", "geo": "US", "r": ["Googlebot"], "rx": ["Googlebot\\/(\\d+\\.\\d+)", "Googlebot-Image\\/(\\d+\\.\\d+)"], "url": "http://www.google.com/bot.html" }, {"id": "googleads", - "n": "Google Ads", + "n": "Google Ads", "geo": "US", "r": ["AdsBot-Google", "AdsBot-Google-Mobile", "Mediapartners-Google"], "rx": ["AdsBot-Google;","AdsBot-Google-Mobile;", "Mediapartners-Google\\/(\\d+\\.\\d+);"], "url": "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers" }, {"id": "googleapi", - "n": "Google API Crawler", + "n": "Google API Crawler", "geo": "US", "r": ["APIs-Google"], "rx": ["APIs-Google"], "url": "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers" }, + {"id": "googleother", + "n": "GoogleOther", "geo": "US", + "r": ["GoogleOther"], + "rx": ["\\sGoogleOther(\\-\\w+)?[\\)\\/]"], + "url": "https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers#googleother" + }, {"id": "applebot", - "n": "Applebot", + "n": "Applebot", "geo": "US", "r": ["Applebot"], "rx": ["Applebot\\/(\\d+\\.\\d+);"], "url": "http://www.apple.com/go/applebot" }, {"id": "slurp", - "n": "Slurp (Yahoo!)", + "n": "Slurp (Yahoo!)", "geo": "US", "r": ["Slurp"], "rx": ["Slurp[\\s;\\)]"], "url": "http://help.yahoo.com/help/us/ysearch/slurp" @@ -41,25 +47,25 @@ "url": "https://duckduckgo.com/duckduckbot.html" }, {"id": "openai", - "n": "OpenAI/ChatGPT", + "n": "OpenAI/ChatGPT", "geo": "US", "r": ["OAI-SearchBot", "ChatGPT-User", "GPTBot"], "rx": ["OAI-SearchBot\\/(\\d+\\.\\d+);", "ChatGPT-User\\/(\\d+\\.\\d+);", "GPTBot\\/(\\d+\\.\\d+);"], "url": "https://platform.openai.com/docs/bots/" }, {"id": "claude", - "n": "Anthropic Claude", + "n": "Anthropic Claude", "geo": "US", "r": ["ClaudeBot", "Claude-User", "Claude-SearchBot"], "rx": ["ClaudeBot\\/(\\d+\\.\\d+);"], "url": "https://darkvisitors.com/agents/claudebot" }, {"id": "perplexity", - "n": "Perplexity", + "n": "Perplexity", "geo": "US", "r": ["PerplexityBot", "Perplexity‑User"], "rx": ["PerplexityBot\\/(\\d+\\.\\d+);", "Perplexity‑User\\/(\\d+\\.\\d+);"], "url": "https://perplexity.ai/perplexitybot" }, {"id": "metabots", - "n": "Meta/Facebook", + "n": "Meta/Facebook", "geo": "US", "r": ["facebookexternalhit", "facebookcatalog","meta-webindexer","meta-externalads","meta-externalagent","meta-externalfetcher"], "rx": ["facebook\\w+\\/(\\d+\\.\\d+)", "meta-\\w+\\/(\\d+\\.\\d+)"], "url": "https://developers.facebook.com/docs/sharing/webmasters/crawler" @@ -89,7 +95,7 @@ "url": "https://ahrefs.com/robot/" }, {"id": "ccbot", - "n": "CommonCrawl Bot", + "n": "CommonCrawl Bot", "geo": "US", "r": ["CCBot"], "rx": ["CCBot\\/(\\d+\\.\\d+)[\\s\\.;]*"], "url": "https://commoncrawl.org/bot.html" @@ -185,7 +191,7 @@ "url": "http://www.sogou.com/docs/help/webmasters.htm#07" }, {"id": "amazon", - "n": "Amazonbot", + "n": "Amazonbot", "geo": "US", "r": ["Amazonbot"], "rx": ["Amazonbot\\/(\\d+\\.\\d+)[;\\s\\(\\.]"], "url": "https://developer.amazon.com/amazonbot" diff --git a/conf/known-clients.json b/config/known-clients.json similarity index 83% rename from conf/known-clients.json rename to config/known-clients.json index 813f5b1..f5bca20 100644 --- a/conf/known-clients.json +++ b/config/known-clients.json @@ -23,6 +23,14 @@ "id": "huawei", "rx": [ "\\sHuaweiBrowser\\/(\\d+\\.\\d+)[\\s\\.]", "\\/harmony360Browser\\/(\\d+\\.\\d+)[\\s\\.]"] }, + {"n": "Ecosia Browser", + "id": "ecosia", + "rx": [ "\\(Ecosia ios@(\\d+)\\." ] + }, + {"n": "Silk", + "id": "silk", + "rx": [ "\\Silk\\/(\\d+)\\." ] + }, {"n": "DuckDuckGo", "id": "ddg", "rx": [ "\\sDdg\\/(\\S+)" ] @@ -59,8 +67,12 @@ "id": "safari", "rx": [ "\\sSafari\\/(\\d+)" ] }, + {"n": "Firefox Old", + "id": "ffold", + "rx": [ "\\sFirefox\\/(\\d\\d)\\.", "\\sFirefox\\s" ] + }, {"n": "Firefox", "id": "firefox", - "rx": [ "\\sFirefox\\/(\\S+)" ] + "rx": [ "\\sFirefox\\/(\\d\\d\\d)\\." ] } ] diff --git a/config/known-ipranges.json b/config/known-ipranges.json new file mode 100644 index 0000000..2542329 --- /dev/null +++ b/config/known-ipranges.json @@ -0,0 +1,160 @@ +{ + "isps": [ + {"i": "hetzner", "n": "Hetzner Online GmbH", "g": "DE"}, + {"i": "huawei", "n": "Huawei Cloud", "g": "HK"} + ], + "5": [ + {"9": {"i": "hetzner", "g": "FI"}}, + {"75": [ + {"128": {"m": 17, "i": "hetzner", "g": "DE"}} + ]} + ], + "23": [ + {"88": [ + {"0": {"m": 17, "i": "hetzner", "g": "DE"}} + ]} + ], + "37": [ + {"27": {"i": "hetzner", "g": "FI"}} + ], + "46": [ + {"4": {"i": "hetzner", "g": "DE"}}, + {"62": [ + {"128": {"m": 17, "i": "hetzner", "g": "FI"}} + ]} + ], + "49": [ + {"12": {"i": "hetzner", "g": "DE"}}, + {"13": {"i": "hetzner", "g": "DE"}} + ], + "65": [ + {"108": {"i": "hetzner", "g": "FI"}}, + {"109": {"i": "hetzner", "g": "FI"}}, + {"210": {"i": "hetzner", "g": "FI"}} + ], + "77": [ + {"42": [ + {"0": {"m": 17, "i": "hetzner", "g": "FI"}} + ]} + ], + "78": [ + {"46": {"i": "hetzner", "g": "DE"}}, + {"47": {"i": "hetzner", "g": "DE"}} + ], + "85": [ + {"10": [ + {"192": {"m": 18, "i": "hetzner", "g": "FI"}} + ]} + ], + "88": [ + {"99": {"i": "hetzner", "g": "DE"}}, + {"198": {"i": "hetzner", "g": "DE"}} + ], + "91": [ + {"98": {"i": "hetzner", "g": "DE"}}, + {"99": {"i": "hetzner", "g": "DE"}}, + {"107": [ + {"128": {"m": 17, "i": "hetzner", "g": "DE"}} + ]} + ], + "94": [ + {"130": {"i": "hetzner", "g": "DE"}} + ], + "95": [ + {"216": {"i": "hetzner", "g": "FI"}}, + {"217": {"i": "hetzner", "g": "FI"}} + ], + "110": [ + {"239": [ + {"64": {"m": 19, "i": "huawei", "g": "SG"}}, + {"96": {"m": 19, "i": "huawei", "g": "SG"}} + ]} + ], + "114": [ + {"119": [ + {"128": {"m": 19, "i": "huawei", "g": "SG"}} + ]} + ], + "116": [ + {"202": {"i": "hetzner", "g": "DE"}}, + {"203": {"i": "hetzner", "g": "DE"}} + ], + "119": [ + {"8": [ + {"32": {"m": 19, "i": "huawei", "g": "HK"}}, + {"96": {"m": 19, "i": "huawei", "g": "HK"}}, + {"160": {"m": 19, "i": "huawei", "g": "SG"}} + ]} + ], + "124": [ + {"243": [ + {"128": {"m": 18, "i": "huawei", "g": "SG"}} + ]} + ], + "128": [ + {"140": [ + {"0": {"m": 17, "i": "hetzner", "g": "DE"}} + ]} + ], + "135": [ + {"181": {"i": "hetzner", "g": "FI"}} + ], + "138": [ + {"199": [ + {"128": {"m": 17, "i": "hetzner", "g": "DE"}} + ]} + ], + "142": [ + {"132": [ + {"128": {"m": 17, "i": "hetzner", "g": "DE"}} + ]} + ], + "154": [ + {"220": [ + {"192": {"m": 19, "i": "huawei", "g": "HK"}} + ]} + ], + "157": [ + {"90": {"i": "hetzner", "g": "DE"}}, + {"180": [ + {"0": {"m": 17, "i": "hetzner", "g": "FI"}} + ]} + ], + "159": [ + {"69": {"i": "hetzner", "g": "DE"}} + ], + "162": [ + {"55": {"i": "hetzner", "g": "DE"}} + ], + "167": [ + {"233": {"i": "hetzner", "g": "DE"}}, + {"235": {"i": "hetzner", "g": "DE"}} + ], + "168": [ + {"119": {"i": "hetzner", "g": "DE"}} + ], + "176": [ + {"9": {"i": "hetzner", "g": "DE"}} + ], + "178": [ + {"63": {"i": "hetzner", "g": "DE"}} + ], + "188": [ + {"34": [ + {"128": {"m": 17, "i": "hetzner", "g": "DE"}} + ]}, + {"40": {"i": "hetzner", "g": "DE"}}, + {"245": {"i": "hetzner", "g": "DE"}} + ], + "195": [ + {"201": {"i": "hetzner", "g": "DE"}} + ], + "213": [ + {"133": [ + {"69": {"m": 19, "i": "hetzner", "g": "DE"}} + ]}, + {"239": [ + {"192": {"m": 18, "i": "hetzner", "g": "DE"}} + ]} + ] +} \ No newline at end of file diff --git a/conf/known-platforms.json b/config/known-platforms.json similarity index 82% rename from conf/known-platforms.json rename to config/known-platforms.json index 7dd452e..817432e 100644 --- a/conf/known-platforms.json +++ b/config/known-platforms.json @@ -15,13 +15,21 @@ "id": "ios", "rx": [ "\\sFxiOS\\/(\\d+\\.\\d+)\\s", "\\siPhone\\sOS\\s([\\d\\._]+)\\s", "\\siPadOS\\s([\\d\\._]+)\\s", "iPad; CPU OS (\\d+)[_\\.\\s]", "\\sCriOS\\/(\\d+\\.\\d+)\\." ] }, + {"n": "FireOS", + "id": "fire", + "rx": [ "\\sKFFOWI[\\)\\s]]", "\\sKFTHWI[\\)\\s]]" , "\\sSilk\\/"] + }, + {"n": "Old Android", + "id": "androidold", + "rx": [ "Android[\\s;\\/](\\d)\\." ] + }, {"n": "Android", "id": "android", - "rx": [ "[\\(\\s]Android\\s([^;]+);" ] + "rx": [ "Android[\\s;\\/](\\d\\d)[\\.;\\s]" ] }, {"n": "Old MacOS", "id": "macosold", - "rx": [ "\\sMac OS X 10[\\._](\\d|1[0-3])[\\._;\\s\\)]", "\\sMac OS X (1[123])[\\._]" ] + "rx": [ "\\sMac OS X 10[\\._](\\d|1[0-3])[\\._;\\s\\)]", "\\sMac OS X (1[123])[\\.;_\\s]" ] }, {"n": "MacOS", "id": "macos", diff --git a/conf/botmon-config.json b/config/user-config.json similarity index 83% rename from conf/botmon-config.json rename to config/user-config.json index c17bd77..3f87f02 100644 --- a/conf/botmon-config.json +++ b/config/user-config.json @@ -9,7 +9,7 @@ "id": "oldClient", "desc": "Obsolete browser version", "bot": 40 }, - {"func": "matchesPlatform", "params": ["winold", "macosold"], + {"func": "matchesPlatform", "params": ["winold", "macosold", "androidold"], "id": "oldOS", "desc": "Obsolete platform version", "bot": 40 }, @@ -17,6 +17,10 @@ "id": "serverOS", "desc": "Server OS", "bot": 40 }, + {"func": "matchesPlatform", "params": ["null"], + "id": "noOS", "desc": "Unknown or missing OS information", + "bot": 40 + }, {"func": "smallPageCount", "params": [1], "id": "onePage", "desc": "Visiter viewed only a single page", "bot": 40 @@ -65,6 +69,10 @@ "id": "isFrom", "desc": "Location is in a known bot-spamming country.", "bot": 50 }, + {"func": "notFromCountry", "params": ["DE", "AT", "CH", "LI", "LU", "BE"], + "id": "notFromHere", "desc": "Location is not among the site’s main target countries.", + "bot": 20 + }, {"func": "matchesCountry", "params": ["ZZ"], "id": "zzCtry", "desc": "Location could not be determined", "bot": 20 @@ -73,12 +81,13 @@ "ipRanges": [ {"from": "3.0.0.0", "to": "3.255.255.254", "label": "Amazon Data Services [US]"}, {"from": "8.127.0.0", "to": "8.223.255.254", "label": "Alibaba [CN]"}, - {"from": "24.240.0.0", "to": "24.243.255.254", "label": "Charter [US]"}, + {"from": "24.240.0.0", "to": "24.247.255.255", "m": 13, "label": "Charter [US]"}, {"from": "27.106.0.0", "to": "27.106.127.254", "label": "Huawei [US]"}, {"from": "34.0.0.0", "to": "34.191.255.254", "label": "Google LLC"}, {"from": "45.0.0.0", "to": "45.255.255.254", "label": "Various small ISPs, mostly BR"}, {"from": "46.250.160.0", "to": "46.250.191.254", "label": "Huawei [MX]"}, {"from": "49.0.200.0", "to": "49.0.255.254", "label": "Huawei [SG]"}, + {"from": "66.249.64.0", "to": "66.249.95.255", "m": 19, "label": "Google LLC [US]"}, {"from": "84.37.35.0", "to": "84.37.255.254", "label": "GTT.net [US]"}, {"from": "94.74.64.0", "to": "94.74.127.254", "label": "Huawei [HK]"}, {"from": "101.0.0.0", "to": "101.255.255.254", "label": "ChinaNet [CN]"}, @@ -102,12 +111,14 @@ {"from": "188.0.0.0", "to": "188.255.255.254", "label": "South-American ISPs (188.x)"}, {"from": "189.0.0.0", "to": "189.255.255.254", "label": "South-American ISPs (189.x)"}, {"from": "190.0.0.0", "to": "190.255.255.254", "label": "South-American ISPs (190.x)"}, - {"from": "192.124.170.0", "to": "192.124.182.254", "label": "Relcom [CZ]"}, + {"from": "192.124.170.0", "to": "192.124.182.254", "label": "Relcom [CZ]"}, {"from": "195.37.0.0", "to": "195.37.255.255", "label": "DFN [DE]"}, - {"from": "2001:4800::::::", "to": "2001:4fff:ffff:ffff:ffff:ffff:ffff:ffff", "label": "Rackspace/Google [US]"}, - {"from": "2001:0ee0::::::", "to": "2001:ee3:ffff:ffff:ffff:ffff:ffff:ffff", "mask": 30, "label": "VNPT [VN]"}, - {"from": "2600:1f00::::::", "to": "2600:1fff:ffff:ffff:ffff:ffff:ffff:ffff", "label": "Amazon Cloud [US]"}, - {"from": "2804:::::::", "to": "2804:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "label": "Inspire [BR]"}, + {"from": "2001:4860::::::", "to": "2001:4860:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "label": "Google LLC [US]"}, + {"from": "2001:0ee0::::::", "to": "2001:ee3:ffff:ffff:ffff:ffff:ffff:ffff", "m": 30, "label": "VNPT [VN]"}, + {"from": "2600:1f00::::::", "to": "2600:1fff:ffff:ffff:ffff:ffff:ffff:ffff", "m": "Amazon Cloud [US]"}, + {"from": "2603:6010::::::", "to": "2603:6010:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "label": "Charter [US]"}, + {"from": "2603:8000::::::", "to": "2603:80ff:ffff:ffff:ffff:ffff:ffff:ffff", "m": 24, "label": "Charter [US]"}, + {"from": "2804:::::::", "to": "2804:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "m": 16, "label": "Inspire [BR]"}, {"from": "2a0a:4cc0::::::", "to": "2a0a:4cc0:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "label": "Netcup [DE]"} ] } \ No newline at end of file diff --git a/img/clients.png b/img/clients.png index fc6a01d..b087ff6 100644 Binary files a/img/clients.png and b/img/clients.png differ diff --git a/img/links.png b/img/links.png new file mode 100644 index 0000000..7fd9869 Binary files /dev/null and b/img/links.png differ diff --git a/img/platforms.png b/img/platforms.png index 46a33b7..0e0eb7e 100644 Binary files a/img/platforms.png and b/img/platforms.png differ diff --git a/lang/en/settings.php b/lang/en/settings.php new file mode 100644 index 0000000..388f996 --- /dev/null +++ b/lang/en/settings.php @@ -0,0 +1,10 @@ + + */ + +$lang['geoiplib'] = 'Add GeoIP Information
(requires PHP module to be installed)'; + $lang['geoiplib_o_disabled'] = 'Disabled'; + $lang['geoiplib_o_phpgeoip'] = 'Use GeoIP Module'; \ No newline at end of file diff --git a/plugin.info.txt b/plugin.info.txt index 8ababe2..b2bf384 100644 --- a/plugin.info.txt +++ b/plugin.info.txt @@ -1,7 +1,7 @@ base botmon author Sascha Leib email ad@hominem.com -date 2025-09-12 +date 2025-09-13 name Bot Monitoring desc A tool for monitoring and analysing bot traffic to your wiki (under development) url https://www.dokuwiki.org/plugin:botmon diff --git a/script.js b/script.js index f17eb69..b6e1592 100644 --- a/script.js +++ b/script.js @@ -21,7 +21,6 @@ const BotMon = { // find the plugin basedir: this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) + '/plugins/botmon/'; - this._DWBaseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/lib/')) + '/'; // read the page language from the DOM: this._lang = document.getRootNode().documentElement.lang || this._lang; @@ -34,7 +33,6 @@ const BotMon = { }, _baseDir: null, - _DWBaseDir: null, _lang: 'en', _today: (new Date()).toISOString().slice(0, 10), _timeDiff: '', @@ -109,7 +107,7 @@ const BotMon = { _formatTime: function(date) { if (date) { - return ('0'+date.getHours()).slice(-2) + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2); + return date.getHours() + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2); } else { return null; } @@ -320,7 +318,7 @@ BotMon.live = { nv._country = ( nv.geo == 'local' ? "localhost" : "Unknown" ); if (nv.geo && nv.geo !== '' && nv.geo !== 'ZZ' && nv.geo !== 'local') { const countryName = new Intl.DisplayNames(['en', BotMon._lang], {type: 'region'}); - nv._country = countryName.of(nv.geo) ?? nv.geo; + nv._country = countryName.of(nv.geo.substring(0,2)) ?? nv.geo; } } catch (err) { console.error(err); @@ -435,7 +433,7 @@ BotMon.live = { // get the page view info: let pv = model._getPageView(visitor, dat); if (!pv) { - console.warn(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`); + console.info(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`); pv = model._makePageView(dat, type); visitor._pageViews.push(pv); } @@ -450,19 +448,21 @@ BotMon.live = { // helper function to create a new "page view" item: _makePageView: function(data, type) { + // console.info('_makePageView', data); // try to parse the referrer: let rUrl = null; try { rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null ); } catch (e) { - console.info(`Invalid referer: “${data.ref}”.`); + console.warn(`Invalid referer: “${data.ref}”.`); } return { _by: type, ip: data.ip, pg: data.pg, + lang: data.lang || '??', _ref: rUrl, _firstSeen: data.ts, _lastSeen: data.ts, @@ -553,9 +553,9 @@ BotMon.live = { if (v._type == BM_USERTYPE.KNOWN_BOT || v._type == BM_USERTYPE.LIKELY_BOT) { /* bots only */ // add bot views to IP range information: - v._pageViews.forEach( pv => { + /*v._pageViews.forEach( pv => { me.addToIPRanges(pv.ip); - }); + });*/ // add to the country lists: me.addToCountries(v.geo, v._country, v._type); @@ -572,10 +572,10 @@ BotMon.live = { }, // visits from IP ranges: - _ipRange: { + /*_ipRange: { ip4: [], ip6: [] - }, + },*/ /** * Adds a visit to the IP range statistics. * @@ -583,7 +583,7 @@ BotMon.live = { * * @param {string} ip The IP address to add. */ - addToIPRanges: function(ip) { + /*addToIPRanges: function(ip) { // #TODO: handle nestled ranges! const me = BotMon.live.data.analytics; @@ -615,8 +615,8 @@ BotMon.live = { it.count += 1; } - }, - getTopBotIPRanges: function(max) { + },*/ + /*getTopBotIPRanges: function(max) { const me = BotMon.live.data.analytics; @@ -649,7 +649,7 @@ BotMon.live = { } return rList; - }, + },*/ /* countries of visits */ _countries: { @@ -885,7 +885,7 @@ BotMon.live = { // Load the list of known bots: BotMon.live.gui.status.showBusy("Loading known bots …"); - const url = BotMon._baseDir + 'conf/known-bots.json'; + const url = BotMon._baseDir + 'config/known-bots.json'; try { const response = await fetch(url); if (!response.ok) { @@ -896,7 +896,7 @@ BotMon.live = { this._ready = true; } catch (error) { - BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message); + BotMon.live.gui.status.setError("Error while loading the known bots file:", error.message); } finally { BotMon.live.gui.status.hideBusy("Status: Done."); BotMon.live.data._dispatch('bots') @@ -961,7 +961,7 @@ BotMon.live = { // Load the list of known bots: BotMon.live.gui.status.showBusy("Loading known clients"); - const url = BotMon._baseDir + 'conf/known-clients.json'; + const url = BotMon._baseDir + 'config/known-clients.json'; try { const response = await fetch(url); if (!response.ok) { @@ -983,7 +983,7 @@ BotMon.live = { match: function(agent) { //console.info('BotMon.live.data.clients.match(',agent,')'); - let match = {"n": "Unknown", "v": -1, "id": null}; + let match = {"n": "Unknown", "v": -1, "id": 'null'}; if (agent) { BotMon.live.data.clients._list.find(client => { @@ -1009,7 +1009,7 @@ BotMon.live = { // return the browser name for a browser ID: getName: function(id) { const it = BotMon.live.data.clients._list.find(client => client.id == id); - return it.n; + return ( it && it.n ? it.n : "Unknown"); //it.n; }, // indicates if the list is loaded and ready to use: @@ -1027,7 +1027,7 @@ BotMon.live = { // Load the list of known bots: BotMon.live.gui.status.showBusy("Loading known platforms"); - const url = BotMon._baseDir + 'conf/known-platforms.json'; + const url = BotMon._baseDir + 'config/known-platforms.json'; try { const response = await fetch(url); if (!response.ok) { @@ -1049,7 +1049,7 @@ BotMon.live = { match: function(cid) { //console.info('BotMon.live.data.platforms.match(',cid,')'); - let match = {"n": "Unknown", "id": null}; + let match = {"n": "Unknown", "id": 'null'}; if (cid) { BotMon.live.data.platforms._list.find(platform => { @@ -1095,13 +1095,10 @@ BotMon.live = { BotMon.live.gui.status.showBusy("Loading list of rules …"); // relative file path to the rules file: - const filePath = 'conf/botmon-config.json'; + const filePath = 'config/default-config.json'; - // check if the user has a configuration file in their DokuWiki installation, - // then load the appropriate file: - this._checkForUserConfig( filePath, (hasUserConfig) => { - this._loadrulesFile(( hasUserConfig ? BotMon._DWBaseDir : BotMon._baseDir ) + filePath); - }); + // load the rules file: + this._loadrulesFile(BotMon._baseDir + filePath); }, /** @@ -1146,35 +1143,13 @@ BotMon.live = { me._ready = true; } catch (error) { - BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message); + BotMon.live.gui.status.setError("Error while loading the config file: " + error.message); } finally { BotMon.live.gui.status.hideBusy("Status: Done."); BotMon.live.data._dispatch('rules') } }, - /** - * Checks if the user has a configuration file in their DokuWiki installation. - * @param {function} whenDone - an optional callback function to call when the check is finished. - */ - _checkForUserConfig: async function(filePath, whenDone = undefined) { - //console.info('BotMon.live.data.rules._checkForUserConfig()'); - - let hasUserConfig = false; - try { - const response = await fetch(BotMon._DWBaseDir + '/' + filePath, { - method: 'HEAD' - }); - hasUserConfig = response.ok; - } catch (err) { - console.info("An error occured while trying to check for a user configuration file:", err); - } finally { - if (whenDone) { - whenDone(hasUserConfig); - } - } - }, - _rulesList: [], // list of rules to find out if a visitor is a bot _threshold: 100, // above this, it is considered a bot. @@ -1243,6 +1218,9 @@ BotMon.live = { matchesPlatform: function(visitor, ...platforms) { const pId = ( visitor._platform ? visitor._platform.id : ''); + + if (visitor._platform.id == null) console.log(visitor._platform); + return platforms.includes(pId); }, @@ -1317,6 +1295,16 @@ BotMon.live = { return false; }, + // the "Accept language" header contains certain entries: + clientAccepts: function(visitor, ...languages) { + //console.info('clientAccepts', visitor.accept, languages); + + if (visitor.accept && languages) {; + return ( visitor.accept.split(',').filter(lang => languages.includes(lang)).length > 0 ); + } + return false; + }, + // Is there an accept-language field defined at all? noAcceptLang: function(visitor) { @@ -1476,7 +1464,7 @@ BotMon.live = { } } catch (error) { - BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); + BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message} – data may be incomplete.`); } finally { BotMon.live.gui.status.hideBusy("Status: Done."); if (onLoaded) { @@ -1557,7 +1545,7 @@ BotMon.live = { } // update the suspected bot IP ranges list: - const botIps = document.getElementById('botmon__today__botips'); + /*const botIps = document.getElementById('botmon__today__botips'); if (botIps) { botIps.appendChild(makeElement('dt', {}, "Bot IP ranges (top 5)")); @@ -1568,7 +1556,7 @@ BotMon.live = { li.appendChild(makeElement('span', {'class': 'count' }, ipInfo.num)); botIps.append(li) }); - } + }*/ // update the top bot countries list: const botCountries = document.getElementById('botmon__today__countries'); @@ -1678,7 +1666,7 @@ BotMon.live = { BotMon.live.gui.status._errorCount += 1; const el = document.getElementById('botmon__today__status'); if (el) { - el.innerText = "An error occurred. Data may be incomplete! See browser console for details"; + el.innerText = "Data may be incomplete."; el.classList.add('error'); } }, @@ -1792,20 +1780,17 @@ BotMon.live = { const platformName = (data._platform ? data._platform.n : 'Unknown'); const clientName = (data._client ? data._client.n: 'Unknown'); + const sumClass = ( data._seenBy.indexOf('srv') < 0 ? 'noServer' : 'hasServer'); + const li = make('li'); // root list item const details = make('details'); - const summary = make('summary'); + const summary = make('summary', { + 'class': sumClass + }); details.appendChild(summary); const span1 = make('span'); /* left-hand group */ - // country flag: - span1.appendChild(make('span', { - 'class': 'icon_only country ctry_' + data.geo.toLowerCase(), - 'data-ctry': (data.geo | 'ZZ'), - 'title': "Country: " + ( data._country || "Unknown") - }, ( data._country || "Unknown") )); - if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* No platform/client for bots */ span1.appendChild(make('span', { /* Platform */ 'class': 'icon_only platform pf_' + (data._platform ? data._platform.id : 'unknown'), @@ -1848,6 +1833,15 @@ BotMon.live = { }, data.id)); } + // country flag: + if (data.geo && data.geo !== 'ZZ') { + span1.appendChild(make('span', { + 'class': 'icon_only country ctry_' + data.geo.toLowerCase(), + 'data-ctry': data.geo, + 'title': "Country: " + ( data._country || "Unknown") + }, ( data._country || "Unknown") )); + } + summary.appendChild(span1); const span2 = make('span'); /* right-hand group */ @@ -1904,7 +1898,21 @@ BotMon.live = { platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); dl.appendChild(make('dt', {}, "IP-Address:")); - dl.appendChild(make('dd', {'class': 'has_icon ipaddr ip' + ipType}, data.ip)); + const ipItem = make('dd', {'class': 'has_icon ipaddr ip' + ipType}); + ipItem.appendChild(make('span', {'class': 'address'} , data.ip)); + ipItem.appendChild(make('a', { + 'class': 'icon_only extlink dnscheck', + 'href': `https://dnschecker.org/ip-location.php?ip=${encodeURIComponent(data.ip)}`, + 'target': 'dnscheck', + 'title': "View this address on DNSChecker.org" + } , "Check Address")); + ipItem.appendChild(make('a', { + 'class': 'icon_only extlink ipinfo', + 'href': `https://ipinfo.io/${encodeURIComponent(data.ip)}`, + 'target': 'ipinfo', + 'title': "View this address on IPInfo.io" + } , "DNS Info")); + dl.appendChild(ipItem); /*dl.appendChild(make('dt', {}, "ID:")); dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/ @@ -1924,7 +1932,7 @@ BotMon.live = { dl.appendChild(make('dd', {'class': 'agent'}, data.agent)); dl.appendChild(make('dt', {}, "Languages:")); - dl.appendChild(make('dd', {'class': 'langs'}, "Client accepts: [" + data.accept + "]; Page: [" + data.lang + ']')); + dl.appendChild(make('dd', {'class': 'langs'}, ` [${data.accept}]`)); if (data.geo && data.geo !=='') { dl.appendChild(make('dt', {}, "Location:")); @@ -1949,37 +1957,56 @@ BotMon.live = { const pageList = make('ul'); /* list all page views */ + data._pageViews.sort( (a, b) => a._firstSeen - b._firstSeen ); data._pageViews.forEach( (page) => { + //console.log("page:",page); + const pgLi = make('li'); - let visitTimeStr = "Bounce"; - const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); - if (visitDuration > 0) { - visitTimeStr = Math.floor(visitDuration / 1000) + "s"; - } + const lGroup = make('span'); // left group: - pgLi.appendChild(make('span', {}, page.pg)); /* DW Page ID */ - if (page._ref) { - pgLi.appendChild(make('span', { - 'data-ref': page._ref.host, - 'title': "Referrer: " + page._ref.full - }, page._ref.site)); - } else { - pgLi.appendChild(make('span', { - }, "No referer")); - } - pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount)); - pgLi.appendChild(make('span', {}, BotMon.t._formatTime(page._firstSeen))); + lGroup.appendChild(make('span', { + 'data-lang': page.lang, + 'title': "PageID: " + page.pg + }, page.pg)); /* DW Page ID */ + + pgLi.appendChild(lGroup); // end of left group + + const rGroup = make('span'); // right group: + + let visitTimeStr = "Bounce"; + const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); + if (visitDuration > 0) { + visitTimeStr = Math.floor(visitDuration / 1000) + "s"; + } + + /*if (page._ref) { + rGroup.appendChild(make('span', { + 'data-ref': page._ref.host, + 'title': "Referrer: " + page._ref.full + }, page._ref.site)); + } else { + rGroup.appendChild(make('span', { + }, "No referer")); + }*/ + //rGroup.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount)); + + // get the time difference: + const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen); + if (tDiff) { + rGroup.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff)); + } else { + rGroup.appendChild(make('span', { + 'class': 'bounce', + 'title': "Visitor bounced"}, "Bounce")); + } + rGroup.appendChild(make('span', { + 'class': 'first-seen', + 'title': "First visited: " + page._firstSeen.toLocaleString() + }, BotMon.t._formatTime(page._firstSeen))); + + pgLi.appendChild(rGroup); // end of right group - // get the time difference: - const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen); - if (tDiff) { - pgLi.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff)); - } else { - pgLi.appendChild(make('span', { - 'class': 'bounce', - 'title': "Visitor bounced"}, "Bounce")); - } pageList.appendChild(pgLi); }); diff --git a/style.less b/style.less index 627b13e..4f5fede 100644 --- a/style.less +++ b/style.less @@ -40,7 +40,7 @@ /* Bot icons */ &.bot::before { background-image: url('img/bots.png') } - &.bot_googlebot::before, &.bot_googleads::before, &.bot_googleapi::before { background-position-y: -20px } + &.bot_googlebot::before, &.bot_googleads::before, &.bot_googleapi::before, &.bot_googleother::before { background-position-y: -20px } &.bot_bingbot::before { background-position-y: -40px } &.bot_applebot::before { background-position-y: -60px } &.bot_openai::before { background-position-y: -80px } @@ -49,8 +49,7 @@ &.bot_seznambot::before { background-position-y: -140px } &.bot_claude::before { background-position-y: -160px } - /* platform icons */ - + /* platform icons */ &.platform::before { background-image: url('img/platforms.png') } &.pf_win10::before { background-position-y: -20px } &.pf_winold::before, dd.platform_winold::before, @@ -59,18 +58,19 @@ &.pf_macosold::before { background-position-y: -80px } &.pf_ios::before { background-position-y: -100px } &.pf_android::before { background-position-y: -120px } + &.pf_androidold::before { background-position-y: -140px } &.pf_linux::before { background-position-y: -160px } &.pf_bsd::before { background-position-y: -180px } &.pf_chromium::before { background-position-y: -200px } &.pf_hmos::before { background-position-y: -220px } &.pf_tizen::before { background-position-y: -240px } + &.pf_fire::before { background-position-y: -260px } /* browser icons */ &.client::before { background-image: url('img/clients.png') } &.cl_firefox::before { background-position-y: -20px } &.cl_safari::before { background-position-y: -40px } &.cl_chrome::before { background-position-y: -60px } - &.cl_chromeold::before { background-position-y: -60px; opacity: 75%; filter: ~"saturate(25%)"; } &.cl_msedge::before { background-position-y: -80px } &.cl_msie::before { background-position-y: -100px } &.cl_opera::before { background-position-y: -120px } @@ -81,6 +81,10 @@ &.cl_vivaldi::before { background-position-y: -220px } &.cl_aol::before { background-position-y: -240px } &.cl_ya::before { background-position-y: -260px } + &.cl_silk::before { background-position-y: -280px } + &.cl_ffold::before { background-position-y: -300px } + &.cl_chromeold::before { background-position-y: -320px } + &.cl_ecosia::before { background-position-y: -340px } /* Country flags */ /* Note: flag images and CSS adapted from: https://github.com/lafeber/world-flags-sprite/ */ @@ -329,13 +333,18 @@ &.typ_php::before { background-position-y: -40px } &.typ_ip::before { background-position-y: -60px } &.typ_usr::before { background-position-y: -80px } + + /* External link icons */ + &.extlink::before { background-image: url('img/links.png') } + &.extlink.dnscheck::before { background-position-y: -20px } + &.extlink.ipinfo::before { background-position-y: -40px } } /* grid layout for the overview: */ .botmon_overview_grid { & { display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; grid-gap: 0 .33em; } dl { @@ -354,7 +363,7 @@ header { background-color: #F0F0F0; color: #333; - border: #666 solid 1px; + border: #999 solid 1px; border-radius: .5rem .5rem 0 0; margin: .5rem 0 1pt 0; padding: .25rem .5rem; @@ -432,7 +441,7 @@ } & > div { padding: .5rem; - border: #CCC solid 1px; + border: #999 solid 1px; border-top-width: 0; border-radius: 0 0 .25rem .25rem; } @@ -448,7 +457,7 @@ } & > details > summary { background-color: #F0F0F0; - border: #CCC solid 1px; + border: #999 solid 1px; } } @@ -483,7 +492,9 @@ border-bottom: #CCC solid 1px; border-radius: .7em; } - + details ul > li > details > summary.noServer { + opacity: 67%; + } details ul > li > details > summary > span { display: flex; align-items: center; @@ -519,26 +530,49 @@ & { display: flex; justify-content: space-between; - align-items: center; + align-items: baseline; + white-space: nowrap; + line-height: 1.2rem; + margin: 0; + padding: 0 .25em; + } + &:nth-child(odd) { + background-color: #DFDFDF; } span { - &.visit-length { - min-width: min-content; - } - &.bounce { - width: 1.25em; height: 1.25em; - overflow: hidden; - } - &.bounce::before { - display: inline-block; - content: ''; - width: 1.25em; height: 1.25em; - background: transparent url('img/bounce.svg') center no-repeat; - background-size: 1.25em; - } + display: inline-block; } } } + span[data-lang] { + overflow: hidden; + text-overflow: ellipsis; + } + span[data-lang]::after { + content: attr(data-lang); + font-size: smaller; + color: #555; + border: #555 solid 1px; + line-height: 1.25; + border-radius: 2pt; + padding: 0 1pt; + margin-left: .2em; + } + span.first-seen { + min-width: 4.2em; + text-align: right;; + } + span.bounce { + width: 1.25em; height: 1.25em; + overflow: hidden; + } + span.bounce::before { + display: inline-block; + content: ''; + width: 1.25em; height: 1.25em; + background: transparent url('img/bounce.svg') center no-repeat; + background-size: 1.25em; + } } } @@ -549,7 +583,7 @@ align-items: center; } li:nth-child(odd) { - background-color: #EEE; + background-color: #DFDFDF; } li.total { border-top: #333 solid 1px; @@ -587,7 +621,7 @@ column-gap: .25rem; background-color: #F0F0F0; color: #333; - border: #666 solid 1px; + border: #999 solid 1px; border-radius: 0 0 .5rem .5rem; margin: 1pt 0 0 0; padding: .25rem .5rem; diff --git a/tick.php b/tick.php index 1d46b1e..67b4550 100644 --- a/tick.php +++ b/tick.php @@ -2,6 +2,12 @@ // Note: this script is normally called in HEAD mode, therefore it can not return any payload. + // quit out if it is called without athe right parameters: + if (!isset($_GET['id']) || !isset($_GET['p'])) { + http_response_code(400); + die("Parameter error."); + } + // what is the session identifier? $sessionId = preg_replace('/[\x00-\x1F{};\"\']/', "\u{FFFD}", $_GET['id']) /* clean json parameter */ ?? session_id()