diff --git a/admin.php b/admin.php
index 18200e9..5bc1797 100644
--- a/admin.php
+++ b/admin.php
@@ -44,7 +44,7 @@ class admin_plugin_botmon extends AdminPlugin {
';
if ($this->hasOldLogFiles()) {
- echo '
Note: There are old log files that can be deleted.
Click here to run a delete script, or use
cron to automatically delete them.
';
+ echo 'Note: There are old log files that can be deleted.
Click here to run a delete script, or use
cron to automatically delete them.
';
}
echo '
@@ -64,8 +64,8 @@ class admin_plugin_botmon extends AdminPlugin {
Web metrics
@@ -75,7 +75,7 @@ class admin_plugin_botmon extends AdminPlugin {
diff --git a/config/rules.json b/config/botmon-config.json
similarity index 93%
rename from config/rules.json
rename to config/botmon-config.json
index efbd8ff..06b704a 100644
--- a/config/rules.json
+++ b/config/botmon-config.json
@@ -9,10 +9,14 @@
"id": "oldClient", "desc": "Obsolete browser version",
"bot": 40
},
- {"func": "matchesPlatform", "params": ["winold", "macosold","winsrvr"],
+ {"func": "matchesPlatform", "params": ["winold", "macosold"],
"id": "oldOS", "desc": "Obsolete platform version",
"bot": 40
},
+ {"func": "matchesPlatform", "params": ["winsrvr", "bsd"],
+ "id": "serverOS", "desc": "Server OS",
+ "bot": 40
+ },
{"func": "smallPageCount", "params": [1],
"id": "onePage", "desc": "Visiter viewed only a single page",
"bot": 40
@@ -59,11 +63,11 @@
},
{"func": "matchesCountry", "params": ["BR", "CN", "RU", "US", "MX", "SG", "IN", "UY"],
"id": "isFrom", "desc": "Location is in a known bot-spamming country.",
- "bot": 40
+ "bot": 50
},
{"func": "notFromCountry", "params": ["DE", "AT", "CH", "LI", "LU", "BE"],
- "id": "notFromHere", "desc": "Location is not in one of the site’s target countries.",
- "bot": 40
+ "id": "notFromHere", "desc": "Location is not among the site’s main target countries.",
+ "bot": 20
}
],
"ipRanges": [
@@ -99,6 +103,7 @@
{"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: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]"},
diff --git a/plugin.info.txt b/plugin.info.txt
index 1fb700c..3292553 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-10
+date 2025-09-11
name Bot Monitoring
desc Live monitoring of bot traffic on your DokuWiki instance (under development)
url https://www.dokuwiki.org/plugin:botmon
diff --git a/script.js b/script.js
index 5de5e06..51e5c82 100644
--- a/script.js
+++ b/script.js
@@ -482,6 +482,7 @@ BotMon.live = {
data: {
totalVisits: 0,
totalPageViews: 0,
+ humanPageViews: 0,
bots: {
known: 0,
suspected: 0,
@@ -535,11 +536,6 @@ BotMon.live = {
v._eval = e.rules;
v._botVal = e.val;
- // add each page view to IP range information (unless it is already from a known bot IP range):
- v._pageViews.forEach( pv => {
- me._addToIPRanges(pv.ip);
- });
-
if (e.isBot) { // likely bots
v._type = BM_USERTYPE.LIKELY_BOT;
this.data.bots.suspected += v._pageViews.length;
@@ -548,13 +544,26 @@ BotMon.live = {
v._type = BM_USERTYPE.HUMAN;
this.data.bots.human += v._pageViews.length;
this.groups.humans.push(v);
- }
- // TODO: find suspected bots
-
+ }
}
- // add to the country lists:
- me._addToCountries(v.geo, v._country, v._type);
+ // perform actions depending on the visitor type:
+ 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 => {
+ me.addToIPRanges(pv.ip);
+ });
+
+ // add to the country lists:
+ me.addToCountries(v.geo, v._country, v._type);
+
+ } else { /* humans only */
+
+ // add browser and platform statistics:
+ me.addBrowserPlatform(v);
+ }
+
});
BotMon.live.gui.status.hideBusy('Done.');
@@ -572,7 +581,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;
@@ -653,7 +662,7 @@ BotMon.live = {
*
* @param {string} iso The ISO 3166-1 alpha-2 country code.
*/
- _addToCountries: function(iso, name, type) {
+ addToCountries: function(iso, name, type) {
const me = BotMon.live.data.analytics;
@@ -674,7 +683,7 @@ BotMon.live = {
arr = me._countries.known_bot;
break;
default:
- console.warn(`Unknown user type ${type} in function _addToCountries.`);
+ console.warn(`Unknown user type ${type} in function addToCountries.`);
}
if (arr) {
@@ -740,6 +749,130 @@ BotMon.live = {
return rList;
}
return [];
+ },
+
+ /* browser and platform of human visitors */
+ _browsers: [],
+ _platforms: [],
+
+ addBrowserPlatform: function(visitor) {
+ //console.info('addBrowserPlatform', visitor);
+
+ const me = BotMon.live.data.analytics;
+
+ // add to browsers list:
+ let browserRec = ( visitor._client ? visitor._client : {'id': 'unknown'});
+ if (visitor._client) {
+ let bRec = me._browsers.find( it => it.id == browserRec.id);
+ if (!bRec) {
+ bRec = {
+ 'id': browserRec.id,
+ 'count': 1
+ };
+ me._browsers.push(bRec);
+ } else {
+ bRec.count += 1;
+ }
+ }
+
+ // add to platforms list:
+ let platformRec = ( visitor._platform ? visitor._platform : {'id': 'unknown'});
+ if (visitor._platform) {
+ let pRec = me._platforms.find( it => it.id == platformRec.id);
+ if (!pRec) {
+ pRec = {
+ 'id': platformRec.id,
+ 'count': 1
+ };
+ me._platforms.push(pRec);
+ } else {
+ pRec.count += 1;
+ }
+ }
+
+ },
+
+ getTopBrowsers: function(max) {
+
+ const me = BotMon.live.data.analytics;
+
+ me._browsers.sort( (a,b) => b.count - a.count);
+
+ // how many browsers to show:
+ const max2 = ( me._browsers.length >= max ? max-1 : max );
+
+ const rArr = []; // return array
+ let total = 0;
+ const others = {
+ 'id': 'other',
+ 'name': "Others",
+ 'count': 0
+ }
+ for (let i=0; i < me._browsers.length; i++) {
+ if (i < max2) {
+ rArr.push({
+ 'id': me._browsers[i].id,
+ 'name': BotMon.live.data.clients.getName(me._browsers[i].id),
+ 'count': me._browsers[i].count
+ });
+ total += me._browsers[i].count;
+ } else {
+ others.count += me._browsers[i].count;
+ total += me._browsers[i].count;
+ }
+ };
+
+ if (me._browsers.length > (max-1)) {
+ rArr.push(others);
+ }
+
+ // update percentages:
+ rArr.forEach( it => {
+ it.pct = Math.round(it.count * 100 / total);
+ })
+
+ return rArr;
+ },
+
+ getTopPlatforms: function(max) {
+
+ const me = BotMon.live.data.analytics;
+
+ me._platforms.sort( (a,b) => b.count - a.count);
+ // how many browsers to show:
+ const max2 = ( me._platforms.length >= max ? max-1 : max );
+
+ const rArr = []; // return array
+ let total = 0;
+ const others = {
+ 'id': 'other',
+ 'name': "Others",
+ 'count': 0
+ }
+ for (let i=0; i < me._platforms.length; i++) {
+ if (i < max2) {
+ rArr.push({
+ 'id': me._platforms[i].id,
+ 'name': BotMon.live.data.platforms.getName(me._platforms[i].id),
+ 'count': me._platforms[i].count
+ });
+ total += me._platforms[i].count;
+ } else {
+ others.count += me._platforms[i].count;
+ total += me._platforms[i].count;
+ }
+ };
+
+ if (me._platforms.length > (max-1)) {
+ rArr.push(others);
+ }
+
+ // update percentages:
+ rArr.forEach( it => {
+ it.pct = Math.round(it.count * 100 / total);
+ })
+
+ return rArr;
}
},
@@ -800,9 +933,9 @@ BotMon.live = {
// check for unknown bots:
if (!botInfo) {
- const botmatch = agent.match(/[^\s](\w*bot)[\/\s;\),$]/i);
+ const botmatch = agent.match(/([\s\d\w]*bot|[\s\d\w]*crawler|[\s\d\w]*spider)[\/\s;\),\\.$]/i);
if(botmatch) {
- botInfo = {'id': "other", 'n': "Other", "bot": botmatch[0] };
+ botInfo = {'id': ( botmatch[1] || "other_" ), 'n': "Other" + ( botmatch[1] ? " (" + botmatch[1] + ")" : "" ) , "bot": botmatch[1] };
}
}
@@ -870,6 +1003,12 @@ BotMon.live = {
return match;
},
+ // 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;
+ },
+
// indicates if the list is loaded and ready to use:
_ready: false,
@@ -929,6 +1068,14 @@ BotMon.live = {
return match;
},
+ // return the platform name for a given ID:
+ getName: function(id) {
+ const it = BotMon.live.data.platforms._list.find( pf => pf.id == id);
+ console.log(it);
+ return ( it ? it.n : 'Unknown' );
+ },
+
+
// indicates if the list is loaded and ready to use:
_ready: false,
@@ -944,7 +1091,7 @@ BotMon.live = {
// Load the list of known bots:
BotMon.live.gui.status.showBusy("Loading list of rules …");
- const url = BotMon._baseDir + 'config/rules.json';
+ const url = BotMon._baseDir + 'config/botmon-config.json';
try {
const response = await fetch(url);
if (!response.ok) {
@@ -1413,6 +1560,46 @@ BotMon.live = {
}
}
+ // update the webmetrics clients list:
+ const wmclients = document.getElementById('botmon__today__wm_clients');
+ if (wmclients) {
+
+ wmclients.appendChild(makeElement('dt', {}, "Browsers (humans only)"));
+
+ const clientList = BotMon.live.data.analytics.getTopBrowsers(5);
+ if (clientList) {
+ clientList.forEach( (cInfo) => {
+ const cDd = makeElement('dd');
+ cDd.appendChild(makeElement('span', {'class': 'has_icon client_' + cInfo.id }, ( cInfo.name ? cInfo.name : cInfo.id)));
+ cDd.appendChild(makeElement('span', {
+ 'class': 'count',
+ 'title': cInfo.count + " page views"
+ }, Math.round(cInfo.pct) + '%'));
+ wmclients.appendChild(cDd);
+ });
+ }
+ }
+
+ // update the webmetrics platforms list:
+ const wmplatforms = document.getElementById('botmon__today__wm_platforms');
+ if (wmplatforms) {
+
+ wmplatforms.appendChild(makeElement('dt', {}, "Platforms (humans only)"));
+
+ const pfList = BotMon.live.data.analytics.getTopPlatforms(5);
+ if (pfList) {
+ pfList.forEach( (pInfo) => {
+ const pDd = makeElement('dd');
+ pDd.appendChild(makeElement('span', {'class': 'has_icon client_' + pInfo.id }, ( pInfo.name ? pInfo.name : pInfo.id)));
+ pDd.appendChild(makeElement('span', {
+ 'class': 'count',
+ 'title': pInfo.count + " page views"
+ }, Math.round(pInfo.pct) + '%'));
+ wmplatforms.appendChild(pDd);
+ });
+ }
+ }
+
}
},