xref: /plugin/botmon/admin.js (revision e56d7b71abb5d1a6e19cb8d0b2e4e35a95ea2380)
1*e56d7b71SSascha Leib"use strict";
2*e56d7b71SSascha Leib/* DokuWiki BotMon Plugin Script file */
3*e56d7b71SSascha Leib/* 14.10.2025 - 0.5.0 - pre-release */
4*e56d7b71SSascha Leib/* Author: Sascha Leib <ad@hominem.info> */
5*e56d7b71SSascha Leib
6*e56d7b71SSascha Leib// enumeration of user types:
7*e56d7b71SSascha Leibconst BM_USERTYPE = Object.freeze({
8*e56d7b71SSascha Leib	'UNKNOWN': 'unknown',
9*e56d7b71SSascha Leib	'KNOWN_USER': 'user',
10*e56d7b71SSascha Leib	'PROBABLY_HUMAN': 'human',
11*e56d7b71SSascha Leib	'LIKELY_BOT': 'likely_bot',
12*e56d7b71SSascha Leib	'KNOWN_BOT': 'known_bot'
13*e56d7b71SSascha Leib});
14*e56d7b71SSascha Leib
15*e56d7b71SSascha Leib// enumeration of log types:
16*e56d7b71SSascha Leibconst BM_LOGTYPE = Object.freeze({
17*e56d7b71SSascha Leib	'SERVER': 'srv',
18*e56d7b71SSascha Leib	'CLIENT': 'log',
19*e56d7b71SSascha Leib	'TICKER': 'tck'
20*e56d7b71SSascha Leib});
21*e56d7b71SSascha Leib
22*e56d7b71SSascha Leib// enumeration of IP versions:
23*e56d7b71SSascha Leibconst BM_IPVERSION = Object.freeze({
24*e56d7b71SSascha Leib	'IPv4': 4,
25*e56d7b71SSascha Leib	'IPv6': 6
26*e56d7b71SSascha Leib});
27*e56d7b71SSascha Leib
28*e56d7b71SSascha Leib/* BotMon root object */
29*e56d7b71SSascha Leibconst BotMon = {
30*e56d7b71SSascha Leib
31*e56d7b71SSascha Leib	init: function() {
32*e56d7b71SSascha Leib		//console.info('BotMon.init()');
33*e56d7b71SSascha Leib
34*e56d7b71SSascha Leib		// find the plugin basedir:
35*e56d7b71SSascha Leib		this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/'))
36*e56d7b71SSascha Leib			+ '/plugins/botmon/';
37*e56d7b71SSascha Leib
38*e56d7b71SSascha Leib		// read the page language from the DOM:
39*e56d7b71SSascha Leib		this._lang = document.getRootNode().documentElement.lang || this._lang;
40*e56d7b71SSascha Leib
41*e56d7b71SSascha Leib		// get the time offset:
42*e56d7b71SSascha Leib		this._timeDiff = BotMon.t._getTimeOffset();
43*e56d7b71SSascha Leib
44*e56d7b71SSascha Leib		// get yesterday's date:
45*e56d7b71SSascha Leib		let d = new Date();
46*e56d7b71SSascha Leib		d.setDate(d.getDate() - 1);
47*e56d7b71SSascha Leib		this._datestr = d.toISOString().slice(0, 10);
48*e56d7b71SSascha Leib
49*e56d7b71SSascha Leib		// init the sub-objects:
50*e56d7b71SSascha Leib		BotMon.t._callInit(this);
51*e56d7b71SSascha Leib	},
52*e56d7b71SSascha Leib
53*e56d7b71SSascha Leib	_baseDir: null,
54*e56d7b71SSascha Leib	_lang: 'en',
55*e56d7b71SSascha Leib	_datestr: '',
56*e56d7b71SSascha Leib	_timeDiff: '',
57*e56d7b71SSascha Leib
58*e56d7b71SSascha Leib	/* internal tools */
59*e56d7b71SSascha Leib	t: {
60*e56d7b71SSascha Leib		/* helper function to call inits of sub-objects */
61*e56d7b71SSascha Leib		_callInit: function(obj) {
62*e56d7b71SSascha Leib			//console.info('BotMon.t._callInit(obj=',obj,')');
63*e56d7b71SSascha Leib
64*e56d7b71SSascha Leib			/* call init / _init on each sub-object: */
65*e56d7b71SSascha Leib			Object.keys(obj).forEach( (key,i) => {
66*e56d7b71SSascha Leib				const sub = obj[key];
67*e56d7b71SSascha Leib				let init = null;
68*e56d7b71SSascha Leib				if (typeof sub === 'object' && sub.init) {
69*e56d7b71SSascha Leib					init = sub.init;
70*e56d7b71SSascha Leib				}
71*e56d7b71SSascha Leib
72*e56d7b71SSascha Leib				// bind to object
73*e56d7b71SSascha Leib				if (typeof init == 'function') {
74*e56d7b71SSascha Leib					const init2 = init.bind(sub);
75*e56d7b71SSascha Leib					init2(obj);
76*e56d7b71SSascha Leib				}
77*e56d7b71SSascha Leib			});
78*e56d7b71SSascha Leib		},
79*e56d7b71SSascha Leib
80*e56d7b71SSascha Leib		/* helper function to calculate the time difference to UTC: */
81*e56d7b71SSascha Leib		_getTimeOffset: function() {
82*e56d7b71SSascha Leib			const now = new Date();
83*e56d7b71SSascha Leib			let offset = now.getTimezoneOffset(); // in minutes
84*e56d7b71SSascha Leib			const sign = Math.sign(offset); // +1 or -1
85*e56d7b71SSascha Leib			offset = Math.abs(offset); // always positive
86*e56d7b71SSascha Leib
87*e56d7b71SSascha Leib			let hours = 0;
88*e56d7b71SSascha Leib			while (offset >= 60) {
89*e56d7b71SSascha Leib				hours += 1;
90*e56d7b71SSascha Leib				offset -= 60;
91*e56d7b71SSascha Leib			}
92*e56d7b71SSascha Leib			return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : '');
93*e56d7b71SSascha Leib		},
94*e56d7b71SSascha Leib
95*e56d7b71SSascha Leib		/* helper function to create a new element with all attributes and text content */
96*e56d7b71SSascha Leib		_makeElement: function(name, atlist = undefined, text = undefined) {
97*e56d7b71SSascha Leib			var r = null;
98*e56d7b71SSascha Leib			try {
99*e56d7b71SSascha Leib				r = document.createElement(name);
100*e56d7b71SSascha Leib				if (atlist) {
101*e56d7b71SSascha Leib					for (let attr in atlist) {
102*e56d7b71SSascha Leib						r.setAttribute(attr, atlist[attr]);
103*e56d7b71SSascha Leib					}
104*e56d7b71SSascha Leib				}
105*e56d7b71SSascha Leib				if (text) {
106*e56d7b71SSascha Leib					r.textContent = text.toString();
107*e56d7b71SSascha Leib				}
108*e56d7b71SSascha Leib			} catch(e) {
109*e56d7b71SSascha Leib				console.error(e);
110*e56d7b71SSascha Leib			}
111*e56d7b71SSascha Leib			return r;
112*e56d7b71SSascha Leib		},
113*e56d7b71SSascha Leib
114*e56d7b71SSascha Leib		/* helper to convert an ip address string to a normalised format: */
115*e56d7b71SSascha Leib		_ip2Num: function(ip) {
116*e56d7b71SSascha Leib			if (!ip) {
117*e56d7b71SSascha Leib				return 'null';
118*e56d7b71SSascha Leib			} else if (ip.indexOf(':') > 0) { /* IP6 */
119*e56d7b71SSascha Leib				return (ip.split(':').map(d => ('0000'+d).slice(-4) ).join(':'));
120*e56d7b71SSascha Leib			} else { /* IP4 */
121*e56d7b71SSascha Leib				return ip.split('.').map(d => ('000'+d).slice(-3) ).join('.');
122*e56d7b71SSascha Leib			}
123*e56d7b71SSascha Leib		},
124*e56d7b71SSascha Leib
125*e56d7b71SSascha Leib		/* helper function to format a Date object to show only the time. */
126*e56d7b71SSascha Leib		/* returns String */
127*e56d7b71SSascha Leib		_formatTime: function(date) {
128*e56d7b71SSascha Leib
129*e56d7b71SSascha Leib			if (date) {
130*e56d7b71SSascha Leib				return date.getHours() + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2);
131*e56d7b71SSascha Leib			} else {
132*e56d7b71SSascha Leib				return null;
133*e56d7b71SSascha Leib			}
134*e56d7b71SSascha Leib
135*e56d7b71SSascha Leib		},
136*e56d7b71SSascha Leib
137*e56d7b71SSascha Leib		/* helper function to show a time difference in seconds or minutes */
138*e56d7b71SSascha Leib		/* returns String */
139*e56d7b71SSascha Leib		_formatTimeDiff: function(dateA, dateB) {
140*e56d7b71SSascha Leib
141*e56d7b71SSascha Leib			// if the second date is ealier, swap them:
142*e56d7b71SSascha Leib			if (dateA > dateB) dateB = [dateA, dateA = dateB][0];
143*e56d7b71SSascha Leib
144*e56d7b71SSascha Leib			// get the difference in milliseconds:
145*e56d7b71SSascha Leib			let ms = dateB - dateA;
146*e56d7b71SSascha Leib
147*e56d7b71SSascha Leib			if (ms > 50) { /* ignore small time spans */
148*e56d7b71SSascha Leib				const h = Math.floor((ms / (1000 * 60 * 60)) % 24);
149*e56d7b71SSascha Leib				const m = Math.floor((ms / (1000 * 60)) % 60);
150*e56d7b71SSascha Leib				const s = Math.floor((ms / 1000) % 60);
151*e56d7b71SSascha Leib
152*e56d7b71SSascha Leib				return ( h>0 ? h + 'h ': '') + ( m>0 ? m + 'm ': '') + ( s>0 ? s + 's': '');
153*e56d7b71SSascha Leib			}
154*e56d7b71SSascha Leib
155*e56d7b71SSascha Leib			return null;
156*e56d7b71SSascha Leib
157*e56d7b71SSascha Leib		},
158*e56d7b71SSascha Leib
159*e56d7b71SSascha Leib		// calcualte a reduced ration between two numbers
160*e56d7b71SSascha Leib		// adapted from https://stackoverflow.com/questions/3946373/math-in-js-how-do-i-get-a-ratio-from-a-percentage
161*e56d7b71SSascha Leib		_getRatio: function(a, b, tolerance) {
162*e56d7b71SSascha Leib
163*e56d7b71SSascha Leib			var bg = b;
164*e56d7b71SSascha Leib			var sm = a;
165*e56d7b71SSascha Leib			if (a > b) {
166*e56d7b71SSascha Leib				var bg = a;
167*e56d7b71SSascha Leib				var sm = b;
168*e56d7b71SSascha Leib			}
169*e56d7b71SSascha Leib
170*e56d7b71SSascha Leib			for (var i = 1; i < 1000000; i++) {
171*e56d7b71SSascha Leib				var d = sm / i;
172*e56d7b71SSascha Leib				var res = bg / d;
173*e56d7b71SSascha Leib				var howClose = Math.abs(res - res.toFixed(0));
174*e56d7b71SSascha Leib				if (howClose < tolerance) {
175*e56d7b71SSascha Leib					if (a > b) {
176*e56d7b71SSascha Leib						return res.toFixed(0) + ':' + i;
177*e56d7b71SSascha Leib					} else {
178*e56d7b71SSascha Leib						return i + ':' + res.toFixed(0);
179*e56d7b71SSascha Leib					}
180*e56d7b71SSascha Leib				}
181*e56d7b71SSascha Leib			}
182*e56d7b71SSascha Leib		}
183*e56d7b71SSascha Leib	}
184*e56d7b71SSascha Leib};
185*e56d7b71SSascha Leib
186*e56d7b71SSascha Leib/* everything specific to the "Latest" tab is self-contained in the "live" object: */
187*e56d7b71SSascha LeibBotMon.live = {
188*e56d7b71SSascha Leib	init: function() {
189*e56d7b71SSascha Leib		//console.info('BotMon.live.init()');
190*e56d7b71SSascha Leib
191*e56d7b71SSascha Leib		// set the title:
192*e56d7b71SSascha Leib		const tDiff = '<abbr title="Coordinated Universal Time">UTC</abbr> ' + (BotMon._timeDiff != '' ? ` (offset: ${BotMon._timeDiff}` : '' ) + ')';
193*e56d7b71SSascha Leib		BotMon.live.gui.status.setTitle(`Data for <time datetime="${BotMon._datestr}">${BotMon._datestr}</time> ${tDiff}`);
194*e56d7b71SSascha Leib
195*e56d7b71SSascha Leib		// init sub-objects:
196*e56d7b71SSascha Leib		BotMon.t._callInit(this);
197*e56d7b71SSascha Leib	},
198*e56d7b71SSascha Leib
199*e56d7b71SSascha Leib	data: {
200*e56d7b71SSascha Leib		init: function() {
201*e56d7b71SSascha Leib			//console.info('BotMon.live.data.init()');
202*e56d7b71SSascha Leib
203*e56d7b71SSascha Leib			// call sub-inits:
204*e56d7b71SSascha Leib			BotMon.t._callInit(this);
205*e56d7b71SSascha Leib		},
206*e56d7b71SSascha Leib
207*e56d7b71SSascha Leib		// this will be called when the known json files are done loading:
208*e56d7b71SSascha Leib		_dispatch: function(file) {
209*e56d7b71SSascha Leib			//console.info('BotMon.live.data._dispatch(,',file,')');
210*e56d7b71SSascha Leib
211*e56d7b71SSascha Leib			// shortcut to make code more readable:
212*e56d7b71SSascha Leib			const data = BotMon.live.data;
213*e56d7b71SSascha Leib
214*e56d7b71SSascha Leib			// set the flags:
215*e56d7b71SSascha Leib			switch(file) {
216*e56d7b71SSascha Leib				case 'rules':
217*e56d7b71SSascha Leib					data._dispatchRulesLoaded = true;
218*e56d7b71SSascha Leib					break;
219*e56d7b71SSascha Leib				case 'ipranges':
220*e56d7b71SSascha Leib					data._dispatchIPRangesLoaded = true;
221*e56d7b71SSascha Leib					break;
222*e56d7b71SSascha Leib				case 'bots':
223*e56d7b71SSascha Leib					data._dispatchBotsLoaded = true;
224*e56d7b71SSascha Leib					break;
225*e56d7b71SSascha Leib				case 'clients':
226*e56d7b71SSascha Leib					data._dispatchClientsLoaded = true;
227*e56d7b71SSascha Leib					break;
228*e56d7b71SSascha Leib				case 'platforms':
229*e56d7b71SSascha Leib					data._dispatchPlatformsLoaded = true;
230*e56d7b71SSascha Leib					break;
231*e56d7b71SSascha Leib				default:
232*e56d7b71SSascha Leib					// ignore
233*e56d7b71SSascha Leib			}
234*e56d7b71SSascha Leib
235*e56d7b71SSascha Leib			// are all the flags set?
236*e56d7b71SSascha Leib			if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded && data._dispatchRulesLoaded && data._dispatchIPRangesLoaded) {
237*e56d7b71SSascha Leib				// chain the log files loading:
238*e56d7b71SSascha Leib				BotMon.live.data.loadLogFile(BM_LOGTYPE.SERVER, BotMon.live.data._onServerLogLoaded);
239*e56d7b71SSascha Leib			}
240*e56d7b71SSascha Leib		},
241*e56d7b71SSascha Leib		// flags to track which data files have been loaded:
242*e56d7b71SSascha Leib		_dispatchBotsLoaded: false,
243*e56d7b71SSascha Leib		_dispatchClientsLoaded: false,
244*e56d7b71SSascha Leib		_dispatchPlatformsLoaded: false,
245*e56d7b71SSascha Leib		_dispatchIPRangesLoaded: false,
246*e56d7b71SSascha Leib		_dispatchRulesLoaded: false,
247*e56d7b71SSascha Leib
248*e56d7b71SSascha Leib		// event callback, after the server log has been loaded:
249*e56d7b71SSascha Leib		_onServerLogLoaded: function() {
250*e56d7b71SSascha Leib			//console.info('BotMon.live.data._onServerLogLoaded()');
251*e56d7b71SSascha Leib
252*e56d7b71SSascha Leib			// chain the client log file to load:
253*e56d7b71SSascha Leib			BotMon.live.data.loadLogFile(BM_LOGTYPE.CLIENT, BotMon.live.data._onClientLogLoaded);
254*e56d7b71SSascha Leib		},
255*e56d7b71SSascha Leib
256*e56d7b71SSascha Leib		// event callback, after the client log has been loaded:
257*e56d7b71SSascha Leib		_onClientLogLoaded: function() {
258*e56d7b71SSascha Leib			//console.info('BotMon.live.data._onClientLogLoaded()');
259*e56d7b71SSascha Leib
260*e56d7b71SSascha Leib			// chain the ticks file to load:
261*e56d7b71SSascha Leib			BotMon.live.data.loadLogFile(BM_LOGTYPE.TICKER, BotMon.live.data._onTicksLogLoaded);
262*e56d7b71SSascha Leib
263*e56d7b71SSascha Leib		},
264*e56d7b71SSascha Leib
265*e56d7b71SSascha Leib		// event callback, after the tiker log has been loaded:
266*e56d7b71SSascha Leib		_onTicksLogLoaded: function() {
267*e56d7b71SSascha Leib			//console.info('BotMon.live.data._onTicksLogLoaded()');
268*e56d7b71SSascha Leib
269*e56d7b71SSascha Leib			// analyse the data:
270*e56d7b71SSascha Leib			BotMon.live.data.analytics.analyseAll();
271*e56d7b71SSascha Leib
272*e56d7b71SSascha Leib			// sort the data:
273*e56d7b71SSascha Leib			// #TODO
274*e56d7b71SSascha Leib
275*e56d7b71SSascha Leib			// display the data:
276*e56d7b71SSascha Leib			BotMon.live.gui.overview.make();
277*e56d7b71SSascha Leib
278*e56d7b71SSascha Leib			//console.log(BotMon.live.data.model._visitors);
279*e56d7b71SSascha Leib
280*e56d7b71SSascha Leib		},
281*e56d7b71SSascha Leib
282*e56d7b71SSascha Leib		// the data model:
283*e56d7b71SSascha Leib		model: {
284*e56d7b71SSascha Leib			// visitors storage:
285*e56d7b71SSascha Leib			_visitors: [],
286*e56d7b71SSascha Leib
287*e56d7b71SSascha Leib			// find an already existing visitor record:
288*e56d7b71SSascha Leib			findVisitor: function(visitor, type) {
289*e56d7b71SSascha Leib				//console.info('BotMon.live.data.model.findVisitor()', type);
290*e56d7b71SSascha Leib				//console.log(visitor);
291*e56d7b71SSascha Leib
292*e56d7b71SSascha Leib				// shortcut to make code more readable:
293*e56d7b71SSascha Leib				const model = BotMon.live.data.model;
294*e56d7b71SSascha Leib
295*e56d7b71SSascha Leib				const timeout = 60 * 60 * 1000; // session timeout: One hour
296*e56d7b71SSascha Leib
297*e56d7b71SSascha Leib				if (visitor._type == BM_USERTYPE.KNOWN_BOT) { // known bots match by their bot ID:
298*e56d7b71SSascha Leib
299*e56d7b71SSascha Leib					for (let i=0; i<model._visitors.length; i++) {
300*e56d7b71SSascha Leib						const v = model._visitors[i];
301*e56d7b71SSascha Leib
302*e56d7b71SSascha Leib						// bots match when their ID matches:
303*e56d7b71SSascha Leib						if (v._bot && v._bot.id == visitor._bot.id) {
304*e56d7b71SSascha Leib							return v;
305*e56d7b71SSascha Leib						}
306*e56d7b71SSascha Leib					}
307*e56d7b71SSascha Leib				} else { // other types match by their DW/PHPIDs:
308*e56d7b71SSascha Leib
309*e56d7b71SSascha Leib					// loop over all visitors already registered and check for ID matches:
310*e56d7b71SSascha Leib					for (let i=0; i<model._visitors.length; i++) {
311*e56d7b71SSascha Leib						const v = model._visitors[i];
312*e56d7b71SSascha Leib
313*e56d7b71SSascha Leib						if ( v.id == visitor.id) { // match the DW/PHP IDs
314*e56d7b71SSascha Leib							return v;
315*e56d7b71SSascha Leib						}
316*e56d7b71SSascha Leib					}
317*e56d7b71SSascha Leib
318*e56d7b71SSascha Leib					// if not found, try to match IP address and user agent:
319*e56d7b71SSascha Leib					for (let i=0; i<model._visitors.length; i++) {
320*e56d7b71SSascha Leib						const v = model._visitors[i];
321*e56d7b71SSascha Leib						if (  v.ip == visitor.ip && v.agent == visitor.agent) {
322*e56d7b71SSascha Leib							return v;
323*e56d7b71SSascha Leib						}
324*e56d7b71SSascha Leib					}
325*e56d7b71SSascha Leib				}
326*e56d7b71SSascha Leib
327*e56d7b71SSascha Leib				return null; // nothing found
328*e56d7b71SSascha Leib			},
329*e56d7b71SSascha Leib
330*e56d7b71SSascha Leib			/* if there is already this visit registered, return the page view item */
331*e56d7b71SSascha Leib			_getPageView: function(visit, view) {
332*e56d7b71SSascha Leib
333*e56d7b71SSascha Leib				// shortcut to make code more readable:
334*e56d7b71SSascha Leib				const model = BotMon.live.data.model;
335*e56d7b71SSascha Leib
336*e56d7b71SSascha Leib				for (let i=0; i<visit._pageViews.length; i++) {
337*e56d7b71SSascha Leib					const pv = visit._pageViews[i];
338*e56d7b71SSascha Leib					if (pv.pg == view.pg) {
339*e56d7b71SSascha Leib						return pv;
340*e56d7b71SSascha Leib					}
341*e56d7b71SSascha Leib				}
342*e56d7b71SSascha Leib				return null; // not found
343*e56d7b71SSascha Leib			},
344*e56d7b71SSascha Leib
345*e56d7b71SSascha Leib			// register a new visitor (or update if already exists)
346*e56d7b71SSascha Leib			registerVisit: function(nv, type) {
347*e56d7b71SSascha Leib				//console.info('registerVisit', nv, type);
348*e56d7b71SSascha Leib
349*e56d7b71SSascha Leib				// shortcut to make code more readable:
350*e56d7b71SSascha Leib				const model = BotMon.live.data.model;
351*e56d7b71SSascha Leib
352*e56d7b71SSascha Leib				// is it a known bot?
353*e56d7b71SSascha Leib				const bot = BotMon.live.data.bots.match(nv.agent);
354*e56d7b71SSascha Leib
355*e56d7b71SSascha Leib				// enrich new visitor with relevant data:
356*e56d7b71SSascha Leib				if (!nv._bot) nv._bot = bot ?? null; // bot info
357*e56d7b71SSascha Leib				nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) ); // user type
358*e56d7b71SSascha Leib				if (bot && bot.geo) {
359*e56d7b71SSascha Leib					if (!nv.geo || nv.geo == '' || nv.geo == 'ZZ') nv.geo = bot.geo;
360*e56d7b71SSascha Leib				} else if (!nv.geo ||nv.geo == '') {
361*e56d7b71SSascha Leib					nv.geo = 'ZZ';
362*e56d7b71SSascha Leib				}
363*e56d7b71SSascha Leib
364*e56d7b71SSascha Leib				// update first and last seen:
365*e56d7b71SSascha Leib				if (!nv._firstSeen) nv._firstSeen = nv.ts; // first-seen
366*e56d7b71SSascha Leib				nv._lastSeen = nv.ts; // last-seen
367*e56d7b71SSascha Leib
368*e56d7b71SSascha Leib				// country name:
369*e56d7b71SSascha Leib				try {
370*e56d7b71SSascha Leib					nv._country = ( nv.geo == 'local' ? "localhost" : "Unknown" );
371*e56d7b71SSascha Leib					if (nv.geo && nv.geo !== '' && nv.geo !== 'ZZ' && nv.geo !== 'local') {
372*e56d7b71SSascha Leib						const countryName = new Intl.DisplayNames(['en', BotMon._lang], {type: 'region'});
373*e56d7b71SSascha Leib						nv._country = countryName.of(nv.geo.substring(0,2)) ?? nv.geo;
374*e56d7b71SSascha Leib					}
375*e56d7b71SSascha Leib				} catch (err) {
376*e56d7b71SSascha Leib					console.error(err);
377*e56d7b71SSascha Leib					nv._country = 'Error';
378*e56d7b71SSascha Leib				}
379*e56d7b71SSascha Leib
380*e56d7b71SSascha Leib				// check if it already exists:
381*e56d7b71SSascha Leib				let visitor = model.findVisitor(nv, type);
382*e56d7b71SSascha Leib				if (!visitor) {
383*e56d7b71SSascha Leib					visitor = nv;
384*e56d7b71SSascha Leib					visitor._seenBy = [type];
385*e56d7b71SSascha Leib					visitor._pageViews = []; // array of page views
386*e56d7b71SSascha Leib					visitor._hasReferrer = false; // has at least one referrer
387*e56d7b71SSascha Leib					visitor._jsClient = false; // visitor has been seen logged by client js as well
388*e56d7b71SSascha Leib					visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info
389*e56d7b71SSascha Leib					visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info
390*e56d7b71SSascha Leib					model._visitors.push(visitor);
391*e56d7b71SSascha Leib				} else { // update existing
392*e56d7b71SSascha Leib					if (visitor._firstSeen > nv.ts) {
393*e56d7b71SSascha Leib						visitor._firstSeen = nv.ts;
394*e56d7b71SSascha Leib					}
395*e56d7b71SSascha Leib				}
396*e56d7b71SSascha Leib
397*e56d7b71SSascha Leib				// find browser
398*e56d7b71SSascha Leib
399*e56d7b71SSascha Leib				// is this visit already registered?
400*e56d7b71SSascha Leib				let prereg = model._getPageView(visitor, nv);
401*e56d7b71SSascha Leib				if (!prereg) {
402*e56d7b71SSascha Leib					// add new page view:
403*e56d7b71SSascha Leib					prereg = model._makePageView(nv, type);
404*e56d7b71SSascha Leib					visitor._pageViews.push(prereg);
405*e56d7b71SSascha Leib				} else {
406*e56d7b71SSascha Leib					// update last seen date
407*e56d7b71SSascha Leib					prereg._lastSeen = nv.ts;
408*e56d7b71SSascha Leib					// increase view count:
409*e56d7b71SSascha Leib					prereg._viewCount += 1;
410*e56d7b71SSascha Leib					prereg._tickCount += 1;
411*e56d7b71SSascha Leib				}
412*e56d7b71SSascha Leib
413*e56d7b71SSascha Leib				// update referrer state:
414*e56d7b71SSascha Leib				visitor._hasReferrer = visitor._hasReferrer ||
415*e56d7b71SSascha Leib					(prereg.ref !== undefined && prereg.ref !== '');
416*e56d7b71SSascha Leib
417*e56d7b71SSascha Leib				// update time stamp for last-seen:
418*e56d7b71SSascha Leib				if (visitor._lastSeen < nv.ts) {
419*e56d7b71SSascha Leib					visitor._lastSeen = nv.ts;
420*e56d7b71SSascha Leib				}
421*e56d7b71SSascha Leib
422*e56d7b71SSascha Leib				// if needed:
423*e56d7b71SSascha Leib				return visitor;
424*e56d7b71SSascha Leib			},
425*e56d7b71SSascha Leib
426*e56d7b71SSascha Leib			// updating visit data from the client-side log:
427*e56d7b71SSascha Leib			updateVisit: function(dat) {
428*e56d7b71SSascha Leib				//console.info('updateVisit', dat);
429*e56d7b71SSascha Leib
430*e56d7b71SSascha Leib				// shortcut to make code more readable:
431*e56d7b71SSascha Leib				const model = BotMon.live.data.model;
432*e56d7b71SSascha Leib
433*e56d7b71SSascha Leib				const type = BM_LOGTYPE.CLIENT;
434*e56d7b71SSascha Leib
435*e56d7b71SSascha Leib				let visitor = BotMon.live.data.model.findVisitor(dat, type);
436*e56d7b71SSascha Leib				if (!visitor) {
437*e56d7b71SSascha Leib					visitor = model.registerVisit(dat, type);
438*e56d7b71SSascha Leib				}
439*e56d7b71SSascha Leib				if (visitor) {
440*e56d7b71SSascha Leib
441*e56d7b71SSascha Leib					if (visitor._lastSeen < dat.ts) {
442*e56d7b71SSascha Leib						visitor._lastSeen = dat.ts;
443*e56d7b71SSascha Leib					}
444*e56d7b71SSascha Leib					if (!visitor._seenBy.includes(type)) {
445*e56d7b71SSascha Leib						visitor._seenBy.push(type);
446*e56d7b71SSascha Leib					}
447*e56d7b71SSascha Leib					visitor._jsClient = true; // seen by client js
448*e56d7b71SSascha Leib				}
449*e56d7b71SSascha Leib
450*e56d7b71SSascha Leib				// find the page view:
451*e56d7b71SSascha Leib				let prereg = BotMon.live.data.model._getPageView(visitor, dat);
452*e56d7b71SSascha Leib				if (prereg) {
453*e56d7b71SSascha Leib					// update the page view:
454*e56d7b71SSascha Leib					prereg._lastSeen = dat.ts;
455*e56d7b71SSascha Leib					if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type);
456*e56d7b71SSascha Leib					prereg._jsClient = true; // seen by client js
457*e56d7b71SSascha Leib				} else {
458*e56d7b71SSascha Leib					// add the page view to the visitor:
459*e56d7b71SSascha Leib					prereg = model._makePageView(dat, type);
460*e56d7b71SSascha Leib					visitor._pageViews.push(prereg);
461*e56d7b71SSascha Leib				}
462*e56d7b71SSascha Leib				prereg._tickCount += 1;
463*e56d7b71SSascha Leib			},
464*e56d7b71SSascha Leib
465*e56d7b71SSascha Leib			// updating visit data from the ticker log:
466*e56d7b71SSascha Leib			updateTicks: function(dat) {
467*e56d7b71SSascha Leib				//console.info('updateTicks', dat);
468*e56d7b71SSascha Leib
469*e56d7b71SSascha Leib				// shortcut to make code more readable:
470*e56d7b71SSascha Leib				const model = BotMon.live.data.model;
471*e56d7b71SSascha Leib
472*e56d7b71SSascha Leib				const type = BM_LOGTYPE.TICKER;
473*e56d7b71SSascha Leib
474*e56d7b71SSascha Leib				// find the visit info:
475*e56d7b71SSascha Leib				let visitor = model.findVisitor(dat, type);
476*e56d7b71SSascha Leib				if (!visitor) {
477*e56d7b71SSascha Leib					console.info(`No visitor with ID “${dat.id}” found, registering as a new one.`);
478*e56d7b71SSascha Leib					visitor = model.registerVisit(dat, type);
479*e56d7b71SSascha Leib				}
480*e56d7b71SSascha Leib				if (visitor) {
481*e56d7b71SSascha Leib					// update visitor:
482*e56d7b71SSascha Leib					if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts;
483*e56d7b71SSascha Leib					if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type);
484*e56d7b71SSascha Leib
485*e56d7b71SSascha Leib					// get the page view info:
486*e56d7b71SSascha Leib					let pv = model._getPageView(visitor, dat);
487*e56d7b71SSascha Leib					if (!pv) {
488*e56d7b71SSascha Leib						console.info(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`);
489*e56d7b71SSascha Leib						pv = model._makePageView(dat, type);
490*e56d7b71SSascha Leib						visitor._pageViews.push(pv);
491*e56d7b71SSascha Leib					}
492*e56d7b71SSascha Leib
493*e56d7b71SSascha Leib					// update the page view info:
494*e56d7b71SSascha Leib					if (!pv._seenBy.includes(type)) pv._seenBy.push(type);
495*e56d7b71SSascha Leib					if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts;
496*e56d7b71SSascha Leib					pv._tickCount += 1;
497*e56d7b71SSascha Leib
498*e56d7b71SSascha Leib				}
499*e56d7b71SSascha Leib			},
500*e56d7b71SSascha Leib
501*e56d7b71SSascha Leib			// helper function to create a new "page view" item:
502*e56d7b71SSascha Leib			_makePageView: function(data, type) {
503*e56d7b71SSascha Leib				// console.info('_makePageView', data);
504*e56d7b71SSascha Leib
505*e56d7b71SSascha Leib				// try to parse the referrer:
506*e56d7b71SSascha Leib				let rUrl = null;
507*e56d7b71SSascha Leib				try {
508*e56d7b71SSascha Leib					rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null );
509*e56d7b71SSascha Leib				} catch (e) {
510*e56d7b71SSascha Leib					console.warn(`Invalid referer: “${data.ref}”.`);
511*e56d7b71SSascha Leib					console.info(data);
512*e56d7b71SSascha Leib				}
513*e56d7b71SSascha Leib
514*e56d7b71SSascha Leib				return {
515*e56d7b71SSascha Leib					_by: type,
516*e56d7b71SSascha Leib					ip: data.ip,
517*e56d7b71SSascha Leib					pg: data.pg,
518*e56d7b71SSascha Leib					lang: data.lang || '??',
519*e56d7b71SSascha Leib					_ref: rUrl,
520*e56d7b71SSascha Leib					_firstSeen: data.ts,
521*e56d7b71SSascha Leib					_lastSeen: data.ts,
522*e56d7b71SSascha Leib					_seenBy: [type],
523*e56d7b71SSascha Leib					_jsClient: ( type !== BM_LOGTYPE.SERVER),
524*e56d7b71SSascha Leib					_viewCount: 1,
525*e56d7b71SSascha Leib					_tickCount: 0
526*e56d7b71SSascha Leib				};
527*e56d7b71SSascha Leib			}
528*e56d7b71SSascha Leib		},
529*e56d7b71SSascha Leib
530*e56d7b71SSascha Leib		// functions to analyse the data:
531*e56d7b71SSascha Leib		analytics: {
532*e56d7b71SSascha Leib
533*e56d7b71SSascha Leib			/**
534*e56d7b71SSascha Leib			 * Initializes the analytics data storage object:
535*e56d7b71SSascha Leib			 */
536*e56d7b71SSascha Leib			init: function() {
537*e56d7b71SSascha Leib				//console.info('BotMon.live.data.analytics.init()');
538*e56d7b71SSascha Leib			},
539*e56d7b71SSascha Leib
540*e56d7b71SSascha Leib			// data storage:
541*e56d7b71SSascha Leib			data: {
542*e56d7b71SSascha Leib				totalVisits: 0,
543*e56d7b71SSascha Leib				totalPageViews: 0,
544*e56d7b71SSascha Leib				humanPageViews: 0,
545*e56d7b71SSascha Leib				bots: {
546*e56d7b71SSascha Leib					known: 0,
547*e56d7b71SSascha Leib					suspected: 0,
548*e56d7b71SSascha Leib					human: 0,
549*e56d7b71SSascha Leib					users: 0
550*e56d7b71SSascha Leib				}
551*e56d7b71SSascha Leib			},
552*e56d7b71SSascha Leib
553*e56d7b71SSascha Leib			// sort the visits by type:
554*e56d7b71SSascha Leib			groups: {
555*e56d7b71SSascha Leib				knownBots: [],
556*e56d7b71SSascha Leib				suspectedBots: [],
557*e56d7b71SSascha Leib				humans: [],
558*e56d7b71SSascha Leib				users: []
559*e56d7b71SSascha Leib			},
560*e56d7b71SSascha Leib
561*e56d7b71SSascha Leib			// all analytics
562*e56d7b71SSascha Leib			analyseAll: function() {
563*e56d7b71SSascha Leib				//console.info('BotMon.live.data.analytics.analyseAll()');
564*e56d7b71SSascha Leib
565*e56d7b71SSascha Leib				// shortcut to make code more readable:
566*e56d7b71SSascha Leib				const model = BotMon.live.data.model;
567*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
568*e56d7b71SSascha Leib
569*e56d7b71SSascha Leib				BotMon.live.gui.status.showBusy("Analysing data …");
570*e56d7b71SSascha Leib
571*e56d7b71SSascha Leib				// loop over all visitors:
572*e56d7b71SSascha Leib				model._visitors.forEach( (v) => {
573*e56d7b71SSascha Leib
574*e56d7b71SSascha Leib					// count visits and page views:
575*e56d7b71SSascha Leib					this.data.totalVisits += 1;
576*e56d7b71SSascha Leib					this.data.totalPageViews += v._pageViews.length;
577*e56d7b71SSascha Leib
578*e56d7b71SSascha Leib					// check for typical bot aspects:
579*e56d7b71SSascha Leib					let botScore = 0;
580*e56d7b71SSascha Leib
581*e56d7b71SSascha Leib					if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots
582*e56d7b71SSascha Leib
583*e56d7b71SSascha Leib						this.data.bots.known += v._pageViews.length;
584*e56d7b71SSascha Leib						this.groups.knownBots.push(v);
585*e56d7b71SSascha Leib
586*e56d7b71SSascha Leib					} else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */
587*e56d7b71SSascha Leib
588*e56d7b71SSascha Leib						this.data.bots.users += v._pageViews.length;
589*e56d7b71SSascha Leib						this.groups.users.push(v);
590*e56d7b71SSascha Leib
591*e56d7b71SSascha Leib					} else {
592*e56d7b71SSascha Leib
593*e56d7b71SSascha Leib						// get evaluation:
594*e56d7b71SSascha Leib						const e = BotMon.live.data.rules.evaluate(v);
595*e56d7b71SSascha Leib						v._eval = e.rules;
596*e56d7b71SSascha Leib						v._botVal = e.val;
597*e56d7b71SSascha Leib
598*e56d7b71SSascha Leib						if (e.isBot) { // likely bots
599*e56d7b71SSascha Leib							v._type = BM_USERTYPE.LIKELY_BOT;
600*e56d7b71SSascha Leib							this.data.bots.suspected += v._pageViews.length;
601*e56d7b71SSascha Leib							this.groups.suspectedBots.push(v);
602*e56d7b71SSascha Leib						} else { // probably humans
603*e56d7b71SSascha Leib							v._type = BM_USERTYPE.PROBABLY_HUMAN;
604*e56d7b71SSascha Leib							this.data.bots.human += v._pageViews.length;
605*e56d7b71SSascha Leib							this.groups.humans.push(v);
606*e56d7b71SSascha Leib						}
607*e56d7b71SSascha Leib					}
608*e56d7b71SSascha Leib
609*e56d7b71SSascha Leib					// perform actions depending on the visitor type:
610*e56d7b71SSascha Leib					if (v._type == BM_USERTYPE.KNOWN_BOT ) { /* known bots only */
611*e56d7b71SSascha Leib
612*e56d7b71SSascha Leib					} else if (v._type == BM_USERTYPE.LIKELY_BOT) { /* probable bots only */
613*e56d7b71SSascha Leib
614*e56d7b71SSascha Leib						// add bot views to IP range information:
615*e56d7b71SSascha Leib						me.addToIpRanges(v);
616*e56d7b71SSascha Leib
617*e56d7b71SSascha Leib					} else { /* humans only */
618*e56d7b71SSascha Leib
619*e56d7b71SSascha Leib						// add browser and platform statistics:
620*e56d7b71SSascha Leib						me.addBrowserPlatform(v);
621*e56d7b71SSascha Leib
622*e56d7b71SSascha Leib						// add
623*e56d7b71SSascha Leib						v._pageViews.forEach( pv => {
624*e56d7b71SSascha Leib							me.addToRefererList(pv._ref);
625*e56d7b71SSascha Leib							me.addToPagesList(pv.pg);
626*e56d7b71SSascha Leib						});
627*e56d7b71SSascha Leib					}
628*e56d7b71SSascha Leib
629*e56d7b71SSascha Leib					// add to the country lists:
630*e56d7b71SSascha Leib					me.addToCountries(v.geo, v._country, v._type);
631*e56d7b71SSascha Leib
632*e56d7b71SSascha Leib				});
633*e56d7b71SSascha Leib
634*e56d7b71SSascha Leib				BotMon.live.gui.status.hideBusy('Done.');
635*e56d7b71SSascha Leib			},
636*e56d7b71SSascha Leib
637*e56d7b71SSascha Leib			// get a list of known bots:
638*e56d7b71SSascha Leib			getTopBots: function(max) {
639*e56d7b71SSascha Leib				//console.info('BotMon.live.data.analytics.getTopBots('+max+')');
640*e56d7b71SSascha Leib
641*e56d7b71SSascha Leib				//console.log(BotMon.live.data.analytics.groups.knownBots);
642*e56d7b71SSascha Leib
643*e56d7b71SSascha Leib				let botsList = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
644*e56d7b71SSascha Leib					return b._pageViews.length - a._pageViews.length;
645*e56d7b71SSascha Leib				});
646*e56d7b71SSascha Leib
647*e56d7b71SSascha Leib				const other = {
648*e56d7b71SSascha Leib					'id': 'other',
649*e56d7b71SSascha Leib					'name': "Others",
650*e56d7b71SSascha Leib					'count': 0
651*e56d7b71SSascha Leib				};
652*e56d7b71SSascha Leib
653*e56d7b71SSascha Leib				const rList = [];
654*e56d7b71SSascha Leib				const max2 = ( botsList.length > max ? max-1 : botsList.length );
655*e56d7b71SSascha Leib				let total = 0; // adding up the items
656*e56d7b71SSascha Leib				for (let i=0; i<botsList.length; i++) {
657*e56d7b71SSascha Leib					const it = botsList[i];
658*e56d7b71SSascha Leib					if (it && it._bot) {
659*e56d7b71SSascha Leib						if (i < max2) {
660*e56d7b71SSascha Leib							rList.push({
661*e56d7b71SSascha Leib								id: it._bot.id,
662*e56d7b71SSascha Leib								name: (it._bot.n ? it._bot.n : it._bot.id),
663*e56d7b71SSascha Leib								count: it._pageViews.length
664*e56d7b71SSascha Leib							});
665*e56d7b71SSascha Leib						} else {
666*e56d7b71SSascha Leib							other.count += it._pageViews.length;
667*e56d7b71SSascha Leib						};
668*e56d7b71SSascha Leib						total += it._pageViews.length;
669*e56d7b71SSascha Leib					}
670*e56d7b71SSascha Leib				};
671*e56d7b71SSascha Leib
672*e56d7b71SSascha Leib				// add the "other" item, if needed:
673*e56d7b71SSascha Leib				if (botsList.length > max2) {
674*e56d7b71SSascha Leib					rList.push(other);
675*e56d7b71SSascha Leib				};
676*e56d7b71SSascha Leib
677*e56d7b71SSascha Leib				rList.forEach( it => {
678*e56d7b71SSascha Leib					it.pct = (it.count * 100 / total);
679*e56d7b71SSascha Leib				});
680*e56d7b71SSascha Leib
681*e56d7b71SSascha Leib				return rList;
682*e56d7b71SSascha Leib			},
683*e56d7b71SSascha Leib
684*e56d7b71SSascha Leib			// most visited pages list:
685*e56d7b71SSascha Leib			_pagesList: [],
686*e56d7b71SSascha Leib
687*e56d7b71SSascha Leib			/**
688*e56d7b71SSascha Leib			 * Add a page view to the list of most visited pages.
689*e56d7b71SSascha Leib			 * @param {string} pageId - The page ID to add to the list.
690*e56d7b71SSascha Leib			 * @example
691*e56d7b71SSascha Leib			 * BotMon.live.data.analytics.addToPagesList('1234567890');
692*e56d7b71SSascha Leib			 */
693*e56d7b71SSascha Leib			addToPagesList: function(pageId) {
694*e56d7b71SSascha Leib				//console.log('BotMon.live.data.analytics.addToPagesList', pageId);
695*e56d7b71SSascha Leib
696*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
697*e56d7b71SSascha Leib
698*e56d7b71SSascha Leib				// already exists?
699*e56d7b71SSascha Leib				let pgObj = null;
700*e56d7b71SSascha Leib				for (let i = 0; i < me._pagesList.length; i++) {
701*e56d7b71SSascha Leib					if (me._pagesList[i].id == pageId) {
702*e56d7b71SSascha Leib						pgObj = me._pagesList[i];
703*e56d7b71SSascha Leib						break;
704*e56d7b71SSascha Leib					}
705*e56d7b71SSascha Leib				}
706*e56d7b71SSascha Leib
707*e56d7b71SSascha Leib				// if not exists, create it:
708*e56d7b71SSascha Leib				if (!pgObj) {
709*e56d7b71SSascha Leib					pgObj = {
710*e56d7b71SSascha Leib						id: pageId,
711*e56d7b71SSascha Leib						count: 1
712*e56d7b71SSascha Leib					};
713*e56d7b71SSascha Leib					me._pagesList.push(pgObj);
714*e56d7b71SSascha Leib				} else {
715*e56d7b71SSascha Leib					pgObj.count += 1;
716*e56d7b71SSascha Leib				}
717*e56d7b71SSascha Leib			},
718*e56d7b71SSascha Leib
719*e56d7b71SSascha Leib			getTopPages: function(max) {
720*e56d7b71SSascha Leib				//console.info('BotMon.live.data.analytics.getTopPages('+max+')');
721*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
722*e56d7b71SSascha Leib				return me._pagesList.toSorted( (a, b) => {
723*e56d7b71SSascha Leib					return b.count - a.count;
724*e56d7b71SSascha Leib				}).slice(0,max);
725*e56d7b71SSascha Leib			},
726*e56d7b71SSascha Leib
727*e56d7b71SSascha Leib			// Referer List:
728*e56d7b71SSascha Leib			_refererList: [],
729*e56d7b71SSascha Leib
730*e56d7b71SSascha Leib			addToRefererList: function(ref) {
731*e56d7b71SSascha Leib				//console.log('BotMon.live.data.analytics.addToRefererList',ref);
732*e56d7b71SSascha Leib
733*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
734*e56d7b71SSascha Leib
735*e56d7b71SSascha Leib				// ignore internal references:
736*e56d7b71SSascha Leib				if (ref && ref.host == window.location.host) {
737*e56d7b71SSascha Leib					return;
738*e56d7b71SSascha Leib				}
739*e56d7b71SSascha Leib
740*e56d7b71SSascha Leib				const refInfo = me.getRefererInfo(ref);
741*e56d7b71SSascha Leib
742*e56d7b71SSascha Leib				// already exists?
743*e56d7b71SSascha Leib				let refObj = null;
744*e56d7b71SSascha Leib				for (let i = 0; i < me._refererList.length; i++) {
745*e56d7b71SSascha Leib					if (me._refererList[i].id == refInfo.id) {
746*e56d7b71SSascha Leib						refObj = me._refererList[i];
747*e56d7b71SSascha Leib						break;
748*e56d7b71SSascha Leib					}
749*e56d7b71SSascha Leib				}
750*e56d7b71SSascha Leib
751*e56d7b71SSascha Leib				// if not exists, create it:
752*e56d7b71SSascha Leib				if (!refObj) {
753*e56d7b71SSascha Leib					refObj = refInfo;
754*e56d7b71SSascha Leib					refObj.count = 1;
755*e56d7b71SSascha Leib					me._refererList.push(refObj);
756*e56d7b71SSascha Leib				} else {
757*e56d7b71SSascha Leib					refObj.count += 1;
758*e56d7b71SSascha Leib				}
759*e56d7b71SSascha Leib			},
760*e56d7b71SSascha Leib
761*e56d7b71SSascha Leib			getRefererInfo: function(url) {
762*e56d7b71SSascha Leib				//console.log('BotMon.live.data.analytics.getRefererInfo',url);
763*e56d7b71SSascha Leib				try {
764*e56d7b71SSascha Leib					url = new URL(url);
765*e56d7b71SSascha Leib				} catch (e) {
766*e56d7b71SSascha Leib					return {
767*e56d7b71SSascha Leib						'id': 'null',
768*e56d7b71SSascha Leib						'n': 'Invalid Referer'
769*e56d7b71SSascha Leib					};
770*e56d7b71SSascha Leib				}
771*e56d7b71SSascha Leib
772*e56d7b71SSascha Leib				// find the referer ID:
773*e56d7b71SSascha Leib				let refId = 'null';
774*e56d7b71SSascha Leib				let refName = 'No Referer';
775*e56d7b71SSascha Leib				if (url && url.host) {
776*e56d7b71SSascha Leib					const hArr = url.host.split('.');
777*e56d7b71SSascha Leib					const tld = hArr[hArr.length-1];
778*e56d7b71SSascha Leib					refId = ( tld == 'localhost' ? tld : hArr[hArr.length-2]);
779*e56d7b71SSascha Leib					refName = hArr[hArr.length-2] + '.' + tld;
780*e56d7b71SSascha Leib				}
781*e56d7b71SSascha Leib
782*e56d7b71SSascha Leib				return {
783*e56d7b71SSascha Leib					'id': refId,
784*e56d7b71SSascha Leib					'n': refName
785*e56d7b71SSascha Leib				};
786*e56d7b71SSascha Leib			},
787*e56d7b71SSascha Leib
788*e56d7b71SSascha Leib			/**
789*e56d7b71SSascha Leib			 * Get a sorted list of the top referers.
790*e56d7b71SSascha Leib			 * The list is sorted in descending order of count.
791*e56d7b71SSascha Leib			 * If the array has more items than the given maximum, the rest of the items are added to an "other" item.
792*e56d7b71SSascha Leib			 * Each item in the list has a "pct" property, which is the percentage of the total count.
793*e56d7b71SSascha Leib			 * @param {number} max - The maximum number of items to return.
794*e56d7b71SSascha Leib			 * @return {Array} The sorted list of top referers.
795*e56d7b71SSascha Leib			 */
796*e56d7b71SSascha Leib			getTopReferers: function(max) {
797*e56d7b71SSascha Leib				//console.info(('BotMon.live.data.analytics.getTopReferers(' + max + ')'));
798*e56d7b71SSascha Leib
799*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
800*e56d7b71SSascha Leib
801*e56d7b71SSascha Leib				return me._makeTopList(me._refererList, max);
802*e56d7b71SSascha Leib			},
803*e56d7b71SSascha Leib
804*e56d7b71SSascha Leib			/**
805*e56d7b71SSascha Leib			 * Create a sorted list of top items from a given array.
806*e56d7b71SSascha Leib			 * The list is sorted in descending order of count.
807*e56d7b71SSascha Leib			 * If the array has more items than the given maximum, the rest of the items are added to an "other" item.
808*e56d7b71SSascha Leib			 * Each item in the list has a "pct" property, which is the percentage of the total count.
809*e56d7b71SSascha Leib			 * @param {Array} arr - The array to sort and truncate.
810*e56d7b71SSascha Leib			 * @param {number} max - The maximum number of items to return.
811*e56d7b71SSascha Leib			 * @return {Array} The sorted list of top items.
812*e56d7b71SSascha Leib			 */
813*e56d7b71SSascha Leib			_makeTopList: function(arr, max) {
814*e56d7b71SSascha Leib				//console.info(('BotMon.live.data.analytics._makeTopList(arr,' + max + ')'));
815*e56d7b71SSascha Leib
816*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
817*e56d7b71SSascha Leib
818*e56d7b71SSascha Leib				// sort the list:
819*e56d7b71SSascha Leib				arr.sort( (a,b) => {
820*e56d7b71SSascha Leib					return b.count - a.count;
821*e56d7b71SSascha Leib				});
822*e56d7b71SSascha Leib
823*e56d7b71SSascha Leib				const rList = []; // return array
824*e56d7b71SSascha Leib				const max2 = ( arr.length >= max ? max-1 : arr.length );
825*e56d7b71SSascha Leib				const other = {
826*e56d7b71SSascha Leib					'id': 'other',
827*e56d7b71SSascha Leib					'name': "Others",
828*e56d7b71SSascha Leib					'count': 0
829*e56d7b71SSascha Leib				};
830*e56d7b71SSascha Leib				let total = 0; // adding up the items
831*e56d7b71SSascha Leib				for (let i=0; Math.min(max, arr.length) > i; i++) {
832*e56d7b71SSascha Leib					const it = arr[i];
833*e56d7b71SSascha Leib					if (it) {
834*e56d7b71SSascha Leib						if (i < max2) {
835*e56d7b71SSascha Leib							const rIt = {
836*e56d7b71SSascha Leib								id: it.id,
837*e56d7b71SSascha Leib								name: (it.n ? it.n : it.id),
838*e56d7b71SSascha Leib								count: it.count
839*e56d7b71SSascha Leib							};
840*e56d7b71SSascha Leib							rList.push(rIt);
841*e56d7b71SSascha Leib						} else {
842*e56d7b71SSascha Leib							other.count += it.count;
843*e56d7b71SSascha Leib						}
844*e56d7b71SSascha Leib						total += it.count;
845*e56d7b71SSascha Leib					}
846*e56d7b71SSascha Leib				}
847*e56d7b71SSascha Leib
848*e56d7b71SSascha Leib				// add the "other" item, if needed:
849*e56d7b71SSascha Leib				if (arr.length > max2) {
850*e56d7b71SSascha Leib					rList.push(other);
851*e56d7b71SSascha Leib				};
852*e56d7b71SSascha Leib
853*e56d7b71SSascha Leib				rList.forEach( it => {
854*e56d7b71SSascha Leib					it.pct = (it.count * 100 / total);
855*e56d7b71SSascha Leib				});
856*e56d7b71SSascha Leib
857*e56d7b71SSascha Leib				return rList;
858*e56d7b71SSascha Leib			},
859*e56d7b71SSascha Leib
860*e56d7b71SSascha Leib			/* countries of visits */
861*e56d7b71SSascha Leib			_countries: {
862*e56d7b71SSascha Leib				'human': [],
863*e56d7b71SSascha Leib				'bot': []
864*e56d7b71SSascha Leib			},
865*e56d7b71SSascha Leib			/**
866*e56d7b71SSascha Leib			 * Adds a country code to the statistics.
867*e56d7b71SSascha Leib			 *
868*e56d7b71SSascha Leib			 * @param {string} iso The ISO 3166-1 alpha-2 country code.
869*e56d7b71SSascha Leib			 */
870*e56d7b71SSascha Leib			addToCountries: function(iso, name, type) {
871*e56d7b71SSascha Leib
872*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
873*e56d7b71SSascha Leib
874*e56d7b71SSascha Leib				// find the correct array:
875*e56d7b71SSascha Leib				let arr = null;
876*e56d7b71SSascha Leib				switch (type) {
877*e56d7b71SSascha Leib
878*e56d7b71SSascha Leib					case BM_USERTYPE.KNOWN_USER:
879*e56d7b71SSascha Leib					case BM_USERTYPE.PROBABLY_HUMAN:
880*e56d7b71SSascha Leib						arr = me._countries.human;
881*e56d7b71SSascha Leib						break;
882*e56d7b71SSascha Leib					case BM_USERTYPE.LIKELY_BOT:
883*e56d7b71SSascha Leib					case BM_USERTYPE.KNOWN_BOT:
884*e56d7b71SSascha Leib						arr = me._countries.bot;
885*e56d7b71SSascha Leib						break;
886*e56d7b71SSascha Leib					default:
887*e56d7b71SSascha Leib						console.warn(`Unknown user type ${type} in function addToCountries.`);
888*e56d7b71SSascha Leib				}
889*e56d7b71SSascha Leib
890*e56d7b71SSascha Leib				if (arr) {
891*e56d7b71SSascha Leib					let cRec = arr.find( it => it.id == iso);
892*e56d7b71SSascha Leib					if (!cRec) {
893*e56d7b71SSascha Leib						cRec = {
894*e56d7b71SSascha Leib							'id': iso,
895*e56d7b71SSascha Leib							'n': name,
896*e56d7b71SSascha Leib							'count': 1
897*e56d7b71SSascha Leib						};
898*e56d7b71SSascha Leib						arr.push(cRec);
899*e56d7b71SSascha Leib					} else {
900*e56d7b71SSascha Leib						cRec.count += 1;
901*e56d7b71SSascha Leib					}
902*e56d7b71SSascha Leib				}
903*e56d7b71SSascha Leib			},
904*e56d7b71SSascha Leib
905*e56d7b71SSascha Leib			/**
906*e56d7b71SSascha Leib			 * Returns a list of countries with visit counts, sorted by visit count in descending order.
907*e56d7b71SSascha Leib			 *
908*e56d7b71SSascha Leib			 * @param {BM_USERTYPE} type array of types type of visitors to return.
909*e56d7b71SSascha Leib			 * @param {number} max The maximum number of entries to return.
910*e56d7b71SSascha Leib			 * @return {Array} A list of objects with properties 'iso' (ISO 3166-1 alpha-2 country code) and 'count' (visit count).
911*e56d7b71SSascha Leib			 */
912*e56d7b71SSascha Leib			getCountryList: function(type, max) {
913*e56d7b71SSascha Leib
914*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
915*e56d7b71SSascha Leib
916*e56d7b71SSascha Leib				// find the correct array:
917*e56d7b71SSascha Leib				let arr = null;
918*e56d7b71SSascha Leib				switch (type) {
919*e56d7b71SSascha Leib
920*e56d7b71SSascha Leib					case 'human':
921*e56d7b71SSascha Leib						arr = me._countries.human;
922*e56d7b71SSascha Leib						break;
923*e56d7b71SSascha Leib					case 'bot':
924*e56d7b71SSascha Leib						arr = me._countries.bot;
925*e56d7b71SSascha Leib						break;
926*e56d7b71SSascha Leib					default:
927*e56d7b71SSascha Leib						console.warn(`Unknown user type ${type} in function getCountryList.`);
928*e56d7b71SSascha Leib						return;
929*e56d7b71SSascha Leib				}
930*e56d7b71SSascha Leib
931*e56d7b71SSascha Leib				return me._makeTopList(arr, max);
932*e56d7b71SSascha Leib			},
933*e56d7b71SSascha Leib
934*e56d7b71SSascha Leib			/* browser and platform of human visitors */
935*e56d7b71SSascha Leib			_browsers: [],
936*e56d7b71SSascha Leib			_platforms: [],
937*e56d7b71SSascha Leib
938*e56d7b71SSascha Leib			addBrowserPlatform: function(visitor) {
939*e56d7b71SSascha Leib				//console.info('addBrowserPlatform', visitor);
940*e56d7b71SSascha Leib
941*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
942*e56d7b71SSascha Leib
943*e56d7b71SSascha Leib				// add to browsers list:
944*e56d7b71SSascha Leib				let browserRec = ( visitor._client ? visitor._client : {'id': 'unknown'});
945*e56d7b71SSascha Leib				if (visitor._client) {
946*e56d7b71SSascha Leib					let bRec = me._browsers.find( it => it.id == browserRec.id);
947*e56d7b71SSascha Leib					if (!bRec) {
948*e56d7b71SSascha Leib						bRec = {
949*e56d7b71SSascha Leib							id: browserRec.id,
950*e56d7b71SSascha Leib							n: browserRec.n,
951*e56d7b71SSascha Leib							count: 1
952*e56d7b71SSascha Leib						};
953*e56d7b71SSascha Leib						me._browsers.push(bRec);
954*e56d7b71SSascha Leib					} else {
955*e56d7b71SSascha Leib						bRec.count += 1;
956*e56d7b71SSascha Leib					}
957*e56d7b71SSascha Leib				}
958*e56d7b71SSascha Leib
959*e56d7b71SSascha Leib				// add to platforms list:
960*e56d7b71SSascha Leib				let platformRec = ( visitor._platform ? visitor._platform : {'id': 'unknown'});
961*e56d7b71SSascha Leib				if (visitor._platform) {
962*e56d7b71SSascha Leib					let pRec = me._platforms.find( it => it.id == platformRec.id);
963*e56d7b71SSascha Leib					if (!pRec) {
964*e56d7b71SSascha Leib						pRec = {
965*e56d7b71SSascha Leib							id: platformRec.id,
966*e56d7b71SSascha Leib							n: platformRec.n,
967*e56d7b71SSascha Leib							count: 1
968*e56d7b71SSascha Leib						};
969*e56d7b71SSascha Leib						me._platforms.push(pRec);
970*e56d7b71SSascha Leib					} else {
971*e56d7b71SSascha Leib						pRec.count += 1;
972*e56d7b71SSascha Leib					}
973*e56d7b71SSascha Leib				}
974*e56d7b71SSascha Leib
975*e56d7b71SSascha Leib			},
976*e56d7b71SSascha Leib
977*e56d7b71SSascha Leib			getTopBrowsers: function(max) {
978*e56d7b71SSascha Leib
979*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
980*e56d7b71SSascha Leib
981*e56d7b71SSascha Leib				return me._makeTopList(me._browsers, max);
982*e56d7b71SSascha Leib			},
983*e56d7b71SSascha Leib
984*e56d7b71SSascha Leib			getTopPlatforms: function(max) {
985*e56d7b71SSascha Leib
986*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
987*e56d7b71SSascha Leib
988*e56d7b71SSascha Leib				return me._makeTopList(me._platforms, max);
989*e56d7b71SSascha Leib			},
990*e56d7b71SSascha Leib
991*e56d7b71SSascha Leib			/* bounces are counted, not calculates: */
992*e56d7b71SSascha Leib			getBounceCount: function(type) {
993*e56d7b71SSascha Leib
994*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
995*e56d7b71SSascha Leib				var bounces = 0;
996*e56d7b71SSascha Leib				const list = me.groups[type];
997*e56d7b71SSascha Leib
998*e56d7b71SSascha Leib				list.forEach(it => {
999*e56d7b71SSascha Leib					bounces += (it._pageViews.length <= 1 ? 1 : 0);
1000*e56d7b71SSascha Leib				});
1001*e56d7b71SSascha Leib
1002*e56d7b71SSascha Leib				return bounces;
1003*e56d7b71SSascha Leib			},
1004*e56d7b71SSascha Leib
1005*e56d7b71SSascha Leib			_ipOwners: [],
1006*e56d7b71SSascha Leib
1007*e56d7b71SSascha Leib			/* adds a visit to the ip ranges arrays */
1008*e56d7b71SSascha Leib			addToIpRanges: function(v) {
1009*e56d7b71SSascha Leib				//console.info('addToIpRanges', v.ip);
1010*e56d7b71SSascha Leib
1011*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
1012*e56d7b71SSascha Leib				const ipRanges = BotMon.live.data.ipRanges;
1013*e56d7b71SSascha Leib
1014*e56d7b71SSascha Leib				let isp = 'null'; // default ISP id
1015*e56d7b71SSascha Leib				let name = 'Unknown'; // default ISP name
1016*e56d7b71SSascha Leib
1017*e56d7b71SSascha Leib				// is there already a known IP range assigned?
1018*e56d7b71SSascha Leib				if (v._ipRange) {
1019*e56d7b71SSascha Leib					isp = v._ipRange.g;
1020*e56d7b71SSascha Leib				}
1021*e56d7b71SSascha Leib
1022*e56d7b71SSascha Leib				let ispRec = me._ipOwners.find( it => it.id == isp);
1023*e56d7b71SSascha Leib				if (!ispRec) {
1024*e56d7b71SSascha Leib					ispRec = {
1025*e56d7b71SSascha Leib						'id': isp,
1026*e56d7b71SSascha Leib						'n': ipRanges.getOwner( isp ) || "Unknown",
1027*e56d7b71SSascha Leib						'count': v._pageViews.length
1028*e56d7b71SSascha Leib					};
1029*e56d7b71SSascha Leib					me._ipOwners.push(ispRec);
1030*e56d7b71SSascha Leib				} else {
1031*e56d7b71SSascha Leib					ispRec.count += v._pageViews.length;
1032*e56d7b71SSascha Leib				}
1033*e56d7b71SSascha Leib			},
1034*e56d7b71SSascha Leib
1035*e56d7b71SSascha Leib			getTopBotISPs: function(max) {
1036*e56d7b71SSascha Leib
1037*e56d7b71SSascha Leib				const me = BotMon.live.data.analytics;
1038*e56d7b71SSascha Leib
1039*e56d7b71SSascha Leib				return me._makeTopList(me._ipOwners, max);
1040*e56d7b71SSascha Leib			},
1041*e56d7b71SSascha Leib		},
1042*e56d7b71SSascha Leib
1043*e56d7b71SSascha Leib		// information on "known bots":
1044*e56d7b71SSascha Leib		bots: {
1045*e56d7b71SSascha Leib			// loads the list of known bots from a JSON file:
1046*e56d7b71SSascha Leib			init: async function() {
1047*e56d7b71SSascha Leib				//console.info('BotMon.live.data.bots.init()');
1048*e56d7b71SSascha Leib
1049*e56d7b71SSascha Leib				// Load the list of known bots:
1050*e56d7b71SSascha Leib				BotMon.live.gui.status.showBusy("Loading known bots …");
1051*e56d7b71SSascha Leib				const url = BotMon._baseDir + 'config/known-bots.json';
1052*e56d7b71SSascha Leib				try {
1053*e56d7b71SSascha Leib					const response = await fetch(url);
1054*e56d7b71SSascha Leib					if (!response.ok) {
1055*e56d7b71SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
1056*e56d7b71SSascha Leib					}
1057*e56d7b71SSascha Leib
1058*e56d7b71SSascha Leib					this._list = await response.json();
1059*e56d7b71SSascha Leib					this._ready = true;
1060*e56d7b71SSascha Leib
1061*e56d7b71SSascha Leib				} catch (error) {
1062*e56d7b71SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known bots file:", error.message);
1063*e56d7b71SSascha Leib				} finally {
1064*e56d7b71SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
1065*e56d7b71SSascha Leib					BotMon.live.data._dispatch('bots')
1066*e56d7b71SSascha Leib				}
1067*e56d7b71SSascha Leib			},
1068*e56d7b71SSascha Leib
1069*e56d7b71SSascha Leib			// returns bot info if the clientId matches a known bot, null otherwise:
1070*e56d7b71SSascha Leib			match: function(agent) {
1071*e56d7b71SSascha Leib				//console.info('BotMon.live.data.bots.match(',agent,')');
1072*e56d7b71SSascha Leib
1073*e56d7b71SSascha Leib				const BotList = BotMon.live.data.bots._list;
1074*e56d7b71SSascha Leib
1075*e56d7b71SSascha Leib				// default is: not found!
1076*e56d7b71SSascha Leib				let botInfo = null;
1077*e56d7b71SSascha Leib
1078*e56d7b71SSascha Leib				if (!agent) return null;
1079*e56d7b71SSascha Leib
1080*e56d7b71SSascha Leib				// check for known bots:
1081*e56d7b71SSascha Leib				BotList.find(bot => {
1082*e56d7b71SSascha Leib					let r = false;
1083*e56d7b71SSascha Leib					for (let j=0; j<bot.rx.length; j++) {
1084*e56d7b71SSascha Leib						const rxr = agent.match(new RegExp(bot.rx[j]));
1085*e56d7b71SSascha Leib						if (rxr) {
1086*e56d7b71SSascha Leib							botInfo = {
1087*e56d7b71SSascha Leib								n : bot.n,
1088*e56d7b71SSascha Leib								id: bot.id,
1089*e56d7b71SSascha Leib								geo: (bot.geo ? bot.geo : null),
1090*e56d7b71SSascha Leib								url: bot.url,
1091*e56d7b71SSascha Leib								v: (rxr.length > 1 ? rxr[1] : -1)
1092*e56d7b71SSascha Leib							};
1093*e56d7b71SSascha Leib							r = true;
1094*e56d7b71SSascha Leib							break;
1095*e56d7b71SSascha Leib						};
1096*e56d7b71SSascha Leib					};
1097*e56d7b71SSascha Leib					return r;
1098*e56d7b71SSascha Leib				});
1099*e56d7b71SSascha Leib
1100*e56d7b71SSascha Leib				// check for unknown bots:
1101*e56d7b71SSascha Leib				if (!botInfo) {
1102*e56d7b71SSascha Leib					const botmatch = agent.match(/([\s\d\w\-]*bot|[\s\d\w\-]*crawler|[\s\d\w\-]*spider)[\/\s;\),\\.$]/i);
1103*e56d7b71SSascha Leib					if(botmatch) {
1104*e56d7b71SSascha Leib						botInfo = {'id': ( botmatch[1] || "other_" ), 'n': "Other" + ( botmatch[1] ? " (" + botmatch[1] + ")" : "" ) , "bot": botmatch[1] };
1105*e56d7b71SSascha Leib					}
1106*e56d7b71SSascha Leib				}
1107*e56d7b71SSascha Leib
1108*e56d7b71SSascha Leib				//console.log("botInfo:", botInfo);
1109*e56d7b71SSascha Leib				return botInfo;
1110*e56d7b71SSascha Leib			},
1111*e56d7b71SSascha Leib
1112*e56d7b71SSascha Leib
1113*e56d7b71SSascha Leib			// indicates if the list is loaded and ready to use:
1114*e56d7b71SSascha Leib			_ready: false,
1115*e56d7b71SSascha Leib
1116*e56d7b71SSascha Leib			// the actual bot list is stored here:
1117*e56d7b71SSascha Leib			_list: []
1118*e56d7b71SSascha Leib		},
1119*e56d7b71SSascha Leib
1120*e56d7b71SSascha Leib		// information on known clients (browsers):
1121*e56d7b71SSascha Leib		clients: {
1122*e56d7b71SSascha Leib			// loads the list of known clients from a JSON file:
1123*e56d7b71SSascha Leib			init: async function() {
1124*e56d7b71SSascha Leib				//console.info('BotMon.live.data.clients.init()');
1125*e56d7b71SSascha Leib
1126*e56d7b71SSascha Leib				// Load the list of known bots:
1127*e56d7b71SSascha Leib				BotMon.live.gui.status.showBusy("Loading known clients");
1128*e56d7b71SSascha Leib				const url = BotMon._baseDir + 'config/known-clients.json';
1129*e56d7b71SSascha Leib				try {
1130*e56d7b71SSascha Leib					const response = await fetch(url);
1131*e56d7b71SSascha Leib					if (!response.ok) {
1132*e56d7b71SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
1133*e56d7b71SSascha Leib					}
1134*e56d7b71SSascha Leib
1135*e56d7b71SSascha Leib					BotMon.live.data.clients._list = await response.json();
1136*e56d7b71SSascha Leib					BotMon.live.data.clients._ready = true;
1137*e56d7b71SSascha Leib
1138*e56d7b71SSascha Leib				} catch (error) {
1139*e56d7b71SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message);
1140*e56d7b71SSascha Leib				} finally {
1141*e56d7b71SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
1142*e56d7b71SSascha Leib					BotMon.live.data._dispatch('clients')
1143*e56d7b71SSascha Leib				}
1144*e56d7b71SSascha Leib			},
1145*e56d7b71SSascha Leib
1146*e56d7b71SSascha Leib			// returns bot info if the user-agent matches a known bot, null otherwise:
1147*e56d7b71SSascha Leib			match: function(agent) {
1148*e56d7b71SSascha Leib				//console.info('BotMon.live.data.clients.match(',agent,')');
1149*e56d7b71SSascha Leib
1150*e56d7b71SSascha Leib				let match = {"n": "Unknown", "v": -1, "id": 'null'};
1151*e56d7b71SSascha Leib
1152*e56d7b71SSascha Leib				if (agent) {
1153*e56d7b71SSascha Leib					BotMon.live.data.clients._list.find(client => {
1154*e56d7b71SSascha Leib						let r = false;
1155*e56d7b71SSascha Leib						for (let j=0; j<client.rx.length; j++) {
1156*e56d7b71SSascha Leib							const rxr = agent.match(new RegExp(client.rx[j]));
1157*e56d7b71SSascha Leib							if (rxr) {
1158*e56d7b71SSascha Leib								match.n = client.n;
1159*e56d7b71SSascha Leib								match.v = (rxr.length > 1 ? rxr[1] : -1);
1160*e56d7b71SSascha Leib								match.id = client.id || null;
1161*e56d7b71SSascha Leib								r = true;
1162*e56d7b71SSascha Leib								break;
1163*e56d7b71SSascha Leib							}
1164*e56d7b71SSascha Leib						}
1165*e56d7b71SSascha Leib						return r;
1166*e56d7b71SSascha Leib					});
1167*e56d7b71SSascha Leib				}
1168*e56d7b71SSascha Leib
1169*e56d7b71SSascha Leib				//console.log(match)
1170*e56d7b71SSascha Leib				return match;
1171*e56d7b71SSascha Leib			},
1172*e56d7b71SSascha Leib
1173*e56d7b71SSascha Leib			// return the browser name for a browser ID:
1174*e56d7b71SSascha Leib			getName: function(id) {
1175*e56d7b71SSascha Leib				const it = BotMon.live.data.clients._list.find(client => client.id == id);
1176*e56d7b71SSascha Leib				return ( it && it.n ? it.n : "Unknown"); //it.n;
1177*e56d7b71SSascha Leib			},
1178*e56d7b71SSascha Leib
1179*e56d7b71SSascha Leib			// indicates if the list is loaded and ready to use:
1180*e56d7b71SSascha Leib			_ready: false,
1181*e56d7b71SSascha Leib
1182*e56d7b71SSascha Leib			// the actual bot list is stored here:
1183*e56d7b71SSascha Leib			_list: []
1184*e56d7b71SSascha Leib
1185*e56d7b71SSascha Leib		},
1186*e56d7b71SSascha Leib
1187*e56d7b71SSascha Leib		// information on known platforms (operating systems):
1188*e56d7b71SSascha Leib		platforms: {
1189*e56d7b71SSascha Leib			// loads the list of known platforms from a JSON file:
1190*e56d7b71SSascha Leib			init: async function() {
1191*e56d7b71SSascha Leib				//console.info('BotMon.live.data.platforms.init()');
1192*e56d7b71SSascha Leib
1193*e56d7b71SSascha Leib				// Load the list of known bots:
1194*e56d7b71SSascha Leib				BotMon.live.gui.status.showBusy("Loading known platforms");
1195*e56d7b71SSascha Leib				const url = BotMon._baseDir + 'config/known-platforms.json';
1196*e56d7b71SSascha Leib				try {
1197*e56d7b71SSascha Leib					const response = await fetch(url);
1198*e56d7b71SSascha Leib					if (!response.ok) {
1199*e56d7b71SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
1200*e56d7b71SSascha Leib					}
1201*e56d7b71SSascha Leib
1202*e56d7b71SSascha Leib					BotMon.live.data.platforms._list = await response.json();
1203*e56d7b71SSascha Leib					BotMon.live.data.platforms._ready = true;
1204*e56d7b71SSascha Leib
1205*e56d7b71SSascha Leib				} catch (error) {
1206*e56d7b71SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message);
1207*e56d7b71SSascha Leib				} finally {
1208*e56d7b71SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
1209*e56d7b71SSascha Leib					BotMon.live.data._dispatch('platforms')
1210*e56d7b71SSascha Leib				}
1211*e56d7b71SSascha Leib			},
1212*e56d7b71SSascha Leib
1213*e56d7b71SSascha Leib			// returns bot info if the browser id matches a known platform:
1214*e56d7b71SSascha Leib			match: function(cid) {
1215*e56d7b71SSascha Leib				//console.info('BotMon.live.data.platforms.match(',cid,')');
1216*e56d7b71SSascha Leib
1217*e56d7b71SSascha Leib				let match = {"n": "Unknown", "id": 'null'};
1218*e56d7b71SSascha Leib
1219*e56d7b71SSascha Leib				if (cid) {
1220*e56d7b71SSascha Leib					BotMon.live.data.platforms._list.find(platform => {
1221*e56d7b71SSascha Leib						let r = false;
1222*e56d7b71SSascha Leib						for (let j=0; j<platform.rx.length; j++) {
1223*e56d7b71SSascha Leib							const rxr = cid.match(new RegExp(platform.rx[j]));
1224*e56d7b71SSascha Leib							if (rxr) {
1225*e56d7b71SSascha Leib								match.n = platform.n;
1226*e56d7b71SSascha Leib								match.v = (rxr.length > 1 ? rxr[1] : -1);
1227*e56d7b71SSascha Leib								match.id = platform.id || null;
1228*e56d7b71SSascha Leib								r = true;
1229*e56d7b71SSascha Leib								break;
1230*e56d7b71SSascha Leib							}
1231*e56d7b71SSascha Leib						}
1232*e56d7b71SSascha Leib						return r;
1233*e56d7b71SSascha Leib					});
1234*e56d7b71SSascha Leib				}
1235*e56d7b71SSascha Leib
1236*e56d7b71SSascha Leib				return match;
1237*e56d7b71SSascha Leib			},
1238*e56d7b71SSascha Leib
1239*e56d7b71SSascha Leib			// return the platform name for a given ID:
1240*e56d7b71SSascha Leib			getName: function(id) {
1241*e56d7b71SSascha Leib				const it = BotMon.live.data.platforms._list.find( pf => pf.id == id);
1242*e56d7b71SSascha Leib				return ( it ? it.n : 'Unknown' );
1243*e56d7b71SSascha Leib			},
1244*e56d7b71SSascha Leib
1245*e56d7b71SSascha Leib
1246*e56d7b71SSascha Leib			// indicates if the list is loaded and ready to use:
1247*e56d7b71SSascha Leib			_ready: false,
1248*e56d7b71SSascha Leib
1249*e56d7b71SSascha Leib			// the actual bot list is stored here:
1250*e56d7b71SSascha Leib			_list: []
1251*e56d7b71SSascha Leib
1252*e56d7b71SSascha Leib		},
1253*e56d7b71SSascha Leib
1254*e56d7b71SSascha Leib		// storage and functions for the known bot IP-Ranges:
1255*e56d7b71SSascha Leib		ipRanges: {
1256*e56d7b71SSascha Leib
1257*e56d7b71SSascha Leib			init: function() {
1258*e56d7b71SSascha Leib				//console.log('BotMon.live.data.ipRanges.init()');
1259*e56d7b71SSascha Leib				// #TODO: Load from separate IP-Ranges file
1260*e56d7b71SSascha Leib				// load the rules file:
1261*e56d7b71SSascha Leib				const me = BotMon.live.data;
1262*e56d7b71SSascha Leib
1263*e56d7b71SSascha Leib				try {
1264*e56d7b71SSascha Leib					BotMon.live.data._loadSettingsFile(['user-ipranges', 'known-ipranges'],
1265*e56d7b71SSascha Leib						(json) => {
1266*e56d7b71SSascha Leib
1267*e56d7b71SSascha Leib						// groups can be just saved in the data structure:
1268*e56d7b71SSascha Leib						if (json.groups && json.groups.constructor.name == 'Array') {
1269*e56d7b71SSascha Leib							me.ipRanges._groups = json.groups;
1270*e56d7b71SSascha Leib						}
1271*e56d7b71SSascha Leib
1272*e56d7b71SSascha Leib						// groups can be just saved in the data structure:
1273*e56d7b71SSascha Leib						if (json.ranges && json.ranges.constructor.name == 'Array') {
1274*e56d7b71SSascha Leib							json.ranges.forEach(range => {
1275*e56d7b71SSascha Leib								me.ipRanges.add(range);
1276*e56d7b71SSascha Leib							})
1277*e56d7b71SSascha Leib						}
1278*e56d7b71SSascha Leib
1279*e56d7b71SSascha Leib						// finished loading
1280*e56d7b71SSascha Leib						BotMon.live.gui.status.hideBusy("Status: Done.");
1281*e56d7b71SSascha Leib						BotMon.live.data._dispatch('ipranges')
1282*e56d7b71SSascha Leib					});
1283*e56d7b71SSascha Leib				} catch (error) {
1284*e56d7b71SSascha Leib					BotMon.live.gui.status.setError("Error while loading the config file: " + error.message);
1285*e56d7b71SSascha Leib				}
1286*e56d7b71SSascha Leib			},
1287*e56d7b71SSascha Leib
1288*e56d7b71SSascha Leib			// the actual bot list is stored here:
1289*e56d7b71SSascha Leib			_list: [],
1290*e56d7b71SSascha Leib			_groups: [],
1291*e56d7b71SSascha Leib
1292*e56d7b71SSascha Leib			add: function(data) {
1293*e56d7b71SSascha Leib				//console.log('BotMon.live.data.ipRanges.add(',data,')');
1294*e56d7b71SSascha Leib
1295*e56d7b71SSascha Leib				const me = BotMon.live.data.ipRanges;
1296*e56d7b71SSascha Leib
1297*e56d7b71SSascha Leib				// convert IP address to easier comparable form:
1298*e56d7b71SSascha Leib				const ip2Num = BotMon.t._ip2Num;
1299*e56d7b71SSascha Leib
1300*e56d7b71SSascha Leib				let item = {
1301*e56d7b71SSascha Leib					'cidr': data.from.replaceAll(/::+/g, '::') + '/' + ( data.m ? data.m : '??' ),
1302*e56d7b71SSascha Leib					'from': ip2Num(data.from),
1303*e56d7b71SSascha Leib					'to': ip2Num(data.to),
1304*e56d7b71SSascha Leib					'm': data.m,
1305*e56d7b71SSascha Leib					'g': data.g
1306*e56d7b71SSascha Leib				};
1307*e56d7b71SSascha Leib				me._list.push(item);
1308*e56d7b71SSascha Leib
1309*e56d7b71SSascha Leib			},
1310*e56d7b71SSascha Leib
1311*e56d7b71SSascha Leib			getOwner: function(gid) {
1312*e56d7b71SSascha Leib
1313*e56d7b71SSascha Leib				const me = BotMon.live.data.ipRanges;
1314*e56d7b71SSascha Leib
1315*e56d7b71SSascha Leib				for (let i=0; i < me._groups.length; i++) {
1316*e56d7b71SSascha Leib					const it = me._groups[i];
1317*e56d7b71SSascha Leib					if (it.id == gid) {
1318*e56d7b71SSascha Leib						return it.name;
1319*e56d7b71SSascha Leib					}
1320*e56d7b71SSascha Leib				}
1321*e56d7b71SSascha Leib				return null;
1322*e56d7b71SSascha Leib			},
1323*e56d7b71SSascha Leib
1324*e56d7b71SSascha Leib			match: function(ip) {
1325*e56d7b71SSascha Leib				//console.log('BotMon.live.data.ipRanges.match(',ip,')');
1326*e56d7b71SSascha Leib
1327*e56d7b71SSascha Leib				const me = BotMon.live.data.ipRanges;
1328*e56d7b71SSascha Leib
1329*e56d7b71SSascha Leib				// convert IP address to easier comparable form:
1330*e56d7b71SSascha Leib				const ipNum = BotMon.t._ip2Num(ip);
1331*e56d7b71SSascha Leib
1332*e56d7b71SSascha Leib				for (let i=0; i < me._list.length; i++) {
1333*e56d7b71SSascha Leib					const ipRange = me._list[i];
1334*e56d7b71SSascha Leib
1335*e56d7b71SSascha Leib					if (ipNum >= ipRange.from && ipNum <= ipRange.to) {
1336*e56d7b71SSascha Leib						return ipRange;
1337*e56d7b71SSascha Leib					}
1338*e56d7b71SSascha Leib
1339*e56d7b71SSascha Leib				};
1340*e56d7b71SSascha Leib				return null;
1341*e56d7b71SSascha Leib			}
1342*e56d7b71SSascha Leib		},
1343*e56d7b71SSascha Leib
1344*e56d7b71SSascha Leib		// storage for the rules and related functions
1345*e56d7b71SSascha Leib		rules: {
1346*e56d7b71SSascha Leib
1347*e56d7b71SSascha Leib			/**
1348*e56d7b71SSascha Leib			 * Initializes the rules data.
1349*e56d7b71SSascha Leib			 *
1350*e56d7b71SSascha Leib			 * Loads the default config file and the user config file (if present).
1351*e56d7b71SSascha Leib			 * The default config file is used if the user config file does not have a certain setting.
1352*e56d7b71SSascha Leib			 * The user config file can override settings from the default config file.
1353*e56d7b71SSascha Leib			 *
1354*e56d7b71SSascha Leib			 * The rules are loaded from the `rules` property of the config files.
1355*e56d7b71SSascha Leib			 * The IP ranges are loaded from the `ipRanges` property of the config files.
1356*e56d7b71SSascha Leib			 *
1357*e56d7b71SSascha Leib			 * If an error occurs while loading the config file, it is displayed in the status bar.
1358*e56d7b71SSascha Leib			 * After the config file is loaded, the status bar is hidden.
1359*e56d7b71SSascha Leib			 */
1360*e56d7b71SSascha Leib			init: async function() {
1361*e56d7b71SSascha Leib				//console.info('BotMon.live.data.rules.init()');
1362*e56d7b71SSascha Leib
1363*e56d7b71SSascha Leib				// Load the list of known bots:
1364*e56d7b71SSascha Leib				BotMon.live.gui.status.showBusy("Loading list of rules …");
1365*e56d7b71SSascha Leib
1366*e56d7b71SSascha Leib				// load the rules file:
1367*e56d7b71SSascha Leib				const me = BotMon.live.data;
1368*e56d7b71SSascha Leib
1369*e56d7b71SSascha Leib				try {
1370*e56d7b71SSascha Leib					BotMon.live.data._loadSettingsFile(['user-config', 'default-config'],
1371*e56d7b71SSascha Leib						(json) => {
1372*e56d7b71SSascha Leib
1373*e56d7b71SSascha Leib							// override the threshold?
1374*e56d7b71SSascha Leib							if (json.threshold) me._threshold = json.threshold;
1375*e56d7b71SSascha Leib
1376*e56d7b71SSascha Leib							// set the rules list:
1377*e56d7b71SSascha Leib							if (json.rules && json.rules.constructor.name == 'Array') {
1378*e56d7b71SSascha Leib								me.rules._rulesList = json.rules;
1379*e56d7b71SSascha Leib							}
1380*e56d7b71SSascha Leib
1381*e56d7b71SSascha Leib							BotMon.live.gui.status.hideBusy("Status: Done.");
1382*e56d7b71SSascha Leib							BotMon.live.data._dispatch('rules')
1383*e56d7b71SSascha Leib						}
1384*e56d7b71SSascha Leib					);
1385*e56d7b71SSascha Leib				} catch (error) {
1386*e56d7b71SSascha Leib					BotMon.live.gui.status.setError("Error while loading the config file: " + error.message);
1387*e56d7b71SSascha Leib				}
1388*e56d7b71SSascha Leib			},
1389*e56d7b71SSascha Leib
1390*e56d7b71SSascha Leib			_rulesList: [], // list of rules to find out if a visitor is a bot
1391*e56d7b71SSascha Leib			_threshold: 100, // above this, it is considered a bot.
1392*e56d7b71SSascha Leib
1393*e56d7b71SSascha Leib			// returns a descriptive text for a rule id
1394*e56d7b71SSascha Leib			getRuleInfo: function(ruleId) {
1395*e56d7b71SSascha Leib				// console.info('getRuleInfo', ruleId);
1396*e56d7b71SSascha Leib
1397*e56d7b71SSascha Leib				// shortcut for neater code:
1398*e56d7b71SSascha Leib				const me = BotMon.live.data.rules;
1399*e56d7b71SSascha Leib
1400*e56d7b71SSascha Leib				for (let i=0; i<me._rulesList.length; i++) {
1401*e56d7b71SSascha Leib					const rule = me._rulesList[i];
1402*e56d7b71SSascha Leib					if (rule.id == ruleId) {
1403*e56d7b71SSascha Leib						return rule;
1404*e56d7b71SSascha Leib					}
1405*e56d7b71SSascha Leib				}
1406*e56d7b71SSascha Leib				return null;
1407*e56d7b71SSascha Leib
1408*e56d7b71SSascha Leib			},
1409*e56d7b71SSascha Leib
1410*e56d7b71SSascha Leib			// evaluate a visitor for lkikelihood of being a bot
1411*e56d7b71SSascha Leib			evaluate: function(visitor) {
1412*e56d7b71SSascha Leib
1413*e56d7b71SSascha Leib				// shortcut for neater code:
1414*e56d7b71SSascha Leib				const me = BotMon.live.data.rules;
1415*e56d7b71SSascha Leib
1416*e56d7b71SSascha Leib				let r =  {	// evaluation result
1417*e56d7b71SSascha Leib					'val': 0,
1418*e56d7b71SSascha Leib					'rules': [],
1419*e56d7b71SSascha Leib					'isBot': false
1420*e56d7b71SSascha Leib				};
1421*e56d7b71SSascha Leib
1422*e56d7b71SSascha Leib				for (let i=0; i<me._rulesList.length; i++) {
1423*e56d7b71SSascha Leib					const rule = me._rulesList[i];
1424*e56d7b71SSascha Leib					const params = ( rule.params ? rule.params : [] );
1425*e56d7b71SSascha Leib
1426*e56d7b71SSascha Leib					if (rule.func) { // rule is calling a function
1427*e56d7b71SSascha Leib						if (me.func[rule.func]) {
1428*e56d7b71SSascha Leib							if(me.func[rule.func](visitor, ...params)) {
1429*e56d7b71SSascha Leib								r.val += rule.bot;
1430*e56d7b71SSascha Leib								r.rules.push(rule.id)
1431*e56d7b71SSascha Leib							}
1432*e56d7b71SSascha Leib						} else {
1433*e56d7b71SSascha Leib							//console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.")
1434*e56d7b71SSascha Leib						}
1435*e56d7b71SSascha Leib					}
1436*e56d7b71SSascha Leib				}
1437*e56d7b71SSascha Leib
1438*e56d7b71SSascha Leib				// is a bot?
1439*e56d7b71SSascha Leib				r.isBot = (r.val >= me._threshold);
1440*e56d7b71SSascha Leib
1441*e56d7b71SSascha Leib				return r;
1442*e56d7b71SSascha Leib			},
1443*e56d7b71SSascha Leib
1444*e56d7b71SSascha Leib			// list of functions that can be called by the rules list to evaluate a visitor:
1445*e56d7b71SSascha Leib			func: {
1446*e56d7b71SSascha Leib
1447*e56d7b71SSascha Leib				// check if client is on the list passed as parameter:
1448*e56d7b71SSascha Leib				matchesClient: function(visitor, ...clients) {
1449*e56d7b71SSascha Leib
1450*e56d7b71SSascha Leib					const clientId = ( visitor._client ? visitor._client.id : '');
1451*e56d7b71SSascha Leib					return clients.includes(clientId);
1452*e56d7b71SSascha Leib				},
1453*e56d7b71SSascha Leib
1454*e56d7b71SSascha Leib				// check if OS/Platform is one of the obsolete ones:
1455*e56d7b71SSascha Leib				matchesPlatform: function(visitor, ...platforms) {
1456*e56d7b71SSascha Leib
1457*e56d7b71SSascha Leib					const pId = ( visitor._platform ? visitor._platform.id : '');
1458*e56d7b71SSascha Leib
1459*e56d7b71SSascha Leib					if (visitor._platform.id == null) console.log(visitor._platform);
1460*e56d7b71SSascha Leib
1461*e56d7b71SSascha Leib					return platforms.includes(pId);
1462*e56d7b71SSascha Leib				},
1463*e56d7b71SSascha Leib
1464*e56d7b71SSascha Leib				// are there at lest num pages loaded?
1465*e56d7b71SSascha Leib				smallPageCount: function(visitor, num) {
1466*e56d7b71SSascha Leib					return (visitor._pageViews.length <= Number(num));
1467*e56d7b71SSascha Leib				},
1468*e56d7b71SSascha Leib
1469*e56d7b71SSascha Leib				// There was no entry in a specific log file for this visitor:
1470*e56d7b71SSascha Leib				// note that this will also trigger the "noJavaScript" rule:
1471*e56d7b71SSascha Leib				noRecord: function(visitor, type) {
1472*e56d7b71SSascha Leib					return !visitor._seenBy.includes(type);
1473*e56d7b71SSascha Leib				},
1474*e56d7b71SSascha Leib
1475*e56d7b71SSascha Leib				// there are no referrers in any of the page visits:
1476*e56d7b71SSascha Leib				noReferrer: function(visitor) {
1477*e56d7b71SSascha Leib
1478*e56d7b71SSascha Leib					let r = false; // return value
1479*e56d7b71SSascha Leib					for (let i = 0; i < visitor._pageViews.length; i++) {
1480*e56d7b71SSascha Leib						if (!visitor._pageViews[i]._ref) {
1481*e56d7b71SSascha Leib							r = true;
1482*e56d7b71SSascha Leib							break;
1483*e56d7b71SSascha Leib						}
1484*e56d7b71SSascha Leib					}
1485*e56d7b71SSascha Leib					return r;
1486*e56d7b71SSascha Leib				},
1487*e56d7b71SSascha Leib
1488*e56d7b71SSascha Leib				// test for specific client identifiers:
1489*e56d7b71SSascha Leib				/*matchesClients: function(visitor, ...list) {
1490*e56d7b71SSascha Leib
1491*e56d7b71SSascha Leib					for (let i=0; i<list.length; i++) {
1492*e56d7b71SSascha Leib						if (visitor._client.id == list[i]) {
1493*e56d7b71SSascha Leib								return true
1494*e56d7b71SSascha Leib						}
1495*e56d7b71SSascha Leib					};
1496*e56d7b71SSascha Leib					return false;
1497*e56d7b71SSascha Leib				},*/
1498*e56d7b71SSascha Leib
1499*e56d7b71SSascha Leib				// unusual combinations of Platform and Client:
1500*e56d7b71SSascha Leib				combinationTest: function(visitor, ...combinations) {
1501*e56d7b71SSascha Leib
1502*e56d7b71SSascha Leib					for (let i=0; i<combinations.length; i++) {
1503*e56d7b71SSascha Leib
1504*e56d7b71SSascha Leib						if (visitor._platform.id == combinations[i][0]
1505*e56d7b71SSascha Leib							&& visitor._client.id == combinations[i][1]) {
1506*e56d7b71SSascha Leib								return true
1507*e56d7b71SSascha Leib						}
1508*e56d7b71SSascha Leib					};
1509*e56d7b71SSascha Leib
1510*e56d7b71SSascha Leib					return false;
1511*e56d7b71SSascha Leib				},
1512*e56d7b71SSascha Leib
1513*e56d7b71SSascha Leib				// is the IP address from a known bot network?
1514*e56d7b71SSascha Leib				fromKnownBotIP: function(visitor) {
1515*e56d7b71SSascha Leib					//console.info('fromKnownBotIP()', visitor.ip);
1516*e56d7b71SSascha Leib
1517*e56d7b71SSascha Leib					const ipInfo = BotMon.live.data.ipRanges.match(visitor.ip);
1518*e56d7b71SSascha Leib
1519*e56d7b71SSascha Leib					if (ipInfo) {
1520*e56d7b71SSascha Leib						visitor._ipInKnownBotRange = true;
1521*e56d7b71SSascha Leib						visitor._ipRange = ipInfo;
1522*e56d7b71SSascha Leib					}
1523*e56d7b71SSascha Leib
1524*e56d7b71SSascha Leib					return (ipInfo !== null);
1525*e56d7b71SSascha Leib				},
1526*e56d7b71SSascha Leib
1527*e56d7b71SSascha Leib				// is the page language mentioned in the client's accepted languages?
1528*e56d7b71SSascha Leib				// the parameter holds an array of exceptions, i.e. page languages that should be ignored.
1529*e56d7b71SSascha Leib				matchLang: function(visitor, ...exceptions) {
1530*e56d7b71SSascha Leib
1531*e56d7b71SSascha Leib					if (visitor.lang && visitor.accept && exceptions.indexOf(visitor.lang) < 0) {
1532*e56d7b71SSascha Leib						return (visitor.accept.split(',').indexOf(visitor.lang) < 0);
1533*e56d7b71SSascha Leib					}
1534*e56d7b71SSascha Leib					return false;
1535*e56d7b71SSascha Leib				},
1536*e56d7b71SSascha Leib
1537*e56d7b71SSascha Leib				// the "Accept language" header contains certain entries:
1538*e56d7b71SSascha Leib				clientAccepts: function(visitor, ...languages) {
1539*e56d7b71SSascha Leib					//console.info('clientAccepts', visitor.accept, languages);
1540*e56d7b71SSascha Leib
1541*e56d7b71SSascha Leib					if (visitor.accept && languages) {;
1542*e56d7b71SSascha Leib						return ( visitor.accept.split(',').filter(lang => languages.includes(lang)).length > 0 );
1543*e56d7b71SSascha Leib					}
1544*e56d7b71SSascha Leib					return false;
1545*e56d7b71SSascha Leib				},
1546*e56d7b71SSascha Leib
1547*e56d7b71SSascha Leib				// Is there an accept-language field defined at all?
1548*e56d7b71SSascha Leib				noAcceptLang: function(visitor) {
1549*e56d7b71SSascha Leib
1550*e56d7b71SSascha Leib					if (!visitor.accept || visitor.accept.length <= 0) { // no accept-languages header
1551*e56d7b71SSascha Leib						return true;
1552*e56d7b71SSascha Leib					}
1553*e56d7b71SSascha Leib					// TODO: parametrize this!
1554*e56d7b71SSascha Leib					return false;
1555*e56d7b71SSascha Leib				},
1556*e56d7b71SSascha Leib				// At least x page views were recorded, but they come within less than y seconds
1557*e56d7b71SSascha Leib				loadSpeed: function(visitor, minItems, maxTime) {
1558*e56d7b71SSascha Leib
1559*e56d7b71SSascha Leib					if (visitor._pageViews.length >= minItems) {
1560*e56d7b71SSascha Leib						//console.log('loadSpeed', visitor._pageViews.length, minItems, maxTime);
1561*e56d7b71SSascha Leib
1562*e56d7b71SSascha Leib						const pvArr = visitor._pageViews.map(pv => pv._lastSeen).sort();
1563*e56d7b71SSascha Leib
1564*e56d7b71SSascha Leib						let totalTime = 0;
1565*e56d7b71SSascha Leib						for (let i=1; i < pvArr.length; i++) {
1566*e56d7b71SSascha Leib							totalTime += (pvArr[i] - pvArr[i-1]);
1567*e56d7b71SSascha Leib						}
1568*e56d7b71SSascha Leib
1569*e56d7b71SSascha Leib						//console.log('     ', totalTime , Math.round(totalTime / (pvArr.length * 1000)), (( totalTime / pvArr.length ) <= maxTime * 1000), visitor.ip);
1570*e56d7b71SSascha Leib
1571*e56d7b71SSascha Leib						return (( totalTime / pvArr.length ) <= maxTime * 1000);
1572*e56d7b71SSascha Leib					}
1573*e56d7b71SSascha Leib				},
1574*e56d7b71SSascha Leib
1575*e56d7b71SSascha Leib				// Country code matches one of those in the list:
1576*e56d7b71SSascha Leib				matchesCountry: function(visitor, ...countries) {
1577*e56d7b71SSascha Leib
1578*e56d7b71SSascha Leib					// ingore if geoloc is not set or unknown:
1579*e56d7b71SSascha Leib					if (visitor.geo) {
1580*e56d7b71SSascha Leib						return (countries.indexOf(visitor.geo) >= 0);
1581*e56d7b71SSascha Leib					}
1582*e56d7b71SSascha Leib					return false;
1583*e56d7b71SSascha Leib				},
1584*e56d7b71SSascha Leib
1585*e56d7b71SSascha Leib				// Country does not match one of the given codes.
1586*e56d7b71SSascha Leib				notFromCountry: function(visitor, ...countries) {
1587*e56d7b71SSascha Leib
1588*e56d7b71SSascha Leib					// ingore if geoloc is not set or unknown:
1589*e56d7b71SSascha Leib					if (visitor.geo && visitor.geo !== 'ZZ') {
1590*e56d7b71SSascha Leib						return (countries.indexOf(visitor.geo) < 0);
1591*e56d7b71SSascha Leib					}
1592*e56d7b71SSascha Leib					return false;
1593*e56d7b71SSascha Leib				}
1594*e56d7b71SSascha Leib			}
1595*e56d7b71SSascha Leib		},
1596*e56d7b71SSascha Leib
1597*e56d7b71SSascha Leib		/**
1598*e56d7b71SSascha Leib		 * Loads a settings file from the specified list of filenames.
1599*e56d7b71SSascha Leib		 * If the file is successfully loaded, it will call the callback function
1600*e56d7b71SSascha Leib		 * with the loaded JSON data.
1601*e56d7b71SSascha Leib		 * If no file can be loaded, it will display an error message.
1602*e56d7b71SSascha Leib		 *
1603*e56d7b71SSascha Leib		 * @param {string[]} fns - list of filenames to load
1604*e56d7b71SSascha Leib		 * @param {function} callback - function to call with the loaded JSON data
1605*e56d7b71SSascha Leib		 */
1606*e56d7b71SSascha Leib		_loadSettingsFile: async function(fns, callback) {
1607*e56d7b71SSascha Leib			//console.info('BotMon.live.data._loadSettingsFile()', fns);
1608*e56d7b71SSascha Leib
1609*e56d7b71SSascha Leib			const kJsonExt = '.json';
1610*e56d7b71SSascha Leib			let loaded = false; // if successfully loaded file
1611*e56d7b71SSascha Leib
1612*e56d7b71SSascha Leib			for (let i=0; i<fns.length; i++) {
1613*e56d7b71SSascha Leib				const filename = fns[i] +kJsonExt;
1614*e56d7b71SSascha Leib				try {
1615*e56d7b71SSascha Leib					const response = await fetch(DOKU_BASE + 'lib/plugins/botmon/config/' + filename);
1616*e56d7b71SSascha Leib					if (!response.ok) {
1617*e56d7b71SSascha Leib						continue;
1618*e56d7b71SSascha Leib					} else {
1619*e56d7b71SSascha Leib						loaded = true;
1620*e56d7b71SSascha Leib					}
1621*e56d7b71SSascha Leib					const json = await response.json();
1622*e56d7b71SSascha Leib					if (callback && typeof callback === 'function') {
1623*e56d7b71SSascha Leib						callback(json);
1624*e56d7b71SSascha Leib					}
1625*e56d7b71SSascha Leib					break;
1626*e56d7b71SSascha Leib				} catch (e) {
1627*e56d7b71SSascha Leib					BotMon.live.gui.status.setError("Error while loading the config file: " + filename);
1628*e56d7b71SSascha Leib				}
1629*e56d7b71SSascha Leib			}
1630*e56d7b71SSascha Leib
1631*e56d7b71SSascha Leib			if (!loaded) {
1632*e56d7b71SSascha Leib				BotMon.live.gui.status.setError("Could not load a config file.");
1633*e56d7b71SSascha Leib			}
1634*e56d7b71SSascha Leib		},
1635*e56d7b71SSascha Leib
1636*e56d7b71SSascha Leib		/**
1637*e56d7b71SSascha Leib		 * Loads a log file (server, page load, or ticker) and parses it.
1638*e56d7b71SSascha Leib		 * @param {String} type - the type of the log file to load (srv, log, or tck)
1639*e56d7b71SSascha Leib		 * @param {Function} [onLoaded] - an optional callback function to call after loading is finished.
1640*e56d7b71SSascha Leib		 */
1641*e56d7b71SSascha Leib		loadLogFile: async function(type, onLoaded = undefined) {
1642*e56d7b71SSascha Leib			//console.info('BotMon.live.data.loadLogFile(',type,')');
1643*e56d7b71SSascha Leib
1644*e56d7b71SSascha Leib			let typeName = '';
1645*e56d7b71SSascha Leib			let columns = [];
1646*e56d7b71SSascha Leib
1647*e56d7b71SSascha Leib			switch (type) {
1648*e56d7b71SSascha Leib				case "srv":
1649*e56d7b71SSascha Leib					typeName = "Server";
1650*e56d7b71SSascha Leib					columns = ['ts','ip','pg','id','typ','usr','agent','ref','lang','accept','geo'];
1651*e56d7b71SSascha Leib					break;
1652*e56d7b71SSascha Leib				case "log":
1653*e56d7b71SSascha Leib					typeName = "Page load";
1654*e56d7b71SSascha Leib					columns = ['ts','ip','pg','id','usr','lt','ref','agent'];
1655*e56d7b71SSascha Leib					break;
1656*e56d7b71SSascha Leib				case "tck":
1657*e56d7b71SSascha Leib					typeName = "Ticker";
1658*e56d7b71SSascha Leib					columns = ['ts','ip','pg','id','agent'];
1659*e56d7b71SSascha Leib					break;
1660*e56d7b71SSascha Leib				default:
1661*e56d7b71SSascha Leib					console.warn(`Unknown log type ${type}.`);
1662*e56d7b71SSascha Leib					return;
1663*e56d7b71SSascha Leib			}
1664*e56d7b71SSascha Leib
1665*e56d7b71SSascha Leib			// Show the busy indicator and set the visible status:
1666*e56d7b71SSascha Leib			BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`);
1667*e56d7b71SSascha Leib
1668*e56d7b71SSascha Leib			// compose the URL from which to load:
1669*e56d7b71SSascha Leib			const url = BotMon._baseDir + `logs/${BotMon._datestr}.${type}.txt`;
1670*e56d7b71SSascha Leib			//console.log("Loading:",url);
1671*e56d7b71SSascha Leib
1672*e56d7b71SSascha Leib			// fetch the data:
1673*e56d7b71SSascha Leib			try {
1674*e56d7b71SSascha Leib				const response = await fetch(url);
1675*e56d7b71SSascha Leib				if (!response.ok) {
1676*e56d7b71SSascha Leib
1677*e56d7b71SSascha Leib					throw new Error(`${response.status} ${response.statusText}`);
1678*e56d7b71SSascha Leib
1679*e56d7b71SSascha Leib				} else {
1680*e56d7b71SSascha Leib
1681*e56d7b71SSascha Leib					// parse the data:
1682*e56d7b71SSascha Leib					const logtxt = await response.text();
1683*e56d7b71SSascha Leib					if (logtxt.length <= 0) {
1684*e56d7b71SSascha Leib						throw new Error(`Empty log file ${url}.`);
1685*e56d7b71SSascha Leib					}
1686*e56d7b71SSascha Leib
1687*e56d7b71SSascha Leib					logtxt.split('\n').forEach((line) => {
1688*e56d7b71SSascha Leib						if (line.trim() === '') return; // skip empty lines
1689*e56d7b71SSascha Leib						const cols = line.split('\t');
1690*e56d7b71SSascha Leib
1691*e56d7b71SSascha Leib						// assign the columns to an object:
1692*e56d7b71SSascha Leib						const data = {};
1693*e56d7b71SSascha Leib						cols.forEach( (colVal,i) => {
1694*e56d7b71SSascha Leib							colName = columns[i] || `col${i}`;
1695*e56d7b71SSascha Leib							const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim());
1696*e56d7b71SSascha Leib							data[colName] = colValue;
1697*e56d7b71SSascha Leib						});
1698*e56d7b71SSascha Leib
1699*e56d7b71SSascha Leib						// register the visit in the model:
1700*e56d7b71SSascha Leib						switch(type) {
1701*e56d7b71SSascha Leib							case BM_LOGTYPE.SERVER:
1702*e56d7b71SSascha Leib								BotMon.live.data.model.registerVisit(data, type);
1703*e56d7b71SSascha Leib								break;
1704*e56d7b71SSascha Leib							case BM_LOGTYPE.CLIENT:
1705*e56d7b71SSascha Leib								data.typ = 'js';
1706*e56d7b71SSascha Leib								BotMon.live.data.model.updateVisit(data);
1707*e56d7b71SSascha Leib								break;
1708*e56d7b71SSascha Leib							case BM_LOGTYPE.TICKER:
1709*e56d7b71SSascha Leib								data.typ = 'js';
1710*e56d7b71SSascha Leib								BotMon.live.data.model.updateTicks(data);
1711*e56d7b71SSascha Leib								break;
1712*e56d7b71SSascha Leib							default:
1713*e56d7b71SSascha Leib								console.warn(`Unknown log type ${type}.`);
1714*e56d7b71SSascha Leib								return;
1715*e56d7b71SSascha Leib						}
1716*e56d7b71SSascha Leib					});
1717*e56d7b71SSascha Leib				}
1718*e56d7b71SSascha Leib
1719*e56d7b71SSascha Leib			} catch (error) {
1720*e56d7b71SSascha Leib				BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message} – data may be incomplete.`);
1721*e56d7b71SSascha Leib			} finally {
1722*e56d7b71SSascha Leib				BotMon.live.gui.status.hideBusy("Status: Done.");
1723*e56d7b71SSascha Leib				if (onLoaded) {
1724*e56d7b71SSascha Leib					onLoaded(); // callback after loading is finished.
1725*e56d7b71SSascha Leib				}
1726*e56d7b71SSascha Leib			}
1727*e56d7b71SSascha Leib		}
1728*e56d7b71SSascha Leib	},
1729*e56d7b71SSascha Leib
1730*e56d7b71SSascha Leib	gui: {
1731*e56d7b71SSascha Leib		init: function() {
1732*e56d7b71SSascha Leib			// init the lists view:
1733*e56d7b71SSascha Leib			this.lists.init();
1734*e56d7b71SSascha Leib		},
1735*e56d7b71SSascha Leib
1736*e56d7b71SSascha Leib		/* The Overview / web metrics section of the live tab */
1737*e56d7b71SSascha Leib		overview: {
1738*e56d7b71SSascha Leib			/**
1739*e56d7b71SSascha Leib			 * Populates the overview part of the today tab with the analytics data.
1740*e56d7b71SSascha Leib			 *
1741*e56d7b71SSascha Leib			 * @method make
1742*e56d7b71SSascha Leib			 * @memberof BotMon.live.gui.overview
1743*e56d7b71SSascha Leib			 */
1744*e56d7b71SSascha Leib			make: function() {
1745*e56d7b71SSascha Leib
1746*e56d7b71SSascha Leib				const data = BotMon.live.data.analytics.data;
1747*e56d7b71SSascha Leib
1748*e56d7b71SSascha Leib				const maxItemsPerList = 5; // how many list items to show?
1749*e56d7b71SSascha Leib
1750*e56d7b71SSascha Leib				// shortcut for neater code:
1751*e56d7b71SSascha Leib				const makeElement = BotMon.t._makeElement;
1752*e56d7b71SSascha Leib
1753*e56d7b71SSascha Leib				const botsVsHumans = document.getElementById('botmon__today__botsvshumans');
1754*e56d7b71SSascha Leib				if (botsVsHumans) {
1755*e56d7b71SSascha Leib					botsVsHumans.appendChild(makeElement('dt', {}, "Page views"));
1756*e56d7b71SSascha Leib
1757*e56d7b71SSascha Leib					for (let i = 0; i <= 5; i++) {
1758*e56d7b71SSascha Leib						const dd = makeElement('dd');
1759*e56d7b71SSascha Leib						let title = '';
1760*e56d7b71SSascha Leib						let value = '';
1761*e56d7b71SSascha Leib						switch(i) {
1762*e56d7b71SSascha Leib							case 0:
1763*e56d7b71SSascha Leib								title = "Known bots:";
1764*e56d7b71SSascha Leib								value = data.bots.known;
1765*e56d7b71SSascha Leib								break;
1766*e56d7b71SSascha Leib							case 1:
1767*e56d7b71SSascha Leib								title = "Suspected bots:";
1768*e56d7b71SSascha Leib								value = data.bots.suspected;
1769*e56d7b71SSascha Leib								break;
1770*e56d7b71SSascha Leib							case 2:
1771*e56d7b71SSascha Leib								title = "Probably humans:";
1772*e56d7b71SSascha Leib								value = data.bots.human;
1773*e56d7b71SSascha Leib								break;
1774*e56d7b71SSascha Leib							case 3:
1775*e56d7b71SSascha Leib								title = "Registered users:";
1776*e56d7b71SSascha Leib								value = data.bots.users;
1777*e56d7b71SSascha Leib								break;
1778*e56d7b71SSascha Leib							case 4:
1779*e56d7b71SSascha Leib								title = "Total:";
1780*e56d7b71SSascha Leib								value = data.totalPageViews;
1781*e56d7b71SSascha Leib								break;
1782*e56d7b71SSascha Leib							case 5:
1783*e56d7b71SSascha Leib								title = "Bots-humans ratio:";
1784*e56d7b71SSascha Leib								value = BotMon.t._getRatio(data.bots.suspected + data.bots.known, data.bots.users + data.bots.human, 100);
1785*e56d7b71SSascha Leib								break;
1786*e56d7b71SSascha Leib							default:
1787*e56d7b71SSascha Leib								console.warn(`Unknown list type ${i}.`);
1788*e56d7b71SSascha Leib						}
1789*e56d7b71SSascha Leib						dd.appendChild(makeElement('span', {}, title));
1790*e56d7b71SSascha Leib						dd.appendChild(makeElement('strong', {}, value));
1791*e56d7b71SSascha Leib						botsVsHumans.appendChild(dd);
1792*e56d7b71SSascha Leib					}
1793*e56d7b71SSascha Leib				}
1794*e56d7b71SSascha Leib
1795*e56d7b71SSascha Leib				// update known bots list:
1796*e56d7b71SSascha Leib				const botElement = document.getElementById('botmon__botslist'); /* Known bots */
1797*e56d7b71SSascha Leib				if (botElement) {
1798*e56d7b71SSascha Leib					botElement.appendChild(makeElement('dt', {}, `Top known bots`));
1799*e56d7b71SSascha Leib
1800*e56d7b71SSascha Leib					let botList = BotMon.live.data.analytics.getTopBots(maxItemsPerList);
1801*e56d7b71SSascha Leib					botList.forEach( (botInfo) => {
1802*e56d7b71SSascha Leib						const bli = makeElement('dd');
1803*e56d7b71SSascha Leib						bli.appendChild(makeElement('span', {'class': 'has_icon bot bot_' + botInfo.id }, botInfo.name));
1804*e56d7b71SSascha Leib						bli.appendChild(makeElement('span', {'class': 'count' }, botInfo.count));
1805*e56d7b71SSascha Leib						botElement.append(bli)
1806*e56d7b71SSascha Leib					});
1807*e56d7b71SSascha Leib				}
1808*e56d7b71SSascha Leib
1809*e56d7b71SSascha Leib				// update the suspected bot IP ranges list:
1810*e56d7b71SSascha Leib				const botIps = document.getElementById('botmon__botips');
1811*e56d7b71SSascha Leib				if (botIps) {
1812*e56d7b71SSascha Leib					botIps.appendChild(makeElement('dt', {}, "Top bot ISPs"));
1813*e56d7b71SSascha Leib
1814*e56d7b71SSascha Leib					const ispList = BotMon.live.data.analytics.getTopBotISPs(5);
1815*e56d7b71SSascha Leib					console.log(ispList);
1816*e56d7b71SSascha Leib					ispList.forEach( (netInfo) => {
1817*e56d7b71SSascha Leib						const li = makeElement('dd');
1818*e56d7b71SSascha Leib						li.appendChild(makeElement('span', {'class': 'has_icon ipaddr ip_' + netInfo.id }, netInfo.name));
1819*e56d7b71SSascha Leib						li.appendChild(makeElement('span', {'class': 'count' }, netInfo.count));
1820*e56d7b71SSascha Leib						botIps.append(li)
1821*e56d7b71SSascha Leib					});
1822*e56d7b71SSascha Leib				}
1823*e56d7b71SSascha Leib
1824*e56d7b71SSascha Leib				// update the top bot countries list:
1825*e56d7b71SSascha Leib				const botCountries = document.getElementById('botmon__botcountries');
1826*e56d7b71SSascha Leib				if (botCountries) {
1827*e56d7b71SSascha Leib					botCountries.appendChild(makeElement('dt', {}, `Top bot Countries`));
1828*e56d7b71SSascha Leib					const countryList = BotMon.live.data.analytics.getCountryList('bot', 5);
1829*e56d7b71SSascha Leib					countryList.forEach( (cInfo) => {
1830*e56d7b71SSascha Leib						const cLi = makeElement('dd');
1831*e56d7b71SSascha Leib						cLi.appendChild(makeElement('span', {'class': 'has_icon country ctry_' + cInfo.id.toLowerCase() }, cInfo.name));
1832*e56d7b71SSascha Leib						cLi.appendChild(makeElement('span', {'class': 'count' }, cInfo.count));
1833*e56d7b71SSascha Leib						botCountries.appendChild(cLi);
1834*e56d7b71SSascha Leib					});
1835*e56d7b71SSascha Leib				}
1836*e56d7b71SSascha Leib
1837*e56d7b71SSascha Leib				// update the webmetrics overview:
1838*e56d7b71SSascha Leib				const wmoverview = document.getElementById('botmon__today__wm_overview');
1839*e56d7b71SSascha Leib				if (wmoverview) {
1840*e56d7b71SSascha Leib
1841*e56d7b71SSascha Leib					const humanVisits = BotMon.live.data.analytics.groups.users.length + BotMon.live.data.analytics.groups.humans.length;
1842*e56d7b71SSascha Leib					const bounceRate = Math.round(100 * (BotMon.live.data.analytics.getBounceCount('users') + BotMon.live.data.analytics.getBounceCount('humans')) / humanVisits);
1843*e56d7b71SSascha Leib
1844*e56d7b71SSascha Leib					wmoverview.appendChild(makeElement('dt', {}, "Humans’ metrics"));
1845*e56d7b71SSascha Leib					for (let i = 0; i <= 4; i++) {
1846*e56d7b71SSascha Leib						const dd = makeElement('dd');
1847*e56d7b71SSascha Leib						let title = '';
1848*e56d7b71SSascha Leib						let value = '';
1849*e56d7b71SSascha Leib						switch(i) {
1850*e56d7b71SSascha Leib							case 0:
1851*e56d7b71SSascha Leib								title = "Page views by registered users:";
1852*e56d7b71SSascha Leib								value = data.bots.users;
1853*e56d7b71SSascha Leib								break;
1854*e56d7b71SSascha Leib							case 1:
1855*e56d7b71SSascha Leib								title = "Page views by “probably humans”:";
1856*e56d7b71SSascha Leib								value = data.bots.human;
1857*e56d7b71SSascha Leib								break;
1858*e56d7b71SSascha Leib							case 2:
1859*e56d7b71SSascha Leib								title = "Total human page views:";
1860*e56d7b71SSascha Leib								value = data.bots.users + data.bots.human;
1861*e56d7b71SSascha Leib								break;
1862*e56d7b71SSascha Leib							case 3:
1863*e56d7b71SSascha Leib								title = "Total human visits:";
1864*e56d7b71SSascha Leib								value = humanVisits;
1865*e56d7b71SSascha Leib								break;
1866*e56d7b71SSascha Leib							case 4:
1867*e56d7b71SSascha Leib								title = "Humans’ bounce rate:";
1868*e56d7b71SSascha Leib								value = bounceRate + '%';
1869*e56d7b71SSascha Leib								break;
1870*e56d7b71SSascha Leib							default:
1871*e56d7b71SSascha Leib								console.warn(`Unknown list type ${i}.`);
1872*e56d7b71SSascha Leib						}
1873*e56d7b71SSascha Leib						dd.appendChild(makeElement('span', {}, title));
1874*e56d7b71SSascha Leib						dd.appendChild(makeElement('strong', {}, value));
1875*e56d7b71SSascha Leib						wmoverview.appendChild(dd);
1876*e56d7b71SSascha Leib					}
1877*e56d7b71SSascha Leib				}
1878*e56d7b71SSascha Leib
1879*e56d7b71SSascha Leib				// update the webmetrics clients list:
1880*e56d7b71SSascha Leib				const wmclients = document.getElementById('botmon__today__wm_clients');
1881*e56d7b71SSascha Leib				if (wmclients) {
1882*e56d7b71SSascha Leib
1883*e56d7b71SSascha Leib					wmclients.appendChild(makeElement('dt', {}, "Browsers"));
1884*e56d7b71SSascha Leib
1885*e56d7b71SSascha Leib					const clientList = BotMon.live.data.analytics.getTopBrowsers(maxItemsPerList);
1886*e56d7b71SSascha Leib					if (clientList) {
1887*e56d7b71SSascha Leib						clientList.forEach( (cInfo) => {
1888*e56d7b71SSascha Leib							const cDd = makeElement('dd');
1889*e56d7b71SSascha Leib							cDd.appendChild(makeElement('span', {'class': 'has_icon client cl_' + cInfo.id }, ( cInfo.name ? cInfo.name : cInfo.id)));
1890*e56d7b71SSascha Leib							cDd.appendChild(makeElement('span', {
1891*e56d7b71SSascha Leib								'class': 'count',
1892*e56d7b71SSascha Leib								'title': cInfo.count + " page views"
1893*e56d7b71SSascha Leib							}, cInfo.pct.toFixed(1) + '%'));
1894*e56d7b71SSascha Leib							wmclients.appendChild(cDd);
1895*e56d7b71SSascha Leib						});
1896*e56d7b71SSascha Leib					}
1897*e56d7b71SSascha Leib				}
1898*e56d7b71SSascha Leib
1899*e56d7b71SSascha Leib				// update the webmetrics platforms list:
1900*e56d7b71SSascha Leib				const wmplatforms = document.getElementById('botmon__today__wm_platforms');
1901*e56d7b71SSascha Leib				if (wmplatforms) {
1902*e56d7b71SSascha Leib
1903*e56d7b71SSascha Leib					wmplatforms.appendChild(makeElement('dt', {}, "Platforms"));
1904*e56d7b71SSascha Leib
1905*e56d7b71SSascha Leib					const pfList = BotMon.live.data.analytics.getTopPlatforms(maxItemsPerList);
1906*e56d7b71SSascha Leib					if (pfList) {
1907*e56d7b71SSascha Leib						pfList.forEach( (pInfo) => {
1908*e56d7b71SSascha Leib							const pDd = makeElement('dd');
1909*e56d7b71SSascha Leib							pDd.appendChild(makeElement('span', {'class': 'has_icon platform pf_' + pInfo.id }, ( pInfo.name ? pInfo.name : pInfo.id)));
1910*e56d7b71SSascha Leib							pDd.appendChild(makeElement('span', {
1911*e56d7b71SSascha Leib								'class': 'count',
1912*e56d7b71SSascha Leib								'title': pInfo.count + " page views"
1913*e56d7b71SSascha Leib							}, pInfo.pct.toFixed(1) + '%'));
1914*e56d7b71SSascha Leib							wmplatforms.appendChild(pDd);
1915*e56d7b71SSascha Leib						});
1916*e56d7b71SSascha Leib					}
1917*e56d7b71SSascha Leib				}
1918*e56d7b71SSascha Leib
1919*e56d7b71SSascha Leib				// update the top bot countries list:
1920*e56d7b71SSascha Leib				const usrCountries = document.getElementById('botmon__today__wm_countries');
1921*e56d7b71SSascha Leib				if (usrCountries) {
1922*e56d7b71SSascha Leib					usrCountries.appendChild(makeElement('dt', {}, `Top visitor Countries:`));
1923*e56d7b71SSascha Leib					const usrCtryList = BotMon.live.data.analytics.getCountryList('human', 5);
1924*e56d7b71SSascha Leib					usrCtryList.forEach( (cInfo) => {
1925*e56d7b71SSascha Leib						const cLi = makeElement('dd');
1926*e56d7b71SSascha Leib						cLi.appendChild(makeElement('span', {'class': 'has_icon country ctry_' + cInfo.id.toLowerCase() }, cInfo.name));
1927*e56d7b71SSascha Leib						cLi.appendChild(makeElement('span', {'class': 'count' }, cInfo.count));
1928*e56d7b71SSascha Leib						usrCountries.appendChild(cLi);
1929*e56d7b71SSascha Leib					});
1930*e56d7b71SSascha Leib				}
1931*e56d7b71SSascha Leib
1932*e56d7b71SSascha Leib				// update the top pages;
1933*e56d7b71SSascha Leib				const wmpages = document.getElementById('botmon__today__wm_pages');
1934*e56d7b71SSascha Leib				if (wmpages) {
1935*e56d7b71SSascha Leib
1936*e56d7b71SSascha Leib					wmpages.appendChild(makeElement('dt', {}, "Top pages"));
1937*e56d7b71SSascha Leib
1938*e56d7b71SSascha Leib					const pgList = BotMon.live.data.analytics.getTopPages(maxItemsPerList);
1939*e56d7b71SSascha Leib					if (pgList) {
1940*e56d7b71SSascha Leib						pgList.forEach( (pgInfo) => {
1941*e56d7b71SSascha Leib							const pgDd = makeElement('dd');
1942*e56d7b71SSascha Leib							pgDd.appendChild(makeElement('a', {
1943*e56d7b71SSascha Leib								'class': 'page_icon',
1944*e56d7b71SSascha Leib								'href': DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pgInfo.id),
1945*e56d7b71SSascha Leib								'target': 'preview',
1946*e56d7b71SSascha Leib								'title': "PageID: " + pgInfo.id
1947*e56d7b71SSascha Leib							}, pgInfo.id));
1948*e56d7b71SSascha Leib							pgDd.appendChild(makeElement('span', {
1949*e56d7b71SSascha Leib								'class': 'count',
1950*e56d7b71SSascha Leib								'title': pgInfo.count + " page views"
1951*e56d7b71SSascha Leib							}, pgInfo.count));
1952*e56d7b71SSascha Leib							wmpages.appendChild(pgDd);
1953*e56d7b71SSascha Leib						});
1954*e56d7b71SSascha Leib					}
1955*e56d7b71SSascha Leib				}
1956*e56d7b71SSascha Leib
1957*e56d7b71SSascha Leib				// update the top referrers;
1958*e56d7b71SSascha Leib				const wmreferers = document.getElementById('botmon__today__wm_referers');
1959*e56d7b71SSascha Leib				if (wmreferers) {
1960*e56d7b71SSascha Leib
1961*e56d7b71SSascha Leib					wmreferers.appendChild(makeElement('dt', {}, "Referers"));
1962*e56d7b71SSascha Leib
1963*e56d7b71SSascha Leib					const refList = BotMon.live.data.analytics.getTopReferers(maxItemsPerList);
1964*e56d7b71SSascha Leib					if (refList) {
1965*e56d7b71SSascha Leib						refList.forEach( (rInfo) => {
1966*e56d7b71SSascha Leib							const rDd = makeElement('dd');
1967*e56d7b71SSascha Leib							rDd.appendChild(makeElement('span', {'class': 'has_icon referer ref_' + rInfo.id }, rInfo.name));
1968*e56d7b71SSascha Leib							rDd.appendChild(makeElement('span', {
1969*e56d7b71SSascha Leib								'class': 'count',
1970*e56d7b71SSascha Leib								'title': rInfo.count + " references"
1971*e56d7b71SSascha Leib							}, rInfo.pct.toFixed(1) + '%'));
1972*e56d7b71SSascha Leib							wmreferers.appendChild(rDd);
1973*e56d7b71SSascha Leib						});
1974*e56d7b71SSascha Leib					}
1975*e56d7b71SSascha Leib				}
1976*e56d7b71SSascha Leib			}
1977*e56d7b71SSascha Leib		},
1978*e56d7b71SSascha Leib
1979*e56d7b71SSascha Leib		status: {
1980*e56d7b71SSascha Leib			setText: function(txt) {
1981*e56d7b71SSascha Leib				const el = document.getElementById('botmon__today__status');
1982*e56d7b71SSascha Leib				if (el && BotMon.live.gui.status._errorCount <= 0) {
1983*e56d7b71SSascha Leib					el.innerText = txt;
1984*e56d7b71SSascha Leib				}
1985*e56d7b71SSascha Leib			},
1986*e56d7b71SSascha Leib
1987*e56d7b71SSascha Leib			setTitle: function(html) {
1988*e56d7b71SSascha Leib				const el = document.getElementById('botmon__today__title');
1989*e56d7b71SSascha Leib				if (el) {
1990*e56d7b71SSascha Leib					el.innerHTML = html;
1991*e56d7b71SSascha Leib				}
1992*e56d7b71SSascha Leib			},
1993*e56d7b71SSascha Leib
1994*e56d7b71SSascha Leib			setError: function(txt) {
1995*e56d7b71SSascha Leib				console.error(txt);
1996*e56d7b71SSascha Leib				BotMon.live.gui.status._errorCount += 1;
1997*e56d7b71SSascha Leib				const el = document.getElementById('botmon__today__status');
1998*e56d7b71SSascha Leib				if (el) {
1999*e56d7b71SSascha Leib					el.innerText = "Data may be incomplete.";
2000*e56d7b71SSascha Leib					el.classList.add('error');
2001*e56d7b71SSascha Leib				}
2002*e56d7b71SSascha Leib			},
2003*e56d7b71SSascha Leib			_errorCount: 0,
2004*e56d7b71SSascha Leib
2005*e56d7b71SSascha Leib			showBusy: function(txt = null) {
2006*e56d7b71SSascha Leib				BotMon.live.gui.status._busyCount += 1;
2007*e56d7b71SSascha Leib				const el = document.getElementById('botmon__today__busy');
2008*e56d7b71SSascha Leib				if (el) {
2009*e56d7b71SSascha Leib					el.style.display = 'inline-block';
2010*e56d7b71SSascha Leib				}
2011*e56d7b71SSascha Leib				if (txt) BotMon.live.gui.status.setText(txt);
2012*e56d7b71SSascha Leib			},
2013*e56d7b71SSascha Leib			_busyCount: 0,
2014*e56d7b71SSascha Leib
2015*e56d7b71SSascha Leib			hideBusy: function(txt = null) {
2016*e56d7b71SSascha Leib				const el = document.getElementById('botmon__today__busy');
2017*e56d7b71SSascha Leib				BotMon.live.gui.status._busyCount -= 1;
2018*e56d7b71SSascha Leib				if (BotMon.live.gui.status._busyCount <= 0) {
2019*e56d7b71SSascha Leib					if (el) el.style.display = 'none';
2020*e56d7b71SSascha Leib					if (txt) BotMon.live.gui.status.setText(txt);
2021*e56d7b71SSascha Leib				}
2022*e56d7b71SSascha Leib			}
2023*e56d7b71SSascha Leib		},
2024*e56d7b71SSascha Leib
2025*e56d7b71SSascha Leib		lists: {
2026*e56d7b71SSascha Leib			init: function() {
2027*e56d7b71SSascha Leib
2028*e56d7b71SSascha Leib				// function shortcut:
2029*e56d7b71SSascha Leib				const makeElement = BotMon.t._makeElement;
2030*e56d7b71SSascha Leib
2031*e56d7b71SSascha Leib				const parent = document.getElementById('botmon__today__visitorlists');
2032*e56d7b71SSascha Leib				if (parent) {
2033*e56d7b71SSascha Leib
2034*e56d7b71SSascha Leib					for (let i=0; i < 4; i++) {
2035*e56d7b71SSascha Leib
2036*e56d7b71SSascha Leib						// change the id and title by number:
2037*e56d7b71SSascha Leib						let listTitle = '';
2038*e56d7b71SSascha Leib						let listId = '';
2039*e56d7b71SSascha Leib						let infolink = null;
2040*e56d7b71SSascha Leib						switch (i) {
2041*e56d7b71SSascha Leib							case 0:
2042*e56d7b71SSascha Leib								listTitle = "Registered users";
2043*e56d7b71SSascha Leib								listId = 'users';
2044*e56d7b71SSascha Leib								break;
2045*e56d7b71SSascha Leib							case 1:
2046*e56d7b71SSascha Leib								listTitle = "Probably humans";
2047*e56d7b71SSascha Leib								listId = 'humans';
2048*e56d7b71SSascha Leib								break;
2049*e56d7b71SSascha Leib							case 2:
2050*e56d7b71SSascha Leib								listTitle = "Suspected bots";
2051*e56d7b71SSascha Leib								listId = 'suspectedBots';
2052*e56d7b71SSascha Leib								infolink = 'https://leib.be/sascha/projects/dokuwiki/botmon/info/suspected_bots';
2053*e56d7b71SSascha Leib								break;
2054*e56d7b71SSascha Leib							case 3:
2055*e56d7b71SSascha Leib								listTitle = "Known bots";
2056*e56d7b71SSascha Leib								listId = 'knownBots';
2057*e56d7b71SSascha Leib								infolink = 'https://leib.be/sascha/projects/dokuwiki/botmon/info/known_bots';
2058*e56d7b71SSascha Leib								break;
2059*e56d7b71SSascha Leib							default:
2060*e56d7b71SSascha Leib								console.warn('Unknown list number.');
2061*e56d7b71SSascha Leib						}
2062*e56d7b71SSascha Leib
2063*e56d7b71SSascha Leib						const details = makeElement('details', {
2064*e56d7b71SSascha Leib							'data-group': listId,
2065*e56d7b71SSascha Leib							'data-loaded': false
2066*e56d7b71SSascha Leib						});
2067*e56d7b71SSascha Leib						const title = details.appendChild(makeElement('summary'));
2068*e56d7b71SSascha Leib						title.appendChild(makeElement('span', {'class': 'title'}, listTitle));
2069*e56d7b71SSascha Leib						if (infolink) {
2070*e56d7b71SSascha Leib							title.appendChild(makeElement('a', {
2071*e56d7b71SSascha Leib								'class': 'ext_info',
2072*e56d7b71SSascha Leib								'target': '_blank',
2073*e56d7b71SSascha Leib								'href': infolink,
2074*e56d7b71SSascha Leib								'title': "More information"
2075*e56d7b71SSascha Leib							}, "Info"));
2076*e56d7b71SSascha Leib						}
2077*e56d7b71SSascha Leib						details.addEventListener("toggle", this._onDetailsToggle);
2078*e56d7b71SSascha Leib
2079*e56d7b71SSascha Leib						parent.appendChild(details);
2080*e56d7b71SSascha Leib
2081*e56d7b71SSascha Leib					}
2082*e56d7b71SSascha Leib				}
2083*e56d7b71SSascha Leib			},
2084*e56d7b71SSascha Leib
2085*e56d7b71SSascha Leib			_onDetailsToggle: function(e) {
2086*e56d7b71SSascha Leib				//console.info('BotMon.live.gui.lists._onDetailsToggle()');
2087*e56d7b71SSascha Leib
2088*e56d7b71SSascha Leib				const target = e.target;
2089*e56d7b71SSascha Leib
2090*e56d7b71SSascha Leib				if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet
2091*e56d7b71SSascha Leib					target.setAttribute('data-loaded', 'loading');
2092*e56d7b71SSascha Leib
2093*e56d7b71SSascha Leib					const fillType = target.getAttribute('data-group');
2094*e56d7b71SSascha Leib					const fillList = BotMon.live.data.analytics.groups[fillType];
2095*e56d7b71SSascha Leib					if (fillList && fillList.length > 0) {
2096*e56d7b71SSascha Leib
2097*e56d7b71SSascha Leib						const ul = BotMon.t._makeElement('ul');
2098*e56d7b71SSascha Leib
2099*e56d7b71SSascha Leib						fillList.forEach( (it) => {
2100*e56d7b71SSascha Leib							ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType));
2101*e56d7b71SSascha Leib						});
2102*e56d7b71SSascha Leib
2103*e56d7b71SSascha Leib						target.appendChild(ul);
2104*e56d7b71SSascha Leib						target.setAttribute('data-loaded', 'true');
2105*e56d7b71SSascha Leib					} else {
2106*e56d7b71SSascha Leib						target.setAttribute('data-loaded', 'false');
2107*e56d7b71SSascha Leib					}
2108*e56d7b71SSascha Leib
2109*e56d7b71SSascha Leib				}
2110*e56d7b71SSascha Leib			},
2111*e56d7b71SSascha Leib
2112*e56d7b71SSascha Leib			_makeVisitorItem: function(data, type) {
2113*e56d7b71SSascha Leib
2114*e56d7b71SSascha Leib				// shortcut for neater code:
2115*e56d7b71SSascha Leib				const make = BotMon.t._makeElement;
2116*e56d7b71SSascha Leib
2117*e56d7b71SSascha Leib				let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
2118*e56d7b71SSascha Leib				if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
2119*e56d7b71SSascha Leib
2120*e56d7b71SSascha Leib				const platformName = (data._platform ? data._platform.n : 'Unknown');
2121*e56d7b71SSascha Leib				const clientName = (data._client ? data._client.n: 'Unknown');
2122*e56d7b71SSascha Leib
2123*e56d7b71SSascha Leib				const sumClass = ( !data._seenBy || data._seenBy.indexOf(BM_LOGTYPE.SERVER) < 0 ? 'noServer' : 'hasServer');
2124*e56d7b71SSascha Leib
2125*e56d7b71SSascha Leib				const li = make('li'); // root list item
2126*e56d7b71SSascha Leib				const details = make('details');
2127*e56d7b71SSascha Leib				const summary = make('summary', {
2128*e56d7b71SSascha Leib					'class': sumClass
2129*e56d7b71SSascha Leib				});
2130*e56d7b71SSascha Leib				details.appendChild(summary);
2131*e56d7b71SSascha Leib
2132*e56d7b71SSascha Leib				const span1 = make('span'); /* left-hand group */
2133*e56d7b71SSascha Leib
2134*e56d7b71SSascha Leib				if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* No platform/client for bots */
2135*e56d7b71SSascha Leib					span1.appendChild(make('span', { /* Platform */
2136*e56d7b71SSascha Leib						'class': 'icon_only platform pf_' + (data._platform ? data._platform.id : 'unknown'),
2137*e56d7b71SSascha Leib						'title': "Platform: " + platformName
2138*e56d7b71SSascha Leib					}, platformName));
2139*e56d7b71SSascha Leib
2140*e56d7b71SSascha Leib					span1.appendChild(make('span', { /* Client */
2141*e56d7b71SSascha Leib						'class': 'icon_only client client cl_' + (data._client ? data._client.id : 'unknown'),
2142*e56d7b71SSascha Leib						'title': "Client: " + clientName
2143*e56d7b71SSascha Leib					}, clientName));
2144*e56d7b71SSascha Leib				}
2145*e56d7b71SSascha Leib
2146*e56d7b71SSascha Leib				// identifier:
2147*e56d7b71SSascha Leib				if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */
2148*e56d7b71SSascha Leib
2149*e56d7b71SSascha Leib					const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown");
2150*e56d7b71SSascha Leib					span1.appendChild(make('span', { /* Bot */
2151*e56d7b71SSascha Leib						'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown'),
2152*e56d7b71SSascha Leib						'title': "Bot: " + botName
2153*e56d7b71SSascha Leib					}, botName));
2154*e56d7b71SSascha Leib
2155*e56d7b71SSascha Leib				} else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */
2156*e56d7b71SSascha Leib
2157*e56d7b71SSascha Leib					span1.appendChild(make('span', { /* User */
2158*e56d7b71SSascha Leib						'class': 'has_icon user_known',
2159*e56d7b71SSascha Leib						'title': "User: " + data.usr
2160*e56d7b71SSascha Leib					}, data.usr));
2161*e56d7b71SSascha Leib
2162*e56d7b71SSascha Leib				} else { /* others */
2163*e56d7b71SSascha Leib
2164*e56d7b71SSascha Leib
2165*e56d7b71SSascha Leib					/*span1.appendChild(make('span', { // IP-Address
2166*e56d7b71SSascha Leib						'class': 'has_icon ipaddr ip' + ipType,
2167*e56d7b71SSascha Leib						'title': "IP-Address: " + data.ip
2168*e56d7b71SSascha Leib					}, data.ip));*/
2169*e56d7b71SSascha Leib
2170*e56d7b71SSascha Leib					span1.appendChild(make('span', { /* Internal ID */
2171*e56d7b71SSascha Leib						'class': 'has_icon session typ_' + data.typ,
2172*e56d7b71SSascha Leib						'title': "ID: " + data.id
2173*e56d7b71SSascha Leib					}, data.id));
2174*e56d7b71SSascha Leib				}
2175*e56d7b71SSascha Leib
2176*e56d7b71SSascha Leib				// country flag:
2177*e56d7b71SSascha Leib				if (data.geo && data.geo !== 'ZZ') {
2178*e56d7b71SSascha Leib					span1.appendChild(make('span', {
2179*e56d7b71SSascha Leib						'class': 'icon_only country ctry_' + data.geo.toLowerCase(),
2180*e56d7b71SSascha Leib						'data-ctry': data.geo,
2181*e56d7b71SSascha Leib						'title': "Country: " + ( data._country || "Unknown")
2182*e56d7b71SSascha Leib					}, ( data._country || "Unknown") ));
2183*e56d7b71SSascha Leib				}
2184*e56d7b71SSascha Leib
2185*e56d7b71SSascha Leib				// referer icons:
2186*e56d7b71SSascha Leib				if ((data._type == BM_USERTYPE.PROBABLY_HUMAN || data._type == BM_USERTYPE.LIKELY_BOT) && data.ref) {
2187*e56d7b71SSascha Leib					const refInfo = BotMon.live.data.analytics.getRefererInfo(data.ref);
2188*e56d7b71SSascha Leib					span1.appendChild(make('span', {
2189*e56d7b71SSascha Leib						'class': 'icon_only referer ref_' + refInfo.id,
2190*e56d7b71SSascha Leib						'title': "Referer: " + data.ref
2191*e56d7b71SSascha Leib					}, refInfo.n));
2192*e56d7b71SSascha Leib				}
2193*e56d7b71SSascha Leib
2194*e56d7b71SSascha Leib				summary.appendChild(span1);
2195*e56d7b71SSascha Leib				const span2 = make('span'); /* right-hand group */
2196*e56d7b71SSascha Leib
2197*e56d7b71SSascha Leib					span2.appendChild(make('span', { /* first-seen */
2198*e56d7b71SSascha Leib						'class': 'has_iconfirst-seen',
2199*e56d7b71SSascha Leib						'title': "First seen: " + data._firstSeen.toLocaleString() + " UTC"
2200*e56d7b71SSascha Leib					}, BotMon.t._formatTime(data._firstSeen)));
2201*e56d7b71SSascha Leib
2202*e56d7b71SSascha Leib					span2.appendChild(make('span', { /* page views */
2203*e56d7b71SSascha Leib						'class': 'has_icon pageviews',
2204*e56d7b71SSascha Leib						'title': data._pageViews.length + " page view(s)"
2205*e56d7b71SSascha Leib					}, data._pageViews.length));
2206*e56d7b71SSascha Leib
2207*e56d7b71SSascha Leib				summary.appendChild(span2);
2208*e56d7b71SSascha Leib
2209*e56d7b71SSascha Leib				// add details expandable section:
2210*e56d7b71SSascha Leib				details.appendChild(BotMon.live.gui.lists._makeVisitorDetails(data, type));
2211*e56d7b71SSascha Leib
2212*e56d7b71SSascha Leib				li.appendChild(details);
2213*e56d7b71SSascha Leib				return li;
2214*e56d7b71SSascha Leib			},
2215*e56d7b71SSascha Leib
2216*e56d7b71SSascha Leib			_makeVisitorDetails: function(data, type) {
2217*e56d7b71SSascha Leib
2218*e56d7b71SSascha Leib				// shortcut for neater code:
2219*e56d7b71SSascha Leib				const make = BotMon.t._makeElement;
2220*e56d7b71SSascha Leib
2221*e56d7b71SSascha Leib				let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
2222*e56d7b71SSascha Leib				if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
2223*e56d7b71SSascha Leib				const platformName = (data._platform ? data._platform.n : 'Unknown');
2224*e56d7b71SSascha Leib				const clientName = (data._client ? data._client.n: 'Unknown');
2225*e56d7b71SSascha Leib
2226*e56d7b71SSascha Leib				const dl = make('dl', {'class': 'visitor_details'});
2227*e56d7b71SSascha Leib
2228*e56d7b71SSascha Leib				if (data._type == BM_USERTYPE.KNOWN_BOT) {
2229*e56d7b71SSascha Leib
2230*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */
2231*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'icon_only bot bot_' + (data._bot ? data._bot.id : 'unknown')},
2232*e56d7b71SSascha Leib						(data._bot ? data._bot.n : 'Unknown')));
2233*e56d7b71SSascha Leib
2234*e56d7b71SSascha Leib					if (data._bot && data._bot.url) {
2235*e56d7b71SSascha Leib						dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */
2236*e56d7b71SSascha Leib						const botInfoDd = dl.appendChild(make('dd'));
2237*e56d7b71SSascha Leib						botInfoDd.appendChild(make('a', {
2238*e56d7b71SSascha Leib							'href': data._bot.url,
2239*e56d7b71SSascha Leib							'target': '_blank'
2240*e56d7b71SSascha Leib						}, data._bot.url)); /* bot info link*/
2241*e56d7b71SSascha Leib
2242*e56d7b71SSascha Leib					}
2243*e56d7b71SSascha Leib
2244*e56d7b71SSascha Leib				} else { /* not for bots */
2245*e56d7b71SSascha Leib
2246*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "Client:")); /* client */
2247*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon client cl_' + (data._client ? data._client.id : 'unknown')},
2248*e56d7b71SSascha Leib						clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) ));
2249*e56d7b71SSascha Leib
2250*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "Platform:")); /* platform */
2251*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon platform pf_' + (data._platform ? data._platform.id : 'unknown')},
2252*e56d7b71SSascha Leib						platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) ));
2253*e56d7b71SSascha Leib
2254*e56d7b71SSascha Leib					/*dl.appendChild(make('dt', {}, "ID:"));
2255*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/
2256*e56d7b71SSascha Leib				}
2257*e56d7b71SSascha Leib
2258*e56d7b71SSascha Leib				dl.appendChild(make('dt', {}, "IP-Address:"));
2259*e56d7b71SSascha Leib				const ipItem = make('dd', {'class': 'has_icon ipaddr ip' + ipType});
2260*e56d7b71SSascha Leib					ipItem.appendChild(make('span', {'class': 'address'} , data.ip));
2261*e56d7b71SSascha Leib					ipItem.appendChild(make('a', {
2262*e56d7b71SSascha Leib						'class': 'icon_only extlink ipinfo',
2263*e56d7b71SSascha Leib						'href': `https://ipinfo.io/${encodeURIComponent(data.ip)}`,
2264*e56d7b71SSascha Leib						'target': 'ipinfo',
2265*e56d7b71SSascha Leib						'title': "View this address on IPInfo.io"
2266*e56d7b71SSascha Leib					} , "DNS Info"));
2267*e56d7b71SSascha Leib					ipItem.appendChild(make('a', {
2268*e56d7b71SSascha Leib						'class': 'icon_only extlink abuseipdb',
2269*e56d7b71SSascha Leib						'href': `https://www.abuseipdb.com/check/${encodeURIComponent(data.ip)}`,
2270*e56d7b71SSascha Leib						'target': 'abuseipdb',
2271*e56d7b71SSascha Leib						'title': "Check this address on AbuseIPDB.com"
2272*e56d7b71SSascha Leib					} , "Check on AbuseIPDB"));
2273*e56d7b71SSascha Leib				dl.appendChild(ipItem);
2274*e56d7b71SSascha Leib
2275*e56d7b71SSascha Leib				if (Math.abs(data._lastSeen - data._firstSeen) < 100) {
2276*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "Seen:"));
2277*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString()));
2278*e56d7b71SSascha Leib				} else {
2279*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "First seen:"));
2280*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString()));
2281*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "Last seen:"));
2282*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString()));
2283*e56d7b71SSascha Leib				}
2284*e56d7b71SSascha Leib
2285*e56d7b71SSascha Leib				dl.appendChild(make('dt', {}, "User-Agent:"));
2286*e56d7b71SSascha Leib				dl.appendChild(make('dd', {'class': 'agent'}, data.agent));
2287*e56d7b71SSascha Leib
2288*e56d7b71SSascha Leib				dl.appendChild(make('dt', {}, "Languages:"));
2289*e56d7b71SSascha Leib				dl.appendChild(make('dd', {'class': 'langs'}, ` [${data.accept}]`));
2290*e56d7b71SSascha Leib
2291*e56d7b71SSascha Leib				if (data.geo && data.geo !=='') {
2292*e56d7b71SSascha Leib					dl.appendChild(make('dt', {}, "Location:"));
2293*e56d7b71SSascha Leib					dl.appendChild(make('dd', {
2294*e56d7b71SSascha Leib						'class': 'has_icon country ctry_' + data.geo.toLowerCase(),
2295*e56d7b71SSascha Leib						'data-ctry': data.geo,
2296*e56d7b71SSascha Leib						'title': "Country: " + data._country
2297*e56d7b71SSascha Leib					}, data._country + ' (' + data.geo + ')'));
2298*e56d7b71SSascha Leib				}
2299*e56d7b71SSascha Leib
2300*e56d7b71SSascha Leib				dl.appendChild(make('dt', {}, "Session ID:"));
2301*e56d7b71SSascha Leib				dl.appendChild(make('dd', {'class': 'has_icon session typ_' + data.typ}, data.id));
2302*e56d7b71SSascha Leib
2303*e56d7b71SSascha Leib				dl.appendChild(make('dt', {}, "Seen by:"));
2304*e56d7b71SSascha Leib				dl.appendChild(make('dd', undefined, data._seenBy.join(', ') ));
2305*e56d7b71SSascha Leib
2306*e56d7b71SSascha Leib				dl.appendChild(make('dt', {}, "Visited pages:"));
2307*e56d7b71SSascha Leib				const pagesDd = make('dd', {'class': 'pages'});
2308*e56d7b71SSascha Leib				const pageList = make('ul');
2309*e56d7b71SSascha Leib
2310*e56d7b71SSascha Leib				/* list all page views */
2311*e56d7b71SSascha Leib				data._pageViews.sort( (a, b) => a._firstSeen - b._firstSeen );
2312*e56d7b71SSascha Leib				data._pageViews.forEach( (page) => {
2313*e56d7b71SSascha Leib					pageList.appendChild(BotMon.live.gui.lists._makePageViewItem(page));
2314*e56d7b71SSascha Leib				});
2315*e56d7b71SSascha Leib				pagesDd.appendChild(pageList);
2316*e56d7b71SSascha Leib				dl.appendChild(pagesDd);
2317*e56d7b71SSascha Leib
2318*e56d7b71SSascha Leib				/* bot evaluation rating */
2319*e56d7b71SSascha Leib				if (data._type !== BM_USERTYPE.KNOWN_BOT && data._type !== BM_USERTYPE.KNOWN_USER) {
2320*e56d7b71SSascha Leib					dl.appendChild(make('dt', undefined, "Bot rating:"));
2321*e56d7b71SSascha Leib					dl.appendChild(make('dd', {'class': 'bot-rating'}, ( data._botVal ? data._botVal : '–' ) + ' (of ' + BotMon.live.data.rules._threshold + ')'));
2322*e56d7b71SSascha Leib
2323*e56d7b71SSascha Leib					/* add bot evaluation details: */
2324*e56d7b71SSascha Leib					if (data._eval) {
2325*e56d7b71SSascha Leib						dl.appendChild(make('dt', {}, "Bot evaluation:"));
2326*e56d7b71SSascha Leib						const evalDd = make('dd', {'class': 'eval'});
2327*e56d7b71SSascha Leib						const testList = make('ul');
2328*e56d7b71SSascha Leib						data._eval.forEach( test => {
2329*e56d7b71SSascha Leib
2330*e56d7b71SSascha Leib							const tObj = BotMon.live.data.rules.getRuleInfo(test);
2331*e56d7b71SSascha Leib							let tDesc = tObj ? tObj.desc : test;
2332*e56d7b71SSascha Leib
2333*e56d7b71SSascha Leib							// special case for Bot IP range test:
2334*e56d7b71SSascha Leib							if (tObj.func == 'fromKnownBotIP') {
2335*e56d7b71SSascha Leib								const rangeInfo = BotMon.live.data.ipRanges.match(data.ip);
2336*e56d7b71SSascha Leib								if (rangeInfo) {
2337*e56d7b71SSascha Leib									const owner = BotMon.live.data.ipRanges.getOwner(rangeInfo.g);
2338*e56d7b71SSascha Leib									tDesc += ' (range: “' + rangeInfo.cidr + '”, ' + owner + ')';
2339*e56d7b71SSascha Leib								}
2340*e56d7b71SSascha Leib							}
2341*e56d7b71SSascha Leib
2342*e56d7b71SSascha Leib							// create the entry field
2343*e56d7b71SSascha Leib							const tstLi = make('li');
2344*e56d7b71SSascha Leib							tstLi.appendChild(make('span', {
2345*e56d7b71SSascha Leib								'data-testid': test
2346*e56d7b71SSascha Leib							}, tDesc));
2347*e56d7b71SSascha Leib							tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') ));
2348*e56d7b71SSascha Leib							testList.appendChild(tstLi);
2349*e56d7b71SSascha Leib						});
2350*e56d7b71SSascha Leib
2351*e56d7b71SSascha Leib						// add total row
2352*e56d7b71SSascha Leib						const tst2Li = make('li', {
2353*e56d7b71SSascha Leib							'class': 'total'
2354*e56d7b71SSascha Leib						});
2355*e56d7b71SSascha Leib						/*tst2Li.appendChild(make('span', {}, "Total:"));
2356*e56d7b71SSascha Leib						tst2Li.appendChild(make('span', {}, data._botVal));
2357*e56d7b71SSascha Leib						testList.appendChild(tst2Li);*/
2358*e56d7b71SSascha Leib
2359*e56d7b71SSascha Leib						evalDd.appendChild(testList);
2360*e56d7b71SSascha Leib						dl.appendChild(evalDd);
2361*e56d7b71SSascha Leib					}
2362*e56d7b71SSascha Leib				}
2363*e56d7b71SSascha Leib				// return the element to add to the UI:
2364*e56d7b71SSascha Leib				return dl;
2365*e56d7b71SSascha Leib			},
2366*e56d7b71SSascha Leib
2367*e56d7b71SSascha Leib			// make a page view item:
2368*e56d7b71SSascha Leib			_makePageViewItem: function(page) {
2369*e56d7b71SSascha Leib				//console.log("makePageViewItem:",page);
2370*e56d7b71SSascha Leib
2371*e56d7b71SSascha Leib				// shortcut for neater code:
2372*e56d7b71SSascha Leib				const make = BotMon.t._makeElement;
2373*e56d7b71SSascha Leib
2374*e56d7b71SSascha Leib				// the actual list item:
2375*e56d7b71SSascha Leib				const pgLi = make('li');
2376*e56d7b71SSascha Leib
2377*e56d7b71SSascha Leib				const row1 = make('div', {'class': 'row'});
2378*e56d7b71SSascha Leib
2379*e56d7b71SSascha Leib					row1.appendChild(make('a', { // page id is the left group
2380*e56d7b71SSascha Leib						'href': DOKU_BASE + 'doku.php?id=' + encodeURIComponent(page.pg),
2381*e56d7b71SSascha Leib						'target': 'preview',
2382*e56d7b71SSascha Leib						'hreflang': page.lang,
2383*e56d7b71SSascha Leib						'title': "PageID: " + page.pg
2384*e56d7b71SSascha Leib					}, page.pg)); /* DW Page ID */
2385*e56d7b71SSascha Leib
2386*e56d7b71SSascha Leib					// get the time difference:
2387*e56d7b71SSascha Leib					row1.appendChild(make('span', {
2388*e56d7b71SSascha Leib						'class': 'first-seen',
2389*e56d7b71SSascha Leib						'title': "First visited: " + page._firstSeen.toLocaleString() + " UTC"
2390*e56d7b71SSascha Leib					}, BotMon.t._formatTime(page._firstSeen)));
2391*e56d7b71SSascha Leib
2392*e56d7b71SSascha Leib				pgLi.appendChild(row1);
2393*e56d7b71SSascha Leib
2394*e56d7b71SSascha Leib				/* LINE 2 */
2395*e56d7b71SSascha Leib
2396*e56d7b71SSascha Leib				const row2 = make('div', {'class': 'row'});
2397*e56d7b71SSascha Leib
2398*e56d7b71SSascha Leib					// page referrer:
2399*e56d7b71SSascha Leib					if (page._ref) {
2400*e56d7b71SSascha Leib						row2.appendChild(make('span', {
2401*e56d7b71SSascha Leib							'class': 'referer',
2402*e56d7b71SSascha Leib							'title': "Referrer: " + page._ref.href
2403*e56d7b71SSascha Leib						}, page._ref.hostname));
2404*e56d7b71SSascha Leib					} else {
2405*e56d7b71SSascha Leib						row2.appendChild(make('span', {
2406*e56d7b71SSascha Leib							'class': 'referer'
2407*e56d7b71SSascha Leib						}, "No referer"));
2408*e56d7b71SSascha Leib					}
2409*e56d7b71SSascha Leib
2410*e56d7b71SSascha Leib					// visit duration:
2411*e56d7b71SSascha Leib					let visitTimeStr = "Bounce";
2412*e56d7b71SSascha Leib					const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime();
2413*e56d7b71SSascha Leib					if (visitDuration > 0) {
2414*e56d7b71SSascha Leib						visitTimeStr = Math.floor(visitDuration / 1000) + "s";
2415*e56d7b71SSascha Leib					}
2416*e56d7b71SSascha Leib					const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen);
2417*e56d7b71SSascha Leib					if (tDiff) {
2418*e56d7b71SSascha Leib						row2.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff));
2419*e56d7b71SSascha Leib					} else {
2420*e56d7b71SSascha Leib						row2.appendChild(make('span', {
2421*e56d7b71SSascha Leib							'class': 'bounce',
2422*e56d7b71SSascha Leib							'title': "Visitor bounced"}, "Bounce"));
2423*e56d7b71SSascha Leib					}
2424*e56d7b71SSascha Leib
2425*e56d7b71SSascha Leib				pgLi.appendChild(row2);
2426*e56d7b71SSascha Leib
2427*e56d7b71SSascha Leib				return pgLi;
2428*e56d7b71SSascha Leib			}
2429*e56d7b71SSascha Leib		}
2430*e56d7b71SSascha Leib	}
2431*e56d7b71SSascha Leib};
2432*e56d7b71SSascha Leib
2433*e56d7b71SSascha Leib/* launch only if the BotMon admin panel is open: */
2434*e56d7b71SSascha Leibif (document.getElementById('botmon__admin')) {
2435*e56d7b71SSascha Leib	BotMon.init();
2436*e56d7b71SSascha Leib}