* @package Mammut\Info */ class Statistic extends \Mammut\StrictObject { const _VERSION_ = '1.1.0.1'; protected $tables = array(); // internal storage of table names /** * * @var \Mammut\DB\DB */ protected $db = null; // anti brute force settings protected $blacklist_time = 600; // timespan which should be scaned for brute force attacks (sec) protected $blacklist_count = 200; // number of connections in the timespan which triggers the blacklisting (num) protected $blacklist_bantime = 900; // how long a ip gets banned (sec) protected $blacklist = array(); protected $blacklisted = NULL; protected $session_time = 600; // how long a session will be marked as online (10 min) protected $session_lifetime = 172800; // lifetime of session entries (2 days) protected $current_deltime = 172800; // the delay to the cleanup of the current-table (2 days) const COUNT_VISITS = 'visits'; // overall page visits const COUNT_HITS = 'hits'; // overall page views const COUNT_ONLINE = 'online'; // visitors currently online const COUNT_ONUSER = 'onuser'; // users currently online const COUNT_V_24H = 'v24h'; // 24h page visits const COUNT_H_24H = 'h24h'; // 24h page views const MAX_ONLINE = 'mx_online'; // maximum visitors online const MAX_ONUSER = 'mx_onuser'; // maximum users online const MAX_V_24H = 'mx_v24h'; // maximum 24h page visits const MAX_H_24H = 'mx_h24h'; // maximum 24h page views /** * creates a new statistic object * * to initialize the statistic object, use something linke this: *
* $sinf = array(); * $_SESSION['statistics'] = &$sinf; * $stat = new MFStatistic($myDB, 'statistic', $sinf); ** * @param DB $database * database connection which should be used * @param string $tablePrefix * name prefix, * tables are * _hour complete statistic on hour base * _hour_bot same as _hour, but with user agents which are known as bots * _current last 48h statistic * _current_bot same as _current, but with user agents which are known as * bots * _referer referer storage (no bots here) * _values hit count, max count etc... * @param string $sessionId * unique session identifier */ public function __construct(DB &$database, $tablePrefix) { $this->tables['agents'] = $tablePrefix . '_agents'; $this->tables['targets'] = $tablePrefix . '_targets'; $this->tables['bothits'] = $tablePrefix . '_bothits'; $this->tables['full'] = $tablePrefix . '_full'; $this->tables['current'] = $tablePrefix . '_current'; $this->tables['inout'] = $tablePrefix . '_inout'; $this->tables['referer'] = $tablePrefix . '_referer'; $this->tables['values'] = $tablePrefix . '_values'; $this->tables['sessions'] = $tablePrefix . '_sessions'; $this->tables['blacklist'] = $tablePrefix . '_blacklist'; $this->db = &$database; } public function setSessionTime($time) { $this->session_time = (int) $time; } public function getSessionTime() { return (int) $this->session_time; } /** * updates the statistic * * @param int $siteid * identificator (on systems with mutliple sites) * @param int $userid * id (0 for anonymous) * @param string $module * ident (section) * @param string $view * ident of the module * @param bool $ignoreBF * brute force ips in statistic */ public function update($siteid, $sessionid, $userid, $module, $view, $extra = '', $ignoreBF = true) { $old_tz = date_default_timezone_get(); date_default_timezone_set('UTC'); $oldAbortSetting = ignore_user_abort(true); $db = &$this->db; $uts = time(); $utsh = gmmktime(gmdate('H', $uts), 0, 0, gmdate('m', $uts), gmdate('d', $uts), gmdate('Y', $uts)); $ip = &$_SERVER['REMOTE_ADDR']; $target = &$_SERVER['REQUEST_URI']; $efn_site = $db->escapeColumnName('site_id'); $efn_ip = $db->escapeColumnName('ip'); $efn_lasthit = $db->escapeColumnName('lasthit'); $efn_date = $db->escapeColumnName('timestamp'); $efn_targetid = $db->escapeColumnName('target_id'); $efn_toggel = $db->escapeColumnName('toggel'); // update blacklist $sql = 'UPDATE ' . $db->escapeTableName($this->tables['blacklist']) . ' SET ' . $efn_lasthit . '=' . $uts . ',' . $efn_toggel . '=(' . $efn_toggel . '+1)%4' . ' WHERE ' . $efn_site . '=' . $siteid . ' AND ' . $efn_ip . '=' . $db->escapeValue($ip) . ' AND (' . $efn_lasthit . '>' . ($uts - $this->blacklist_bantime) . ' OR ' . $efn_lasthit . '=0)'; $db->query($sql); if($this->isBlacklisted($siteid) && !$ignoreBF) return false; // attacker! // get or create target id // building where cause of the query $where = array( // this data will be used for the insert, too. the crc mod 2000000000 is because the usage of 32 bit unsigned integer fields. 'site_id' => $siteid,'file' => $_SERVER['PHP_SELF'], 'file_hash' => crc32($_SERVER['PHP_SELF']) % 2000000000, // improves performance 'instance' => $module, 'view' => $view,'extra' => $extra,'extra_hash' => crc32($extra) % 2000000000) // improves performance ; $conditions = array(); foreach($where as $field=>$value) { $conditions[] = $db->escapeColumnName($field) . '=' . $db->escapeValue($value); } $where_st = implode(' AND ', $conditions); // building select query $sql = 'SELECT ' . $db->escapeColumnName('id') . ' FROM ' . $db->escapeTableName($this->tables['targets']); $sql .= ' WHERE ' . $where_st; $row = $db->getArray($sql); if(empty($row['id'])) { // add new entries $data = $where; $db->table($this->tables['targets'])->insert($data); $targetId = $db->getInsertId(); } else $targetId = $row['id']; unset($data, $where, $where_st, $field, $value, $sql, $row); // cleanup $browser = UserAgent::getInstance(); if($browser->isBot()) { // check if the visiter is a automated web crawler $botService = $browser->getBotService(); $data = array( 'site_id' => $siteid,'botname' => $botService, 'date' => $db->uts2datetimeCol($uts),'target_id' => $targetId, 'uri' => $_SERVER['REQUEST_URI']); $db->table($this->tables['bothits'])->insert($data); return true; } $efn_uid = $db->escapeColumnName('user_id'); $efn_sid = $db->escapeColumnName('session_id'); // TODO: add the out info in the inout table // clean up the session table $sql = 'DELETE FROM ' . $db->escapeTableName($this->tables['sessions']) . ' WHERE dead=1'; $db->query($sql); // update existing sessions $sql = 'UPDATE ' . $db->escapeTableName($this->tables['sessions']) . ' SET ' . $efn_lasthit . '=' . $uts . ',' . $efn_toggel . '=(' . $efn_toggel . '+1)%4,' . $efn_uid . '=' . $db->escapeValue((int) $userid) . ',' . $efn_targetid . '=' . $db->escapeValue($targetId) . ' WHERE ' . $efn_site . '=' . $siteid . ' AND dead=0' . ' AND ' . $efn_sid . '=' . $db->escapeValue($sessionid) . ' AND ' . $efn_ip . '=' . $db->escapeValue($ip) . ' AND (' . $efn_lasthit . '>' . ($uts - $this->session_time) . ' OR ' . $efn_lasthit . '=0)'; $db->query($sql); $num = $db->getAffectedRowCount(); // marking outdated sessions $ts = $uts - $this->session_time; $sql = 'UPDATE ' . $db->escapeTableName($this->tables['sessions']) . ' SET dead=1 WHERE ' . $efn_lasthit . ''; $db->executeP($sql, array($ts)); if($num > 1) { // something went wrong, so we try to fix it $sql = 'SELECT * FROM ' . $db->escapeTableName($this->tables['sessions']) . ' WHERE ' . $efn_site . '=' . $siteid . ' AND ' . $efn_sid . '=' . $db->escapeValue($sessionid) . ' AND ' . $efn_ip . '=' . $db->escapeValue($ip) . ' ORDER BY ' . $efn_date . ' ASC'; // backup the oldest dataset $data = $db->getArray($sql); $sql = 'DELETE FROM ' . $db->escapeTableName($this->tables['sessions']) . ' WHERE ' . $efn_site . '=' . $siteid . ' AND ' . $efn_sid . '=' . $db->escapeValue($sessionid) . ' AND ' . $efn_ip . '=' . $db->escapeValue($ip); $db->query($sql); // restore the dataset $db->table($this->tables['sessions'])->insert($data); } $newVisitor = false; if($num == 0) { // no updates mean the user is a new visitor, so we add session entry $newVisitor = true; $data = array( 'site_id' => $siteid,'session_id' => $sessionid,'ip' => $ip,'timestamp' => $uts, 'lasthit' => $uts,'target_id' => (int) $targetId,'user_id' => (int) $userid, 'toggel' => 0,'dead' => 0); $db->table($this->tables['sessions'])->insert($data); $data = array( 'site_id' => $siteid,'type' => 0,'timekey' => new \DateTime('@' . $uts), 'agent_id' => -1,'target_id' => (int) $targetId); $r = $db->table($this->tables['inout'])->select($data); $data2 = $r->fetchArray(); $r->close(); if(empty($data2)) { $data['hits'] = 1; $data2 = $db->table($this->tables['inout'])->insert($data); } else { $data2 = array('hits' => $data2['hits'] + 1); $db->table($this->tables['inout'])->update($data2, $data); } if($this->blacklist_time > 0) { // blacklist add check // we don't need to check this on old sessions, because they cannot be // a bf-attacker $ts = $uts - $this->blacklist_time; $sql = 'SELECT COUNT(*) AS c FROM ' . $db->escapeTableName($this->tables['sessions']) . ' WHERE ' . $efn_site . '=?' . ' AND ' . $efn_date . '=' . $efn_lasthit . ' AND ' . $efn_lasthit . '>?' . ' AND ' . $efn_ip . '=?'; $num = $db->getObjectP($sql, array($siteid,$ts,$ip))->c; if($num > $this->blacklist_count) { if(!$this->isBlacklisted($siteid)) { $data = array( 'site_id' => $siteid,'ip' => $ip,'timestamp' => $uts, 'lasthit' => $uts); $db->table($this->tables['blacklist'])->insert($data); $this->blacklisted = true; // update cached value } $newVisitor = $ignoreBF; } } } unset($num, $efn_toggel); // cleanup $efn_key = $db->escapeColumnName('key'); $efn_value = $db->escapeColumnName('value'); $sql = 'UPDATE ' . $db->escapeTableName($this->tables['values']) . ' SET ' . $efn_value . '=' . $efn_value . '+1' . ' WHERE ' . $efn_site . '=' . $siteid; // if the user is a new visitor, update both entries. if not, update just the hits value if($newVisitor) $sql .= ' AND ' . $efn_key . ' IN (' . $db->escapeValue('hits') . ',' . $db->escapeValue('visits') . ')'; else $sql .= ' AND ' . $efn_key . '=' . $db->escapeValue('hits'); $db->query($sql); unset($sql, $efn_key, $efn_value); // cleanup if($db->getAffectedRowCount() == 0) { // add new entries $data = array('site_id' => $siteid,'key' => 'visits','value' => 1); $db->table($this->tables['values'])->insert($data); $data['key'] = 'hits'; $db->table($this->tables['values'])->insert($data); } $this->updateMaxValues($siteid); // update the 'current data table $ts = $db->uts2datetimeCol($uts - $this->current_deltime); $db->executeP('DELETE FROM ' . $db->escapeTableName($this->tables['current']) . 'WHERE ' . $db->escapeColumnName('date') . '', array( $ts)); $data = array( 'site_id' => $siteid,'date' => $db->uts2datetimeCol($uts), 'visit' => $newVisitor ? 1 : 0,'hit' => 1,'user' => (int) $userid); $db->table($this->tables['current'])->insert($data); $referer = &$_SERVER['HTTP_REFERER']; $self = empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST']; if(($referer != '') && (!preg_match('%^((http|https|ftp)://){0,1}[a-zA-Z0-9_.-]*' . $self . '%i', $referer))) { $data = array( 'site_id' => $siteid,'timekey' => new DateTime(), 'referer' => gzdeflate($referer, 9),'target_id' => (int) $targetId, 'target' => $target); $db->table($this->tables['referer'])->insert($data); } $agentId = $this->getAgentId($browser); // building where cause of the query $where = array( // this data will be used for the insert, too 'site_id' => $siteid,'timekey' => $db->uts2datetimeCol($utsh), 'target_id' => (int) $targetId,'agent_id' => (int) $agentId); $where_st = '1'; $conditions = array(); foreach($where as $field=>$value) $conditions[] = $db->escapeColumnName($field) . '=' . $db->escapeValue($value); $where_st = implode(' AND ', $conditions); unset($conditions); $efn_hits = $db->escapeColumnName('hits'); $efn_visits = $db->escapeColumnName('visits'); // building update query $sql = 'UPDATE ' . $db->escapeTableName($this->tables['full']); $sql .= ' SET ' . $efn_hits . '=' . $efn_hits . '+1'; if($newVisitor) $sql .= ', ' . $efn_visits . '=' . $efn_visits . '+1'; $sql .= ' WHERE ' . $where_st; $db->query($sql); if($db->getAffectedRowCount() == 0) { // add new entries $data = $where; $data['hits'] = 1; $data['visits'] = $newVisitor ? 1 : 0; $db->table($this->tables['full'])->insert($data); } unset($data, $where, $where_st, $field, $value, $sql, $row); // cleanup // restore the previous value ignore_user_abort($oldAbortSetting); date_default_timezone_set($old_tz); } /** * updates the max values if nessesary */ private function updateMaxValues($siteId) { // get current values and update max values if needed $online = $this->getCount(self::COUNT_ONLINE, $siteId); $onuser = $this->getCount(self::COUNT_ONUSER, $siteId); $db = &$this->db; $result = $db->getObjectListP('SELECT * FROM ' . $db->escapeTableName($this->tables['values']) . ' WHERE ' . $db->escapeColumnName('site_id') . '=?', array( $siteId)); $values = array(); foreach($result as $row) $values[$row->key] = $row->value; // max online counter if(!isset($values['max_on'])) { $data = array('site_id' => $siteId,'key' => 'max_on','value' => $online); $db->table($this->tables['values'])->insert($data); } elseif($values['max_on'] < $online) { $where = array('site_id' => $siteId,'key' => 'max_on'); $data = array('value' => $online); $db->table($this->tables['values'])->update($data, $where); } // max user online counter if(!isset($values['max_uon'])) { $data = array('site_id' => $siteId,'key' => 'max_uon','value' => $onuser); $db->table($this->tables['values'])->insert($data); } elseif($values['max_uon'] < $onuser) { $where = array('site_id' => $siteId,'key' => 'max_uon'); $data = array('value' => $onuser); $db->table($this->tables['values'])->update($data, $where); } } /** * scans the agent table for a matching entry. * if none is found, a new one is created. the id * of the entry is returned */ public function getAgentId(&$browser) { $db = &$this->db; // try to get user agent id, add him if not found $row = $db->getArrayP('SELECT ' . $db->escapeColumnName('id') . ' FROM ' . $db->escapeTableName($this->tables['agents']) . 'WHERE ' . $db->escapeColumnName('name') . '=?', array( $browser->getAgentString())); if(empty($row['id'])) { $data = array('name' => $browser->getAgentString()); $db->table($this->tables['agents'])->insert($data); return (int) $db->getInsertId(); } else return (int) $row['id']; } /** * returns the count of visits/hits in a specific * * @param string $what * of the MFStatistic::VAL_* constants * @return int count of the selected type, or false if type was invalid */ public function getCount($what = self::COUNT_VISITS, $siteid = 0) { $db = &$this->db; $valuetable = $db->table($this->tables['values']); $uts = time(); try { switch($what) { case self::COUNT_VISITS: case self::COUNT_HITS: $infodata = array(); $infodata['site_id'] = $siteid; $infodata['key'] = ($what == self::COUNT_VISITS ? 'visits' : 'hits'); $result = $valuetable->select($infodata); if($result->getRowCount() > 0) $value = $result->fetchObject()->value; else $value = 0; $result->close(); return (int) $value; break; case self::MAX_ONLINE: case self::MAX_ONUSER: $infodata = array(); $infodata['site_id'] = $siteid; $infodata['key'] = ($what == self::MAX_ONLINE ? 'max_on' : 'max_uon'); $result = $valuetable->select($infodata); if($result->getRowCount() > 0) $value = $result->fetchObject()->value; else $value = 0; $result->close(); return (int) $value; break; case self::COUNT_ONLINE: case self::COUNT_ONUSER: $sql = 'SELECT COUNT(*) AS c FROM ' . $db->escapeTableName($this->tables['sessions']) . ' WHERE ' . $db->escapeColumnName('site_id') . '=?' . ' AND ' . $db->escapeColumnName('lasthit') . '>?'; if($what == self::COUNT_ONUSER) $sql .= ' AND ' . $db->escapeColumnName('user_id') . '>0'; return (int) $db->getObjectP($sql, array($siteid,$uts - $this->session_time))->c; break; case self::COUNT_V_24H: $datekey = date('YmdH', time() - (24 * 3600)); $v24h = (int) $db->getObject('SELECT SUM(`visits`) AS c FROM ' . $db->escapeTableName($this->tables['hour']) . ' WHERE (`hour` + DAY *100 + MONTH *10000 + year *1000000) > ' . $datekey . ' AND `siteid`=' . $siteid)->c; return $v24h; break; case self::MAX_V_24H: $infodata = array(); $infodata['siteid'] = $siteid; $infodata['key'] = 'max_24h'; $result = $valuetable->select($infodata); if($result->getRowCount() > 0) $value = $result->fetchObject()->value; else $value = 0; $result->close(); return (int) $value; break; } } catch(SQLException $e) { echo 'SQL ERROR:' . $e->getMessage() . ' (SQL=' . $e->getQuery() . ')