1/* DokuWiki BotMon Plugin Script file */ 2/* 03.09.2025 - 0.1.7 - pre-release */ 3/* Authors: Sascha Leib <ad@hominem.info> */ 4 5const BotMon = { 6 7 init: function() { 8 //console.info('BotMon.init()'); 9 10 // find the plugin basedir: 11 this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) 12 + '/plugins/botmon/'; 13 14 // read the page language from the DOM: 15 this._lang = document.getRootNode().documentElement.lang || this._lang; 16 17 // get the time offset: 18 this._timeDiff = BotMon.t._getTimeOffset(); 19 20 // init the sub-objects: 21 BotMon.t._callInit(this); 22 }, 23 24 _baseDir: null, 25 _lang: 'en', 26 _today: (new Date()).toISOString().slice(0, 10), 27 _timeDiff: '', 28 29 /* internal tools */ 30 t: { 31 32 /* helper function to call inits of sub-objects */ 33 _callInit: function(obj) { 34 //console.info('BotMon.t._callInit(obj=',obj,')'); 35 36 /* call init / _init on each sub-object: */ 37 Object.keys(obj).forEach( (key,i) => { 38 const sub = obj[key]; 39 let init = null; 40 if (typeof sub === 'object' && sub.init) { 41 init = sub.init; 42 } 43 44 // bind to object 45 if (typeof init == 'function') { 46 const init2 = init.bind(sub); 47 init2(obj); 48 } 49 }); 50 }, 51 52 /* helper function to calculate the time difference to UTC: */ 53 _getTimeOffset: function() { 54 const now = new Date(); 55 let offset = now.getTimezoneOffset(); // in minutes 56 const sign = Math.sign(offset); // +1 or -1 57 offset = Math.abs(offset); // always positive 58 59 let hours = 0; 60 while (offset >= 60) { 61 hours += 1; 62 offset -= 60; 63 } 64 return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); 65 }, 66 67 /* helper function to create a new element with all attributes and text content */ 68 _makeElement: function(name, atlist = undefined, text = undefined) { 69 var r = null; 70 try { 71 r = document.createElement(name); 72 if (atlist) { 73 for (let attr in atlist) { 74 r.setAttribute(attr, atlist[attr]); 75 } 76 } 77 if (text) { 78 r.textContent = text.toString(); 79 } 80 } catch(e) { 81 console.error(e); 82 } 83 return r; 84 } 85 } 86}; 87 88/* everything specific to the "Today" tab is self-contained in the "live" object: */ 89BotMon.live = { 90 init: function() { 91 //console.info('BotMon.live.init()'); 92 93 // set the title: 94 const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')'; 95 BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</time> ${tDiff}`); 96 97 // init sub-objects: 98 BotMon.t._callInit(this); 99 }, 100 101 data: { 102 init: function() { 103 //console.info('BotMon.live.data.init()'); 104 105 // call sub-inits: 106 BotMon.t._callInit(this); 107 }, 108 109 // this will be called when the known json files are done loading: 110 _dispatch: function(file) { 111 //console.info('BotMon.live.data._dispatch(,',file,')'); 112 113 // shortcut to make code more readable: 114 const data = BotMon.live.data; 115 116 // set the flags: 117 switch(file) { 118 case 'bots': 119 data._dispatchBotsLoaded = true; 120 break; 121 case 'clients': 122 data._dispatchClientsLoaded = true; 123 break; 124 case 'platforms': 125 data._dispatchPlatformsLoaded = true; 126 break; 127 default: 128 // ignore 129 } 130 131 // are all the flags set? 132 if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded) { 133 // chain the log files loading: 134 BotMon.live.data.loadLogFile('srv', BotMon.live.data._onServerLogLoaded); 135 } 136 }, 137 // flags to track which data files have been loaded: 138 _dispatchBotsLoaded: false, 139 _dispatchClientsLoaded: false, 140 _dispatchPlatformsLoaded: false, 141 142 // event callback, after the server log has been loaded: 143 _onServerLogLoaded: function() { 144 //console.info('BotMon.live.data._onServerLogLoaded()'); 145 146 // chain the client log file to load: 147 BotMon.live.data.loadLogFile('log', BotMon.live.data._onClientLogLoaded); 148 }, 149 150 // event callback, after the client log has been loaded: 151 _onClientLogLoaded: function() { 152 //console.info('BotMon.live.data._onClientLogLoaded()'); 153 154 // chain the ticks file to load: 155 BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded); 156 157 }, 158 159 // event callback, after the tiker log has been loaded: 160 _onTicksLogLoaded: function() { 161 //console.info('BotMon.live.data._onTicksLogLoaded()'); 162 163 // analyse the data: 164 BotMon.live.data.analytics.analyseAll(); 165 166 // sort the data: 167 // #TODO 168 169 // display the data: 170 BotMon.live.gui.overview.make(); 171 172 //console.log(BotMon.live.data.model._visitors); 173 174 }, 175 176 model: { 177 // visitors storage: 178 _visitors: [], 179 180 // find an already existing visitor record: 181 findVisitor: function(id) { 182 183 // shortcut to make code more readable: 184 const model = BotMon.live.data.model; 185 186 // loop over all visitors already registered: 187 for (let i=0; i<model._visitors.length; i++) { 188 const v = model._visitors[i]; 189 if (v && v.id == id) return v; 190 } 191 return null; // nothing found 192 }, 193 194 /* if there is already this visit registered, return it (used for updates) */ 195 _getVisit: function(visit, view) { 196 197 // shortcut to make code more readable: 198 const model = BotMon.live.data.model; 199 200 201 for (let i=0; i<visit._pageViews.length; i++) { 202 const pv = visit._pageViews[i]; 203 if (pv.pg == view.pg && // same page id, and 204 view.ts.getTime() - pv._firstSeen.getTime() < 1200000) { // seen less than 20 minutes ago 205 return pv; // it is the same visit. 206 } 207 } 208 return null; // not found 209 }, 210 211 // register a new visitor (or update if already exists) 212 registerVisit: function(dat) { 213 //console.info('registerVisit', dat); 214 215 // shortcut to make code more readable: 216 const model = BotMon.live.data.model; 217 218 // check if it already exists: 219 let visitor = model.findVisitor(dat.id); 220 if (!visitor) { 221 const bot = BotMon.live.data.bots.match(dat.agent); 222 223 model._visitors.push(dat); 224 visitor = dat; 225 visitor._firstSeen = dat.ts; 226 visitor._lastSeen = dat.ts; 227 visitor._isBot = ( bot ? 1.0 : 0.0 ); // likelihood of being a bot; primed to 0% or 100% in case of a known bot 228 visitor._pageViews = []; // array of page views 229 visitor._hasReferrer = false; // has at least one referrer 230 visitor._jsClient = false; // visitor has been seen logged by client js as well 231 visitor._client = bot ?? BotMon.live.data.clients.match(dat.agent) ?? null; // client info (browser, bot, etc.) 232 visitor._platform = BotMon.live.data.platforms.match(dat.agent); // platform info 233 234 // known bots get the bot ID as identifier: 235 if (bot) visitor.id = bot.id; 236 } 237 238 // find browser 239 240 // is this visit already registered? 241 let prereg = model._getVisit(visitor, dat); 242 if (!prereg) { 243 // add the page view to the visitor: 244 prereg = { 245 _by: 'srv', 246 ip: dat.ip, 247 pg: dat.pg, 248 ref: dat.ref || '', 249 _firstSeen: dat.ts, 250 _lastSeen: dat.ts, 251 _jsClient: false 252 }; 253 visitor._pageViews.push(prereg); 254 } 255 256 // update referrer state: 257 visitor._hasReferrer = visitor._hasReferrer || 258 (prereg.ref !== undefined && prereg.ref !== ''); 259 260 // update time stamp for last-seen: 261 visitor._lastSeen = dat.ts; 262 263 // if needed: 264 return visitor; 265 }, 266 267 // updating visit data from the client-side log: 268 updateVisit: function(dat) { 269 //console.info('updateVisit', dat); 270 271 // shortcut to make code more readable: 272 const model = BotMon.live.data.model; 273 274 let visitor = model.findVisitor(dat.id); 275 if (!visitor) { 276 visitor = model.registerVisit(dat); 277 } 278 if (visitor) { 279 visitor._lastSeen = dat.ts; 280 visitor._jsClient = true; // seen by client js 281 } else { 282 console.warn(`No visit with ID ${dat.id}.`); 283 return; 284 } 285 286 // find the page view: 287 let prereg = model._getVisit(visitor, dat); 288 if (prereg) { 289 // update the page view: 290 prereg._lastSeen = dat.ts; 291 prereg._jsClient = true; // seen by client js 292 } else { 293 // add the page view to the visitor: 294 prereg = { 295 _by: 'log', 296 ip: dat.ip, 297 pg: dat.pg, 298 ref: dat.ref || '', 299 _firstSeen: dat.ts, 300 _lastSeen: dat.ts, 301 _jsClient: true 302 }; 303 visitor._pageViews.push(prereg); 304 } 305 }, 306 307 // updating visit data from the ticker log: 308 updateTicks: function(dat) { 309 //console.info('updateTicks', dat); 310 311 // shortcut to make code more readable: 312 const model = BotMon.live.data.model; 313 314 // find the visit info: 315 let visitor = model.findVisitor(dat.id); 316 if (!visitor) { 317 console.warn(`No visitor with ID ${dat.id}, registering a new one.`); 318 visitor = model.registerVisit(dat); 319 } 320 if (visitor) { 321 // update "last seen": 322 if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts; 323 324 // get the page view info: 325 const pv = model._getVisit(visitor, dat); 326 if (pv) { 327 // update the page view info: 328 if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts; 329 } else { 330 console.warn(`No page view for visit ID ${dat.id}, page ${dat.pg}, registering a new one.`); 331 332 // add a new page view to the visitor: 333 const newPv = { 334 _by: 'tck', 335 ip: dat.ip, 336 pg: dat.pg, 337 ref: '', 338 _firstSeen: dat.ts, 339 _lastSeen: dat.ts, 340 _jsClient: false 341 }; 342 visitor._pageViews.push(newPv); 343 } 344 345 } else { 346 console.warn(`No visit with ID ${dat.id}.`); 347 return; 348 } 349 350 } 351 }, 352 353 analytics: { 354 355 init: function() { 356 console.info('BotMon.live.data.analytics.init()'); 357 }, 358 359 // data storage: 360 data: { 361 totalVisits: 0, 362 totalPageViews: 0, 363 bots: { 364 known: 0, 365 suspected: 0, 366 human: 0, 367 users: 0 368 } 369 }, 370 371 // sort the visits by type: 372 groups: { 373 knownBots: [], 374 suspectedBots: [], 375 humans: [], 376 users: [] 377 }, 378 379 // all analytics 380 analyseAll: function() { 381 //console.info('BotMon.live.data.analytics.analyseAll()'); 382 383 // shortcut to make code more readable: 384 const model = BotMon.live.data.model; 385 386 // loop over all visitors: 387 model._visitors.forEach( (v) => { 388 389 // count visits and page views: 390 this.data.totalVisits += 1; 391 this.data.totalPageViews += v._pageViews.length; 392 393 // check for typical bot aspects: 394 let botScore = v._isBot; // start with the known bot score 395 396 if (v._isBot >= 1.0) { // known bots 397 398 this.data.bots.known += 1; 399 this.groups.knownBots.push(v); 400 401 } if (v.usr && v.usr != '') { // known users 402 this.groups.users.push(v); 403 this.data.bots.users += 1; 404 } else { 405 // not a known bot, nor a known user; check other aspects: 406 407 // no referrer at all: 408 if (!v._hasReferrer) botScore += 0.2; 409 410 // no js client logging: 411 if (!v._jsClient) botScore += 0.2; 412 413 // average time between page views less than 30s: 414 if (v._pageViews.length > 1) { 415 botScore -= 0.2; // more than one view: good! 416 let totalDiff = 0; 417 for (let i=1; i<v._pageViews.length; i++) { 418 const diff = v._pageViews[i]._firstSeen.getTime() - v._pageViews[i-1]._lastSeen.getTime(); 419 totalDiff += diff; 420 } 421 const avgDiff = totalDiff / (v._pageViews.length - 1); 422 if (avgDiff < 30000) botScore += 0.2; 423 else if (avgDiff < 60000) botScore += 0.1; 424 } 425 426 // decide based on the score: 427 if (botScore >= 0.5) { 428 this.data.bots.suspected += 1; 429 this.groups.suspectedBots.push(v); 430 } else { 431 this.data.bots.human += 1; 432 this.groups.humans.push(v); 433 } 434 } 435 }); 436 437 console.log(this.data); 438 console.log(this.groups); 439 } 440 441 }, 442 443 bots: { 444 // loads the list of known bots from a JSON file: 445 init: async function() { 446 //console.info('BotMon.live.data.bots.init()'); 447 448 // Load the list of known bots: 449 BotMon.live.gui.status.showBusy("Loading known bots …"); 450 const url = BotMon._baseDir + 'data/known-bots.json'; 451 try { 452 const response = await fetch(url); 453 if (!response.ok) { 454 throw new Error(`${response.status} ${response.statusText}`); 455 } 456 457 BotMon.live.data.bots._list = await response.json(); 458 BotMon.live.data.bots._ready = true; 459 460 // TODO: allow using the bots list... 461 } catch (error) { 462 BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message); 463 } finally { 464 BotMon.live.gui.status.hideBusy("Status: Done."); 465 BotMon.live.data._dispatch('bots') 466 } 467 }, 468 469 // returns bot info if the clientId matches a known bot, null otherwise: 470 match: function(client) { 471 //console.info('BotMon.live.data.bots.match(',client,')'); 472 473 if (client) { 474 for (let i=0; i<BotMon.live.data.bots._list.length; i++) { 475 const bot = BotMon.live.data.bots._list[i]; 476 for (let j=0; j<bot.rx.length; j++) { 477 if (client.match(new RegExp(bot.rx[j]))) { 478 return bot; // found a match 479 } 480 } 481 return null; // not found! 482 } 483 } 484 }, 485 486 // indicates if the list is loaded and ready to use: 487 _ready: false, 488 489 // the actual bot list is stored here: 490 _list: [] 491 }, 492 493 clients: { 494 // loads the list of known clients from a JSON file: 495 init: async function() { 496 //console.info('BotMon.live.data.clients.init()'); 497 498 // Load the list of known bots: 499 BotMon.live.gui.status.showBusy("Loading known clients"); 500 const url = BotMon._baseDir + 'data/known-clients.json'; 501 try { 502 const response = await fetch(url); 503 if (!response.ok) { 504 throw new Error(`${response.status} ${response.statusText}`); 505 } 506 507 BotMon.live.data.clients._list = await response.json(); 508 BotMon.live.data.clients._ready = true; 509 510 } catch (error) { 511 BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message); 512 } finally { 513 BotMon.live.gui.status.hideBusy("Status: Done."); 514 BotMon.live.data._dispatch('clients') 515 } 516 }, 517 518 // returns bot info if the user-agent matches a known bot, null otherwise: 519 match: function(cid) { 520 //console.info('BotMon.live.data.clients.match(',cid,')'); 521 522 let match = {"n": "Unknown", "v": -1, "id": null}; 523 524 if (cid) { 525 BotMon.live.data.clients._list.find(client => { 526 let r = false; 527 for (let j=0; j<client.rx.length; j++) { 528 const rxr = cid.match(new RegExp(client.rx[j])); 529 if (rxr) { 530 match.n = client.n; 531 match.v = (rxr.length > 1 ? rxr[1] : -1); 532 match.id = client.id || null; 533 r = true; 534 break; 535 } 536 } 537 return r; 538 }); 539 } 540 541 return match; 542 }, 543 544 // indicates if the list is loaded and ready to use: 545 _ready: false, 546 547 // the actual bot list is stored here: 548 _list: [] 549 550 }, 551 552 platforms: { 553 // loads the list of known platforms from a JSON file: 554 init: async function() { 555 //console.info('BotMon.live.data.platforms.init()'); 556 557 // Load the list of known bots: 558 BotMon.live.gui.status.showBusy("Loading known platforms"); 559 const url = BotMon._baseDir + 'data/known-platforms.json'; 560 try { 561 const response = await fetch(url); 562 if (!response.ok) { 563 throw new Error(`${response.status} ${response.statusText}`); 564 } 565 566 BotMon.live.data.platforms._list = await response.json(); 567 BotMon.live.data.platforms._ready = true; 568 569 } catch (error) { 570 BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message); 571 } finally { 572 BotMon.live.gui.status.hideBusy("Status: Done."); 573 BotMon.live.data._dispatch('platforms') 574 } 575 }, 576 577 // returns bot info if the browser id matches a known platform: 578 match: function(cid) { 579 //console.info('BotMon.live.data.platforms.match(',cid,')'); 580 581 let match = {"n": "Unknown", "id": null}; 582 583 if (cid) { 584 BotMon.live.data.platforms._list.find(platform => { 585 let r = false; 586 for (let j=0; j<platform.rx.length; j++) { 587 const rxr = cid.match(new RegExp(platform.rx[j])); 588 if (rxr) { 589 match.n = platform.n; 590 match.v = (rxr.length > 1 ? rxr[1] : -1); 591 match.id = platform.id || null; 592 r = true; 593 break; 594 } 595 } 596 return r; 597 }); 598 } 599 600 return match; 601 }, 602 603 // indicates if the list is loaded and ready to use: 604 _ready: false, 605 606 // the actual bot list is stored here: 607 _list: [] 608 609 }, 610 611 loadLogFile: async function(type, onLoaded = undefined) { 612 // console.info('BotMon.live.data.loadLogFile(',type,')'); 613 614 let typeName = ''; 615 let columns = []; 616 617 switch (type) { 618 case "srv": 619 typeName = "Server"; 620 columns = ['ts','ip','pg','id','typ','usr','agent','ref']; 621 break; 622 case "log": 623 typeName = "Page load"; 624 columns = ['ts','ip','pg','id','usr','lt','ref','agent']; 625 break; 626 case "tck": 627 typeName = "Ticker"; 628 columns = ['ts','ip','pg','id','agent']; 629 break; 630 default: 631 console.warn(`Unknown log type ${type}.`); 632 return; 633 } 634 635 // Show the busy indicator and set the visible status: 636 BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`); 637 638 // compose the URL from which to load: 639 const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`; 640 //console.log("Loading:",url); 641 642 // fetch the data: 643 try { 644 const response = await fetch(url); 645 if (!response.ok) { 646 throw new Error(`${response.status} ${response.statusText}`); 647 } 648 649 const logtxt = await response.text(); 650 651 logtxt.split('\n').forEach((line) => { 652 if (line.trim() === '') return; // skip empty lines 653 const cols = line.split('\t'); 654 655 // assign the columns to an object: 656 const data = {}; 657 cols.forEach( (colVal,i) => { 658 colName = columns[i] || `col${i}`; 659 const colValue = (colName == 'ts' ? new Date(colVal) : colVal); 660 data[colName] = colValue; 661 }); 662 663 // register the visit in the model: 664 switch(type) { 665 case 'srv': 666 BotMon.live.data.model.registerVisit(data); 667 break; 668 case 'log': 669 BotMon.live.data.model.updateVisit(data); 670 break; 671 case 'tck': 672 BotMon.live.data.model.updateTicks(data); 673 break; 674 default: 675 console.warn(`Unknown log type ${type}.`); 676 return; 677 } 678 }); 679 680 if (onLoaded) { 681 onLoaded(); // callback after loading is finished. 682 } 683 684 } catch (error) { 685 BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 686 } finally { 687 BotMon.live.gui.status.hideBusy("Status: Done."); 688 } 689 } 690 }, 691 692 gui: { 693 init: function() { 694 // init the lists view: 695 this.lists.init(); 696 }, 697 698 overview: { 699 make: function() { 700 const data = BotMon.live.data.analytics.data; 701 const parent = document.getElementById('botmon__today__content'); 702 if (parent) { 703 704 const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 1000) / 10; 705 706 jQuery(parent).prepend(jQuery(` 707 <details id="botmon__today__overview" open> 708 <summary>Overview</summary> 709 <div class="grid-3-columns"> 710 <dl> 711 <dt>Web metrics</dt> 712 <dd><span>Total visits:</span><span>${data.totalVisits}</span></dd> 713 <dd><span>Total page views:</span><span>${data.totalPageViews}</span></dd> 714 <dd><span>Bounce rate:</span><span>${bounceRate} %</span></dd> 715 <dd><span>∅ load time:</span><span>${data.avgLoadTime} ms</span></dd> 716 </dl> 717 <dl> 718 <dt>Bots vs. Humans</dt> 719 <dd><span>Known bots:</span><span>${data.bots.known}</span></dd> 720 <dd><span>Suspected bots:</span><span>${data.bots.suspected}</span></dd> 721 <dd><span>Probably humans:</span><span>${data.bots.human}</span></dd> 722 <dd><span>Registered users:</span><span>${data.bots.users}</span></dd> 723 </dl> 724 <dl id="botmon__botslist"> 725 <dt>Known bots</dt> 726 </dl> 727 </div> 728 </details> 729 `)); 730 } 731 } 732 }, 733 status: { 734 setText: function(txt) { 735 const el = document.getElementById('botmon__today__status'); 736 if (el && BotMon.live.gui.status._errorCount <= 0) { 737 el.innerText = txt; 738 } 739 }, 740 741 setTitle: function(html) { 742 const el = document.getElementById('botmon__today__title'); 743 if (el) { 744 el.innerHTML = html; 745 } 746 }, 747 748 setError: function(txt) { 749 console.error(txt); 750 BotMon.live.gui.status._errorCount += 1; 751 const el = document.getElementById('botmon__today__status'); 752 if (el) { 753 el.innerText = "An error occured. See the browser log for details!"; 754 el.classList.add('error'); 755 } 756 }, 757 _errorCount: 0, 758 759 showBusy: function(txt = null) { 760 BotMon.live.gui.status._busyCount += 1; 761 const el = document.getElementById('botmon__today__busy'); 762 if (el) { 763 el.style.display = 'inline-block'; 764 } 765 if (txt) BotMon.live.gui.status.setText(txt); 766 }, 767 _busyCount: 0, 768 769 hideBusy: function(txt = null) { 770 const el = document.getElementById('botmon__today__busy'); 771 BotMon.live.gui.status._busyCount -= 1; 772 if (BotMon.live.gui.status._busyCount <= 0) { 773 if (el) el.style.display = 'none'; 774 if (txt) BotMon.live.gui.status.setText(txt); 775 } 776 } 777 }, 778 779 lists: { 780 init: function() { 781 782 const parent = document.getElementById('botmon__today__visitorlists'); 783 if (parent) { 784 785 for (let i=0; i < 4; i++) { 786 787 // change the id and title by number: 788 let listTitle = ''; 789 let listId = ''; 790 switch (i) { 791 case 0: 792 listTitle = "Registered users"; 793 listId = 'users'; 794 break; 795 case 1: 796 listTitle = "Probably humans"; 797 listId = 'humans'; 798 break; 799 case 2: 800 listTitle = "Suspected bots"; 801 listId = 'suspectedBots'; 802 break; 803 case 3: 804 listTitle = "Known bots"; 805 listId = 'knownBots'; 806 break; 807 default: 808 console.warn('Unknwon list number.'); 809 } 810 811 const details = BotMon.t._makeElement('details', { 812 'data-group': listId, 813 'data-loaded': false 814 }); 815 details.appendChild(BotMon.t._makeElement('summary', 816 undefined, 817 listTitle 818 )); 819 details.addEventListener("toggle", this._onDetailsToggle); 820 821 parent.appendChild(details); 822 823 } 824 } 825 }, 826 827 _onDetailsToggle: function(e) { 828 console.info('BotMon.live.gui.lists._onDetailsToggle()'); 829 830 const target = e.target; 831 832 if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet 833 target.setAttribute('data-loaded', 'loading'); 834 835 const fillType = target.getAttribute('data-group'); 836 const fillList = BotMon.live.data.analytics.groups[fillType]; 837 if (fillList && fillList.length > 0) { 838 839 const ul = BotMon.t._makeElement('ul'); 840 841 fillList.forEach( (it) => { 842 ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); 843 }); 844 845 target.appendChild(ul); 846 target.setAttribute('data-loaded', 'true'); 847 } else { 848 target.setAttribute('data-loaded', 'false'); 849 } 850 851 } 852 }, 853 854 _makeVisitorItem: function(data, type) { 855 856 // shortcut for neater code: 857 const make = BotMon.t._makeElement; 858 859 const li = make('li'); // root list item 860 const details = make('details'); 861 const summary = make('summary'); 862 details.appendChild(summary); 863 864 const span1 = make('span'); /* left-hand group */ 865 866 let typeDescr = "Seen by: " + ( data.typ == 'php' ? "Server-only": "Server + Client"); 867 span1.appendChild(make('span', { /* Type */ 868 'class': 'icon type type_' + data.typ, 869 'title': typeDescr 870 }, data.typ)); 871 872 span1.appendChild(make('span', { /* ID */ 873 'class': 'id' 874 }, data.id)); 875 876 const platformName = (data._platform ? data._platform.n : 'Unknown'); 877 span1.appendChild(make('span', { /* Platform */ 878 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), 879 'title': "Platform: " + platformName 880 }, platformName)); 881 882 const clientName = (data._client ? data._client.n: 'Unknown') 883 span1.appendChild(make('span', { /* Client */ 884 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), 885 'title': "Client: " + clientName 886 }, clientName)); 887 888 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 889 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 890 span1.appendChild(make('span', { /* IP-Address */ 891 'class': 'icon ipaddr ip' + ipType, 892 'title': "IP-Address: " + data.ip 893 }, data.ip)); 894 895 summary.appendChild(span1); 896 const span2 = make('span'); /* right-hand group */ 897 898 span2.appendChild(make('time', { /* Last seen */ 899 'data-field': 'last-seen', 900 'datetime': (data._lastSeen ? data._lastSeen : 'unknown') 901 }, (data._lastSeen ? data._lastSeen.getHours() + ':' + data._lastSeen.getMinutes() + ':' + data._lastSeen.getSeconds() : 'Unknown'))); 902 903 summary.appendChild(span2); 904 905 // create expanable section: 906 907 const dl = make('dl', {'class': 'visitor_details'}); 908 909 dl.appendChild(make('dt', {}, "Client:")); /* client */ 910 dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, 911 clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); 912 913 dl.appendChild(make('dt', {}, "Platform:")); /* platform */ 914 dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, 915 platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); 916 917 dl.appendChild(make('dt', {}, "IP-Address:")); 918 dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); 919 920 if ((data._lastSeen - data._firstSeen) < 1) { 921 dl.appendChild(make('dt', {}, "Seen:")); 922 dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); 923 } else { 924 dl.appendChild(make('dt', {}, "First seen:")); 925 dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); 926 dl.appendChild(make('dt', {}, "Last seen:")); 927 dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); 928 } 929 930 dl.appendChild(make('dt', {}, "User-Agent:")); 931 dl.appendChild(make('dd', {'class': 'agent' + ipType}, data.agent)); 932 933 dl.appendChild(make('dt', {}, "Visited pages:")); 934 const pagesDd = make('dd', {'class': 'pages'}); 935 const pageList = make('ul'); 936 937 data._pageViews.forEach( (page) => { 938 const pgLi = make('li'); 939 940 let visitTimeStr = "Bounce"; 941 const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); 942 if (visitDuration > 0) { 943 visitTimeStr = Math.floor(visitDuration / 1000) + "s"; 944 } 945 946 pgLi.appendChild(make('span', {}, page.pg)); 947 pgLi.appendChild(make('span', {}, page.ref)); 948 pgLi.appendChild(make('span', {}, visitTimeStr)); 949 pageList.appendChild(pgLi); 950 }); 951 952 pagesDd.appendChild(pageList); 953 dl.appendChild(pagesDd); 954 955 details.appendChild(dl); 956 957 li.appendChild(details); 958 return li; 959 } 960 } 961 } 962}; 963 964/* launch only if the BotMon admin panel is open: */ 965if (document.getElementById('botmon__admin')) { 966 BotMon.init(); 967}