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