├── test.php ├── phpcount.sql └── phpcount.php /test.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phpcount.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 3.3.7deb6 3 | -- http://www.phpmyadmin.net 4 | -- 5 | -- Host: localhost 6 | -- Generation Time: Nov 04, 2011 at 06:31 PM 7 | -- Server version: 5.1.49 8 | -- PHP Version: 5.3.3-7+squeeze3 9 | 10 | SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; 11 | 12 | 13 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 14 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 15 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 16 | /*!40101 SET NAMES utf8 */; 17 | 18 | -- 19 | -- Database: `phpcount` 20 | -- 21 | 22 | -- -------------------------------------------------------- 23 | 24 | -- 25 | -- Table structure for table `hits` 26 | -- 27 | 28 | CREATE TABLE IF NOT EXISTS `hits` ( 29 | `pageid` varchar(100) NOT NULL, 30 | `isunique` tinyint(1) NOT NULL, 31 | `hitcount` int(10) unsigned NOT NULL, 32 | KEY `pageid` (`pageid`) 33 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 34 | 35 | -- 36 | -- add primary key 37 | -- 38 | alter table hits add primary key (pageid, isunique); 39 | 40 | -- 41 | -- Dumping data for table `hits` 42 | -- 43 | 44 | 45 | -- -------------------------------------------------------- 46 | 47 | -- 48 | -- Table structure for table `nodupes` 49 | -- 50 | 51 | CREATE TABLE IF NOT EXISTS `nodupes` ( 52 | `ids_hash` char(64) NOT NULL, 53 | `time` bigint(20) unsigned NOT NULL, 54 | PRIMARY KEY (`ids_hash`) 55 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 56 | 57 | -- 58 | -- Dumping data for table `nodupes` 59 | -- 60 | 61 | -------------------------------------------------------------------------------- /phpcount.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /* 21 | * This PHP Class provides a hit counter that is able to track unique hits 22 | * without recording the visitor's IP address in the database. It does so by 23 | * recording the hash of the IP address and page name. 24 | * 25 | * By hashing the IP address with page name as salt, you prevent yourself from 26 | * being able to track a user as they navigate your site. You also prevent 27 | * yourself from being able to recover anyone's IP address without brute forcing 28 | * through all of the assigned IP address blocks in use by the internet. 29 | * 30 | * Contact: havoc AT defuse.ca 31 | * WWW: https://defuse.ca/ 32 | * 33 | * USAGE: 34 | * In your script, use reqire_once() to import this script, then call the 35 | * functions like PHPCount::AddHit(...); See each function for help. 36 | * 37 | * NOTE: You must set the database credentials in the InitDB method. 38 | */ 39 | 40 | class PHPCount 41 | { 42 | /* 43 | * Defines how many seconds a hit should be rememberd for. This prevents the 44 | * database from perpetually increasing in size. Thirty days (the default) 45 | * works well. If someone visits a page and comes back in a month, it will be 46 | * counted as another unique hit. 47 | */ 48 | const HIT_OLD_AFTER_SECONDS = 2592000; // default: 30 days. 49 | 50 | // Don't count hits from search robots and crawlers. 51 | const IGNORE_SEARCH_BOTS = true; 52 | 53 | // Don't count the hit if the browser sends the DNT: 1 header. 54 | const HONOR_DO_NOT_TRACK = false; 55 | 56 | private static $IP_IGNORE_LIST = array( 57 | '127.0.0.1', 58 | ); 59 | 60 | private static $DB = false; 61 | 62 | private static function InitDB() 63 | { 64 | if(self::$DB) 65 | return; 66 | 67 | try 68 | { 69 | // TODO: Set the database login credentials. 70 | self::$DB = new PDO( 71 | 'mysql:host=SET_THIS_TO_HOSTNAME;dbname=SET_THIS_TO_DBNAME', 72 | 'SET_THIS_TO_THE_USERNAME', // Username 73 | 'SET_THIS_TO_THE_PASSWORD', // Password 74 | array(PDO::ATTR_PERSISTENT => true) 75 | ); 76 | } 77 | catch(Exception $e) 78 | { 79 | die('Failed to connect to phpcount database'); 80 | } 81 | } 82 | 83 | public static function setDBAdapter($db) 84 | { 85 | self::$DB = $db; 86 | return $db; 87 | } 88 | 89 | /* 90 | * Adds a hit to a page specified by a unique $pageID string. 91 | */ 92 | public static function AddHit($pageID) 93 | { 94 | if(self::IGNORE_SEARCH_BOTS && self::IsSearchBot()) 95 | return false; 96 | if(in_array($_SERVER['REMOTE_ADDR'], self::$IP_IGNORE_LIST)) 97 | return false; 98 | if( 99 | self::HONOR_DO_NOT_TRACK && 100 | isset($_SERVER['HTTP_DNT']) && $_SERVER['HTTP_DNT'] == "1" 101 | ) { 102 | return false; 103 | } 104 | 105 | self::InitDB(); 106 | 107 | self::Cleanup(); 108 | if(self::UniqueHit($pageID)) 109 | { 110 | self::CountHit($pageID, true); 111 | self::LogHit($pageID); 112 | } 113 | self::CountHit($pageID, false); 114 | 115 | return true; 116 | } 117 | 118 | /* 119 | * Returns (int) the amount of hits a page has 120 | * $pageID - the page identifier 121 | * $unique - true if you want unique hit count 122 | */ 123 | public static function GetHits($pageID, $unique = false) 124 | { 125 | self::InitDB(); 126 | 127 | $q = self::$DB->prepare( 128 | 'SELECT hitcount FROM hits 129 | WHERE pageid = :pageid AND isunique = :isunique' 130 | ); 131 | $q->bindParam(':pageid', $pageID); 132 | $q->bindParam(':isunique', $unique); 133 | $q->execute(); 134 | 135 | if(($res = $q->fetch()) !== FALSE) 136 | { 137 | return (int)$res['hitcount']; 138 | } 139 | else 140 | { 141 | //die("Missing hit count from database!"); 142 | return 0; 143 | } 144 | } 145 | 146 | /* 147 | * Returns the total amount of hits to the entire website 148 | * When $unique is FALSE, it returns the sum of all non-unique hit counts 149 | * for every page. When $unique is TRUE, it returns the sum of all unique 150 | * hit counts for every page, so the value that's returned IS NOT the 151 | * amount of site-wide unique hits, it is the sum of each page's unique 152 | * hit count. 153 | */ 154 | public static function GetTotalHits($unique = false) 155 | { 156 | self::InitDB(); 157 | 158 | $q = self::$DB->prepare( 159 | 'SELECT hitcount FROM hits WHERE isunique = :isunique' 160 | ); 161 | $q->bindParam(':isunique', $unique); 162 | $q->execute(); 163 | $rows = $q->fetchAll(); 164 | 165 | $total = 0; 166 | foreach($rows as $row) 167 | { 168 | $total += (int)$row['hitcount']; 169 | } 170 | return $total; 171 | } 172 | 173 | /*====================== PRIVATE METHODS =============================*/ 174 | 175 | private static function IsSearchBot() 176 | { 177 | // Of course, this is not perfect, but it at least catches the major 178 | // search engines that index most often. 179 | $keywords = array( 180 | 'bot', 181 | 'spider', 182 | 'spyder', 183 | 'crawlwer', 184 | 'walker', 185 | 'search', 186 | 'yahoo', 187 | 'holmes', 188 | 'htdig', 189 | 'archive', 190 | 'tineye', 191 | 'yacy', 192 | 'yeti', 193 | ); 194 | 195 | $agent = strtolower($_SERVER['HTTP_USER_AGENT']); 196 | 197 | foreach($keywords as $keyword) 198 | { 199 | if(strpos($agent, $keyword) !== false) 200 | return true; 201 | } 202 | 203 | return false; 204 | } 205 | 206 | private static function UniqueHit($pageID) 207 | { 208 | $ids_hash = self::IDHash($pageID); 209 | 210 | $q = self::$DB->prepare( 211 | 'SELECT `time` FROM nodupes WHERE ids_hash = :ids_hash' 212 | ); 213 | $q->bindParam(':ids_hash', $ids_hash); 214 | $q->execute(); 215 | 216 | if(($res = $q->fetch()) !== false) 217 | { 218 | if($res['time'] > time() - self::HIT_OLD_AFTER_SECONDS) 219 | return false; 220 | else 221 | return true; 222 | } 223 | else 224 | { 225 | return true; 226 | } 227 | } 228 | 229 | private static function LogHit($pageID) 230 | { 231 | $ids_hash = self::IDHash($pageID); 232 | 233 | $q = self::$DB->prepare( 234 | 'SELECT `time` FROM nodupes WHERE ids_hash = :ids_hash' 235 | ); 236 | $q->bindParam(':ids_hash', $ids_hash); 237 | $q->execute(); 238 | 239 | $curTime = time(); 240 | 241 | if(($res = $q->fetch()) !== false) 242 | { 243 | $s = self::$DB->prepare( 244 | 'UPDATE nodupes SET `time` = :time WHERE ids_hash = :ids_hash' 245 | ); 246 | $s->bindParam(':time', $curTime); 247 | $s->bindParam(':ids_hash', $ids_hash); 248 | $s->execute(); 249 | } 250 | else 251 | { 252 | $s = self::$DB->prepare( 253 | 'INSERT INTO nodupes (ids_hash, `time`) 254 | VALUES( :ids_hash, :time )' 255 | ); 256 | $s->bindParam(':time', $curTime); 257 | $s->bindParam(':ids_hash', $ids_hash); 258 | $s->execute(); 259 | } 260 | } 261 | 262 | private static function CountHit($pageID, $unique) 263 | { 264 | $q = self::$DB->prepare( 265 | 'INSERT INTO hits (pageid, isunique, hitcount) VALUES (:pageid, :isunique, 1) ' . 266 | 'ON DUPLICATE KEY UPDATE hitcount = hitcount + 1' 267 | ); 268 | $q->bindParam(':pageid', $pageID); 269 | $unique = $unique ? '1' : '0'; 270 | $q->bindParam(':isunique', $unique); 271 | $q->execute(); 272 | } 273 | 274 | private static function IDHash($pageID) 275 | { 276 | $visitorID = $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']; 277 | return hash("SHA256", $pageID . $visitorID); 278 | } 279 | 280 | private static function Cleanup() 281 | { 282 | $last_interval = time() - self::HIT_OLD_AFTER_SECONDS; 283 | 284 | $q = self::$DB->prepare( 285 | 'DELETE FROM nodupes WHERE `time` < :time' 286 | ); 287 | $q->bindParam(':time', $last_interval); 288 | $q->execute(); 289 | } 290 | } 291 | --------------------------------------------------------------------------------