egscripts/egstats/egstats.php

605 lines
No EOL
22 KiB
PHP
Executable file

#!/usr/bin/php5
<?php
/**
* This set of scripts is indended to accomplish the following:
*
* For each of a number of configured "domains" on this or another
* server, this script will
* a. mirror (rsync) a designated HTTP common access_logs to a
* local access_log.
* b. if it does not already exist, create the appropriate AWStats
* configuration file, including global and Egressive-specific
* configuration defaults as required.
* c. if it has not be performed already, initialise the
* statistics database
* d. update the statistics database
* e. produce a series of HTML pages based on the statistics
* f. copy the relevant icon and graphics files into a suitable
* subdirectory so that they're available for the HTML pages to include
* g. put those HTML pages into a web-viewable directory appropriate to
* the month within the domain's directory
* h. create a sensible "index.html" page so that a browser clicking on
* that month directory sees the summary page for that month and can
* access the other, more specific pages linked in that summary
*/
/* PEAR_LIB_DIR PEAR library directory. */
define('PEAR_LIB_DIR', '/usr/share/pear');
// dummy error constant for this example
define('MYCLASS_ERROR_CODE', 1);
/* system constants */
define('EG_APP','egstats');
define('EG_DIR','/etc/egscripts/egstats');
define('DOMAIN_DIR',EG_DIR.'/conf');
define('DOMAIN_CONF_SUFFIX','conf');
// the rsync command
define('RSYNC','/usr/bin/rsync');
// rsync command flags
define('FLAGS','-e ssh --stats');
// the chown command
define('CHOWN','/bin/chown');
// AWStats related defines
define('AWSBIN','/var/www/cgi-bin/awstats.pl');
define('AWSDIR','/usr/share/awstats');
define('AWS_DB_DIR','/var/lib/awstats');
define('AWS_DB_SUFFIX','txt');
define('AWS_ARGS','-update');
define('AWS_TOOLS_DIR',AWSDIR.'/tools');
define('AWSALLBIN',AWS_TOOLS_DIR.'/awstats_buildstaticpages.pl');
define('AWSALL_ARGS','-update -awstatsprog='.AWSBIN);
// AWStats configuration defaults
define('AWSTATS_LOCAL_DEFAULTS','local_defaults.inc');
define('AWSTATS_GLOBAL_DEFAULTS','global_defaults.inc');
define('AWSCONF_DIR',EG_DIR.'/awstatsconf');
define('AWSCONF_PREFIX','awstats');
define('AWSCONF_SUFFIX','conf');
// XHTML output details
define('AWS_OUT_PREFIX','awstats');
define('AWS_OUT_SUFFIX','html');
define('AWS_OUT_INDEX','index.html');
/**
* Program includes go below this
*/
/* PHP ini set to local PEAR library */
ini_set('include_path', PEAR_LIB_DIR);
/* Require main PEAR class and PLUM classes. */
require_once('PEAR.php');
/* include the logging functionality */
require_once('Log.php');
/* include file reading/writing functionality */
require_once('File.php');
/* include command line argument processing functionality */
require_once('Console/Getopt.php');
/* include command line argument processing functionality */
require_once('Config.php');
/**
* Domain class - information related to a particular virtual domain
* requiring awstats reports (http://awstats.sourceforge.net)
*
* @author Dave Lane <dave@egressive.com>
* @copyright Copyright (C) 2005 Egressive Limited (www.egressive.com)
* @version $Id: egawstats.php 0 2005-08-18 08:07:26Z dave $
* @package egawstats
*/
class Domain extends PEAR {
/**
* @var array $domain a mixed array of "name" and "values"
* for the current domain.
* @access private
*/
private $domain = array();
/**
* @var array $domain_array two dimensional array containing arrays
* of parsed domains.
* @access private
*/
private $domain_array = array();
/**
* @var array $report_dates two dimensional array containing years and
* and months [yyyy][mm] for which the domain yields results.
* @access private
*/
private $report_dates = null;
/**
* @var object $logger - reference to a PEAR log object
* @access private
*/
private $logger = null;
/**
* @var string $short_args - one letter command line arguments
* obeys GNU args conventions - args take form -a or -b value or -ab value
* for multiple arguments
* @access private
*/
private $short_args = "psa";
/**
* @var string $long_args - full word command line arguments, more memorable
* obeys GNU args conventions - args take form --flag or --flagwithvalue=value
* @access private
*/
private $long_args = array('rebuildpages','rebuildstats','rebuildconfs');
/**
* @var array $options_array - array of command line arguments and values
* from the Console/Getopts PEAR class
* @access private
*/
private $options_array = array();
/**
* @var array $options - array of just the command line arguments and values
* @access private
*/
private $options = array();
public function __construct($logger) {
$this->logger = $logger;
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
//
// Error handling
//
// function callback
//$this->setErrorHandling(PEAR_ERROR_CALLBACK, array(&$this,'handleError'));
$this->setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
}
/**
* get the command line arguments
*/
public function get_args() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
$cli_args = new Console_Getopt();
$args = $cli_args->readPHPArgv();
array_shift($args);
$this->options_array = $cli_args->getopt2($args,$this->short_args,$this->long_args);
$this->options = $this->options_array[0];
//print_r($this->options);
}
/**
* put the command line arguments into an array that's useful
* elsewhere in the code...
*/
public function process_args() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
// first, get the command line arguments
$this->get_args();
//print_r($this->options);
if (count($this->options) and
(count($this->options_array[0]) or count($this->options_array[1]))) {
foreach($this->options as $option) {
list($key,$value) = $option;
if ($value) {
$this->logger->log('option['.$key.'] = '.$value,PEAR_LOG_DEBUG);
} else {
$this->logger->log('option '.$key.' specified',PEAR_LOG_DEBUG);
}
}
} else {
$this->logger->log('no command line options supplied',PEAR_LOG_DEBUG);
}
}
/**
* put the domain definitions in the configuration files into an
* array that's useful elsewhere in the code...
*/
public function process_configs() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
$conf_files = glob(DOMAIN_DIR.'/*.'.DOMAIN_CONF_SUFFIX);
//print_r($conf_files);
$config = new Config();
foreach ($conf_files as $conf_file) {
$conf =& $config->parseConfig($conf_file,"XML");
}
// assign the configuration info to the domain array
$this->domain_array = $conf->toArray();
}
/**
* run the rsync that synchronises the logs on the main machine to the
* copy on the machine running awstats
*/
private function sync_access_logs() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
// the current domain is $this->domain...
// check to see that we actually have to sync a remote file for this
// domain...
if (isset($this->domain['remote']['file']) and
isset($this->domain['remote']['user'])) {
$command = RSYNC.' '.FLAGS.' '.$this->domain['remote']['user'].'@'.
$this->domain['remote']['host'].':'.$this->domain['remote']['path'].'/'.
$this->domain['remote']['file'].' '.$this->domain['local']['path'].'/'.
$this->domain['local']['file'];
if (!is_dir($this->domain['local']['path'])) {
$this->logger->log($this->domain['local']['path']." doesn't exist yet, ".
'creating it...',PEAR_LOG_INFO);
// if it's not there, make the dir (rwxr-xr-x)
if (!mkdir($this->domain['local']['path'],0755)) {
$this->logger->log('failed to create dir '.$this->domain['local']['path'].
': '.$command,PEAR_LOG_ERR);
return false;
}
// set appropriate file ownership
chown($this->domain['local']['path'],$this->domain['local']['user']);
chgrp($this->domain['local']['path'],$this->domain['local']['group']);
}
// run the command and capture the output...
$output = array();
$this->logger->log('rsyncing domain '.$this->domain['name'].
': '.$command,PEAR_LOG_INFO);
$return_value = exec($command,$output);
//print_r($output);
// if the rsync was successful, it'll return a value like this
if (strstr($return_value,"total size ")) {
$this->logger->log('rsync: success - '.$return_value.' ',PEAR_LOG_INFO);
$this->logger->log(' '.$output[11],PEAR_LOG_INFO);
$this->logger->log(' '.$output[12],PEAR_LOG_INFO);
}
else {
$this->logger->log('rsync: failed - '.$return_value.' ',PEAR_LOG_ERR);
return false;
}
}
// if we don't have a remote file, just move on to the next step...
return true;
}
/**
* generate a valid AWSTATs configuration file if it doesn't already exist
*/
private function create_awstats_config() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
// the current domain is $this->domain...
// the filename for this domain's AWSTATs configuration file
$filename = AWSCONF_PREFIX.'.'.$this->domain['name'].'.'.AWSCONF_SUFFIX;
// add the path, too...
$fullname = AWSCONF_DIR.'/'.$filename;
// if it doesn't exist, then...
if (!file_exists($fullname)) {
// construct the conf file contents based on this template...
$file_content[] = "#";
$file_content[] = "# site specific settings for ".$this->domain['name'];
$file_content[] = "SiteDomain=\"".$this->domain['name']."\"";
if (is_array($this->domain['aliases']['alias'])) {
$aliases = implode(' ',$this->domain['aliases']['alias']);
} else {
$aliases = $this->domain['aliases']['alias'];
}
$aliases = $this->domain['name'].' '.$aliases;
$file_content[] = "HostAliases=\"".$aliases."\"";
$file_content[] = "SkipHosts=\"".$this->domain['skipHost']."\"";
$file_content[] = "LogFile=\"".$this->domain['local']['path']."/".
$this->domain['local']['file']."\"";
$file_content[] = "DirIcons=\"".$this->domain['iconPath']."\"";
$file_content[] = "#";
$file_content[] = "# local defaults";
$file_content[] = "Include \"includes/".AWSTATS_LOCAL_DEFAULTS."\"";
$file_content[] = "#";
$file_content[] = "# global defaults (true for all Awstats installs)";
$file_content[] = "Include \"includes/".AWSTATS_GLOBAL_DEFAULTS."\"";
foreach ($file_content as $index => $line) {
$no = $index + 1;
$this->logger->log('conf file '.$no.': '.$line,PEAR_LOG_DEBUG);
$file_content[$index] .= "\n";
}
// and then attempt to write it...
if (is_writable(AWSCONF_DIR)) {
// write the file with
if ($bytes = file_put_contents($fullname,$file_content)) {
$this->logger->log('wrote conf file: '.$fullname.
'('.$bytes.' bytes, '.count($file_content).' lines)',PEAR_LOG_INFO);
} else {
$this->logger->log('failed to write conf file: '
.$fullname.'!',PEAR_LOG_ERR);
}
} else {
$this->logger->log('awstats config directory'.AWSCONF_DIR.
' is unwritable!',PEAR_LOG_ERR);
}
} else {
$this->logger->log('conf file: '.$fullname.
' already exists.',PEAR_LOG_INFO);
}
return true;
}
/**
* run AWSTATs script to update stats in the database since last run
* or, if run for first time, initialise the database
*/
private function update_stats() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
$command = AWSBIN.' '.AWS_ARGS.' -config='.$this->domain['name'];
// run the command and capture the output...
$output = array();
$this->logger->log('updating stats for '.$this->domain['name'].
': '.$command,PEAR_LOG_INFO);
$return_value = exec($command,$output);
//print_r($output);
// if the rsync was successful, it'll return a value like this
if (strstr($return_value,"Found ")) {
$this->logger->log('update stats: success - '.
$return_value,PEAR_LOG_INFO);
}
else {
$this->logger->log('update stats: failed - '.
$output[2].' ',PEAR_LOG_ERR);
return false;
}
return true;
}
/**
* figure out the months for which AWSTATs are available
*/
private function get_stats_months() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
if ($this->report_dates == null) {
$files = glob(AWS_DB_DIR.'/*.'.AWS_DB_SUFFIX);
// the current domain is $this->domain...
$this->report_dates = array();
foreach ($files as $filename) {
if ($pos = strpos($filename,$this->domain['name'])) {
$start = $pos-5;
$length = 4;
$year = substr($filename,$start,$length);
$start = $pos-7;
$length = 2;
$month = substr($filename,$start,$length);
$this->logger->log('found file - '.
$filename.' '.$month.'/'.$year,PEAR_LOG_DEBUG);
$dates[$year] .= ','.$month;
}
}
//print_r($dates);
// sort them so they're in year -> month order...
ksort($dates);
foreach ($dates as $year => $months_string) {
$this->report_dates[$year] = array_slice(explode(',',$months_string),1);
}
//array_multisort($this->report_dates);
reset($this->report_dates);
//print_r($this->report_dates);
}
return true;
}
/**
* generate the AWSTATs output pages in XHTML format for
* the specified time period...
*/
private function produce_xhtml_pages() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
// first make sure that the stats output directory is there
// if not, create it and set its ownership...
$statsdir = $this->domain['statsPath'];
if (!is_dir($statsdir)) {
$this->logger->log('making '.$statsdir,PEAR_LOG_INFO);
if (!mkdir($statsdir,0755)) {
$this->logger->log('failed to create dir '.$statsdir,PEAR_LOG_ERR);
return false;
}
// set appropriate file ownership
chown($statsdir,$this->domain['local']['user']);
chgrp($statsdir,$this->domain['local']['group']);
}
if ($this->get_stats_months()) {
//print_r($dates);
foreach ($this->report_dates as $year => $months) {
$yeardir = $this->domain['statsPath'].'/'.$year;
if (!is_dir($yeardir)) {
$this->logger->log('making '.$yeardir,PEAR_LOG_INFO);
if (!mkdir($yeardir,0755)) {
$this->logger->log('failed to create dir '.$yeardir,PEAR_LOG_ERR);
return false;
}
// set appropriate file ownership
chown($yeardir,$this->domain['local']['user']);
chgrp($yeardir,$this->domain['local']['group']);
} else {
$this->logger->log('found '.$yeardir,PEAR_LOG_INFO);
}
foreach ($months as $month) {
$monthdir = $yeardir.'/'.$month;
if (!is_dir($monthdir)) {
$this->logger->log('making '.$monthdir,PEAR_LOG_INFO);
if (!mkdir($monthdir,0755)) {
$this->logger->log('failed to create dir '.$monthdir,PEAR_LOG_ERR);
return false;
}
// set appropriate file ownership
chown($monthdir,$this->domain['local']['user']);
chgrp($monthdir,$this->domain['local']['group']);
} else {
$this->logger->log('found '.$monthdir,PEAR_LOG_INFO);
}
$currentmonth = date('m');
$currentyear = date('Y');
$index_page = $monthdir.'/index.html';
//$this->logger->log("year=$currentyear/$year, month=$currentmonth/$month",PEAR_LOG_DEBUG);
if (!is_link($index_page) or ($currentmonth == $month and $currentyear == $year)) {
// having found the monthdir, it's time to create the pages!
$command = AWSALLBIN.' '.AWSALL_ARGS.' -config='.$this->domain['name'].
' -month='.$month.' -year='.$year.
' -dir='.$monthdir.'';
$this->logger->log('generating stats output: '.$command,PEAR_LOG_INFO);
$output = array();
$return_value = exec($command,$output);
if (strstr($return_value,"Main HTML page is ")) {
$idx = count($output) - 2;
$this->logger->log('success: '.$output[$idx],PEAR_LOG_INFO);
$this->logger->log('creating link for index page',PEAR_LOG_INFO);
symlink($monthdir.'/'.AWS_OUT_PREFIX.'.'.$this->domain['name'].
'.'.AWS_OUT_SUFFIX,$index_page);
}
}
}
}
}
}
/**
* generate an XHMTL page with a basic index of statisics results for the relevant
* months...
*/
private function create_xhtml_index() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
// if there are any months worth writing about...
if ($this->get_stats_months()) {
$currentmonth = date('m');
$currentdate = date('d-m-y');
//print_r($this->report_dates);
foreach ($this->report_dates as $year => $months) {
$yeardir = $this->domain['statsPath'].'/'.$year;
foreach ($months as $month) {
$monthdir = $yeardir.'/'.$month;
$index_page = $monthdir.'/index.html';
}
}
/* // the filename for this domain's AWSTATs configuration file
$filename = AWS_OUT_INDEX;
// add the path, too...
$fullname = $this->domain['statsPath'].'/'.$filename;
// if it doesn't exist, then...
if (!file_exists($fullname)) {
// construct the conf file contents based on this template...
$file_content[] = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
$file_content[] = "# site specific settings for ".$this->domain['name'];
$file_content[] = "SiteDomain=\"".$this->domain['name']."\"";
if (is_array($this->domain['aliases']['alias'])) {
$aliases = implode(' ',$this->domain['aliases']['alias']);
} else {
$aliases = $this->domain['aliases']['alias'];
}
$aliases = $this->domain['name'].' '.$aliases;
$file_content[] = "HostAliases=\"".$aliases."\"";
$file_content[] = "SkipHosts=\"".$this->domain['skipHost']."\"";
$file_content[] = "LogFile=\"".$this->domain['local']['path']."/".
$this->domain['local']['file']."\"";
$file_content[] = "DirIcons=\"".$this->domain['iconPath']."\"";
$file_content[] = "#";
$file_content[] = "# local defaults";
$file_content[] = "Include \"includes/".AWSTATS_LOCAL_DEFAULTS."\"";
$file_content[] = "#";
$file_content[] = "# global defaults (true for all Awstats installs)";
$file_content[] = "Include \"includes/".AWSTATS_GLOBAL_DEFAULTS."\"";
foreach ($file_content as $index => $line) {
$no = $index + 1;
$this->logger->log('conf file '.$no.': '.$line,PEAR_LOG_DEBUG);
$file_content[$index] .= "\n";
}
// and then attempt to write it...
if (is_writable(AWSCONF_DIR)) {
// write the file with
if ($bytes = file_put_contents($fullname,$file_content)) {
$this->logger->log('wrote conf file: '.$fullname.
'('.$bytes.' bytes, '.count($file_content).' lines)',PEAR_LOG_INFO);
} else {
$this->logger->log('failed to write conf file: '
.$fullname.'!',PEAR_LOG_ERR);
}
} else {
$this->logger->log('awstats config directory'.AWSCONF_DIR.
' is unwritable!',PEAR_LOG_ERR);
}
} else {
$this->logger->log('conf file: '.$fullname.
' already exists.',PEAR_LOG_INFO);
}
return true;
*/
}
}
/**
* run the whole process...
*/
public function run() {
$this->logger->log(__CLASS__.'->'.__FUNCTION__,PEAR_LOG_DEBUG);
// process command line argments, if any
$this->process_args();
// get domain configuration files...
$this->process_configs();
//$this->domain = $this->domain_array['root']['domain'][0];
foreach ($this->domain_array['root']['domain'] as $this->domain) {
// initialise this value
$this->report_dates = null;
// first check to see if there're any access records that need to be
// sync'd
$this->sync_access_logs();
// if it doesn't already exist, create a configuration file...
$this->create_awstats_config();
// update the statistics based on the current access records
// if it hasn't already been created, initialise the stats db
$this->update_stats();
// produce XHTML pages
$this->produce_xhtml_pages();
// and create an up-to-date index of the relevant reports...
$this->create_xhtml_index();
}
}
}
/**
* deal with errors in a uniform way...
*/
function handleError($error) {
global $logger;
//print 'egstats error: '.$error->getMessage().'\n';
$logger->log(EG_APP.' error: '.$error->getMessage(),'.',PEAR_LOG_ERR);
}
// initialise logging
$logger = &Log::singleton('console','','egstats.php');
$logger->log("***** Starting egstats.php *****",PEAR_LOG_DEBUG);
// set up default error handling
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
/**
* Program logic goes below this
*/
$conf_filename="";
$domain = new Domain($logger);
$domain->run();
// final logging message
$logger->log("===== Finishing egstats.php =====",PEAR_LOG_DEBUG);
?>