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