<?php declare(strict_types=1); 
    namespace TotalCounter; # 
    if (!defined('PmWiki')) exit ();
    const TOTALCOUNTERNAME =  'TotalCounter';
    const TOTALCOUNTERV = '2024-11-13';
    $RecipeInfo[TOTALCOUNTERNAME]['Version'] = TOTALCOUNTERV;
    $FmtPV['$TotalCounterVersion'] = "'" . $RecipeInfo[TOTALCOUNTERNAME]['Version'] . "'"; // return version as a custom page variable
/*
    statistic counter for PmWiki
    copyright (c) 2005/2006 Yuri Giuntoli (www.giuntoli.com), 2007-2024 various contributors

    This PHP script is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by the Free Software Foundation.

    This PHP script is not part of the standard PmWiki distribution.

    0.1 - 23.06.2005
        First version, counts page views and total views.
    0.2 - 20.11.2005
        Added action=totalcounter which displays a page with statistics summary.
    0.3 - 24.11.2005
        Added logging of users, browsers, operating systems, referers and locations.
    0.4 - 28.11.2005
        Optimization of the detection routines.
        Improved detection of the user.
        Added logging of web bots.
    0.5 - 02.12.2005
        Added possibility to blacklist specific items from being logged.
        Modified regex for better referer and location detection.
        Added extended description of location in statistic summary.
    0.6 - 14.12.2005
        Added possibility to DNS lookup the location in case the server doesn't do it automatically.
        Added detection of location when user is sitting behind a proxy server.
        Added possibility to blacklist with regexes for pages, users, referers and locations.
        Listed pages now are link to the actual page.
        Added possibility to assign a password authorization level (edit, admin, etc).
    1.0 - 21.12.2005
        Corrected a bug when the page is the default page.
        Corrected a bug which assigned a browser when pages were crawled by a web bot.
        Optimization of array routines.
        Public release.
    1.1 - 03.01.2006
        Fixed a bug when no bots are present yet.
        Now users work with both UserAuth and AuthUser.
        Added recognition for other popular web bots.
        Added configuration of bars color in the statistics page.
        Added numbers on items (configurable) in the statistics page.
    1.1b - 05.01.2006
        Fixed a bug with empty blacklist array.
        Fixed an alignment problem in the statistics page.
        Fixed a problem which treated Group/Page different from Group.Page.
        Added version display in the statistics page.
    1.1c - 17.01.2006
        Fixed a problem with the markup to work with 2.1.beta20.
    1.2 - 24.01.2006
        Added links to profile pages for the users.
        Reduced locking loop to 5 seconds.
    1.3 - 30.01.2006
        Suppressed the modification to $pagename, now uses internal variable.
        Fixed a bug when remote location is in upper case.
        Changed creation of lock directory to lock file, to prevent problems with some providers.
    1.4 - 31.01.2006
        Optimized the detection of the current page (using ResolvePageName).
        Added statistic count of languages (when used with the MultiLanguage recipe).
    1.4b - 20.02.2006
        Added blacklist support for languages.
        Some fixes about arrays.
    1.5 - 07.03.2006
        Added {$PageViews} page variable.
        Fixed a problem when ResolvePageName function does not exist (earlier versions of PmWiki). # removed 2024
        Fixed a problem with PHP version <4.3.
    1.6 - 11.06.2006 Florian Xaver:
         Added os: "DOS"
         Added browser: "Arachne GPL"
         Added browser: "Blazer"
         Changed 'palmos' to 'palm'
        Schlaefer: a daily page counter, a short input field to set the $TotalCounterMaxItems. Changes he mades have a ## comment.
    1.7 - 26.07.2006 Florian Xaver:
         Fixed bug, which resets counter. Now there should be no problems with slow servers anymore.

        IMPORTANT: If you get errors on your server, please change creating and deleting
                   of the directory $lockfilename with creating and deleting of a file. This code
                   is commented.
    1.8 - 2007-01-01 - Dave Carver
        Added ($TotalCounterGEOIP) variable.
        Added ($TotalCounterEnableGeoIp) - Set to 1 to use MaxMind's GEOIP Database
           for country identification. Make sure to turn off Lookup (set to 0).
        Added code to get Location by looking up GEOIP
            Added code to hopefully fix resets of the file.
            Added ignore_user_abort(true) to keep file from resetting.
            Defaults to 'admin' level for viewing of stats.
            Minor code refactoring to only open the file in write mode when action=browse

        1.8a - 2007-01-21 - Florian Xaver
                Improved/Fixed handling of userlanguage plug-in: (uses $userlang2 instead of $userlang)
                Fixed handling of "File Downloads" (no "." at the filename)
    1.9 - 2007-10-01 - Mateusz Czaplinski
        Added time statistics (last day, last month,...).
        Chmods can be disabled via configuration option.
    1.9.1 - 2008-01-22 - Mateusz Czaplinski
        A fix which tries to ensure that the site won't get locked up by TC's lockfile.
        Added $TotalCounterFile & $TotalCounterLockfile configuration variables.
    1.9.2 - 2010-02-08 - Peter Bowers
        Tiny fix to allow Google Chrome browser to be identified correctly.
  1.9.2 - 2014-10-04 php 5.3.3 Nigel
    Incorporate Nigel's upgrade to replace deprecated eregi() function with preg_replace() to make it php5.3.3 compliant
  1.9.3 - 2014-10-29 - Bianka Martinovic
    Replaced two occurences of /e with Markup_e()
  1.10.0 - 2014-11-12 - Simon
    div with class totalcounter to allow styling; friendly names for counts; don't show LastYears of zero; add logfile; log unknowns; add more robots,
    skip unknown OS if bot; skip unknown referer if bot; skip unknown location if bot; use smaller instead of small; right align percentage; 
    $TotalCounterEnableGeoIp default to 0; enable https referers; use $FmtPV for page variables; Add $TotalCounterEnableUsers; add OSes; use number_format ();
    add $TotalCounterCountBots
  1.11.0 - 2017-10-19 - Said Achmiz
  	Fixed blacklist logic; now it properly blacklists things/people. Also fixed $TotalCounterEnableUsers flag, it works now.
  1.11.1 - 2017-10-20 - Said Achmiz
  	Fixed dumb bug.
  1.12 - 2022-01-22 - Simon
    quote defined constant to remove warning, add a few more domains, bots, and Edge browser, 
    use namespace, strict_types=1, use type hints, use PSFT to replace strftime, refactoring, fix location
  1.13 - 2022-07-06 - Simon
    add more bots, use Lock()
  2024-10-25 Simon
    Many updates for PHP 8.3 warnings and errors, removed use of eval, more use of Lock(), more lines displayed
  2024-11-10 Simon
    Refactor HTTP_USER_AGENT parsing into functions, show small percentages with 1 decimal place, add bots, OS, browser, fix logic, 
	no longer count non-existent pages, action no longer works on non-existent pages
	Auth level to view set to read
  2024-11-13
    Fix index error, exclusively use file_get_contents, fix errors if stats are not available

The following strings can be internationalised (note case) (using $[ ])
  * Last, More, statistics
  * Count, Percent
  * hours, day, week, month, year, years, today
  * Page, pages, views
  * Previous
  * File, downloads
  * Users, Languages, Browsers, Locations, Referers, Operating Systems, Web bots
See https://useragentstring.com/ and https://developers.whatismybrowser.com/useragents/explore/ to assist in identifying browser strings
See https://www.pmwiki.org/wiki/PmWiki/OtherVariables#FmtPV
*/
const NL = "\n";
const BR = '</br />' . NL;
const DATEFMT = 'DateFmt';
const GRAPHNAME ='GraphName';
const BARMAXWIDTH = 250; # px
const BARCELLWIDTH = '260px';
const NULLUSERAGENT = '_nulluseragentvalue_';
const USERAGENTEMPTY = 'User Agent empty';
const MSGFMTID = __NAMESPACE__ . '\\' . TOTALCOUNTERNAME;

// These constants are cell or column names in the TotalCounter array. DO NOT change.
const PREVIOUSYEARS = 'LastYears';
const LASTYEAR = 'LastYear';
const LASTMONTH = 'LastYear';
const LASTWEEK = 'LastWeek';
const LASTDAY = 'LastDay';
const KEYPAGES = 'Pages';
const KEYUSERS = 'Users';
const KEYBROWSERS = 'Browsers';
const KEYOSES = 'OSes';
const KEYREFERERS = 'Referers'; # spelling as per mispelling in HTTP header spec
const KEYLOCATIONS = 'Locations';
const KEYBOTS = 'Bots';
const KEYLANGUAGES = 'Languages';
const KEYPAGESTODAYCOUNTER = 'PagesTodayCounter';
const KEYPAGESTODAYDAY = 'PagesTodayDay';
const KEYLASTTIMESTAMP = 'LastTimestamp';
const KEYDATECREATED = 'DateCreated';
const KEYTOTAL = 'Total';
const KEYTOTALCOUNTERVERSION = 'TotalCounterVersion';
const UNKNOWN = 'Unknown';
#
const ACTIONNAMECHECK = 'totalcountercheck'; // ?action=totalcountercheck
\SDV($TotalCounterActionName, 'totalcounter'); // ?action=totalcounter
\SDV($TotalCounterAuthLevel, 'read');
\SDV($TotalCounterMaxItems, 30); // default 30
\SDV($TotalCounterEnableLookup, 0); // default 0
\SDV($TotalCounterBarColor, '#5af'); // default '#5af'
\SDV($TotalCounterCountBots, 0);  // default 0
\SDV($TotalCounterShowNumbers, 1);  // default 1
\SDV($TotalCounterEnableGeoIp, 0); // default 0
\SDV($TotalCounterGeoIPData, "$WorkDir/GeoIP.dat");
\SDV($TotalCounterEnableDownload, 0); //default 0
\SDV($TotalCounterDownloadManager, "$WorkDir/.download.manager");
\SDV($TotalCounterEnableChmods, 1); // default 1
\SDV($TotalCounterEnableUsers, 0); // default 0
\SDV($TotalCounterFile, "$WorkDir/totalcounter.stat");
\SDV($TotalCounterLockfile, "$WorkDir/totalcounter.lock");
\SDV($TotalCounterLogfile, "$WorkDir/totalcounter.log"); 
\SDV($TotalCounterEnableLog, 0); # default 0 (off), 1 (on, captures unknowns), 2 log server details (verbose)
/*##
\SDV($TotalCounterBrowsersUnset, ''); // used to remove individual totals from file, don't use this unless you know what you are doing
\SDV($TotalCounterOSesUnset, ''); // used to remove individual totals from file, don't use this unless you know what you are doing
\SDV($TotalCounterLocationsUnset, ''); // used to remove individual totals from file, don't use this unless you know what you are doing
\SDV($TotalCounterBotsUnset, ''); // used to remove individual totals from file, don't use this unless you know what you are doing
\SDV($TotalCounterReferersUnset, ''); // used to remove individual totals from file, don't use this unless you know what you are doing
##*/

\SDV($HTMLStylesFmt[TOTALCOUNTERNAME],
      '.TCbar {background-color:$TotalCounterBarColor; min-height:13px; width:13px; color:#fff;}' .NL
    . '.TCtxtr {text-align:right;}' .NL
    . '.TCtxtl {text-align:left;}' .NL
    . '.TCtxth {font-weight: bold;}' .NL
    . '.TCprogress {margin-left:auto; margin-right:auto;}' . NL
    . 'table.totalcounter td {font-size:x-small; text-align:left}' . NL); 
    
\SDVA($TotalCounterMonthsShort,
    array('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'));

\SDV($TotalCounterBlacklist[KEYPAGES], array ());
\SDV($TotalCounterBlacklist[KEYUSERS], array ());
\SDV($TotalCounterBlacklist[KEYBROWSERS], array ());
\SDV($TotalCounterBlacklist[KEYOSES], array ());
\SDV($TotalCounterBlacklist[KEYREFERERS], array ());
\SDV($TotalCounterBlacklist[KEYLOCATIONS], array ());
\SDV($TotalCounterBlacklist[KEYBOTS], array ());
\SDV($TotalCounterBlacklist[KEYLANGUAGES], array ());

## by MateuszCzaplinski
## last day, last week, ... - data & display descriptions
\SDVA($TotalCounterTimeBins, array(
    LASTDAY => array(             # LastDay = 24 hours; 1 hour = 60*60sec
        GRAPHNAME=>'$[Last] $[day] ($[hours])', 'max'=>24, 'atom'=>60*60, //
	    DATEFMT => function($now, $atom, $maxnr, $nr) { 
		    return date("H:00", $now - $atom * ($maxnr - 1 - $nr)); 
			}
	),
    LASTWEEK => array(            # LastWeek = 7 days
        GRAPHNAME=>'$[Last] $[week]', 'max'=>7,  'atom'=>24*60*60, //
		DATEFMT => function($now, $atom, $maxnr, $nr) { 
		    return date("D", $now - $atom * ($maxnr - 1 - $nr)); 
			}
		),
    LASTMONTH => array(
        GRAPHNAME=>'$[Last] $[month]', 'max'=>31, 'atom'=>24*60*60, //
		DATEFMT => function($now, $atom, $maxnr, $nr) { 
		    return date("j", $now - $atom * ($maxnr - 1 - $nr)); 
			}
		),
    LASTYEAR => array(            # date('n') is the month of the year
        GRAPHNAME=>'$[Last] $[year]', 'max'=>12, 'atom'=>'n', //
		DATEFMT => function($now, $atom, $maxnr, $nr) use ($TotalCounterMonthsShort) {
                     return $TotalCounterMonthsShort[(12 + intval(date($atom, $now)) - $maxnr + $nr) % 12];
                 }
		),
    PREVIOUSYEARS => array(
        GRAPHNAME=>'$[Previous] $[years]', 'max'=>30, 'atom'=>'Y', //
		DATEFMT => function($now, $atom, $maxnr, $nr) { 
		    return strval(intval(date($atom, $now)) - ($maxnr - 1 - $nr)); 
			}
		)
));

