Stats improvements

This commit is contained in:
Sascha Leib
2025-10-27 21:07:05 +01:00
parent 2c8aaf6cac
commit 6b6cd387da
15 changed files with 221 additions and 114 deletions

2
.gitignore vendored
View File

@@ -2,5 +2,5 @@
logs/*.log.txt
logs/*.srv.txt
logs/*.tck.txt
config/user-config.json
config/user-*.json
php_errors.log

View File

@@ -1,10 +1,8 @@
# DokuWiki Bot Monitoring Plugin
Plugin for live-monitoring your DokuWiki instance for bot activity
IMPORTANT: This is an experimental plugin to investigate bot traffic. This is not a "install and forget" software, but rather requires that you actively look at and manage the log files.
IMPORTANT: This is an experimental plugin to investigate bot traffic. This is not a "install and forget" software, but rather requires that you actively look at and manage data for this to be useful.
This plugin creates various log files in its own "logs" directory. These files can get quite large, and you should check them actively.
In addition to collecting a lot fo information about bot activity on your server, it now also has a simple Captcha function that you can use to block off bots from downloading your precious content. It is however advisable to only activate this after you already have a better understanding of your own site's traffic patterns (both by bots and by humans) to avoid over-blocking legitimate users.
Also, these files can get quite large and fill up your server. Make sure to manually delete older files from time to time!
For more information, please see the DokuWiki Plugin page at: https://www.dokuwiki.org/plugin:botmon
For more information, please see the DokuWiki Plugin page at: https://www.dokuwiki.org/plugin:botmon and the documentation found at: https://leib.be/sascha/projects/dokuwiki/botmon/index

View File

@@ -251,7 +251,7 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
echo DOKU_TAB . DOKU_TAB . "cj.async=true;cj.defer=true;cj.type='text/javascript';" . NL;
echo DOKU_TAB . DOKU_TAB . "cj.src='".DOKU_BASE."lib/plugins/botmon/captcha.js';" . NL;
echo DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(cj);" . NL;
echo DOKU_TAB . "});";
echo DOKU_TAB . "});" . NL;
// add the translated strings for the captcha:
echo DOKU_TAB . '$BMLocales = {' . NL;

View File

@@ -653,6 +653,7 @@
& {
display: grid;
grid-template-columns: min-content auto;
gap: .25em .5em;
border-left: transparent none 0;
margin: 0 .5rem .25rem 0;
}

259
admin.js
View File

@@ -161,6 +161,7 @@ const BotMon = {
var bg = b;
var sm = a;
if (a == 0 || b == 0) return '—';
if (a > b) {
var bg = a;
var sm = b;
@@ -291,7 +292,8 @@ BotMon.live = {
// shortcut to make code more readable:
const model = BotMon.live.data.model;
//const timeout = 60 * 60 * 1000; // session timeout: One hour
// combine Bot networks to one visitor?
const combineNets = (BMSettings.hasOwnProperty('combineNets') ? BMSettings['combineNets'] : true);;
if (visitor._type == BM_USERTYPE.KNOWN_BOT) { // known bots match by their bot ID:
@@ -304,6 +306,23 @@ BotMon.live = {
}
}
} else if (combineNets && visitor.hasOwnProperty('_ipRange')) { // combine with other visits from the same range
let nonRangeVisitor = null;
for (let i=0; i<model._visitors.length; i++) {
const v = model._visitors[i];
if ( v.hasOwnProperty('_ipRange') && v._ipRange.g == visitor._ipRange.g ) { // match the IPRange Group IDs
return v;
} else if ( v.id.trim() !== '' && v.id == visitor.id) { // match the DW/PHP IDs
nonRangeVisitor = v;
}
}
// if no ip range was found, return the non-range visitor instead
if (nonRangeVisitor) return nonRangeVisitor;
} else { // other types match by their DW/PHPIDs:
// loop over all visitors already registered and check for ID matches:
@@ -346,6 +365,7 @@ BotMon.live = {
registerVisit: function(nv, type) {
//console.info('registerVisit', nv, type);
// shortcut to make code more readable:
const model = BotMon.live.data.model;
@@ -365,6 +385,12 @@ BotMon.live = {
if (!nv._firstSeen) nv._firstSeen = nv.ts; // first-seen
nv._lastSeen = nv.ts; // last-seen
// known bot IP range?
if (nv._type == BM_USERTYPE.UNKNOWN) { // only for unknown visitors
const ipInfo = BotMon.live.data.ipRanges.match(nv.ip);
if (ipInfo) nv._ipRange = ipInfo;
}
// country name:
try {
nv._country = ( nv.geo == 'local' ? "localhost" : "Unknown" );
@@ -549,9 +575,9 @@ BotMon.live = {
const cStr = cObj._str();
switch (cStr) {
case 'Y':
case 'NY': return "Blocked by captcha";
case 'YN': return "Captcha solved";
case 'W': return "IP Address whitelisted";
case 'NY': return "Blocked.";
case 'YN': return "Solved";
case 'W': return "Whitelisted";
case 'H': return "HEAD request, no captcha";
default: return "Undefined: " + cStr;
}
@@ -570,14 +596,36 @@ BotMon.live = {
// data storage:
data: {
totalVisits: 0,
totalPageViews: 0,
humanPageViews: 0,
bots: {
known: 0,
visits: {
bots: 0,
suspected: 0,
human: 0,
users: 0
humans: 0,
users: 0,
total: 0
},
views: {
bots: 0,
suspected: 0,
humans: 0,
users: 0,
total: 0
},
loads: {
bots: 0,
suspected: 0,
humans: 0,
users: 0,
total: 0
},
captcha: {
bots_blocked: 0,
bots_passed: 0,
bots_whitelisted: 0,
humans_blocked: 0,
humans_passed: 0,
sus_blocked: 0,
sus_passed: 0,
sus_whitelisted: 0
}
},
@@ -595,6 +643,7 @@ BotMon.live = {
// shortcut to make code more readable:
const model = BotMon.live.data.model;
const data = BotMon.live.data.analytics.data;
const me = BotMon.live.data.analytics;
BotMon.live.gui.status.showBusy("Analysing data …");
@@ -602,21 +651,35 @@ BotMon.live = {
// loop over all visitors:
model._visitors.forEach( (v) => {
// count visits and page views:
this.data.totalVisits += 1;
this.data.totalPageViews += v._viewCount;
const captchaStr = v._captcha._str();
// count total visits and page views:
data.visits.total += 1;
data.loads.total += v._loadCount;
data.views.total += v._viewCount;
// check for typical bot aspects:
let botScore = 0;
if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots
this.data.bots.known += v._viewCount;
data.visits.bots += 1;
data.views.bots += v._viewCount;
this.groups.knownBots.push(v);
// captcha counter
if (captchaStr == 'Y') {
data.captcha.bots_blocked += 1;
} else if (captchaStr == 'YN') {
data.captcha.bots_passed += 1;
} else if (captchaStr == 'W') {
data.captcha.bots_whitelisted += 1;
}
} else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */
this.data.bots.users += v._viewCount;
data.visits.users += 1;
data.views.users += v._viewCount;
this.groups.users.push(v);
} else {
@@ -627,30 +690,54 @@ BotMon.live = {
v._botVal = e.val;
if (e.isBot) { // likely bots
v._type = BM_USERTYPE.LIKELY_BOT;
this.data.bots.suspected += v._viewCount;
data.visits.suspected += 1;
data.views.suspected += v._viewCount;
this.groups.suspectedBots.push(v);
// captcha counter
if (captchaStr == 'Y') {
data.captcha.sus_blocked += 1;
} else if (captchaStr == 'YN') {
data.captcha.sus_passed += 1;
} else if (captchaStr == 'W') {
data.captcha.sus_whitelisted += 1;
}
} else { // probably humans
v._type = BM_USERTYPE.PROBABLY_HUMAN;
this.data.bots.human += v._viewCount;
data.visits.humans += 1;
data.views.humans += v._viewCount;
this.groups.humans.push(v);
// captcha counter
if (captchaStr == 'Y') {
data.captcha.humans_blocked += 1;
} else if (captchaStr == 'YN') {
data.captcha.humans_passed += 1;
}
}
}
// perform actions depending on the visitor type:
if (v._type == BM_USERTYPE.KNOWN_BOT ) { /* known bots only */
// no specific actions here.
} else if (v._type == BM_USERTYPE.LIKELY_BOT) { /* probable bots only */
// add bot views to IP range information:
me.addToIpRanges(v);
} else { /* humans only */
} else { /* registered users and probable humans */
// add browser and platform statistics:
me.addBrowserPlatform(v);
// add
// add to referrer and pages lists:
v._pageViews.forEach( pv => {
me.addToRefererList(pv._ref);
me.addToPagesList(pv.pg);
@@ -1064,7 +1151,6 @@ BotMon.live = {
} else { // no known IP range, let's collect necessary information:
// collect basic IP address info:
if (ipType == BM_IPVERSION.IPv6) {
ipSeg = ipAddr.split(':');
@@ -1604,14 +1690,7 @@ BotMon.live = {
fromKnownBotIP: function(visitor) {
//console.info('fromKnownBotIP()', visitor.ip);
const ipInfo = BotMon.live.data.ipRanges.match(visitor.ip);
if (ipInfo) {
visitor._ipInKnownBotRange = true;
visitor._ipRange = ipInfo;
}
return (ipInfo !== null);
return visitor.hasOwnProperty('_ipRange');
},
// is the page language mentioned in the client's accepted languages?
@@ -1910,47 +1989,48 @@ BotMon.live = {
*/
make: function() {
const data = BotMon.live.data.analytics.data;
const maxItemsPerList = 5; // how many list items to show?
const useCaptcha = BMSettings.useCaptcha || false;
const kNoData = ''; // shown when data is missing
const kSeparator = '/';
// shortcut for neater code:
// shortcuts for neater code:
const makeElement = BotMon.t._makeElement;
const data = BotMon.live.data.analytics.data;
const botsVsHumans = document.getElementById('botmon__today__botsvshumans');
if (botsVsHumans) {
botsVsHumans.appendChild(makeElement('dt', {}, "Page views"));
botsVsHumans.appendChild(makeElement('dt', {}, "Bot statistics"));
for (let i = 0; i <= 5; i++) {
for (let i = 0; i <= ( useCaptcha ? 5 : 3 ); i++) {
const dd = makeElement('dd');
let title = '';
let value = '';
switch(i) {
case 0:
title = "Known bots:";
value = data.bots.known || kNoData;
title = "Total (loads/views/visits):";
value = (data.loads.total || kNoData) + kSeparator + (data.views.total || kNoData) + kSeparator + (data.visits.total || kNoData);
break;
case 1:
title = "Suspected bots:";
value = data.bots.suspected || kNoData;
title = "Known bots (views/visits):";
value = (data.views.bots || kNoData) + kSeparator + (data.visits.bots || kNoData);
break;
case 2:
title = "Probably humans:";
value = data.bots.human || kNoData;
title = "Suspected bots (views/visits):";
value = (data.visits.suspected || kNoData) + kSeparator + (data.views.suspected || kNoData)
break;
case 3:
title = "Registered users:";
value = data.bots.users || kNoData;
title = "Bots-humans ratio (views/visits):";
value = BotMon.t._getRatio(data.views.suspected + data.views.bots, data.views.users + data.views.humans, 100) + kSeparator + BotMon.t._getRatio(data.visits.suspected + data.visits.bots, data.visits.users + data.visits.humans, 100);
break;
case 4:
title = "Total:";
value = data.totalPageViews || kNoData;
title = "Known bots blocked/passed/whitelisted:";
value = data.captcha.bots_blocked + kSeparator + data.captcha.bots_passed + kSeparator + data.captcha.bots_whitelisted;
break;
case 5:
title = "Bots-humans ratio:";
value = BotMon.t._getRatio(data.bots.suspected + data.bots.known, data.bots.users + data.bots.human, 100);
title = "Suspected bots blocked/passed/whitelisted:";
value = data.captcha.sus_blocked + kSeparator + data.captcha.sus_passed + kSeparator + data.captcha.sus_whitelisted;
break;
default:
console.warn(`Unknown list type ${i}.`);
@@ -2007,7 +2087,7 @@ BotMon.live = {
const wmoverview = document.getElementById('botmon__today__wm_overview');
if (wmoverview) {
const humanVisits = BotMon.live.data.analytics.groups.users.length + BotMon.live.data.analytics.groups.humans.length;
const humanVisits = data.views.total;
const bounceRate = Math.round(100 * (BotMon.live.data.analytics.getBounceCount('users') + BotMon.live.data.analytics.getBounceCount('humans')) / humanVisits);
wmoverview.appendChild(makeElement('dt', {}, "Humans metrics"));
@@ -2017,20 +2097,20 @@ BotMon.live = {
let value = '';
switch(i) {
case 0:
title = "Registered users page views:";
value = data.bots.users || kNoData;
title = "Registered users (views/visits):";
value = (data.views.users || kNoData) + kSeparator + (data.visits.users || kNoData);
break;
case 1:
title = "Probably humans” page views:";
value = data.bots.human || kNoData;
title = "Probably humans (views/visits):";
value = (data.views.humans || kNoData) + kSeparator + (data.visits.humans || kNoData);
break;
case 2:
title = "Total human page views:";
value = (data.bots.users + data.bots.human) || kNoData;
value = (data.views.users + data.views.humans) || kNoData;
break;
case 3:
title = "Total human visits:";
value = humanVisits || kNoData;
value = data.views.total || kNoData;
break;
case 4:
title = "Humans bounce rate:";
@@ -2292,6 +2372,10 @@ BotMon.live = {
const sumClass = ( !data._seenBy || data._seenBy.indexOf(BM_LOGTYPE.SERVER) < 0 ? 'noServer' : 'hasServer');
// combine with other networks?
const combineNets = (BMSettings.hasOwnProperty('combineNets') ? BMSettings['combineNets'] : true)
&& data.hasOwnProperty('_ipRange');
const li = make('li'); // root list item
const details = make('details');
const summary = make('summary', {
@@ -2301,17 +2385,17 @@ BotMon.live = {
const span1 = make('span'); /* left-hand group */
if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* No platform/client for bots */
span1.appendChild(make('span', { /* Platform */
/*if (data._type !== BM_USERTYPE.KNOWN_BOT) { // No platform/client for bots // disabled because no longer relevant
span1.appendChild(make('span', { // Platform
'class': 'icon_only platform pf_' + (data._platform ? data._platform.id : 'unknown'),
'title': "Platform: " + platformName
}, platformName));
span1.appendChild(make('span', { /* Client */
span1.appendChild(make('span', { // Client
'class': 'icon_only client client cl_' + (data._client ? data._client.id : 'unknown'),
'title': "Client: " + clientName
}, clientName));
}
}*/
// identifier:
if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */
@@ -2331,16 +2415,22 @@ BotMon.live = {
} else { /* others */
span1.appendChild(make('span', { // IP-Address
'class': 'has_icon ipaddr ip' + ipType,
'title': "IP-Address: " + data.ip
}, data.ip));
if (combineNets) {
/*span1.appendChild(make('span', { // Internal ID
'class': 'has_icon session typ_' + data.typ,
'title': "ID: " + data.id
}, data.id));*/
const ispName = BotMon.live.data.ipRanges.getOwner( data._ipRange.g ) || data._ipRange.g;
span1.appendChild(make('span', { // IP-Address
'class': 'has_icon ipaddr ipnet',
'title': "IP-Range: " + data._ipRange.g
}, ispName));
} else {
span1.appendChild(make('span', { // IP-Address
'class': 'has_icon ipaddr ip' + ipType,
'title': "IP-Address: " + data.ip
}, data.ip));
}
}
span1.appendChild(make('span', { /* page views */
@@ -2362,6 +2452,7 @@ BotMon.live = {
const span2 = make('span'); /* right-hand group */
// country flag:
if (!combineNets) { // not for combined networks
if (data.geo && data.geo !== 'ZZ') {
span2.appendChild(make('span', {
'class': 'icon_only country ctry_' + data.geo.toLowerCase(),
@@ -2369,21 +2460,22 @@ BotMon.live = {
'title': "Country: " + ( data._country || "Unknown")
}, ( data._country || "Unknown") ));
}
}
span2.appendChild(make('span', { // seen-by icon:
'class': 'icon_only seenby sb_' + data._seenBy.join(''),
'title': "Seen by: " + data._seenBy.join('+')
}, data._seenBy.join(', ')));
span2.appendChild(make('span', { // seen-by icon:
'class': 'icon_only seenby sb_' + data._seenBy.join(''),
'title': "Seen by: " + data._seenBy.join('+')
}, data._seenBy.join(', ')));
// captcha status:
const cCode = ( data._captcha ? data._captcha._str() : '');
if (cCode !== '') {
const cTitle = model._makeCaptchaTitle(data._captcha)
span2.appendChild(make('span', { // captcha status
'class': 'icon_only captcha cap_' + cCode,
'title': "Captcha-status: " + cTitle
}, cTitle));
}
// captcha status:
const cCode = ( data._captcha ? data._captcha._str() : '');
if (cCode !== '') {
const cTitle = model._makeCaptchaTitle(data._captcha)
span2.appendChild(make('span', { // captcha status
'class': 'icon_only captcha cap_' + cCode,
'title': "Captcha-status: " + cTitle
}, cTitle));
}
summary.appendChild(span2);
@@ -2569,6 +2661,13 @@ BotMon.live = {
dl.appendChild(evalDd);
}
}
// for debugging only. Disable on production:
dl.appendChild(make('dt', {}, "Debug info:"));
const dbgDd = make('dd', {'class': 'debug'});
dbgDd.innerHTML = '<pre>' + JSON.stringify(data, null, 4) + '</pre>';
dl.appendChild(dbgDd);
// return the element to add to the UI:
return dl;
},

View File

@@ -43,7 +43,7 @@ class admin_plugin_botmon extends AdminPlugin {
$pluginPath = $conf['basedir'] . 'lib/plugins/' . $this->getPluginName();
/* Plugin Headline */
echo '<div id="botmon__admin">
echo NL . '<div id="botmon__admin">
<h1>Bot Monitoring Plugin</h1>
<nav id="botmon__tabs">
<ul class="tabs" role="tablist">
@@ -92,7 +92,7 @@ class admin_plugin_botmon extends AdminPlugin {
</article>
<article role="tabpanel" id="botmon__log" hidden>
<h2 class="a11y">Process log</h2>
<ul id="botmon__loglist">';
<ul id="botmon__loglist">' . NL;
/* proces old logs */
if ($hasOldLogFiles) {
@@ -101,15 +101,17 @@ class admin_plugin_botmon extends AdminPlugin {
$helper->cleanup();
} else {
echo '<li>No files to process.</li>';
echo DOKU_TAB . DOKU_TAB . DOKU_TAB . '<li>No files to process.</li>' . NL;
}
echo '</article>' . NL;
echo '<script>
BMSettings = {
showday: ' . json_encode($this->getConf('showday')) . '
echo DOKU_TAB . DOKU_TAB . '</ul>' . NL . DOKU_TAB . '</article>' . NL;
echo DOKU_TAB . '<script>
const BMSettings = {
showday: ' . json_encode($this->getConf('showday')) . ',
combineNets: ' . json_encode($this->getConf('combineNets')) . ',
useCaptcha: ' . json_encode($this->getConf('useCaptcha') !== 'disabled') . '
};
</script>';
</script>' . NL;
echo '</div><!-- End of BotMon Admin Tool -->';
}

View File

@@ -178,7 +178,8 @@ const $BMCaptcha = {
const hash = $BMCaptcha.digest.hash(dat.join('|'));
// set the cookie:
document.cookie = "DWConfirm=" + hash + ';path=/;';
document.cookie = "DWConfirm=" + encodeURIComponent(hash) + ';path=/;hostOnly;session;sameSite=strict;'
+ (document.location.protocol === 'https:' ? 'secure;' : '');
} catch (err) {
console.error(err);

View File

@@ -5,7 +5,8 @@
* @author Sascha Leib <sascha@leib.be>
*/
$conf['showday'] = 'yesterday';
$conf['showday'] = 'today';
$conf['combineNets'] = true;
$conf['geoiplib'] = 'disabled';
$conf['useCaptcha'] = 'disabled';
$conf['captchaSeed'] = 'c53bc5f94929451987efa6c768d8856b';

View File

@@ -8,10 +8,11 @@
$meta['showday'] = array('multichoice',
'_choices' => array ('yesterday', 'today'));
$meta['combineNets'] = array('onoff');
$meta['geoiplib'] = array('multichoice',
'_choices' => array ('disabled', 'phpgeoip'));
//$meta['useCaptcha'] = array('onoff');
$meta['useCaptcha'] = array('multichoice',
'_choices' => array ('disabled', 'loremipsum', 'dada'));
$meta['captchaSeed'] = array('string');

View File

@@ -70,7 +70,7 @@
"bot": 30
},
{"func": "blockedByCaptcha", "params": [],
"id": "blockedByCaptcha", "desc": "Visitor was blocked by captcha",
"id": "blockedByCaptcha", "desc": "Visitor did not solve the captcha",
"bot": 20
},
{"func": "whitelistedByCaptcha", "params": [],

View File

@@ -1,23 +1,23 @@
{
"groups": [
{"id": "alibaba", "name": "Alibaba"},
{"id": "amazon", "name": "Amazon"},
{"id": "alibaba", "name": "Alibaba Network"},
{"id": "amazon", "name": "Amazon Data Centres"},
{"id": "bezeq", "name": "Bezeq Int."},
{"id": "brasilnet", "name": "BrasilNet"},
{"id": "charter", "name": "Charter Inc."},
{"id": "chinanet", "name": "Chinanet"},
{"id": "cloudflare", "name": "Cloudflare Inc."},
{"id": "cnisp", "name": "China ISP"},
{"id": "charter", "name": "Charter Inc. Range"},
{"id": "chinanet", "name": "ChinaNet"},
{"id": "cloudflare", "name": "Cloudflare Network"},
{"id": "cnisp", "name": "China ISP Range"},
{"id": "cnmob", "name": "China Mobile"},
{"id": "google", "name": "Google LLC"},
{"id": "google", "name": "Google LLC Network"},
{"id": "hetzner", "name": "Hetzner US"},
{"id": "huawei", "name": "Huawei"},
{"id": "huawei", "name": "Huawei Network"},
{"id": "misc_sa", "name": "Misc. SA ISPs"},
{"id": "tencent", "name": "Tencent"},
{"id": "tencent", "name": "Tencent Network"},
{"id": "unicom", "name": "China Unicom"},
{"id": "vnpt", "name": "Vietnam Telecom"},
{"id": "vdsina", "name": "VDSina NL"},
{"id": "zenlayer", "name": "Zenlayer"}
{"id": "vdsina", "name": "VDSina Network"},
{"id": "zenlayer", "name": "Zenlayer Network"}
],
"ranges": [
{"from": "1.92.0.0", "to": "1.95.255.254", "m": 14, "g": "huawei"},
@@ -56,6 +56,7 @@
{"from": "111.119.192.0", "to": "111.119.255.254", "m": 18, "g": "huawei"},
{"from": "113.160.0.0", "to": "113.191.255.254", "m": 11, "g": "vnpt"},
{"from": "114.208.0.0", "to": "114.223.255.254", "m": 12, "g": "unicom"},
{"from": "114.119.0.0", "to": "114.119.255.254", "m": 11, "g": "huawei"},
{"from": "114.224.0.0", "to": "114.255.255.254", "m": 11, "g": "unicom"},
{"from": "119.8.0.0", "to": "119.8.255.254", "m": 16, "g": "huawei"},
{"from": "119.13.0.0", "to": "119.13.255.254", "m": 16, "g": "huawei"},
@@ -64,6 +65,7 @@
{"from": "122.9.0.0", "to": "122.9.255.254", "m": 16, "g": "huawei"},
{"from": "123.16.0.0", "to": "123.31.255.254", "m": 12, "g": "vnpt"},
{"from": "124.243.128.0", "to": "124.243.191.254", "m": 18, "g": "huawei"},
{"from": "136.107.0.0", "to": "136.125.255.254", "m": "+", "g": "google"},
{"from": "138.59.0.0", "to": "138.59.225.254", "m": 16, "g": "misc_sa"},
{"from": "138.121.0.0", "to": "138.121.225.254", "m": 16, "g": "misc_sa"},
{"from": "142.147.128.0", "to": "1142.147.255.254", "m": 17, "g": "w2obj"},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -6,9 +6,9 @@
*/
// Captcha dialog locale strings:
$lang['bm_dlgTitle'] = 'Benutzerüberprüfung';
$lang['bm_dlgSubtitle'] = 'Bitte bestätige, dass du kein Bot bist:';
$lang['bm_dlgConfirm'] = 'Klicke, um zu bestätigen.';
$lang['bm_dlgChecking'] = 'Wird überprüft&nbsp;&hellip;';
$lang['bm_dlgTitle'] = 'Benutzer&uuml;berpr&uuml;fung';
$lang['bm_dlgSubtitle'] = 'Bitte best&auml;tige, dass du kein Bot bist:';
$lang['bm_dlgConfirm'] = 'Klicke, um zu best&auml;tigen.';
$lang['bm_dlgChecking'] = 'Wird &uuml;berpr&uuml;ft&nbsp;&hellip;';
$lang['bm_dlgLoading'] = 'Seite wird geladen&nbsp;&hellip;';
$lang['bm_dlgError'] = 'Es ist ein Fehler aufgetreten.';

View File

@@ -9,6 +9,8 @@ $lang['showday'] = 'Which data to show in the “Latest” tab:';
$lang['showday_o_yesterday'] = 'Last full day (yesterday)';
$lang['showday_o_today'] = 'Ongoing logs (today)';
$lang['combineNets'] = 'Combine visits from known IP-ranges into one entry:';
$lang['geoiplib'] = 'Add GeoIP Information<br><small>(requires PHP module to be installed)</small>';
$lang['geoiplib_o_disabled'] = 'Disabled';
$lang['geoiplib_o_phpgeoip'] = 'Use GeoIP Module';