diff --git a/action.php b/action.php index 95d3980..6d790a5 100644 --- a/action.php +++ b/action.php @@ -54,26 +54,11 @@ class action_plugin_monitor extends DokuWiki_Action_Plugin { ]; /* Write out client info to a server log: */ - - // what is the session identifier? - $sessionId = $_COOKIE['DokuWiki'] ?? null; - if (!$sessionId) { - if (session_id()) { - // if a session ID is set, use it - $sessionId = 'P:' . session_id(); - } else { - // if no session ID is set, use an empty string - $sessionId = ''; - } - } else { - // if no cookie is set, use the session ID - $sessionId = 'D:' . $sessionId; - } - $logArr = Array( $_SERVER['REMOTE_ADDR'] ?? '', /* remote IP */ $INFO['id'] ?? '', /* page ID */ - $sessionId, /* Session ID */ + $_COOKIE['DokuWiki'] ?? null, /* Dokuwiki Session ID */ + session_id(), /* PHP Session ID */ $username, $_SERVER['HTTP_USER_AGENT'] ?? '' /* User agent */ ); diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..9354857 --- /dev/null +++ b/admin.php @@ -0,0 +1,56 @@ + +*/ + +/** + * All DokuWiki plugins to extend the admin function + * need to inherit from this class +**/ +class admin_plugin_monitor extends AdminPlugin { + + /** + * Return the path to the icon being displayed in the main admin menu. + * + * @return string full path to the icon file + **/ + public function getMenuIcon() { + $plugin = $this->getPluginName(); + return DOKU_PLUGIN . $plugin . '/img/admin.svg'; + } + + /** + * output appropriate html + */ + public function html() { + + $svg = ''; + + /* Plugin Headline */ + echo '
'; + echo '

Monitor Plugin

'; + + /* tab navigation */ + echo ''; + + /* Live tab */ + echo '
'; + echo '

Today

'; + echo '

Loading …