\SDVA($HandleActions, array (
    $TotalCounterActionName => __NAMESPACE__ . '\HandleTotalCounter'
));
\SDVA($HandleAuth, array (
    $TotalCounterActionName => $TotalCounterAuthLevel
));
global $TotalCounter;

\SDV($TotalCounterDebug, false); # set default debug setting
# set debug flag
$totalCounter_debugOn = boolval ($TotalCounterDebug); # if on writes input and output to web page
if ($totalCounter_debugOn) {
     tcmsg (__FILE__, $RecipeInfo[TOTALCOUNTERNAME]['Version'] . ' using "' . $WorkDir . '" with action=' . $action 
     . ', log=' . $TotalCounterEnableLog . ', IP lookup: '. $TotalCounterEnableLookup);
}
$TotalCounterLog        = boolval ($TotalCounterEnableLog > 0);
$TotalCounterLogVerbose = boolval ($TotalCounterEnableLog == 2);
if ($TotalCounterMaxItems <= 0)
    $TotalCounterMaxItems = 1;
# set up some debug actions for fixing the TotalCounter file
/*
if ($totalCounter_debugOn) {
    $HandleActions [ACTIONNAMECHECK] = __NAMESPACE__ . '\HandleTotalCounterCheck';
    $HandleAuth    [ACTIONNAMECHECK] = 'admin';
} # end if debug
*/
$statFileName = $TotalCounterFile;
$lockfilename = $TotalCounterLockfile;
$logfilename = $TotalCounterLogfile;
$psft = function_exists('\PSFT') ? '\PSFT' : 'strftime';
$logFileTime = $psft ("%Y-%m-%d %H:%M:%S "); # identify this run, note trailing space

$geoIpFile = $TotalCounterGeoIPData;
// clear cached information about file
clearstatcache();
// script to carry on working after the user has cancelled request or browser session closed
ignore_user_abort(true);

//------------------------------------------------------------------------------------

if ($TotalCounterLog) {
    $logfilehandle = fopen($logfilename, 'a'); # create or open logfile for appending
    if ($logfilehandle === false) {
        tcmsg ('fopen failed', 'File: ' . $logfilename, error_get_last());
        if ($totalCounter_debugOn) \Abort ($MessagesFmt [MSGFMTID]);
    }
} 
if ($TotalCounterLogVerbose) { # write for every call, this can be very verbose
  	$logMsg = 'Verbose: '; # initialise
	if (!empty ($_SERVER['HTTP_X_FORWARDED_FOR']))  $logMsg .= 'XFF:"' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '" ';
	if (!empty ($_SERVER['HTTP_X_FORWARDED_HOST'])) $logMsg .= 'XFH:"' . $_SERVER['HTTP_X_FORWARDED_HOST'] . '" ';
	if (!empty ($_SERVER['HTTP_FORWARDED']))        $logMsg .= 'Fw:"' .  $_SERVER['HTTP_FORWARDED'] . '" ';
	if (!empty ($_SERVER['HTTP_FORWARDED-HOST']))   $logMsg .= 'FH:"' .  $_SERVER['HTTP_FORWARDED-HOST'] . '" ';
	if (!empty ($_SERVER['REMOTE_HOST']))           $logMsg .= 'RH:"' .  $_SERVER['REMOTE_HOST'] . '" ';
	if (!empty ($_SERVER['REMOTE_ADDR']))           $logMsg .= 'RA:"' .  $_SERVER['REMOTE_ADDR'] . '" ';
	if (!empty ($_SERVER['HTTP_USER_AGENT']))       $logMsg .= 'UA:"' .  $_SERVER['HTTP_USER_AGENT'] . '" ';
	if (!empty ($_SERVER['HTTP_REFERER']))          $logMsg .= 'Rf:"' .  $_SERVER['HTTP_REFERER'] . '" ';
	if (!empty ($_SERVER['HTTP_USER_REFERER']))     $logMsg .= 'URf:"' . $_SERVER['HTTP_USER_REFERER'] . '" ';
	\Lock(2); # acquire exclusive lock
    $fwritestatus = fwrite($logfilehandle, $logFileTime . $logMsg . NL);
	\Lock(0); # release lock
    if (false === $fwritestatus) {
          tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
          if ($totalCounter_debugOn) \Abort ($MessagesFmt [MSGFMTID]);
     }
 }
$tcPageName = \ResolvePageName($pagename);
$tcPageExists = true;
switch (true) {
	case (empty($tcPageName)):
		$tcPageExists = false;
        $tcPageName = UNKNOWN; #"$DefaultGroup.$DefaultName";
		if ($TotalCounterLog) {
			\Lock(2); # acquire exclusive lock
			$fwritestatus = fwrite($logfilehandle, $logFileTime . 'Called with empty pagename: "' . $pagename . '"' . NL);
			\Lock(0); # release lock
			if ($fwritestatus === false) {
				tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
			}
		}
		break;
	case (\PageExists($tcPageName)): # PmWiki function
	    break;
    default: # pagename does not exist
	    $tcPageExists = false;
		if ($TotalCounterLog) {
			\Lock(2); # acquire exclusive lock
			$fwritestatus = fwrite($logfilehandle, $logFileTime . 'Called with non-existent page: "' . $tcPageName . '"' . NL);
			\Lock(0); # release lock
			if ($fwritestatus === false) {
				tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
			}
		}
} # end switch
if (!$tcPageExists) { # page does not exist
	return; #  finished TotalCounter processing <=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=<=
}
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// update counts from page being browsed
if ($action == 'browse') {

    //find users
    if (isset ($AuthId)) {
        $tc_user = $AuthId;
    } else {
        if (isset ($Author)) {
            $tc_user = $Author;
        } else {
            session_start();
            if (isset ($_SESSION['authid'])) {
                $tc_user = $_SESSION['authid'][0];
            } else {
                $tc_user = 'Guest (not authenticated)';
            } // end isset $_SESSION
        } // end if else isset $Author
    } // end if else isset $AuthId

## detect user agent
    $tc_bot = NULLUSERAGENT;
    $tc_browser = NULLUSERAGENT;
	$tcBotFound = false;
	$tcBrowserFound = false;
	switch (true) {
		case empty($_SERVER['HTTP_USER_AGENT']): # it happens often
			if ($TotalCounterLogVerbose) {
			  	$logMsg = 'UA empty: '; # initialise
				if (!empty ($_SERVER['REMOTE_HOST'])) $logMsg .= 'RH:"' . $_SERVER['REMOTE_HOST'] . '" ';
				\Lock(2); # acquire exclusive lock
				$fwritestatus = fwrite($logfilehandle, $logFileTime . $logMsg . '; action: "' . $action . '"' . NL);
				\Lock(0); # release lock
				if ($fwritestatus === false) {
					tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
				}
			}
			$tc_bot = USERAGENTEMPTY;
			$tc_browser = USERAGENTEMPTY;
			break;
		default: # user agent not empty
		    $tc_bot = detectWebBot ($_SERVER['HTTP_USER_AGENT']);
			if ($tc_bot !== NULLUSERAGENT) { # found a bot
				$tcBotFound = true; 
				break;
			}
		    $tc_browser = detectBrowser ($_SERVER['HTTP_USER_AGENT']);
			if ($tc_browser !== NULLUSERAGENT) {
				$tcBrowserFound = true;
				break;
			}
	    	$tc_browser = UNKNOWN;
		    $tcBrowserFound = false; # don't count unknown;
		    if ($TotalCounterLog) {
			  	$logMsg = 'Browser unknown: '; # initialise
				if (!empty ($_SERVER['HTTP_X_FORWARDED_FOR'])) $logMsg .= 'XFF:"' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '" ';
				if (!empty ($_SERVER['HTTP_FORWARDED']))       $logMsg .= 'Fw:"' .  $_SERVER['HTTP_FORWARDED'] . '" ';
				if (!empty ($_SERVER['HTTP_FORWARDED-HOST']))  $logMsg .= 'FH:"' .  $_SERVER['HTTP_FORWARDED-HOST'] . '" ';
				if (!empty ($_SERVER['REMOTE_HOST']))          $logMsg .= 'RH:"' .  $_SERVER['REMOTE_HOST'] . '" ';
				if (!empty ($_SERVER['REMOTE_ADDR']))          $logMsg .= 'RA:"' .  $_SERVER['REMOTE_ADDR'] . '" ';
				if (!empty ($_SERVER['HTTP_USER_AGENT']))      $logMsg .= 'UA:"' .  $_SERVER['HTTP_USER_AGENT'] . '" ';
  			    \Lock(2); # acquire exclusive lock
	  		    $fwritestatus = fwrite($logfilehandle, $logFileTime . $logMsg . NL);
		  	    \Lock(0); # release lock
			    if ($fwritestatus === false) {
				    tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
			    }
			}
	} # end switch
    // decide if we are counting this visit
    $tc_count_visit = ((!$tcBotFound) // don't count bots
                   || ($TotalCounterCountBots == 1)); // count bots (all visits)
  
    $tcOSFound = false;
    if ($tc_count_visit) { # don't count bots by default
      // find operating system
		if ($tc_browser == USERAGENTEMPTY) {
			$tc_os = USERAGENTEMPTY;
		} else {
			$tc_os = detectOS ($_SERVER['HTTP_USER_AGENT']);
            if ($tc_os == NULLUSERAGENT) {
				$tc_os = UNKNOWN;
				if ($TotalCounterLog) {
				    $logMsg = 'OpSystem unknown: '; # initialise
				    if (!empty ($_SERVER['HTTP_X_FORWARDED_FOR'])) $logMsg .= 'XFF:"' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '" ';
				    if (!empty ($_SERVER['HTTP_FORWARDED']))       $logMsg .= 'Fw:"' .  $_SERVER['HTTP_FORWARDED'] . '" ';
				    if (!empty ($_SERVER['HTTP_FORWARDED-HOST']))  $logMsg .= 'FH:"' .  $_SERVER['HTTP_FORWARDED-HOST'] . '" ';
				    if (!empty ($_SERVER['REMOTE_HOST']))          $logMsg .= 'RH:"' .  $_SERVER['REMOTE_HOST'] . '" ';
				    if (!empty ($_SERVER['REMOTE_ADDR']))          $logMsg .= 'RA:"' .  $_SERVER['REMOTE_ADDR'] . '" ';
				    if (!empty ($_SERVER['HTTP_USER_AGENT']))      $logMsg .= 'UA:"' .  $_SERVER['HTTP_USER_AGENT'] . '" ';
					\Lock(2); # acquire exclusive lock
					$fwritestatus = fwrite($logfilehandle, $logFileTime .  $logMsg . NL);
					\Lock(0); # release lock
					if ($fwritestatus === false) {
						tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
					}
				}
            } else {
				$tcOSFound = true;
			}
        }
    } // end find OS

	if ($tc_count_visit) { # don't count bots by default
		// find referrer domain
		$matches = [];
		$referer = '';
		$tc_referer = UNKNOWN;
		if (!empty ($_SERVER['HTTP_REFERER'])) {
			# remove the schema, see https://regex101.com/r/epmzHv/2
			if (1 == preg_match("/^(?:https?:\/\/)?([^\/:\r\n]+)/", $_SERVER['HTTP_REFERER'], $matches)) {
				$referer = $matches[1];
			}
		}
		if (!empty($referer)) {
			$tc_referer = $referer;
		}
		if ($tc_referer == UNKNOWN) {
			if ($tcBotFound) { # skip referer if it is a bot and referer not identified // 1.10.0
				unset ($tc_referer);
			}; // end !empty $tc_bot
			if ($TotalCounterLog and !empty($_SERVER['HTTP_REFERER'])) {
			    $logMsg = 'Referer unknown: '; # initialise
			    if (!empty ($_SERVER['HTTP_X_FORWARDED_FOR'])) $logMsg .= 'XFF:"' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '" ';
			    if (!empty ($_SERVER['HTTP_FORWARDED']))       $logMsg .= 'Fw:"' .  $_SERVER['HTTP_FORWARDED'] . '" ';
			    if (!empty ($_SERVER['HTTP_FORWARDED-HOST']))  $logMsg .= 'FH:"' .  $_SERVER['HTTP_FORWARDED-HOST'] . '" ';
			    if (!empty ($_SERVER['REMOTE_HOST']))          $logMsg .= 'RH:"' .  $_SERVER['REMOTE_HOST'] . '" ';
			    if (!empty ($_SERVER['REMOTE_ADDR']))          $logMsg .= 'RA:"' .  $_SERVER['REMOTE_ADDR'] . '" ';
			    if (!empty ($_SERVER['HTTP_USER_AGENT']))      $logMsg .= 'UA:"' .  $_SERVER['HTTP_USER_AGENT'] . '" ';
				\Lock(2); # acquire exclusive lock
				$fwritestatus = fwrite($logfilehandle, $logFileTime . $logMsg . ' ^' . $referer . '^ ["' . implode ('", "', $matches) . '"] ' . NL);
				\Lock(0); # release lock
				if ($fwritestatus === false) {
					tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
				}
			}
		} // end find referrer
	} // end count visit referrer
	if ($tc_count_visit) { # don't count bots by default
		// find location
		$dbgloc = '';
		# de-facto standard header for identifying the originating IP address of a client connecting to a web server through an HTTP proxy or a load balancer
		$dbgloc = ''; # initialise
		$thehost = ''; # initialise
		switch (true) {
			case (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])):
			    $dbgloc .= 'XFF';
			    if (false !== strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',')) { 
			    	$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
			    	$thehost = trim($ips[0]); # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
		    	} else {
			    	$thehost = $_SERVER['HTTP_X_FORWARDED_FOR'];
			    }  
				break;
            case (!empty($_SERVER['HTTP_FORWARDED'])):
		    	$dbgloc = 'Fw';
		    	$posn = strpos($_SERVER['HTTP_FORWARDED'], 'for=');
			    if (false !== $posn) { 
			    	$ips = explode(';', substr($_SERVER['HTTP_FORWARDED'], $posn + 4)); #for=192.0.2.60;proto=http
			    	$ips = explode(',', $ips[0]); # 192.0.2.43, for=198.51.100.17
		    		$thehost = trim($ips[0]);
		    	} else {
		    		$thehost = $_SERVER['HTTP_FORWARDED'];
		    	}     
                break;	
            case (!empty($_SERVER['REMOTE_HOST'])):
		        if (false !== strpos($_SERVER['REMOTE_HOST'], ',')) { 
			         $dbgloc = 'RH+';
			         # empty($_SERVER['HTTP_X_FORWARDED_FOR']) and empty($_SERVER['HTTP_FORWARDED'])
			         $ips = explode(',', $_SERVER['REMOTE_HOST']);
			         $thehost = trim($ips[0]);       
		        } else {
			        $dbgloc = 'RH';
			        $thehost = $_SERVER['REMOTE_HOST'];
		        }
				break;
			default: # no header information !
			    break;
		} # end switch
