Reviving the HTML-Access Counter

Stock photo of a five-strike symbol, from https://www.pexels.com/photo/5-strike-symbol-1010973/

I’ve recently come across the web page of a former acquaintance that still featured a visitor counter. Reminiscent of the 1990s, I was surprised to find the famous “WWW Homepage Access Counter and Clock” still available on numerous mirrors. Last updated on 11 November 1999 and written in C, however, it not only had to be compiled, but was also designed to work from the /cgi-bin directory only. It looked futile to even attempt to make this work on a modern web server, so in how few lines of code was it possible to write a functionally compatible alternative, ideally one that worked without cookies (so no consent is required)?

Prerequisites

You’ll need a web server with access to the file system. Create a subdirectory in which the counter-related files will reside, called /counter in my case.

The overall directory structure should look something like:

/counter
    /SleekDB       (grab from GitHub, contains SleekDB.php etc)
    /database      (initially empty, must have 0777 permissions)
    count.php      (copy code from below)
    styleXXX.gif   (downloaded from one of the mirror sites)

Configuration and insertion

The only configuration required is to set the number of counter digits and the time before considering a session finished. Both variables are defined at the top of the file.

Then save the code as count.php in the /counter directory and include the script as an image file into your web page, i.e., <img src="count.php" />.

This post has been viewed times since 27 March 2021
<?php

/********************************************************************/
// Configuration parameters

$numdigits = 6;                    // number of digits in output image
$holdofftime = 300;  // how long before considering visits as separate

// Import digits style file
$numbers = imagecreatefromgif(__DIR__ . "/styleB.gif");
$digitW = 15;
$digitH = 20;
// originally from http://www.eeb.ele.tue.nl/software/counter.html

/********************************************************************/
// Open SleekDB (file-based) database
require_once "./SleekDB/Store.php"; // from https://sleekdb.github.io/
$databaseDirectory = __DIR__ . "/database";
$logStore = new \SleekDB\Store("visitors", $databaseDirectory);
if (!is_file($databaseDirectory . "/.htaccess")) {
  file_put_contents($databaseDirectory."/.htaccess", "Deny from all");
} // Deny direct file access to any database file

/********************************************************************/
// Try to detect client IP address (best-effort, no guarantees)
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
  $IP = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  $IP = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
  $IP = $_SERVER['REMOTE_ADDR'];
} else {
  $IP = null;
}

// Hash IP (if known) and user agent to protect sensitive detail
if (filter_var($IP, FILTER_VALIDATE_IP)) { 
  $identity = hash("sha256", $IP . $_SERVER['HTTP_USER_AGENT']);
} else {
  $identity = null;
}

/********************************************************************/
// Get current counter value
$countDB = $logStore->findOneBy(["type", "=", "COUNTER"]);
if ($countDB == null) { // create new counter at first invocation
  $countDB = $logStore->insert(["type" => "COUNTER", "count" => 0]); 
}
$counter = $countDB['count'];

/********************************************************************/
// Purge outdated visitor entries
$logStore->deleteBy([["type", "=" ,"VISITOR"],
                     ["time", "<" ,time() - $holdofftime]]);

/********************************************************************/
// Check if an entry exists for the current visitor
$visitor = null;
if ($identity != null) { // verify that we can identify the user
  $visitor = $logStore->findOneBy([["type", "=", "VISITOR"], 
                                   ["unique", "=", $identity]]);
  if ($visitor == null) { // new visitor: create a new DB entry
    $logStore->insert(["type" => "VISITOR", 
                       "unique" => $identity, 
                       "time" => time()]); 
  } else { // update the timestamp of an ongoing session
    $logStore->updateById($visitor['_id'], ["time" => time()]); 
  }
}

/********************************************************************/
// Increment counter
if ($visitor == null || $identity == null) {
  $counter = $counter + 1;
  $logStore->updateById($countDB['_id'], ["count" => $counter]); 
}

/********************************************************************/
// Prepare output placeholder
$outputImage = imagecreatetruecolor($numdigits*$digitW, $digitH);
for ($i=0; $i<$numdigits; $i++) { // Stitch the digits together
  $value = ($counter / pow(10, ($numdigits-1-$i))) % 10;
  imagecopymerge($outputImage, $numbers, $i*$digitW, 0, 
                 $value*$digitW, 0, $digitW, $digitH, 100);
}

/********************************************************************/
// Output the image, and disable caching an outdated counter value
header('Content-Type: image/gif');
header('Cache-Control: no-cache,no-store,max-age=0,must-revalidate');
header('Expires: Mon, 31 Jul 1992 07:32:00 GMT'); // long long ago
header('Pragma: no-cache');
imagegif($outputImage); // Output the image to the stream

/********************************************************************/
// Cleanup: Free used image buffer
imagedestroy($outputImage);
?>

Outputting inline graphics

The script can also be configured to return a base64 representation of the image file for direct inclusion into a HTML page. Simply replace the five lines to set the headers and output the GIF image by the following code:

function tobase64($buffer) { 
  return ("data:image/gif;base64," . base64_encode($buffer)); 
}

ob_start("tobase64");   // Create output buffer to capture GIF
imagegif($outputImage); // Output to buffer
ob_end_flush();         // Flush buffer

Then simply insert the counter value into your page as follows:

<img src="<?php include 'count.php'; ?>" />