'; + echo '
'; + echo ''; + echo '
'; + echo '
'; + + } +} \ No newline at end of file diff --git a/data/known-bots.json b/data/known-bots.json new file mode 100644 index 0000000..bbfddf1 --- /dev/null +++ b/data/known-bots.json @@ -0,0 +1,50 @@ +[ + {"id": "google", + "n": "GoogleBot", + "r": ["Googlebot"], + "rx": ["Googlebot\\/(\\d+\\.\\d+);"], + "url": "http://www.google.com/bot.html" + }, + {"id": "googlead", + "n": "Google AdsBot", + "r": ["AdsBot-Google", "AdsBot-Google-Mobile", "Mediapartners-Google"], + "rx": ["AdsBot-Google;","AdsBot-Google-Mobile;", "Mediapartners-Google\\/(\\d+\\.\\d+);"], + "url": "http://www.google.com/mobile/adsbot.html" + }, + {"id": "googleapi", + "n": "Google APIs", + "r": ["APIs-Google"], + "rx": ["APIs-Google"], + "url": "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers" + }, + {"id": "bing", + "n": "Bingbot", + "r": ["bingbot"], + "rx": ["bingbot\\/(\\d+\\.\\d+);"], + "url": "http://www.bing.com/bingbot.htm" + }, + {"id": "apple", + "n": "Applebot", + "r": ["Applebot"], + "rx": ["Applebot\\/(\\d+\\.\\d+);"], + "url": "http://www.apple.com/go/applebot" + }, + {"id": "openai", + "n": "OpenAI/ChatGPT Bots", + "r": ["OAI-SearchBot", "ChatGPT-User", "GPTBot"], + "rx": ["OAI-SearchBot\\/(\\d+\\.\\d+);", "ChatGPT-User\\/(\\d+\\.\\d+);", "GPTBot\\/(\\d+\\.\\d+);"], + "url": "https://platform.openai.com/docs/bots/" + }, + {"id": "yandex", + "n": "Yandex Bots", + "r": ["YandexBot", "YandexAdNet", "YandexBlogs", "YandexImages", "YandexImageResizer", "YandexMarket", "YandexMedia", "YandexOntoDB", "YandexSitelinks","YandexSpravBot", "YandexVertis", "YandexVerticals", "YandexVideo", "YandexWebmaster", "YandexComBot"], + "rx": ["Yandex\\w+\\/(\\d+\\.\\d+);"], + "url": "http://yandex.com/bots" + }, + {"id": "seznam", + "n": "SeznamBot Crawler", + "r": ["SeznamBot"], + "rx": ["SeznamBot\\/(\\d+\\.\\d+);"], + "url": "https://o-seznam.cz/napoveda/vyhledavani/en/seznambot-crawler/" + } +] \ No newline at end of file diff --git a/img/admin.svg b/img/admin.svg new file mode 100644 index 0000000..031843f --- /dev/null +++ b/img/admin.svg @@ -0,0 +1 @@ +Monitor \ No newline at end of file diff --git a/img/spinner.svg b/img/spinner.svg new file mode 100644 index 0000000..733fb91 --- /dev/null +++ b/img/spinner.svg @@ -0,0 +1,168 @@ + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..d276ce2 --- /dev/null +++ b/script.js @@ -0,0 +1,233 @@ +/* DokuWiki Monitor Plugin Script file */ +/* 29.08.2025 - 1.0.5 - initial release */ +/* Authors: Sascha Leib */ + +const Monitor = { + + init: function() { + //console.info('Monitor.init()'); + + // find the plugin basedir: + this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) + + '/plugins/monitor/'; + + // read the page language from the DOM: + this._lang = document.getRootNode().documentElement.lang || this._lang; + + // get the time offset: + this._timeDiff = Monitor.t._getTimeOffset(); + + // init the sub-objects: + Monitor.t._callInit(this); + }, + + _baseDir: null, + _lang: 'en', + _today: (new Date()).toISOString().slice(0, 10), + _timeDiff: '', + + /* internal tools */ + t: { + + /* helper function to call inits of sub-objects */ + _callInit: function(obj) { + //console.info('Monitor.t._callInit(obj=',obj,')'); + + /* call init / _init on each sub-object: */ + Object.keys(obj).forEach( (key,i) => { + const sub = obj[key]; + let init = null; + if (typeof sub === 'object' && sub.init) { + init = sub.init; + } + + // bind to object + if (typeof init == 'function') { + const init2 = init.bind(sub); + init2(obj); + } + }); + }, + + /* helper function to calculate the time difference to UTC: */ + _getTimeOffset: function() { + const now = new Date(); + let offset = now.getTimezoneOffset(); // in minutes + const sign = Math.sign(offset); // +1 or -1 + offset = Math.abs(offset); // always positive + + let hours = 0; + while (offset >= 60) { + hours += 1; + offset -= 60; + } + return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); + } + } +} + +/* everything specific to the "Today" tab is self-contained here: */ +Monitor.today = { + init: function() { + //console.info('Monitor.today.init()'); + + // set the title: + const tDiff = (Monitor._timeDiff != '' ? ` (${Monitor._timeDiff})` : ' (UTC)' ); + Monitor.today.status.setTitle(`Showing visits for ${tDiff}`); + + // init sub-objects: + Monitor.t._callInit(this); + }, + + data: { + init: function() { + //console.info('Monitor.today.data.init()'); + + // call sub-inits: + Monitor.t._callInit(this); + + // load the first log file: + Monitor.today.data.loadLogFile('srv'); + }, + + bots: { + // loads the list of known bots from a JSON file: + init: async function() { + //console.info('Monitor.today.data.bots.init()'); + + // Load the list of known bots: + Monitor.today.status.showBusy("Loading known bots …"); + const url = Monitor._baseDir + 'data/known-bots.json'; + + console.log(url); + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`); + } + + Monitor.today.data.bots._list = await response.json(); + Monitor.today.data.bots._ready = true; + + // TODO: allow using the bots list... + } catch (error) { + Monitor.today.status.setError("Error while loading the ’known bots’ file: " + error.message); + } finally { + Monitor.today.status.hideBusy("Done."); + } + }, + + // returns bot info if the clientId matches a known bot, null otherwise: + match: function(clientId) { + + // TODO! + }, + + // indicates if the list is loaded and ready to use: + _ready: false, + + // the actual bot list is stored here: + _list: [] + }, + + loadLogFile: async function(type) { + console.info('Monitor.today.data.loadLogFile(',type,')'); + + let typeName = ''; + let columns = []; + + switch (type) { + case "srv": + typeName = "Server"; + columns = ['ts','ip','pg','id','usr','client']; + break; + break; + case "log": + typeName = "Page load"; + columns = ['ts','ip','pg','id','usr']; + break; + case "tck": + typeName = "Ticker"; + columns = ['ts','ip','pg','id']; + break; + default: + console.warn(`Unknown log type ${type}.`); + return; + } + + // Load the list of known bots: + Monitor.today.status.showBusy(`Loading ${typeName} log file  …`); + + const url = Monitor._baseDir + `logs/${Monitor._today}.${type}`; + console.log("Loading:",url); + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`); + } + + const events = await response.text(); + console.log(events); + //Monitor.today.data.serverEvents._ready = true; + + // TODO: parse the file... + } catch (error) { + Monitor.today.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); + } finally { + Monitor.today.status.hideBusy("Done."); + } + } + }, + + status: { + setText: function(txt) { + const el = document.getElementById('monitor__today__status'); + if (el && Monitor.today.status._errorCount <= 0) { + el.innerText = txt; + } + }, + + setTitle: function(html) { + const el = document.getElementById('monitor__today__title'); + if (el) { + el.innerHTML = html; + } + }, + + setError: function(txt) { + console.error(txt); + Monitor.today.status._errorCount += 1; + const el = document.getElementById('monitor__today__status'); + if (el) { + el.innerText = "An error occured. See the browser log for details!"; + el.classList.add('error'); + } + }, + _errorCount: 0, + + showBusy: function(txt = null) { + Monitor.today.status._busyCount += 1; + const el = document.getElementById('monitor__today__busy'); + if (el) { + el.style.display = 'inline-block'; + } + if (txt) Monitor.today.status.setText(txt); + }, + _busyCount: 0, + + hideBusy: function(txt = null) { + const el = document.getElementById('monitor__today__busy'); + Monitor.today.status._busyCount -= 1; + if (Monitor.today.status._busyCount <= 0) { + if (el) el.style.display = 'none'; + if (txt) Monitor.today.status.setText(txt); + } + } + } +} + +/* check if the nustat admin panel is open: */ +if (document.getElementById('monitor__admin')) { + Monitor.init(); +} \ No newline at end of file diff --git a/style.less b/style.less new file mode 100644 index 0000000..54e5365 --- /dev/null +++ b/style.less @@ -0,0 +1,49 @@ +#monitor__admin { + + section[role="tabpanel"] { + margin: .25rem 0; + } + + #monitor__today { + + header { + & { + background-color: #FBFAF9; + color: #333; + border: #EEE solid 1px; + border-radius: .25rem .25rem 0 0; + margin: 0 0 .25rem 0; + } + h3 { + font-size: 1rem; + padding: .25em .5em; margin: 0; + } + } + footer { + & { + display: flex; + align-items: center; + column-gap: .25rem; + background-color: #FBFAF9; + color: #333; + border: #EEE solid 1px; + border-radius: 0 0 .25rem .25rem; + margin: .25rem 0 0 0; + padding: .25rem .5rem; + } + & > svg { + width: 1.25em; height: 1.25em; + fill: #333; + flex-shrink: 0; + } + & > span { + font-size: .96rem; + line-height: 1.25rem; + } + & > span.error { + color: #961D1B; + font-weight: bold; + } + } + } +} \ No newline at end of file