/* this code replaced by the above switch statement 
		if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
			$dbgloc .= 'XFF';
			if (false !== strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',')) { 
				$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
				$thehost = trim($ips[0]); # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
			} else {
				$thehost = $_SERVER['HTTP_X_FORWARDED_FOR'];
			}  
		} elseif (!empty($_SERVER['HTTP_FORWARDED'])) {
			$dbgloc = 'Fw';
			$posn = strpos($_SERVER['HTTP_FORWARDED'], 'for=');
			if (false !== $posn) { 
				$ips = explode(';', substr($_SERVER['HTTP_FORWARDED'], $posn + 4)); #for=192.0.2.60;proto=http
				$ips = explode(',', $ips[0]); # 192.0.2.43, for=198.51.100.17
				$thehost = trim($ips[0]);
			} else {
				$thehost = $_SERVER['HTTP_FORWARDED'];
			}      
		} elseif (false !== strpos($_SERVER['REMOTE_HOST'], ',')) { 
			 $dbgloc = 'RH+';
			 # empty($_SERVER['HTTP_X_FORWARDED_FOR']) and empty($_SERVER['HTTP_FORWARDED'])
			 $ips = explode(',', $_SERVER['REMOTE_HOST']);
			 $thehost = trim($ips[0]);       
		} else {
			$dbgloc = 'RH';
			$thehost = $_SERVER['REMOTE_HOST'];
		}
*/
		// match any character not digits or period backwards from end of string
		if (1 == preg_match("/[^\.0-9]+$/", $thehost, $matches)) { 
			$loc = $matches[0];
		}
		$tc_location = UNKNOWN;
		while (true) {
			if (!empty($loc)) {
				$dbgloc .= '=';
				$tc_location = $loc;
				break;
			}
			if ($TotalCounterEnableLookup == 1) {
				$hostbyaddr = gethostbyaddr($_SERVER['REMOTE_ADDR']);
				$dbgloc .= 'L^' . $hostbyaddr . '^';
				if (false === $hostbyaddr) {
					$tc_location = UNKNOWN;
				} else {
					$prmRetVal = preg_match("/[^\.0-9]+$/", $hostbyaddr, $matches); // match any character not digits or period (IP address?)
					switch (true) {
						case ($prmRetVal === false): # error occurred
							$tc_location = UNKNOWN;
							break;
						case ($prmRetVal == 1): # match found
							$gloc = $matches[0];
							break;
						case ($prmRetVal == 0): # match not found
						   $tc_location = UNKNOWN;
						   break;
					} # end switch
					if (!empty($gloc)) {
						$tc_location = $gloc;
						break;
					}
				}
			}
			if ($TotalCounterEnableGeoIp == 1) {  
				$dbgloc .= 'G';
				include ('geoip/geoip.inc'); # return the two letter country code corresponding to a hostname or an IP address
				$gi = geoip_open($geoIpFile, GEOIP_STANDARD); # https://www.php.net/manual/en/function.geoip-country-code-by-name.php
				$gccba = geoip_country_code_by_addr($gi, $_SERVER['REMOTE_ADDR']);
				geoip_close($gi);
				if (!false === $gccba) {
					$tc_location = $gccba;
					break;
				}                
			}
			# https://regex101.com/r/ONcmD7/1
			if (1 == preg_match("/(?:10\.[0-9]{1,3}|172\.(?:1[6-9]|2[0-9]|3[0-1])|192\.168)(?:\.[0-2]?[0-9]{1,2}){2}/", $thehost, $matches)) {
				$dbgloc .= 'P';
				# match for private IP address https://en.wikipedia.org/wiki/Private_network
				$tc_location = 'Private IP (' . stristr ($matches [0], '.', true) . ')';
				break;
			}
			$dbgloc .= '.';
			break; 
		} # end while true
		if ($tc_location == UNKNOWN) {
			if ($tcBotFound) { # skip location if it is a bot and location not identified
				unset ($tc_location);
			} elseif ($TotalCounterLogVerbose) { # this happens so often we log only if verbose 
			    $logMsg = 'Location unknown: '; # initialise
				if (!empty ($_SERVER['HTTP_X_FORWARDED_FOR'])) $logMsg .= 'XFF:"' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '" ';
				if (!empty ($_SERVER['HTTP_FORWARDED']))       $logMsg .= 'Fw:"' .  $_SERVER['HTTP_FORWARDED'] . '" ';
				if (!empty ($_SERVER['HTTP_FORWARDED-HOST']))  $logMsg .= 'FH:"' .  $_SERVER['HTTP_FORWARDED-HOST'] . '" ';
				if (!empty ($_SERVER['REMOTE_HOST']))          $logMsg .= 'RH:"' .  $_SERVER['REMOTE_HOST'] . '" ';
				if (!empty ($_SERVER['REMOTE_ADDR']))          $logMsg .= 'RA:"' .  $_SERVER['REMOTE_ADDR'] . '" ';
				# if (!empty ($_SERVER['HTTP_USER_AGENT']))      $logMsg .= 'UA:"' .  $_SERVER['HTTP_USER_AGENT'] . '" '; # don't lok this variable
				\Lock(2); # acquire exclusive lock
				$fwritestatus = fwrite($logfilehandle, $logFileTime . $logMsg . ' (' . $thehost . ') ' . $dbgloc . NL);
				\Lock(0); # release lock
				if ($fwritestatus === false) {
				  tcmsg ('fwrite failed', 'File: ' . $logfilename, error_get_last());
				}
			}    
		} else { //end = Unknown
		    $tc_location = strtolower($tc_location);
		    $tc_location = str_ireplace ([UNKNOWN, 'private ip'], [UNKNOWN, 'Private IP'], $tc_location);
		}
	} // end count visits location
} // end if action = browse
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

$oldumask = umask(0); # set the default file permissions for newly created files and directories

$TotalCounterDownloads = FALSE;
if ($TotalCounterEnableDownload == 1) {
  $downloadfile = $TotalCounterDownloadManager;
  if (file_exists($downloadfile)) {
      $TotalCounterDownloadsFileContents = tc_file_get_contents($downloadfile);
      if (FALSE === $TotalCounterDownloadsFileContents) {
          tcmsg ('tc_file_get_contents failed', 'File: ' . $downloadfile, error_get_last()); 
	  } else {
          $TotalCounterDownloads = unserialize($TotalCounterDownloadsFileContents, ['allowed_classes' => false]);
          if (FALSE === $TotalCounterDownloads)
              tcmsg ('unserialize failed', 'File: ' . $downloadfile, error_get_last());
	  }
  }
}

if (file_exists($statFileName)) {
    $TotalCounterFileContents = tc_file_get_contents($statFileName);
    if (FALSE === $TotalCounterFileContents) {
        tcmsg ('tc_file_get_contents failed', 'File: ' . $statFileName, error_get_last());
		echo $MessagesFmt [MSGFMTID];
		return $MessagesFmt [MSGFMTID]; # failed to read file, lets get out of here
    }
    $TotalCounter = unserialize($TotalCounterFileContents, ['allowed_classes' => false]);
    if (FALSE === $TotalCounter) {
        tcmsg ('tc_file unserialize failed', 'File: ' . $statFileName, error_get_last());
		echo $MessagesFmt [MSGFMTID];
		return $MessagesFmt [MSGFMTID]; # failed to unserialize file, lets get out of here
    }
} else { # stat file does not exist
    touch($statFileName); # create the stat file
	$TotalCounter = [];
	$TotalCounter[KEYDATECREATED] = date ('c'); # ISO 8601 format
    $TotalCounter[KEYTOTAL] = 0;
    $TotalCounter[KEYPAGES][$tcPageName] = 0;
}
#
$PageCount = 0; # initialise
$TotalCount = 0; # initialise
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
if (($action == 'browse') && ($tcPageExists)) {
    if( dblock($statFileName) ) {
        $TotalCounterFileContents = tc_file_get_contents($statFileName);
        if (FALSE === $TotalCounterFileContents) {
            tcmsg ($statFileName . 'tc_file_get_contents failed', 'File: ' . $statFileName, error_get_last());
		}
        $TotalCounter = unserialize($TotalCounterFileContents, ['allowed_classes' => false]);
        if (FALSE === $TotalCounter) {
            tcmsg ('tc_file unserialize failed', 'File: ' . $statFileName, error_get_last());
		}
/*##
        // code intended only for developers to clean up entries in the stats file
        // use at your own risk
        if ($totalCounter_debugOn) {
            if (!empty ($TotalCounterBrowsersUnset)) {
                if (isset($TotalCounter[KEYBROWSERS][$TotalCounterBrowsersUnset])) {
                    unset ($TotalCounter[KEYBROWSERS][$TotalCounterBrowsersUnset]);
                    tcmsg ('unset Browser', $TotalCounterBrowsersUnset);
                }
            } # end Browsers
            if (!empty ($TotalCounterOSesUnset)) {
                if (isset($TotalCounter[KEYOSES][$TotalCounterOSesUnset])) {
                    unset ($TotalCounter[KEYOSES][$TotalCounterOSesUnset]);
                    tcmsg ('unset OS', $TotalCounterOSesUnset);
                }
            } # end OSes
            if (!empty ($TotalCounterLocationsUnset)) {
                if (isset($TotalCounter[KEYLOCATIONS][$TotalCounterLocationsUnset])) {
                    unset ($TotalCounter[KEYLOCATIONS][$TotalCounterLocationsUnset]);
                    tcmsg ('unset Location', $TotalCounterLocationsUnset);
                }
            } # end Locations
            if (!empty ($TotalCounterBotsUnset)) {
                if (isset($TotalCounter[KEYBOTS][$TotalCounterBotsUnset])) {
                    unset ($TotalCounter[KEYBOTS][$TotalCounterBotsUnset]);
                    tcmsg ('unset Bot', $TotalCounterBotsUnset);
                }
            } # end Bots
            if (!empty ($TotalCounterReferersUnset)) {
                if (isset($TotalCounter[KEYREFERERS][$TotalCounterReferersUnset])) {
                    unset ($TotalCounter[KEYREFERERS][$TotalCounterReferersUnset]);
                    tcmsg ('unset Referer', $TotalCounterReferersUnset);
                }
            } # end Referers
        } # $totalCounter_debugOn
        // end clean up code
##*/
        $TotalCount = ++ $TotalCounter[KEYTOTAL];

		$blacklisted = false;
		
		if (in_array($tc_user, $TotalCounterBlacklist[KEYUSERS]))
			$blacklisted = true;

        if (!$blacklisted && !in_array($tc_user, $TotalCounterBlacklist[KEYUSERS])) {
            if (is_array($TotalCounterBlacklist[KEYUSERS])) {
                foreach ($TotalCounterBlacklist[KEYUSERS] as $value)
                    if (substr($value, 0, 1) == '/' && preg_match($value, $tc_user) > 0)
						$blacklisted = true;
			}
            if (isset ($tc_user)) {
                incrementCount(KEYUSERS, $tc_user);
		    }
        }

        if (!$blacklisted && !in_array($tcPageName, $TotalCounterBlacklist[KEYPAGES])) {
            if (is_array($TotalCounterBlacklist[KEYPAGES]))
                foreach ($TotalCounterBlacklist[KEYPAGES] as $value)
                    if (substr($value, 0, 1) == '/')
                        if (preg_match($value, $tcPageName) > 0)
                            $blacklisted = true;

            if (!$blacklisted) {
				if  (empty ($TotalCounter[KEYPAGES][$tcPageName])) { # initialise
					$TotalCounter[KEYPAGES][$tcPageName] = 0;
					$TotalCounter[KEYPAGESTODAYDAY][$tcPageName] = date("%y%m%d");
					$TotalCounter[KEYPAGESTODAYCOUNTER][$tcPageName] = 0;
				}
                $PageCount = ++ $TotalCounter[KEYPAGES][$tcPageName];
                ## handles the daily counter
                if ($TotalCounter[KEYPAGESTODAYDAY][$tcPageName] == date("%y%m%d"))
                    $PageCountToday = ++ $TotalCounter[KEYPAGESTODAYCOUNTER][$tcPageName];
                else {
                    $TotalCounter[KEYPAGESTODAYDAY][$tcPageName] = date("%y%m%d");
                    $TotalCounter[KEYPAGESTODAYCOUNTER][$tcPageName] = 1;
                }
            } else {
                $PageCount = 0; // blacklisted
            }
        }

        if (!$blacklisted && defined('MULTILANGUAGE')) {
            if (isset ($userlang2)) {
                incrementCount(KEYLANGUAGES, $userlang2);
			}
		}
        if (!$blacklisted && ($tcBrowserFound) && !in_array($tc_browser, $TotalCounterBlacklist[KEYBROWSERS])) {
            incrementCount(KEYBROWSERS, $tc_browser);
        }
        if (!$blacklisted && ($tcBotFound) && !in_array($tc_bot, $TotalCounterBlacklist[KEYBOTS])) {
            incrementCount(KEYBOTS, $tc_bot);
		}
        if (!$blacklisted && ($tcOSFound) && !in_array($tc_os, $TotalCounterBlacklist[KEYOSES])) { // 1.10.0 isset
            incrementCount(KEYOSES, $tc_os);
        }
        switch (true) {
            case $blacklisted: break;
            case !isset ($TotalCounterBlacklist[KEYREFERERS]): break;
            case !isset ($tc_referer): break;
            case !is_array($TotalCounterBlacklist[KEYREFERERS]): break;
            case in_array($tc_referer, $TotalCounterBlacklist[KEYREFERERS]): break;
            default:
               foreach ($TotalCounterBlacklist[KEYREFERERS] as $value) {
                   if (substr($value, 0, 1) == '/')
                        if (preg_match($value, $tc_referer) > 0)
                            $blacklisted = true;
               }
            if (!$blacklisted)
                incrementCount(KEYREFERERS, $tc_referer);
        } # end switch
# 
        if (!$blacklisted && isset ($tc_location) && !in_array($tc_location, $TotalCounterBlacklist[KEYLOCATIONS])) { // 1.10.0 isset
            incrementCount(KEYLOCATIONS, $tc_location);
        }

        if (!$blacklisted && defined('MULTILANGUAGE')) {
            if (! in_array($tc_location, $TotalCounterBlacklist[KEYLANGUAGES]))
                incrementCount(KEYLANGUAGES, $userlang2);
		}
        ## by MateuszCzaplinski
        ## last day, last week, ... - collect data
        if (!$blacklisted && (!$tcBotFound)) { // don't count if bot
            $TCnow = time(); # fix current time for duration of processing
            foreach ($TotalCounterTimeBins as $n=>$a)
                TCbins($n, $a['max'], $a['atom'], $TCnow);
            $TotalCounter[KEYLASTTIMESTAMP] = $TCnow;
            $TotalCounter[KEYTOTALCOUNTERVERSION] = TOTALCOUNTERV;
        }

        dbexport_unlock($statFileName, serialize($TotalCounter), 'w');
    } else { // could not acquire a lockfile
        // check if the lockfile isn't a stale one, try to delete it if so
        dblock_remove_stale($statFileName);
    } # end if browse
} else {
    $TotalCount = $TotalCounter[KEYTOTAL];
    $PageCount = empty ($TotalCounter[KEYPAGES][$tcPageName]) ? 0 : $TotalCounter[KEYPAGES][$tcPageName];
    ## by Schlaefer (==?) - fixed
    incrementCount(KEYPAGESTODAYCOUNTER, $tcPageName);
    if (empty ($TotalCounter[KEYPAGESTODAYDAY][$tcPageName])) {
        $TotalCounter[KEYPAGESTODAYDAY][$tcPageName] = date("%y%m%d");
	}
}
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
	//add the {$PageCount} and {$TotalCount} markup
	$FmtPV['$PageCount'] = "'" . number_format ($PageCount) . "'"; // return count for current page
	$FmtPV['$TotalCount'] = "'" . number_format ((float) $TotalCount) . "'"; // return total count for wiki

	## by Schlaefer
	## adds vars for the input form
	$FmtPV['$TotalCounterMaxItems'] = "'" . (!empty($_REQUEST['TotalCounterMaxItems']) ? $_REQUEST['TotalCounterMaxItems'] : $TotalCounterMaxItems) . "'"; # 

	//add the {$PageViews} page variable (this appears to duplicate $PageCount above)
	$FmtPV['$PageViews'] = 'number_format ($GLOBALS["TotalCounter"]["Pages"][$pagename])'; # allows use in PageLists

	## by Schlaefer
	## add the {$PagesTodayCounter} page variable
	$FmtPV['$PageCountToday'] = 'number_format ($GLOBALS["TotalCounter"]["PagesTodayCounter"][$pagename])'; # allows use in PageLists
	return; # finished TotalCounter processing
