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