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