//=====================================================================================================================
    function incrementCount (string $countType, $counter) {
	// create variable if it does not exist to avoid PHP 8 errors
	# counter may be of type string or int
		global $TotalCounter;
        if (empty($TotalCounter[$countType])) { // If not, initialise it as an empty array
            $TotalCounter[$countType] = [];
        }
        if (empty ($TotalCounter[$countType][$counter])) {
            $TotalCounter[$countType][$counter] = 1;
        } else {
            $TotalCounter[$countType][$counter]++;
        }
    } # end incrementCount
//=====================================================================================================================
function HandleTotalCounter(string $pagename, string $auth = 'read') {
	// handle PmWiki action=totalcounter
    global $action, $TotalCounter, $TotalCounterMaxItems, $TotalCounterBarColor, $TotalCounterShowNumbers;
    global $TotalCount, $TotalCounterEnableDownload, $TotalCounterDownloads, $TotalCounterTimeBins, $TotalCounterBinsFmt, $TotalCounterEnableUsers;
    global $PageStartFmt, $PageEndFmt, $MessagesFmt, $totalCounter_debugOn, $TotalCounterLog, $logfilehandle, $tcPageExists;
    
    //$page = RetrieveAuthPage($pagename, $auth, true, READPAGE_CURRENT);
    $page = \RetrieveAuthPage($pagename, $auth); # PmWiki function
    if (!$page) {
        \Abort('?you are not permitted to perform this action "' . $action . '"'); # PmWiki function
	}
    if (!$tcPageExists) {
        \Abort('?action "'. $action . '" is not available on a non-existent page "' . $pagename . '"'); # PmWiki function
	}
	if ((FALSE === $TotalCounter)) {
        \Abort('?TotalCounter statistics could not be retrieved or unserialized'); # PmWiki function
	}
# 
    $all_locations = SetAllLocations (); # top level domains

    #$Action = 'TotalCounter statistics';

    ## by Schlaefer
    ## sets the max items if provided by the form
    if (array_key_exists ('TotalCounterMaxItems', $_REQUEST))
        $TotalCounterMaxItems = $_REQUEST['TotalCounterMaxItems'];
    $html = '<section class="totalcounter"> <h1>Total Counter $[statistics]</h1>' . NL;

    //------------------------------------------------------------------------------------------------------------
    // PAGES

    $html .= graphHead ('$[Page] $[views]', true);

    arsort($TotalCounter[KEYPAGES]); // sort high to low
    $tar  = array_slice($TotalCounter[KEYPAGES], 0, $TotalCounterMaxItems);
    $tar2 = array_slice($TotalCounter[KEYPAGES], $TotalCounterMaxItems, $TotalCounterMaxItems);
    $tot  = $TotalCount;
    $max  = current($tar);

    $i = 0;
    if (is_array($tar) && $tot) // by Florian Xaver
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, true, $cnt, $tot, $max, ++ $i);
        }
    $html .= graphHead ('', false);
    
    if (is_array($tar2)) {
        $html .= '<details>';
        $html .= '<summary>' . '$[More] $[pages]' . '</summary>' . NL;
        $html .= graphHead ('$[More] $[pages]', true);
        foreach ($tar2 as $pn => $cnt) {
            $html .= graphLine ($pn, true, $cnt, $tot, $max, ++ $i);
        }
        $html .= graphHead ('', false);
        $html .= '</details>';
    }

    ## by Schlaefer

    //------------------------------------------------------------------------------------------------------------
    ## PAGES daily

    $html .= graphHead ('$[Page] $[views] $[today]', true);

    $pageviews = array ();
    foreach ($TotalCounter[KEYPAGESTODAYCOUNTER] as $pn => $cnt) {
        if ($TotalCounter[KEYPAGESTODAYDAY][$pn] === date("%y%m%d"))
            $pageviews[$pn] = $cnt;
    }
    arsort($pageviews);
    $tot  = array_sum($pageviews);
    $tar  = array_slice($pageviews, 0, $TotalCounterMaxItems);
    $tar2 = array_slice($pageviews, $TotalCounterMaxItems, $TotalCounterMaxItems);
    $max  = current($tar);

    $i = 0;
    if (is_array($tar))
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, true, $cnt, $tot, $max, ++ $i);
        }
    $html .= graphHead ('', false); 
    
    if (is_array($tar2)) {
        $html .= '<details>';
        $html .= '<summary>' . '$[More] $[Page] $[views]' . '</summary>' . NL;
        $html .= graphHead ('$[More] $[page] $[views]', true);
        foreach ($tar2 as $pn => $cnt) {
            $html .= graphLine ($pn, true, $cnt, $tot, $max, ++ $i);
        }
        $html .= graphHead ('', false);
        $html .= '</details>';
    }

    //------------------------------------------------------------------------------------------------------------
    // USERS
    # TotalCounterEnableUsers
    if ($TotalCounterEnableUsers == 1) { 
        $html .= graphHead ('$[Users]', true);

        arsort($TotalCounter[KEYUSERS]);
        $tar = array_slice($TotalCounter[KEYUSERS], 0, $TotalCounterMaxItems);
        $max = current($tar);
        $tot = array_sum($TotalCounter[KEYUSERS]);

        $i = 0;
        if (is_array($tar))
            foreach ($tar as $pn => $cnt) {
                $html .= graphLine ($pn, $cnt, $tot, $max, ++ $i);
        }
        $html .= graphHead ('', false); 
    }
    //------------------------------------------------------------------------------------------------------------
    // LANGUAGES

    if (defined('MULTILANGUAGE')) {
        $html .= graphHead ('$[Languages]', true);

        arsort($TotalCounter[KEYLANGUAGES]);
        $tar = array_slice($TotalCounter[KEYLANGUAGES], 0, $TotalCounterMaxItems);
        $max = current($tar);
        $tot = array_sum($TotalCounter[KEYLANGUAGES]);

        $i = 0;
        if (is_array($tar))
            foreach ($tar as $pn => $cnt) {
                $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
            }
    $html .= graphHead ('', false);
    }

    //------------------------------------------------------------------------------------------------------------
    // BROWSERS

    $html .= graphHead ('$[Browsers]', true);

    if (is_array($TotalCounter[KEYBROWSERS])) {
        arsort ($TotalCounter[KEYBROWSERS]);
        $tot = array_sum($TotalCounter[KEYBROWSERS]);
        $tar = array_slice($TotalCounter[KEYBROWSERS], 0, $TotalCounterMaxItems);
    } else { # should never happen
        $tar = [];
        $tot = 0;
    }
    $max = current($tar);
    
    $i = 0;
    if (is_array($tar))
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
        }
    $html .= graphHead ('', false);

    //------------------------------------------------------------------------------------------------------------
    // OPERATING SYSTEMS

    $html .= graphHead ('$[Operating systems]', true);

    if (is_array($TotalCounter[KEYOSES])) {
        arsort($TotalCounter[KEYOSES]);
        $tar = array_slice($TotalCounter[KEYOSES], 0, $TotalCounterMaxItems);
        $tot = array_sum($TotalCounter[KEYOSES]);
    } else { # should never happen
        $tar = [];
        $tot = 0;
    }
    $max = current($tar);

    $i = 0;
    if (is_array($tar))
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
        }
    $html .= graphHead ('', false);

    //------------------------------------------------------------------------------------------------------------
    // REFERERS

    $html .= graphHead ('$[Referers]', true);

    if (is_array($TotalCounter[KEYREFERERS])) {
        arsort($TotalCounter[KEYREFERERS]);
        $tar = array_slice($TotalCounter[KEYREFERERS], 0, $TotalCounterMaxItems);
        $tot = array_sum($TotalCounter[KEYREFERERS]);
    } else { # should never happen
        $tar = [];
        $tot = 0;
    }
    $max = current($tar);

    $i = 0;
    if (is_array($tar))
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
        }
    $html .= graphHead ('', false);

    //------------------------------------------------------------------------------------------------------------
    // LOCATIONS

    $html .= graphHead ('$[Locations]', true);

    if (is_array($TotalCounter[KEYLOCATIONS])) {
        arsort($TotalCounter[KEYLOCATIONS]);
        $tar = array_slice($TotalCounter[KEYLOCATIONS], 0, $TotalCounterMaxItems);
        $tot = array_sum($TotalCounter[KEYLOCATIONS]);
    } else { # should never happen
        $tar = [];
        $tot = 0;
    }
    $max = current($tar);

    $i = 0;
    if (is_array($tar))
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
        }
    $html .= graphHead ('', false);

    //------------------------------------------------------------------------------------------------------------
    // WEB BOTS

    $html .= graphHead ('$[Web bots]', true);

    arsort($TotalCounter[KEYBOTS]);
    $tar = array_slice($TotalCounter[KEYBOTS], 0, $TotalCounterMaxItems);
    $tar2 = array_slice($TotalCounter[KEYBOTS], $TotalCounterMaxItems, $TotalCounterMaxItems);
    $max = current($tar);
    $tot = array_sum($TotalCounter[KEYBOTS]);

    $i = 0;
    if (is_array($tar)) {
        foreach ($tar as $pn => $cnt) {
            $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
        }
    }
    $html .= graphHead ('', false);
   
    if (is_array($tar2)) {
        $html .= '<details>';
        $html .= '<summary>' . '$[More] $[Web bots]' . '</summary>' . NL;
        $html .= graphHead ('$[More] $[Web bots]', true);
        foreach ($tar2 as $pn => $cnt) {
            $html .= graphLine ($pn, false, $cnt, $tot, $max, ++ $i);
        }
        $html .= graphHead ('', false);
        $html .= '</details>';
    }
    
//------------------------------------------------------------------------------------------------------------
    // Downloads

    if ($TotalCounterEnableDownload == 1) { 
        $html .= graphHead ('$[File] $[downloads]', true);

        arsort($TotalCounterDownloads);
        $max = count($TotalCounterDownloads);
        $tot = array_sum($TotalCounterDownloads);

        $i = 0;
        if (is_array($TotalCounterDownloads)) {
            for ($row = 0; $row < $max; $row++) {
                $tablerow = each($TotalCounterDownloads);
                $value = $tablerow['value'];
                $html .= '<tr>' .
                   ($TotalCounterShowNumbers ? TD1 . ++ $i . '.</td>' : '') .
                    '<td>' . $tablerow['key'] . '</td>' .
                    '<td></td>' .
                    '<td></td><td align="right">&nbsp;' . $value . '</td>' .
                    '</tr>' . NL;
            }
        }
        $html .= graphHead ('', false);
    }

