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