Location: PHPKode > scripts > MultiCounter > multicounter/multicounter.class.php
<?php
/**
 * Counter that keeps track of multiple pages, counts each visitor's IP only
 * once a day and even remembers monthly snapshots.
 *
 * A single text file is used for storage, so no database is necessary and no
 * cookies are sent to the browser either.
 *
 * Please note that most methods should not be called directly as this might
 * cause problems with the file lock. A flag ($disabled) is set at the end of
 * the constructor which will prevent some methods from being called. The
 * reason why these methods exist in the first place is to break up the
 * code a little bit...
 *
 * Quick start:
 * just include the class and instantiate it like this:
 *   <?php require_once("multicounter.class.php"); $c = new MultiCounter() ?>
 *   Refer to the constructor's description for more options.
 * get the history of recent months as an array:
 *   $h = $c->getHistory();
 * get all counters as an array:
 *   $a = MultiCounter::getAllCounters();
 *
 * Author: Stefan Ihringer <hide@address.com>
 * Version: 1.0 (01 Sep 2002)
 * Please let me know if you find any bugs so I can update the file on phpclasses.org
 */
class MultiCounter
{
	var $ident;						// identifier of page of which hits are to be counted
	var $value = 0;					// counter's value for the selected page
	var $since = 0;					// timestamp of the first page hit (so you can write "x visitors since July 2000")
	var $access = array();			// most recent IPs for the page and the timestamps (which are keys in this array)
	var $max_ips = 10;				// maximum number of IPs in $access. Increase if you've got lots of hits each day.
	var $timeout = 86400;			// after 24 hours a user is allowed to be counted again
	var $filename = ".counter";		// filename of counter file (plaintext) relative to document root
	var $disabled = FALSE;			// will be set to true if file couldn't be opened. Saving will then be disabled.
	var $fp;						// file pointer
	var $a = array();				// array containing the lines of the whole data file

	/**
	 * Creates a new counter object.
	 *
	 * You may omit the parameters for the constructor. In this case the
	 * current page's file name (without .php extension) is used as an identifier
	 * and the counter is incremented automatically. If you just want to skip
	 * the file name to set $inc=FALSE you may use "" as a filename.
	 *
	 * @param string	identifier of page the counter intended for (limited
	 *					to 16 characters (A-Z, 0-9 and underscore), case
	 *					insensitive, longer identifiers are shortened and
	 *					invalid IDs are set to "unknown".)
	 * @param string	an alternative filename (defaults to ".counter")
	 *					to use for data storage. Relative to document root.
	 * @param bool		TRUE (default if omitted) if the counter should be
	 *					incremented or FALSE if the counter should be
	 *					initialized only (without incrementing it).
	 */
	function MultiCounter($id = "", $fn = "", $inc = TRUE)
	{
		// auto-detect identifier
		if($id == "")
		{
			preg_match("/(\w+)\.php$/", $_SERVER['PHP_SELF'], $matches);
			if(count($matches) > 0)
				$id = $matches[1];
			else
				$id = "unknown";
		}
		// check identifier
		if(!preg_match("/^[\w]+$/", $id))
			$this->ident = "unknown";
		else
			$this->ident = substr(strtolower($id), 0, 16);
		// alternative file name?
		if($fn) $this->filename = $fn;
		// load counter
		$this->open();
		$this->load();
		// increase?
		if($inc)
		{
			$this->increase();
			$this->save();
		}
		$this->close();
		$this->disabled = TRUE;
	}

	/**
	 * Opens and locks the data file.
	 *
	 * If the file could neither be opened nor created, $this->disabled will
	 * be set to TRUE which prevents further loading and saving of the counter.
	 */
	function open()
	{
		if($this->disabled) return;
		$fn = $_SERVER['DOCUMENT_ROOT']."/".$this->filename;
		$this->fp = @fopen($fn,"r+");
		if(!$this->fp)
		{
			// if file not found then try to create it
			$this->fp = @fopen($fn,"w+");
			if(!$this->fp)
			{
				// no write access on server => fail silently
				$this->disabled = TRUE;
				return;
			}
		}
		flock($this->fp, LOCK_EX);
	}