//------------------------------------------------------------------------------------------------------------
    // Time statistics
    ## by MateuszCzaplinski
    
    foreach( $TotalCounterTimeBins as $n=>$a ) {
        $name = $a[GRAPHNAME];
        $dateFmt = $a[DATEFMT];
        $html .= BR . '<hr />' . NL .
          '<article><h2>' . $name. '</h2>' . NL .
          '<table border="0" class="totalcounterstat TC-' . $n. '"><thead>' . NL .
          '<tr><th class="TCtxth TCtxtl" colspan=2>' . $name . '&nbsp;</th>' . 
          '<th class="TCtxth TCtxtr">$[Count]</th></tr>' . NL . 
          '</thead><tbody>' . NL;
		  

    $rows = []; # initialise
    $maxcount = max($TotalCounter[$n]);
    $direction = 'width';
    $maxnr = $a['max'];
    $atom = $a['atom'];

    for ($nr = 0; $nr < $maxnr; $nr++) {
		if (!isset ($TotalCounter[$n][$nr])) continue; # does not exist
        $count = $TotalCounter[$n][$nr];
		$row = ''; # initialise
        if (($n !== PREVIOUSYEARS) || ($n === PREVIOUSYEARS && $count > 0)) {
			# should improve this to cater for varying number of days in a month
            // Directly evaluate the format function to display date column
            $output = $dateFmt(time(), $atom, $maxnr, $nr);
            $row .= "<td valign='bottom' class='TCtxtr'>&nbsp;" . $output . '</td>' . NL; 
            $row .= "<td valign='bottom'>" . NL;
			$row .= "<div class=\"TCbar\" style=\"$direction:" . Round(1 + BARMAXWIDTH * (($maxcount) ? ($count / $maxcount) : 0)) . "px;\"></div>" . NL;
			$row .= "</td>" . NL;
			# display count column
            $row .= "<td valign='bottom' class='TCtxtr'>" . number_format($count) . '</td>' . NL;
            $rows[] = $row;
        }
    } # end for $nr
        $html .= '<tr>'.implode('</tr>'.NL.'<tr>',$rows).'</tr></tbody></table></article>' . NL; // 1.10.0
    }
//------------------------------------------------------------------------------------------------------------

    if ($totalCounter_debugOn) {
        $html .= '<hr /><h2>Messages</h2>' . NL . $MessagesFmt [MSGFMTID] . NL;
    }
        
    $html .= '<hr /><p style="text-align:right; font-size:smaller;">TotalCounter ' . TOTALCOUNTERV . '</p></section>' . NL; 

    \PrintFmt($pagename, array ( # PmWiki function
        & $PageStartFmt,
        $html,
        & $PageEndFmt
    ));

    if ($TotalCounterLog) {
        $fclosestatus = fclose($logfilehandle);
        if ($fclosestatus === false) {
            tcmsg ('fclose failed', '"' . 'File: ' . $logfilename, error_get_last());
            if ($totalCounter_debugOn) \Abort ($MessagesFmt [MSGFMTID]);
        }
    }
    return;
} # end HandleTotalCounter
//
## by MateuszCzaplinski
## Manages an array of counters, each for a specified time interval.
## In $TotalCounter[$name] array there are $max counters. Each counter
## is for time interval of $atom length.
## Note: if $atom is a number, it is a length of interval measured
## in seconds. If $atom is a string, it means date($atom) is executed
## and the result is the index of an interval.
## NOTE: See TODO below
//=====================================================================================================================
	function tc_file_get_contents ($getFileName) {
		\Lock(1); # acquire shared lock
        $contents = file_get_contents ($getFileName);
		\Lock(0); # release lock
		return $contents; 
	} # end function tc_file_get_contents
//=====================================================================================================================
	function TCbins(string $name, int $max, $atom, $TCnow) {
		global $TotalCounter;
		$lastTS = $TotalCounter[KEYLASTTIMESTAMP];
		if( $TCnow < $lastTS ) return; // some error?
		if( !$lastTS ) $TotalCounter[$name] = array_fill(0,$max,0);
		if( is_string($atom) ) {
			$diff = intval (date($atom,$TCnow)) - intval (date($atom,$lastTS));
			if( $diff < 0 ) $diff += $max;
			# TODO: handle time delta > $max
			# Until fixed, if the site has no visitor for about a
			# year, statistics will get falsified (empty years will compress)
		}
		else
			$diff = intval ($TCnow/$atom) - intval ($lastTS/$atom);
			
		if( $diff < 0 ) return;
		if( $diff > 0 ) {
			$a = array_slice($TotalCounter[$name], $diff, max(0,$max-$diff));
			if(!$a) $a = array();
			$a = array_pad($a, $max, 0);
			$TotalCounter[$name] = $a;
		}
		incrementCount($name, $max-1); // put our visit in last bin
	} # end function TCbins
//=====================================================================================================================
// generate graph head and tail
    function graphHead (string $headline='', bool $heading=true):string {
        global $TotalCounterShowNumbers;
		$retval = '';
        if ($heading) { # create table heading
            $retval .=  BR . '<hr /><article><h2>' . $headline . '</h2>' . NL;
            $retval .= '<table class="totalcounterstat" border=\'0\'><thead>' . NL;
            $retval .= '<tr><th class="TCtxth TCtxtl"' . ($TotalCounterShowNumbers ? ' colspan="2"' : '') . '>' . $headline . '&nbsp;</th>' . 
                '<th class="TCtxth TCtxtl" colspan="2">$[Percent]</th>' . 
                '<th class="TCtxth TCtxtr">$[Count]</th></tr>' . NL;
            $retval .= '</thead><tbody>' . NL;
        } else { # create table footer
            $retval .= '</tbody></table></article>' . NL;
        }
        return $retval;
    } # end function graphHead
//=====================================================================================================================
// generate graph line
    function graphLine (string $pname, bool $bpage, int $pcnt, int $ptotal, int $pmax, int $linenr) :string {
        global $TotalCounterShowNumbers;
        $retval = '<tr>' . NL;
        if ($TotalCounterShowNumbers) {
            $retval .= '<td class="TCtxtr" style="font-size:smaller;">' . $linenr . '.</td>';
        }
        $retval .= '<td class="TCtxtl" style="min-width:12em;">';
        if ($bpage) {
            $retval .= "<a href='\$ScriptUrl/$pname'>$pname</a>&nbsp;";
        } else {
            $retval .= $pname;
        }
        $retval .= '</td>' . NL;
		if (0 == $perCent = Round(100 * $pcnt / $ptotal)) $perCent = Round(100 * $pcnt / $ptotal, 1);
        $retval .= '<td class="TCtxtr">' . $perCent . '%</td>' . NL;
        $retval .= '<td style="width:BARCELLWIDTH;"><div class="TCbar" style="width:' . Round(BARMAXWIDTH * $pcnt / $pmax) . 'px;"></div></td>';
        $retval .= '<td  class="TCtxtr">&nbsp;' . number_format ($pcnt) . '</td>';
        return $retval . '</tr>' . NL;
    } # end function graphLine
//=====================================================================================================================

// https://www.exakat.io/en/prevent-multiple-php-scripts-at-the-same-time/ 
// https://stackoverflow.com/questions/6967553/php-flock-alternative
// Modified (breaks and returns 0 on failure,
// or returns 1 on success) by Mateusz Czaplinski, 22.01.2008

	function acquireLock(string $wp) {
		//Check if lock doesn't exist or our target is unwritable
		if (!is_writable($wp))
			return FALSE;
		$lfName = "$wp.l";
		\Lock(2); # lock exclusive; stop race condition
		if(file_exists($lfName)) {
			$retVal = FALSE;
		} else {
		    //create the lock - hide warnings and pass empty if already created from racing
		    $retVal = fopen($lfName, 'x');
		}
		\Lock(0); # release lock
		return $retVal;
	}
//=====================================================================================================================
	function dblock(string $wp):bool {
		global $TotalCounterEnableChmods;
		//Check for lockfile handle - if empty , another process raced the lock so report a failure
		$ftw = acquireLock($wp);
		if( $ftw === FALSE)
			return FALSE;

		if($TotalCounterEnableChmods) chmod($wp, 0444); //set the target file to read-only
		$fwStatus = fwrite($ftw, 'lock'); //write the lockfile with 4bytes
		if($TotalCounterEnableChmods) chmod("$wp.l", 0444); //set the lockfile to read only (OPTIONAL)
		fclose($ftw); //close our lockfile
		clearstatcache(); //Clear the stat cache
		return TRUE;
	}
//=====================================================================================================================
// Note: don't call it if 'dblock()' returned 0 !
	function dbexport_unlock(string $wp, $data, string $meth) {
		global $TotalCounterEnableChmods;
		if($TotalCounterEnableChmods) chmod($wp, 0666); //Set the target file to read+write

		//Write the passed string to the target file then close
		\Lock(2); # acquire exclusive lock
		fwrite($ftw = fopen($wp, $meth), $data);
		fclose($ftw);
		\Lock(0); # release lock
		//Validate the written data using a string comparison
		$check = tc_file_get_contents($wp); # note there is a race condition here
		if ($check !== $data)
			echo "Data Mismatch - Locking FAILED!" . BR;

		chmod("$wp.l", 0666); //Set the lockfile to read+write (OPTIONAL)
		unlink("$wp.l"); //Release the lockfile by removing it
	}
//=====================================================================================================================
	function dblock_remove_stale(string $wp) {
		if (false === $modTime = filemtime("$wp.l")) return;
		// 75 minutes - to make absolutely sure we're not tricked by Daylight
		// Savings on Windows - see https://www.php.net/manual/en/function.stat.php#58404
		if ($modTime+(75*60) < time())
			unlink("$wp.l");
	}
