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}