	/**
	 * Loads the counter's value from the data file.
	 *
	 * The file has to be open for reading.
	 */
	function load()
	{
		if($this->disabled) return;
		// Reads the whole file into memory except for the line containing the current
		// page's hit number which is parsed to yield the required data.
		while(!feof($this->fp))
		{
			// 4096 bytes per line... if you save lots of IPs this might not be enough.
			$buf = fgets($this->fp, 4096);
			$tokens = preg_split("/[ \t]+/", $buf, 4);
			// If line starting with current page identifier has been found, read value und IPs.
			if(count($tokens) == 4 && $tokens[0] == $this->ident)
			{
				$this->value = (integer)$tokens[1];
				$this->since = (integer)$tokens[2];
				$this->access = @unserialize($tokens[3]) or $this->access = array();
				ksort($this->access);
			}
			elseif ($buf != "\n")
				$this->a[] = $buf;
		}
		if($this->since == 0) $this->since = time();
	}

	/**
	 * Increases the counter if visitor's IP isn't blocked.
	 *
	 * This function also does some cleanup: It prevents saving too many
	 * IP adresses and it will create "history comments". A history comment
	 * is a monthly snapshot of the counter. It's saved as a comment (a line
	 * starting with #) so it won't affect the counter's log file in any way.
	 */
	function increase()
	{
		if($this->disabled) return;
		$banned = FALSE;
		$now = time();
		// create a snapshot if more than a month has passed since the
		// last time the counter has been accessed.
		if(count($this->access) > 0)
		{
			end($this->access);
			$old_timestamp = key($this->access);
			$old_month_nr = $this->month_nr($old_timestamp);
			$new_month_nr = $this->month_nr($now);
			if($new_month_nr > $old_month_nr)
			{
				$newline = "# ".$this->ident;
				$newline .= strlen($this->ident) >= 14 ? "\t" : (strlen($this->ident) >= 6 ? "\t\t" : "\t\t\t");
				$newline .= $this->value."\t".$old_timestamp."\t".date("(F Y)",$old_timestamp)."\n";
				$this->a[] = $newline;
			}
		}
		// check if visitor has already accessed the counter recently
		$x = array_search($_SERVER['REMOTE_ADDR'], $this->access);
		if(is_int($x) && $now - $x < $this->timeout)
		{
			$banned = TRUE;
			unset($this->access[$x]);
		}
		if(!$banned) $this->value++;
		// Log this page hit (and clean up while we're at it)
		$this->access[$now] = $_SERVER['REMOTE_ADDR'];
		$i = 0;
		foreach($this->access as $timestamp => $ip)
		{
			$i++;
			if($i > $this->max_ips || $now - $timestamp >= $this->timeout)
				unset($this->access[$timestamp]);
		}
		// create new line for log file
		$newline = $this->ident;
		$newline .= strlen($this->ident) == 16 ? "\t" : (strlen($this->ident) >= 8 ? "\t\t" : "\t\t\t");
		$newline .= $this->value."\t".$this->since."\t".serialize($this->access)."\n";
		$this->a[] = $newline;
	}

	/**
	 * Saves the counter.
	 *
	 * This method writes all lines that have been stored into memory by load()
	 * back to the data file (nicely formatted. The file format is simple.
	 * There are four fields separated by whitespace: identifier, counter,
	 * timestamp of 1st hit and serialized array data.
	 */
	function save()
	{
		if($this->disabled) return;
		rewind($this->fp);
		ftruncate($this->fp, 0);
		foreach($this->a as $line)
			fputs($this->fp, $line);
	}

	/**
	 * Closes the data file.
	 */
	function close()
	{
		if($this->disabled) return;
		flock($this->fp, LOCK_UN);
		fclose($this->fp);
	}

	/**
	 * Returns the counter's value.
	 *
	 * @return int	The number of visits to the page.
	 */
	function getValue()
	{
		return $this->value;
	}