//=====================================================================================================================
function detectWebBot (string $HttpUserAgent): string {
    // find web bot https://developers.whatismybrowser.com/useragents/explore/, https://bigdata-madesimple.com/top-50-open-source-web-crawlers-for-data-mining/
    $tc_bot = NULLUSERAGENT;
	$tc_bot_info = ''; # initialise
    if     (preg_match('/ia_archiver/i',        $HttpUserAgent)) {$tc_bot = 'Alexa';             $tc_bot_info = 'https://support.alexa.com/hc/en-us/articles/200450194-Alexa-s-Web-and-Site-Audit-Crawlers';} 
    elseif (preg_match('/360Spider/i',          $HttpUserAgent)) {$tc_bot = '360Spider';} # 
    elseif (preg_match('/A6-Indexer/i',         $HttpUserAgent)) {$tc_bot = 'A6';                $tc_bot_info = 'http://www.a6corp.com/a6-web-scraping-policy/';}
    elseif (preg_match('/Abonti/i',             $HttpUserAgent)) {$tc_bot = 'Abonti';            $tc_bot_info = 'http://www.abonti.com';}
    elseif (preg_match('/acebookexternalhit/i', $HttpUserAgent)) {$tc_bot = 'Facebook External hit'; $tc_bot_info = 'http://www.facebook.com/externalhit_uatext.php';}
    elseif (preg_match('/Adsbot/i',             $HttpUserAgent)) {$tc_bot = 'Adsbot';            $tc_bot_info = 'https://seostar.co/robot/';}
    elseif (preg_match('/AhrefsBot/i',          $HttpUserAgent)) {$tc_bot = 'Ahrefs';            $tc_bot_info = 'https://ahrefs.com/robot/';}
    elseif (preg_match('/aiohttp/i',            $HttpUserAgent)) {$tc_bot = 'aiohttp';} #
    elseif (preg_match('/ALittle/i',            $HttpUserAgent)) {$tc_bot = 'ALittle Client';} #
    elseif (preg_match('/AnyEvent/i',           $HttpUserAgent)) {$tc_bot = 'AnyEvent';          $tc_bot_info = 'http://software.schmorp.de/pkg/AnyEvent';}
    elseif (preg_match('/AppEngine-Google/i',   $HttpUserAgent)) {$tc_bot = 'AppEngine-Google';  $tc_bot_info = 'http://code.google.com/appengine';}
    elseif (preg_match('/applebot/i',           $HttpUserAgent)) {$tc_bot = 'Applebot';          $tc_bot_info = 'https://support.apple.com/en-us/HT204683';}
    elseif (preg_match('/archive.org_bot/i',    $HttpUserAgent)) {$tc_bot = 'Web archive';       $tc_bot_info = 'https://webarchive.jira.com/wiki/display/ARIH/Robots+Exclusion+Protocol';}
    elseif (preg_match('/AntBot/i',             $HttpUserAgent)) {$tc_bot = 'Ant';               $tc_bot_info = 'http://www.ant.com';}
    elseif (preg_match('/ask jeeves/i',         $HttpUserAgent)) {$tc_bot = 'Ask Jeeves';} #
    elseif (preg_match('/AwarioSmartBot/i',     $HttpUserAgent)) {$tc_bot = 'AwarioSmartBot';    $tc_bot_info = 'https://awario.com/bots.html';}
    elseif (preg_match('/baiduspider/i',        $HttpUserAgent)) {$tc_bot = 'Baidu';             $tc_bot_info = 'http://www.baidu.com/search/spider.html';}
    elseif (preg_match('/becomebot/i',          $HttpUserAgent)) {$tc_bot = 'Become';} #
    elseif (preg_match('/bibalex.org_bot/i',    $HttpUserAgent)) {$tc_bot = 'Bibalex';           $tc_bot_info = 'http://archive.bibalex.org/bot/';}
    elseif (preg_match('/bingbot/i',            $HttpUserAgent)) {$tc_bot = 'Bing';              $tc_bot_info = 'http://www.bing.com/bingbot.htm';}
    elseif (preg_match('/BLEXBot/i',            $HttpUserAgent)) {$tc_bot = 'WebMeUp';           $tc_bot_info = 'http://webmeup-crawler.com/';}
    elseif (preg_match('/Bytespider/i',         $HttpUserAgent)) {$tc_bot = 'Bytespider';        $tc_bot_info = 'spider-feedback@bytedance.com';}
    elseif (preg_match('/CATExplorador/i',      $HttpUserAgent)) {$tc_bot = 'CATExplorador';     $tc_bot_info = 'https://domini.cat/catexplorador/';}
    elseif (preg_match('/CCBot/i',              $HttpUserAgent)) {$tc_bot = 'Common Crawl';      $tc_bot_info = 'http://commoncrawl.org/faqs/';}
    elseif (preg_match('/CensysInspect/i',      $HttpUserAgent)) {$tc_bot = 'Censys Inspect';    $tc_bot_info = 'https://about.censys.io/';}
    elseif (preg_match('/centuryb/i',           $HttpUserAgent)) {$tc_bot = 'centuryb';          $tc_bot_info = 'centuryb.o.t9@gmail.com';}
    elseif (preg_match('/ChatGPT-User/i',       $HttpUserAgent)) {$tc_bot = 'ChatGPT';           $tc_bot_info = 'https://openai.com/bot';}
    elseif (preg_match('/CheckMarkNetwork/i',   $HttpUserAgent)) {$tc_bot = 'CheckMark Network'; $tc_bot_info = 'http://www.checkmarknetwork.com/spider.html';}
    elseif (preg_match('/Cincraw/i',            $HttpUserAgent)) {$tc_bot = 'Cincraw';           $tc_bot_info = 'http://cincrawdata.net/bot/';}
    elseif (preg_match('/ClaudeBot/i',          $HttpUserAgent)) {$tc_bot = 'ClaudeBot';         $tc_bot_info = 'claudebot@anthropic.com';}
    elseif (preg_match('/coccocbot/i',          $HttpUserAgent)) {$tc_bot = 'Coccocbot';         $tc_bot_info = 'http://help.coccoc.com/searchengine';}
    elseif (preg_match('/colly/i',              $HttpUserAgent)) {$tc_bot = 'Colly';             $tc_bot_info = 'https://github.com/gocolly/colly/v2';}
    elseif (preg_match('/Crawlbot\/Nutch/i',    $HttpUserAgent)) {$tc_bot = 'Crawlbot/Nutch';    $tc_bot_info = 'https://nutch.apache.org/';}
    elseif (preg_match('/Crawlson/i',           $HttpUserAgent)) {$tc_bot = 'Crawlson';          $tc_bot_info = 'https://www.crawlson.com/about';}
    elseif (preg_match('/Dalvik/i',             $HttpUserAgent)) {$tc_bot = 'Dalvik'; } #
    elseif (preg_match('/DataForSeoBot/i',      $HttpUserAgent)) {$tc_bot = 'DataForSeoBot';     $tc_bot_info = 'https://dataforseo.com/dataforseo-bot';}
    elseif (preg_match('/Daum/i',               $HttpUserAgent)) {$tc_bot = 'Daum';              $tc_bot_info = 'http://cs.daum.net/faq/15/4118.html?faqId=28966';}
    elseif (preg_match('/deepnoc/i',            $HttpUserAgent)) {$tc_bot = 'Deepnoc';           $tc_bot_info = 'https://deepnoc.com/bot';}
    elseif (preg_match('/Discordbot/i',         $HttpUserAgent)) {$tc_bot = 'Discordbot';        $tc_bot_info = 'https://discordapp.com';}
    elseif (preg_match('/DIVD/i',               $HttpUserAgent)) {$tc_bot = 'DIVD';              $tc_bot_info = 'https://csirt.divd.nl/';}
    elseif (preg_match('/Domains Project/i',    $HttpUserAgent)) {$tc_bot = 'Domains Project';   $tc_bot_info = 'https://domainsproject.org/';}
    elseif (preg_match('/DomainStatsBot/i',     $HttpUserAgent)) {$tc_bot = 'DomainStatsBot';    $tc_bot_info = 'https://domainstats.com/pages/our-bot';}
    elseif (preg_match('/dotbot/i',             $HttpUserAgent)) {$tc_bot = 'DotBot';             $tc_bot_info = 'http://www.opensiteexplorer.org/dotbot';}
    elseif (preg_match('/DuckDuckGo-Favicons-Bot/i', $HttpUserAgent)) {$tc_bot = 'DuckDuckGo-Favicons-Bot'; $tc_bot_info = 'http://duckduckgo.com';}
    elseif (preg_match('/DuckDuckBot/i',        $HttpUserAgent)) {$tc_bot = 'DuckDuckBot';        $tc_bot_info = 'https://duckduckgo.com/duckduckbot';}
    elseif (preg_match('/Dy robot/i',           $HttpUserAgent)) {$tc_bot = 'Dy robot';} #
    elseif (preg_match('/EntferBot/i',          $HttpUserAgent)) {$tc_bot = 'EntferBot';          $tc_bot_info = 'https://entfer.com';}
    elseif (preg_match('/exabot/i',             $HttpUserAgent)) {$tc_bot = 'Exalead';} #
    elseif (preg_match('/Expanse/i',            $HttpUserAgent)) {$tc_bot = 'Expanse';            $tc_bot_info = 'scaninfo@expanseinc.com';}
    elseif (preg_match('/facebookexternalhit/i', $HttpUserAgent)) {$tc_bot = 'Facebook';          $tc_bot_info = 'http://www.facebook.com/externalhit_uatext.php';}
    elseif (preg_match('/fast/i',               $HttpUserAgent)) {$tc_bot = 'Fast/Alltheweb';} #
    elseif (preg_match('/gigabot/i',            $HttpUserAgent) // http://www.gigablast.com/spider.html
         || preg_match('/GigablastOpenSource/i', $HttpUserAgent)) {$tc_bot = 'Gigablast';         $tc_bot_info = 'https://github.com/gigablast/open-source-search-engine';}
    elseif (preg_match('/Go-http-client/i',     $HttpUserAgent)) {$tc_bot = 'Go-http-client';     $tc_bot_info = 'Go-http-client';}
    elseif (preg_match('/googlebot/i',          $HttpUserAgent)) {$tc_bot = 'Google';             $tc_bot_info = 'http://www.google.com/bot.html';}
    elseif (preg_match('/GPTBot/i',             $HttpUserAgent)) {$tc_bot = 'GPTBot';             $tc_bot_info = 'https://openai.com/gptbot';}
    elseif (preg_match('/Grammarly/i',          $HttpUserAgent)) {$tc_bot = 'Grammarly';          $tc_bot_info = 'http://www.grammarly.com';}
    elseif (preg_match('/hgfAlphaXCrawl/i',     $HttpUserAgent)) {$tc_bot = 'hgfAlphaXCrawl';     $tc_bot_info = 'https://www.fim.uni-passau.de/data-science/forschung/open-search';}
    elseif (preg_match('/InfoTigerBot/i',       $HttpUserAgent)) {$tc_bot = 'InfoTigerBot';       $tc_bot_info = 'https://infotiger.com/bot';}
    elseif (preg_match('/InternetMeasurement/i', $HttpUserAgent)) {$tc_bot = 'InternetMeasurement'; $tc_bot_info = 'https://internet-measurement.com/';}
    elseif (preg_match('/James BOT/i',          $HttpUserAgent)) {$tc_bot = 'CognitiveSEO';       $tc_bot_info = 'http://cognitiveseo.com/bot.html';}
    elseif (preg_match('/libwww-perl/i',        $HttpUserAgent)) {$tc_bot = 'Libwww-perl';        $tc_bot_info = 'libwww-perl';}
    elseif (preg_match('/linkdexbot/i',         $HttpUserAgent)) {$tc_bot = 'Linkdex';            $tc_bot_info = 'http://www.linkdex.com/bots/';}
    elseif (preg_match('/linkfluence/i',        $HttpUserAgent)) {$tc_bot = 'Linkfluence';        $tc_bot_info = 'https://linkfluence.com/';}
    elseif (preg_match('/Mediapartners-Google/i', $HttpUserAgent)) {$tc_bot = 'Google'; $tc_bot_info = 'https://support.google.com/webmasters/answer/1061943?hl=en';}
    elseif (preg_match('/AdsBot-Google/i',      $HttpUserAgent)) {$tc_bot = 'Google';} #
    elseif (preg_match('/grub-client/i',        $HttpUserAgent)) {$tc_bot = 'Grub';} #
    elseif (preg_match('/java/i',               $HttpUserAgent)) {$tc_bot = 'Java';} #
    elseif (preg_match('/libcurl/i',            $HttpUserAgent)) {$tc_bot = 'cURL';} #
    elseif (preg_match('/curl/i',               $HttpUserAgent)) {$tc_bot = 'cURL';} #
    elseif (preg_match('/slurp@inktomi.com/i',  $HttpUserAgent)) {$tc_bot = 'Inktomi';} #
    elseif (preg_match('/Keybot Translation-Search-Machine/i', $HttpUserAgent)) {$tc_bot = 'Keybot Translation-Search-Machine';}
    elseif (preg_match('/Knowledge AI/i',       $HttpUserAgent)) {$tc_bot = 'Knowledge AI';} #
    elseif (preg_match('/Linespider/i',         $HttpUserAgent)) {$tc_bot = 'Linespider';         $tc_bot_info = 'https://lin.ee/4dwXkTH';}
    elseif (preg_match('/ltx71/i',              $HttpUserAgent)) {$tc_bot = 'ltx71';              $tc_bot_info = 'http://ltx71.com/';}
    elseif (preg_match('/Mail\.RU_Bot/i',       $HttpUserAgent)) {$tc_bot = 'mail.ru';            $tc_bot_info = 'http://go.mail.ru/help/robots';}
    elseif (preg_match('/marginalia/i',         $HttpUserAgent)) {$tc_bot = 'marginalia';         $tc_bot_info = 'https://search.marginalia.nu';}
    elseif (preg_match('/meanpathbot/i',        $HttpUserAgent)) {$tc_bot = 'meanpath';} # 
    elseif (preg_match('/MJ12bot/i',            $HttpUserAgent)) {$tc_bot = 'Majestic';           $tc_bot_info = 'http://www.majestic12.co.uk/bot.php';}
    elseif (preg_match('/MojeekBot/i',          $HttpUserAgent)) {$tc_bot = 'MojeekBot';          $tc_bot_info = 'https://www.mojeek.com/bot.html';}
    elseif (preg_match('/msnbot/i',             $HttpUserAgent)) {$tc_bot = 'MSN';} #
    elseif (preg_match('/NerdyBot/i',           $HttpUserAgent)) {$tc_bot = 'Nerdy data';} #
    elseif (preg_match('/NetcraftSurveyAgent/i', $HttpUserAgent)) {$tc_bot = 'Netcraft';          $tc_bot_info = 'info@netcraft.com';}
    elseif (preg_match('/Netcraft Web Server Survey/i', $HttpUserAgent)) {$tc_bot = 'Netcraft'; } #
    elseif (preg_match('/NetpeakCheckerBot/i',  $HttpUserAgent)) {$tc_bot = 'NetpeakCheckerBot';  $tc_bot_info = 'https://netpeaksoftware.com/checker';}
    elseif (preg_match('/NetSystemsResearch/i', $HttpUserAgent)) {$tc_bot = 'NetSystemsResearch'; $tc_bot_info = 'https://netsystemsresearch.com';}
    elseif (preg_match('/Neevabot/i',           $HttpUserAgent)) {$tc_bot = 'Neevabot';           $tc_bot_info = 'https://neeva.com/neevabot';}
    elseif (preg_match('/Nimo Software/i',      $HttpUserAgent)) {$tc_bot = 'Nimo Software HTTP Retriever';} #
    elseif (preg_match('/NLNZ_IAHarvester/i',   $HttpUserAgent)) {$tc_bot = 'NLNZ_IAHarvester';   $tc_bot_info = 'https://natlib.govt.nz/publishers-and-authors/web-harvesting/domain-harvest';}
    elseif (preg_match('/node-fetch/i',         $HttpUserAgent)) {$tc_bot = 'Node-fetch';         $tc_bot_info = 'https://github.com/bitinn/node-fetch';}
    elseif (preg_match('/OAI-SearchBot/i',      $HttpUserAgent)) {$tc_bot = 'OpenAI';             $tc_bot_info = 'https://openai.com/searchbot';}
    elseif (preg_match('/Pandalytics/i',        $HttpUserAgent)) {$tc_bot = 'Pandalytics';        $tc_bot_info = 'https://domainsbot.com/pandalytics/';}
    elseif (preg_match('/panscient/i',          $HttpUserAgent)) {$tc_bot = 'Panscient';          $tc_bot_info = 'panscient.com';}
    elseif (preg_match('/PerplexityBot/i',      $HttpUserAgent)) {$tc_bot = 'PerplexityBot';      $tc_bot_info = 'https://docs.perplexity.ai/docs/perplexity-bot';}
    elseif (preg_match('/petalbot/i',           $HttpUserAgent)) {$tc_bot = 'Petalbot';           $tc_bot_info = 'https://petalsearch.com/';}
    elseif (preg_match('/PHP-Curl-Class/i',     $HttpUserAgent)) {$tc_bot = 'PHP-Curl-Class';     $tc_bot_info = 'https://github.com/php-curl-class/php-curl-class';}
    elseif (preg_match('/Pinterest/i',          $HttpUserAgent)) {$tc_bot = 'Pinterest';          $tc_bot_info = 'http://www.pinterest.com';}
    elseif (preg_match('/PocketParser/i',       $HttpUserAgent)) {$tc_bot = 'PocketParser';       $tc_bot_info = 'https://getpocket.com/pocketparser_ua';}
    elseif (preg_match('/python-requests/i',    $HttpUserAgent)) {$tc_bot = 'Python-requests';} // # python-requests
    elseif (preg_match('/python-urllib/i',      $HttpUserAgent)) {$tc_bot = 'Python-urllib';} // # python-urllib
    elseif (preg_match('/Qwantify/i',           $HttpUserAgent)) {$tc_bot = 'Qwantify';           $tc_bot_info = 'https://www.qwant.com/';}
    elseif (preg_match('/RepoLookoutBot/i',     $HttpUserAgent)) {$tc_bot = 'RepoLookoutBot';     $tc_bot_info = 'abuse reports to abuse@repo-lookout.org';}
    elseif (preg_match('/SafeDNSBot/i',         $HttpUserAgent)) {$tc_bot = 'SafeDNSBot';         $tc_bot_info = 'https://www.safedns.com/searchbot';}
    elseif (preg_match('/Scrapy/i',             $HttpUserAgent)) {$tc_bot = 'Scrapy';             $tc_bot_info = 'https://scrapy.org';}
    elseif (preg_match('/scooter/i',            $HttpUserAgent)) {$tc_bot = 'Altavista';          $tc_bot_info = 'deprecated http://www.siteware.ch/webresources/useragents/spiders/altavista.html';}
    elseif (preg_match('/Screaming/i',          $HttpUserAgent)) {$tc_bot = 'Screaming Frog SEO Spider'; $tc_bot_info = 'https://www.screamingfrog.co.uk/seo-spider/';}
    elseif (preg_match('/SISTRIX/i',            $HttpUserAgent)) {$tc_bot = 'SISTRIX';            $tc_bot_info = 'http://crawler.007ac9.net/';}
    elseif (preg_match('/Crawler/i',            $HttpUserAgent)) {$tc_bot = 'SISTRIX';            $tc_bot_info = 'http://crawler.007ac9.net/';}
    elseif (preg_match('/SeekportBot/i',        $HttpUserAgent)) {$tc_bot = 'SeekportBot';        $tc_bot_info = 'https://bot.seekport.com';}
    elseif (preg_match('/SemrushBot/i',         $HttpUserAgent)) {$tc_bot = 'SemrushBot';         $tc_bot_info = 'http://www.semrush.com/bot.html';}
    elseif (preg_match('/SenutoBot/i',          $HttpUserAgent)) {$tc_bot = 'SenutoBot';          $tc_bot_info = 'https://www.senuto.com/';}
    elseif (preg_match('/SEOkicks/i',           $HttpUserAgent)) {$tc_bot = 'SEOkicks';           $tc_bot_info = 'https://www.seokicks.de/robot.html';}
    elseif (preg_match('/serpstatbot/i',        $HttpUserAgent)) {$tc_bot = 'Serpstatbot';        $tc_bot_info = 'https://serpstatbot.com/; abuse@serpstatbot.com';}
    elseif (preg_match('/SeznamBot/i',          $HttpUserAgent)) {$tc_bot = 'SeznamBot';          $tc_bot_info = 'http://napoveda.seznam.cz/en/seznambot-intro/';}
    elseif (preg_match('/Sidetrade/i',          $HttpUserAgent)) {$tc_bot = 'Sidetrade indexer bot'; } #
    elseif (preg_match('/SiteExplorer/i',       $HttpUserAgent)) {$tc_bot = 'Site Explorer';      $tc_bot_info = 'http://siteexplorer.info/';}
    elseif (preg_match('/snapchat/i',           $HttpUserAgent)) {$tc_bot = 'Snapchat URL Preview Service'; $tc_bot_info = 'https://developers.snap.com/robots';}
    elseif (preg_match('/Sogou/i',              $HttpUserAgent)) {$tc_bot = 'Sogou web spider';   $tc_bot_info = 'http://www.sogou.com/docs/help/webmasters.htm#07';}
    elseif (preg_match('/SpiceworksAgentShell/i', $HttpUserAgent)) {$tc_bot = 'Spiceworks Agent Shell'; $tc_bot_info = 'https://community.spiceworks.com/support/inventory-online/docs/deploy-agent';}
    elseif (preg_match('/SurdotlyBot/i',        $HttpUserAgent)) {$tc_bot = 'SurdotlyBot';        $tc_bot_info = '    http://sur.ly/bot.html';}
    elseif (preg_match('/synapse/i',            $HttpUserAgent)) {$tc_bot = 'Synapse';} #
    elseif (preg_match('/t3versionsBot/i',      $HttpUserAgent)) {$tc_bot = 'T3versionsBot';      $tc_bot_info = 'https://www.t3versions.com/bot';}
    elseif (preg_match('/TenMillionDomainsBot/i', $HttpUserAgent)) {$tc_bot = 'TenMillionDomainsBot'; $tc_bot = 'https://github.com/tonywangcn/ten-million-domains';}
    elseif (preg_match('/ThinkChaos/i',         $HttpUserAgent)) {$tc_bot = 'ThinkChaos'; } #
    elseif (preg_match('/Timpibot/i',           $HttpUserAgent)) {$tc_bot = 'Timpibot';           $tc_bot_info = 'http://www.timpi.io';}
    elseif (preg_match('/Turnitin/i',           $HttpUserAgent)) {$tc_bot = 'Turnitin';           $tc_bot_info = 'https://bit.ly/2UvnfoQ';}
    elseif (preg_match('/Twisted PageGetter/i', $HttpUserAgent)) {$tc_bot = 'Twisted PageGetter'; $tc_bot_info = 'https://twistedmatrix.com/trac/';}
    elseif (preg_match('/Twitterbot/i',         $HttpUserAgent)) {$tc_bot = 'Twitterbot'; } #
    elseif (preg_match('/unirest-java/i',       $HttpUserAgent)) {$tc_bot = 'unirest-java'; } #
    elseif (preg_match('/W3C-checklink/i',      $HttpUserAgent)) {$tc_bot = 'W3C-checklink'; } #
    elseif (preg_match('/webpage-inspector/i',  $HttpUserAgent)) {$tc_bot = 'Webpage Inspector';  $tc_bot_info = 'webpage-inspector.com';}
    elseif (preg_match('/webprosbot/i',         $HttpUserAgent)) {$tc_bot = 'Webprosbot';         $tc_bot_info = 'mailto:abuse-6337@webpros.com';}
    elseif (preg_match('/WebTarantula/i',       $HttpUserAgent)) {$tc_bot = 'WebTarantula';       $tc_bot_info = 'http://webtarantula.com/';}
    elseif (preg_match('/wget/i',               $HttpUserAgent)) {$tc_bot = 'wget';               $tc_bot_info = 'https://www.gnu.org/software/wget/';}
    elseif (preg_match('/woorankreview/i',      $HttpUserAgent)) {$tc_bot = 'WooRankReview';      $tc_bot_info = 'https://www.woorank.com/';}
    elseif (preg_match('/wp_is_mobile/i',       $HttpUserAgent)) {$tc_bot = 'Wp_is_mobile'; } #
    elseif (preg_match('/Xenu/i',               $HttpUserAgent)) {$tc_bot = 'Xenu Link Sleuth'; } #
    elseif (preg_match('/XoviBot/i',            $HttpUserAgent)) {$tc_bot = 'Xovi';               $tc_bot_info = 'http://www.xovibot.net/';}
    elseif (preg_match('/yahoo! slurp/i',       $HttpUserAgent)) {$tc_bot = 'Yahoo!';             $tc_bot_info = 'http://help.yahoo.com/help/us/ysearch/slurp';}
    elseif (preg_match('/YandexBot/i',          $HttpUserAgent)) {$tc_bot = 'Yandex';             $tc_bot_info = 'http://yandex.com/bots';}
    elseif (preg_match('/Yeti/i',               $HttpUserAgent)) {$tc_bot = 'Yeti';               $tc_bot_info = 'http://naver.me/spd';}
    elseif (preg_match('/YisouSpider/i',        $HttpUserAgent)) {$tc_bot = 'YisouSpider'; } #
    elseif (preg_match('/YottaaMonitor/i',      $HttpUserAgent)) {$tc_bot = 'Yottaa';             $tc_bot_info = 'http://www.yottaa.com/blog/bid/223629/Google-Analytics-How-to-Segment-and-Filter-Robot-Traffic';}
    elseif (preg_match('/zyborg/i',             $HttpUserAgent)
         || preg_match('/zealbot/i',            $HttpUserAgent)) {$tc_bot = 'WiseNut!';} #
    elseif (preg_match('/2ip bot/i',            $HttpUserAgent)) {$tc_bot = '2ip bot';            $tc_bot_info = 'http://2ip.io';}
#	
    return $tc_bot;
} # end detectWebBot
//=====================================================================================================================

function detectBrowser (string $HttpUserAgent): string {
	// not a bot, so find the browser, https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
	$tc_browser = NULLUSERAGENT;
    if     (preg_match('/arachne/i',     $HttpUserAgent)) $tc_browser = 'Arachne GPL';
    elseif (preg_match('/vivaldi/i',     $HttpUserAgent)) $tc_browser = 'Vivaldi';
    elseif (preg_match('/blazer/i',      $HttpUserAgent)) $tc_browser = 'Blazer';
    elseif (preg_match('/brave/i',       $HttpUserAgent)) $tc_browser = 'Brave';
    elseif (preg_match('/opera/i',       $HttpUserAgent)
         || preg_match('/OPR/i',         $HttpUserAgent)) $tc_browser = 'Opera'; # must be before Chrome below
    elseif (preg_match('/webtv/i',       $HttpUserAgent)) $tc_browser = 'WebTV';
    elseif (preg_match('/camino/i',      $HttpUserAgent)) $tc_browser = 'Camino';
    elseif (preg_match('/MAXTHON/i',     $HttpUserAgent)) $tc_browser = 'MAXTHON'; # must be before msie below # http://www.maxthon.com/
    elseif (preg_match('/netpositive/i', $HttpUserAgent)) $tc_browser = 'NetPositive';
    elseif (preg_match('/internet explorer/i', $HttpUserAgent)
         || preg_match('/msie/i',        $HttpUserAgent)
         || preg_match('/IEMobile/i',    $HttpUserAgent)
         || preg_match('/mspie/i',       $HttpUserAgent)
         || preg_match('/trident/i',     $HttpUserAgent) ) $tc_browser = 'MS Internet Explorer'; # add trident
    elseif (preg_match('/avant browser/i', $HttpUserAgent)
         || preg_match('/advanced browser/i', $HttpUserAgent)) $tc_browser = 'Avant Browser';
    elseif (preg_match('/galeon/i',      $HttpUserAgent)) $tc_browser = 'Galeon';
    elseif (preg_match('/konqueror/i',   $HttpUserAgent)) $tc_browser = 'Konqueror';
    elseif (preg_match('/icab/i',        $HttpUserAgent)) $tc_browser = 'iCab';
    elseif (preg_match('/FBAN\/FBIOS/i', $HttpUserAgent)) $tc_browser = 'Facebook In-App Browser';
    elseif (preg_match('/Nmap Scripting Engine/i', $HttpUserAgent)) $tc_browser = 'Nmap'; # http://nmap.org/book/nse.html
    elseif (preg_match('/omniweb/i',     $HttpUserAgent)) $tc_browser = 'OmniWeb';
    elseif (preg_match('/phoenix/i',     $HttpUserAgent)) $tc_browser = 'Phoenix';
    elseif (preg_match('/firebird/i',    $HttpUserAgent)) $tc_browser = 'Firebird';
    elseif (preg_match('/seamonkey/i',   $HttpUserAgent)) $tc_browser = 'Seamonkey'; # must be before Firefox
    elseif (preg_match('/firefox/i',     $HttpUserAgent)) $tc_browser = 'Firefox';
    elseif (preg_match('/netscape/i',    $HttpUserAgent)) $tc_browser = 'Netscape'; # must be before Mozilla below
    elseif (preg_match('/minimo/i',      $HttpUserAgent)) $tc_browser = 'Minimo';
    elseif (preg_match('/mozilla/i',     $HttpUserAgent)
         && preg_match('/rv:[0-9].[0-9][a-b]/i', $HttpUserAgent)) $tc_browser = 'Mozilla'; #
    elseif (preg_match('/mozilla/i',     $HttpUserAgent)
         && preg_match('/rv:[0-9].[0-9]/i', $HttpUserAgent)) $tc_browser = 'Mozilla'; #
    elseif (preg_match('/YaBrowser/i',   $HttpUserAgent)) $tc_browser = 'Yandex browser'; # http://help.yandex.ru/yabrowser/?lang=en
    elseif (preg_match('/libwww/i',      $HttpUserAgent)) {
        if (preg_match('/amaya/i',       $HttpUserAgent)) {
            $tc_browser = 'Amaya';
        } else {
            $tc_browser = 'Text browser';
        }
    }
    elseif (preg_match('/edge/i',        $HttpUserAgent)) $tc_browser = 'Edge'; // must be before Safari
    elseif (preg_match('/chromium/i',    $HttpUserAgent)) $tc_browser = 'Chromium'; // must be before Chrome
    elseif (preg_match('/chrome/i',      $HttpUserAgent)) $tc_browser = 'Chrome'; // must be before Safari
    elseif (preg_match('/safari/i',      $HttpUserAgent)) $tc_browser = 'Safari'; // must be after Chrome above
    elseif (preg_match('/elinks/i',      $HttpUserAgent)) $tc_browser = 'ELinks';
    elseif (preg_match('/offbyone/i',    $HttpUserAgent)) $tc_browser = 'Off By One';
    elseif (preg_match('/playstation portable/i', $HttpUserAgent)) $tc_browser = 'PlayStation Portable';
    elseif (preg_match('/links/i',       $HttpUserAgent)) $tc_browser = 'Links';
    elseif (preg_match('/ibrowse/i',     $HttpUserAgent)) $tc_browser = 'iBrowse';
    elseif (preg_match('/w3m/i',         $HttpUserAgent)) $tc_browser = 'w3m';
    elseif (preg_match('/aweb/i',        $HttpUserAgent)) $tc_browser = 'AWeb';
    elseif (preg_match('/voyager/i',     $HttpUserAgent)) $tc_browser = 'Voyager';
    elseif (preg_match('/oregano/i',     $HttpUserAgent)) $tc_browser = 'Oregano';
	return $tc_browser;
} # end detectBrowser
//=====================================================================================================================
function detectOS (string $HttpUserAgent):string {
	// find operating system
	    $tc_os = NULLUSERAGENT;
        if     (preg_match('/android/i',   $HttpUserAgent)) $tc_os = 'Android'; // # must be before linux below
        elseif (preg_match('/linux/i',     $HttpUserAgent)) $tc_os = 'Linux';
        elseif (preg_match('/irix/i',      $HttpUserAgent)) $tc_os = 'IRIX';
        elseif (preg_match('/hp-ux/i',     $HttpUserAgent)) $tc_os = 'HP-Unix';
        elseif (preg_match('/os2/i',       $HttpUserAgent)) $tc_os = 'OS/2';
        elseif (preg_match('/beos/i',      $HttpUserAgent)) $tc_os = 'BeOS';
        elseif (preg_match('/sunos/i',     $HttpUserAgent)) $tc_os = 'SunOS';
        elseif (preg_match('/palm/i',      $HttpUserAgent)) $tc_os = 'PalmOS';
        elseif (preg_match('/cygwin/i',    $HttpUserAgent)) $tc_os = 'Cygwin';
        elseif (preg_match('/amiga/i',     $HttpUserAgent)) $tc_os = 'Amiga';
        elseif (preg_match('/unix/i',      $HttpUserAgent)) $tc_os = 'Unix';
        elseif (preg_match('/qnx/i',       $HttpUserAgent)) $tc_os = 'QNX';
        elseif (preg_match('/Windows Phone/i', $HttpUserAgent)) $tc_os = 'Windows Phone'; # must be before Windows below
        elseif (preg_match('/windows/i',   $HttpUserAgent)) $tc_os = 'Windows'; #
        elseif (preg_match('/openbsd/i',   $HttpUserAgent)) $tc_os = 'OpenBSD'; #
        elseif (preg_match('/iphone os/i', $HttpUserAgent)) $tc_os = 'iPhone'; # must be before Mac OS below
        elseif (preg_match('/mac os/i',    $HttpUserAgent)) $tc_os = 'Mac'; 
        elseif (preg_match('/cros/i',      $HttpUserAgent)) $tc_os = 'Chrome OS';
        elseif (preg_match('/symbian/i',   $HttpUserAgent)) $tc_os = 'Symbian';
        elseif (preg_match('/risc/i',      $HttpUserAgent)) $tc_os = 'RISC';
        elseif (preg_match('/dreamcast/i', $HttpUserAgent)) $tc_os = 'Dreamcast';
        elseif (preg_match('/freebsd/i',   $HttpUserAgent)) $tc_os = 'FreeBSD';
        elseif (preg_match('/dos/i',       $HttpUserAgent)) $tc_os = 'dos';
    return $tc_os;
} # end detectOS
//=====================================================================================================================
    function SetAllLocations ():array {
// Define top level domains
        return array (
        'localhost' => 'localhost',
        UNKNOWN => 'Unknown',
# original top level domains
        'com' => 'Commercial',
        'net' => 'Networks',
        'org' => 'Organizations',
        'int' => 'International organizations',
        'edu' => 'US higher Education',
        'gov' => 'US Government',
        'mil' => 'US Dept of Defense',
# selected ICANN TLDs
        'academy' => 'Academy',
        'aero' => 'Aviation',
        'biz' => 'Business organizations',
        'church' => 'Churches',
        'city' => 'City',
        'club' => 'Clubs',
        'community' => 'Community',
        'coop' => 'Co-operative organizations',
        'education' => 'Education insitiutes',
        'info' => 'Information',
        'international' => 'International entities',
        'mobi' => 'mobile devices',
        'museum' => 'Museums',
        'name' => 'Personal',
        'place' => 'Place',
        'travel' => 'Travelling',
        'universite' => 'University',
        'wiki' => 'Wikis',
# selected geographic TLDs
        'africa' => 'Africa', 
        'asia' => 'Asia',
        'berlin' => 'Berlin',
        'brussels' => 'Brussels',
        'kiwi' => 'Kiwi',
        'london' => 'London',
        'paris' => 'Paris',
        'quebec' => 'Quebec',
        'scot' => 'Scotland',
# country code top level https://icannwiki.org/Country_code_top-level_domain
# updates from https://isotc.iso.org/livelink/livelink?func=ll&objId=16944257&objAction=browse&viewType=1
        'ac' => 'Ascension Island',
        'ad' => 'Andorra',
        'ae' => 'United Arab Emirates',
        'af' => 'Afghanistan',
        'ag' => 'Antigua & Barbuda',
        'ai' => 'Anguilla',
        'al' => 'Albania',
        'am' => 'Armenia',
        'an' => 'Netherlands Antilles',
        'ao' => 'Angola',
        'aq' => 'Antarctica',
        'ar' => 'Argentina',
        'as' => 'American Samoa',
        'at' => 'Austria',
        'au' => 'Australia',
        'aw' => 'Aruba',
        'ax' => 'Åland',
        'az' => 'Azerbaijan',

        'ba' => 'Bosnia & Herzegovina',
        'bb' => 'Barbados',
        'bd' => 'Bangladesh',
        'be' => 'Belgium',
        'bf' => 'Burkina Faso',
        'bg' => 'Bulgaria',
        'bh' => 'Bahrain',
        'bi' => 'Burundi',
        'bj' => 'Benin',
        'bm' => 'Bermuda',
        'bn' => 'Brunei Darussalam',
        'bo' => 'Bolivia',
        'br' => 'Brazil',
        'bs' => 'Bahamas',
        'bt' => 'Bhutan',
        'bv' => 'Bouvet Island',
        'bw' => 'Botswana',
        'by' => 'Belarus',
        'bz' => 'Belize',

        'ca' => 'Canada',
        'cc' => 'Cocos (Keeling) Islands',
        'cd' => 'Democratic republic of Congo',
        'cf' => 'Central African Republic',
        'cg' => 'Congo',
        'ch' => 'Switzerland',
        'ci' => 'Ivory Coast',
        'ck' => 'Cook Islands',
        'cl' => 'Chile',
        'cm' => 'Cameroon',
        'cn' => 'China',
        'co' => 'Colombia',
        'cr' => 'Costa Rica',
        'cs' => 'Czechoslovakia/Sebia & Montenegro', // deleted
        'cu' => 'Cuba',
        'cv' => 'Cape Verde',
        'cw' => 'Curaçao',
        'cx' => 'Christmas Island',
        'cy' => 'Cyprus',
        'cz' => 'Czech Republic',

        'de' => 'Germany',
        'dj' => 'Djibouti',
        'dk' => 'Denmark',
        'dm' => 'Dominica',
        'do' => 'Dominican Republic',
        'dz' => 'Algeria',

        'ec' => 'Ecuador',
        'ee' => 'Estonia',
        'eg' => 'Egypt',
        'eh' => 'Western Sahara',
        'er' => 'Eritrea',
        'es' => 'Spain',
        'et' => 'Ethiopia',
        'eu' => 'European Union',

        'fi' => 'Finland',
        'fj' => 'Fiji',
        'fk' => 'Falkland Islands',
        'fm' => 'Micronesia',
        'fo' => 'Faroe Islands',
        'fr' => 'France',

        'ga' => 'Gabon',
        'gb' => 'United Kingdom',
        'gd' => 'Grenada',
        'ge' => 'Georgia',
        'gf' => 'French Guiana',
        'gg' => 'Guernsey',
        'gh' => 'Ghana',
        'gi' => 'Gibraltar',
        'gl' => 'Greenland',
        'gm' => 'Gambia',
        'gn' => 'Guinea',
        'gp' => 'Guadeloupe',
        'gq' => 'Equatorial Guinea',
        'gr' => 'Greece',
        'gs' => 'South Georgia & South Sandwich Islands',
        'gt' => 'Guatemala',
        'gu' => 'Guam',
        'gw' => 'Guinea-Bissau',
        'gy' => 'Guyana',

        'hk' => 'Hong Kong',
        'hm' => 'Heard & McDonald Islands',
        'hn' => 'Honduras',
        'hr' => 'Croatia',
        'ht' => 'Haiti',
        'hu' => 'Hungary',

        'id' => 'Indonesia',
        'ie' => 'Ireland',
        'il' => 'Israel',
        'im' => 'Isle of Man',
        'in' => 'India',
        'io' => 'British Indian Ocean Territory',
        'iq' => 'Iraq',
        'ir' => 'Iran',
        'is' => 'Iceland',
        'it' => 'Italy',

        'je' => 'Jersey',
        'jm' => 'Jamaica',
        'jo' => 'Jordan',
        'jp' => 'Japan',

        'ke' => 'Kenya',
        'kg' => 'Kyrgyzstan',
        'kh' => 'Cambodia',
        'ki' => 'Kiribati',
        'km' => 'Comoros',
        'kn' => 'Saint Kitts & Nevis',
        'kp' => 'North Korea',
        'kr' => 'South Korea',
        'kw' => 'Kuwait',
        'ky' => 'Cayman Islands',
        'kz' => 'Kazakhstan',

        'la' => 'Laos',
        'lb' => 'Lebanon',
        'lc' => 'Saint Lucia',
        'li' => 'Liechtenstein',
        'lk' => 'Sri Lanka',
        'lr' => 'Liberia',
        'ls' => 'Lesotho',
        'lt' => 'Lithuania',
        'lu' => 'Luxembourg',
        'lv' => 'Latvia',
        'ly' => 'Libyan Arab Jamahiriya',

        'ma' => 'Morocco',
        'mc' => 'Monaco',
        'md' => 'Moldova',
        'me' => 'Montenegro',
        'mg' => 'Madagascar',
        'mh' => 'Marshall Islands',
        'mk' => 'North Macedonia',
        'ml' => 'Mali',
        'mm' => 'Myanmar',
        'mn' => 'Mongolia',
        'mo' => 'Macau',
        'mp' => 'Northern Mariana Islands',
        'mq' => 'Martinique',
        'mr' => 'Mauritania',
        'ms' => 'Montserrat',
        'mt' => 'Malta',
        'mu' => 'Mauritius',
        'mv' => 'Maldives',
        'mw' => 'Malawi',
        'mx' => 'Mexico',
        'my' => 'Malaysia',
        'mz' => 'Mozambique',

        'na' => 'Namibia',
        'nc' => 'New Caledonia',
        'ne' => 'Niger',
        'nf' => 'Norfolk Island',
        'ng' => 'Nigeria',
        'ni' => 'Nicaragua',
        'nl' => 'The Netherlands',
        'no' => 'Norway',
        'np' => 'Nepal',
        'nr' => 'Nauru',
        'nu' => 'Niue',
        'nz' => 'New Zealand',

        'om' => 'Oman',

        'pa' => 'Panama',
        'pe' => 'Peru',
        'pf' => 'French Polynesia',
        'pg' => 'Papua New Guinea',
        'ph' => 'Philippines',
        'pk' => 'Pakistan',
        'pl' => 'Poland',
        'pm' => 'St. Pierre & Miquelon',
        'pn' => 'Pitcairn',
        'pr' => 'Puerto Rico',
        'ps' => 'Palestine',
        'pt' => 'Portugal',
        'pw' => 'Palau',
        'py' => 'Paraguay',

        'qa' => 'Qatar',

        're' => 'Réunion',
        'ro' => 'Romania',
        'rs' => 'Serbia',
        'ru' => 'Russia',
        'rw' => 'Rwanda',

        'sa' => 'Saudi Arabia',
        'sb' => 'Solomon Islands',
        'sc' => 'Seychelles',
        'sd' => 'Sudan',
        'se' => 'Sweden',
        'sg' => 'Singapore',
        'sh' => 'St. Helena',
        'si' => 'Slovenia',
        'sj' => 'Svalbard & Jan Mayen Islands',
        'sk' => 'Slovakia',
        'sl' => 'Sierra Leone',
        'sm' => 'San Marino',
        'sn' => 'Senegal',
        'so' => 'Somalia',
        'sr' => 'Surinam',
        'st' => 'Sao Tome & Principe',
        'su' => 'USSR',
        'sv' => 'El Salvador',
        'sy' => 'Syrian Arab Republic',
        'sz' => 'Swaziland',

        'tc' => 'The Turks & Caicos Islands',
        'td' => 'Chad',
        'tf' => 'French Southern Territories',
        'tg' => 'Togo',
        'th' => 'Thailand',
        'tj' => 'Tajikistan',
        'tk' => 'Tokelau',
        'tl' => 'Timor-Leste',
        'tm' => 'Turkmenistan',
        'tn' => 'Tunisia',
        'to' => 'Tonga',
        'tp' => 'East Timor',
        'tr' => 'Turkey',
        'tt' => 'Trinidad & Tobago',
        'tv' => 'Tuvalu',
        'tw' => 'Taiwan',
        'tz' => 'Tanzania',
    
        'ua' => 'Ukraine',
        'ug' => 'Uganda',
        'uk' => 'United Kingdom',
        'um' => 'United States Minor Outlying Islands',
        'us' => 'United States',
        'uy' => 'Uruguay',
        'uz' => 'Uzbekistan',

        'va' => 'Vatican City',
        'vc' => 'Saint Vincent & the Grenadines',
        've' => 'Venezuela',
        'vg' => 'British Virgin Islands',
        'vi' => 'US Virgin Islands',
        'vn' => 'Vietnam',
        'vu' => 'Vanuatu',

        'wf' => 'Wallis & Futuna Islands',
        'ws' => 'Samoa',

        'ye' => 'Yemen',
        'yt' => 'Mayotte',
        'yu' => 'Yugoslavia',

        'za' => 'South Africa',
        'zm' => 'Zambia',
        'zr' => 'Zaire', // deprecated
        'zw' => 'Zimbabwe',
        
    );
}
//=====================================================================================================================
// Record message to PmWiki for display by (:message:) directive
    function tcmsg(string $smsgprefix, string $smsgdata, array $LastError = []) {
        global $MessagesFmt, $TotalCounterLog, $logfilehandle, $logFileTime;
        if (!isset ($MessagesFmt [MSGFMTID])) $MessagesFmt [MSGFMTID] = [];
        $TcMsgs = '';
        $TcMsgs .= '<i>' . $smsgprefix . '</i>: ' . \PHSC($smsgdata);
        if (!empty ($LastError)) $TcMsgs .= ' ' . TOTALCOUNTERNAME . ' {' . implode (', ', $LastError) . '}';
        $MessagesFmt [MSGFMTID] [] = $TcMsgs . BR;
        if ($TotalCounterLog) { # also write message to the totalcounter log
            $TcLogMsg = '';
            $TcLogMsg .= $smsgprefix . '> ' . \PHSC($smsgdata);
            if (!empty ($LastError)) $TcLogMsg .= ' ' . TOTALCOUNTERNAME . ' {' . implode (', ', $LastError) . '}';
            \Lock(2); # acquire exclusive lock
            $fwritestatus = fwrite($logfilehandle, $logFileTime . $TcLogMsg . NL);
            \Lock(0); # release lock
        }
    }
# end TotalCOunter