	/**
	 * Returns the date of the first hit.
	 *
	 * @return int	The UNIX time (seconds since 1.1.1970) of the first hit.
	 */
	function getSince()
	{
		return $this->since;
	}

	/**
	 * Returns the month number for a given month/year combination or a timestamp.
	 *
	 * There are two ways to call this method: If you use one parameter only, a
	 * UNIX timestamp is expected. If you use two parameters, the first one is
	 * the year and the second one is the month.
	 *
	 * @param int	The year (e.g. 2002) OR a UNIX timestamp
	 * @param int	The month (between 1 and 12) if the 1st parameter was a year
	 * @return int	The result of $year * 12 + ($month - 1)
	 */
	function month_nr($a, $b = NULL)	{
		if(is_null($b))
		{
			$tmp = getdate($a);
			return $tmp['year'] * 12 + $tmp['mon'] - 1;
		}
		else
			return $a * 12 + $b - 1;
	}

	/**
	 * Returns the monthly history of the counter.
	 *
	 * Contrary to the storage format of the data file, this array contains the
	 * hits of each month and not the total number of hits. The array's keys
	 * are in the form "mm/yyyy" (e.g. "08/2002"). Months with zero hits are
	 * also included even though they are sometimes not listed in the data file.
	 *
	 * @return array	An array containing the monthly counter statistics.
	 */
	function getHistory()
	{
		$h = array();
		// "collect" all relevant history comments for this counter
		foreach($this->a as $line)
			if(preg_match('/^# '.$this->ident.'[\t ]+(\d+)[\t ]+(\d+)[\t ]\([a-z]+ \d\d\d\d\)/i', $line, $matches))
				$h["{$matches[2]}"] = $matches[1];
		// add month of last access (which might not be over yet)
		if(count($this->access) > 0)
		{
			end($this->access);
			$h["".key($this->access)] = $this->value;
		}
		ksort($h);
		// $old_* variables contain the values of the last known month
		// $new_* variables contain the values of the loop's current, new month
		// Every month that is missing in between will be filled up by the algorithm
		// which internally uses "month numbers" (years and months combined to count sequencially)
		$result = array();
		$old_month_nr = 0;
		$old_hits = 0;
		foreach($h as $timestamp => $new_hits)
		{
			$new_month_nr = $this->month_nr($timestamp);
			if($old_month_nr == 0) $old_month_nr = $new_month_nr - 1;	// first loop only
			do
			{
				$old_month_nr++;
				$y = floor($old_month_nr/12);
				$m = ($old_month_nr%12)+1;
				if($m < 10)
					$m = "0".$m;
				$result[$m."/".$y] = $old_month_nr<$new_month_nr?0:($new_hits - $old_hits);
			}
			while($old_month_nr < $new_month_nr);
			$old_hits = $new_hits;
		}
		return $result;
	}

	/**
	 * Returns all counters saved in a file (for stats)
	 * (static method)
	 *
	 * @return array	An array containing all counter identifiers
	 */
	function getAllCounters($file = ".counter")
	{
		$result = array();
		$fn = $_SERVER['DOCUMENT_ROOT']."/".$file;
		$fp = @fopen($fn,"r+");
		if(!$fp) return $result;
		flock($fp, LOCK_SH);
		// read line-by-line
		while(!feof($fp))
		{
			$buf = fgets($fp, 4096);
			if(preg_match("/^(\w+)[\t ]+\d+[\t ]+\d+[\t ]+.*$/i", $buf, $matches))
			{
				// matched data line
				$result[] = $matches[1];
			}
			else if(preg_match("/^# (\w+)[\t ]+\d+[\t ]+\d+[\t ]\([a-z]+ \d\d\d\d\)$/i", $buf, $matches))
			{
				// matched history comment
				$result[] = $matches[1];
			}
		}
		flock($fp, LOCK_UN);
		fclose($fp);
		// remove duplicates in array
		return array_unique($result);
	}
}

?>
Return current item: MultiCounter