#!/usr/bin/perl -w

# $Id: clamav-update.pl,v 1.41 2007/12/13 01:29:12 okamura Exp $

=head1 NAME

clamav-update.pl - auto updater for ClamAV

=head1 SYNOPSIS

 clamav-update.pl [options]
 clamav-update.pl --help|-h
 clamav-update.pl --version|-V

=head1 DESCRIPTION

B<clamav-update.pl> is automatic customizable updater for general software.
Mainly, It is developed to update Clam AntiVirus.
Default configuration is designed for ClamAV Engine for ClamXav user
on Mac OS X.

B<clamav-update.pl> acts as:

1. Check execution situation for security.

Only root can execute B<clamav-update.pl>.
If someone execute it with other user's privilege, it show warning and quit.
Don't add setuid to it.
If do so, it can not be executed.

2. Analizes arguments and loads configurations.

Default configuration file is F</usr/local/clamXav/etc/clamav-update.conf>.

3. Compares newest version and current version of target.

B<clamav-update.pl> will continue
if and only if newest version is newer than current version.
The method of getting the version strings and the method of comparing them
are defined by the configuration.

4. Executes phases defined on configuration files.

If errors are detected in any phase, execute commands set defined as roll back
in that phase. and return error.

In other words, we assumes only followings.

  + root uses clamav-update.pl.
  + clamav-update.pl updates target software,
    if newer version of it is released. 

Method of updating is given from
your configuration file or command line options.

=cut

use strict;
use utf8;
no encoding;
#-------------------------------------------------------------------------------
# モジュール
#-------------------------------------------------------------------------------
# システム
use Getopt::Long	qw(:config no_ignore_case);
use Pod::Usage;
use Sys::Syslog	qw(:DEFAULT setlogsock);
use Carp;
use Cwd;
use File::Temp	qw(tempdir);

# プロジェクト


#-------------------------------------------------------------------------------
# グローバル変数宣言
#-------------------------------------------------------------------------------
use vars qw(
	$Version
	%ErrorCode
	$SyslogOpened
	%Option

	%Setting
	%Phase
	$LatestVersion
	$CurrentVersion
	$PhaseSpecifer
);


#-------------------------------------------------------------------------------
# サブルーチン
#-------------------------------------------------------------------------------
sub GetOptionValue {
	my	@name = @_;
	my	$ref;

	$ref = \%Option;
	foreach (@name) {
		unless (exists($ref->{$_}) and defined($ref->{$_})) {
			$ref = undef;
		}
	}
	continue {
		$ref = $ref->{$_};
	}

	return $ref	if (defined($ref));

	$ref = $Setting{option};
	foreach (@name) {
		unless (exists($ref->{$_}) and defined($ref->{$_})) {
			$ref = undef;
		}
	}
	continue {
		$ref = $ref->{$_};
	}

	return $ref;
}

sub Log;

sub _Syslog {
	my	($priority, $format, @args) = @_;

	syslog $priority, $format, @args;
}

sub _Fdlog {
	my	($priority, $format, @args) = @_;
	my	$now;

	return 0	if (IsMasked($priority));
	$format = '%s '.$format."\n";
	$now = localtime;
	if (ref($Setting{logging}->{setlogsock}) eq 'GLOB') { 
		printf {$Setting{logging}->{setlogsock}} $format, $now, @args;
	}
	else {
		printf STDERR $format, $now, @args;
	}
}

sub IsMasked {
	my	($priority) = @_;

	return setlogmask(0) < Sys::Syslog::LOG_UPTO(Sys::Syslog::xlate($priority));
}

sub SetEnv {
	foreach my $varname (keys %{$Setting{environment}}) {
		$ENV{$varname} = $Setting{environment}->{$varname};
		Log 'info', 'setenv %s: %s', $varname, $ENV{$varname};
	}
}

sub Prepare {
	PrepareLogging();
	if (-f GetOptionValue('config')) {
		LoadConfig(GetOptionValue('config'));
		SetEnv();
		PrepareLogging();
	}
	else {
		SetEnv();
	}
	
	foreach my $option (keys %{$Setting{option}}) {
		my	$value = GetOptionValue($option);

		if ($value ne $Setting{option}->{$option}) {
			$Setting{option}->{$option} = $value;
		}
	}

	Log 'info', 'Start clamav-update.pl --config %s', GetOptionValue('config');
}

sub Setlogmask {
	my	($level) = @_;
	my	$new;
	my	$old;

	$new = Sys::Syslog::LOG_UPTO(Sys::Syslog::xlate($level));
	$old = setlogmask $new;
	return $old;
}

sub PrepareLogging {
	if ($SyslogOpened) {
		closelog();
	}

	# setlogmask
	if (defined $Setting{logging}->{setlogmask}) {
		Setlogmask $Setting{logging}->{setlogmask};
	}

	# setlogsock
	if (ref($Setting{logging}->{setlogsock}) eq 'GLOB') {
		my	$currentSelected = select $Setting{logging}->{setlogsock};

		*Log = *_Fdlog;
		$| = 1;
		select $currentSelected;
	}
	else {
		if (ref($Setting{logging}->{setlogsock}) eq 'ARRAY') {
			unless (setlogsock @{$Setting{logging}->{setlogsock}}) {
				Log 'crit', 'setlogsock failed: [%s]',
					"@{$Setting{logging}->{setlogsock}}";
			}
		}
		elsif (ref($Setting{logging}->{setlogsock}) eq '') {
			unless (setlogsock $Setting{logging}->{setlogsock}) {
				Log 'crit', 'setlogsock failed: %s',
					$Setting{logging}->{setlogsock};
			}
		}
		else {
			Log 'crit', 'unknown logsock type';
			return undef;
		}

		*Log = *_Syslog;
		openlog $Setting{logging}->{openlog}->{ident},
			join(',', @{$Setting{logging}->{openlog}->{logopt}}),
			$Setting{logging}->{openlog}->{facility};
		$SyslogOpened = 1;
	}
}

sub CloseLog {
	if ($SyslogOpened) {
		closelog();
	}
	elsif (*Log eq *_Fdlog) {
		close $Setting{logging}->{setlogsock};
	}
}

sub LoadConfig {
	my	($path) = @_;

	Log 'info', 'Loading: %s', $path;
	eval {require $path};
	if ($@) {
		Log 'crit', 'Can\'t load the configration file %s: %s', $path, $@;
		exit $ErrorCode{loadConfig};
	}
}

sub ReadCommandResult {
	my	@command = @_;
	my	$fh;
	my	$ret = undef;

	Log 'info', 'execute: %s', "@command";
	if (open $fh, '-|', @command) {
		my	$line;

		$ret = '';
		while ($line = <$fh>) {
			$ret .= $line;
		}

		close($fh);

		unless ($? == 0) {
			if ($? == -1) {
				Log 'crit', 'fail to execute';
			}
			elsif ($? & 127) {
				if ($? & 128) {
					Log 'err', 'child died with coredump: %d', ($? & 127);
				}
				else {
					Log 'err', 'child died without coredump: %d', ($? & 127);
				}
			}
			elsif ($?) {
				Log 'err', 'child exited: %d', $? >> 8;
			}
			$ret = undef;
		}
	}
	else {
		Log 'err', 'can\'t execute: %s', $!;
	}

	return $ret;
}

sub Version {
	my	%spec = @_;
	my	$version = undef;

	if (ref($spec{command}) eq 'CODE') {
		$version = &{$spec{command}}();
	}
	elsif (ref($spec{command}) eq 'ARRAY') {
		$version = ReadCommandResult(@{$spec{command}});
	}
	elsif (ref($spec{command}) eq '') {
		$version = ReadCommandResult($spec{command});
	}
	else {
		Log 'crit', 'unsupported version command type: ',
			ref($spec{command}) ? ref($spec{command}) : 'ATOM'
		;
	}

	if (defined($version)) {
		if ($version =~ m/$spec{regexp}/) {
			$version = $1;
		}
		else {
			Log 'debug', 'regexp: %s', $spec{regexp};
			Log 'crit', 'unexpected response: %s', $version;
			$version = '';
		}
	}
	else {
		Log 'warning', 'version command failed';
	}

	return $version;
}

sub LatestVersion {
	my	$version = Version(%{$Setting{version}->{latest}});

	if ($version) {
		Log 'info', 'latest version is %s', $version; 
	}
	return $version;
}

sub CurrentVersion {
	return ''	if (GetOptionValue('force'));
	my	$version = Version(%{$Setting{version}->{current}});

	if ($version) {
		Log 'info', 'current version is %s', $version; 
	}
	return $version;
}

sub ChangeDirectory {
	my	($dir) = @_;
	my	$oldDir = getcwd();

	Log 'info', 'change directory to %s', $dir;
	unless (chdir($dir)) {
		Log 'err', 'can\'t change directory to %s', $dir;
		return undef;
	}
	return $oldDir;
}

sub	DoCommand {
	my	@command = @_;

	Log 'info', 'execute: %s', "@command";
	system(@command);
	if ($? == -1) {
		Log 'crit', 'fail to execute';
		return 0;
	}
	elsif ($? & 127) {
		if ($? & 128) {
			Log 'err', 'child died with coredump: %d', ($? & 127);
		}
		else {
			Log 'err', 'child died without coredump: %d', ($? & 127);
		}
		return 0;
	}
	elsif ($?) {
		Log 'err', 'child exited: %d', $? >> 8;
		return 0;
	}

	return 1;
}

sub DoSeqence {
	my	($phaseName, $type) = @_;
	my	@sequence;
	my	$isSuccess = 1;

	if (
		exists $Phase{$phaseName}->{$type}
			and
		defined($Phase{$phaseName}->{$type})
	) {
		if (ref($Phase{$phaseName}->{$type}) eq 'ARRAY') {
			@sequence = @{$Phase{$phaseName}->{$type}};
		}
		else {
			Log 'crit',
				'PHASE %s: type %s: unsupported command sequence type: %s',
				$phaseName, $type,
				ref($Phase{$phaseName}->{$type}) eq ''
					? 'ATOM'
					: ref($Phase{$phaseName}->{$type}) 
			;
			$isSuccess = 0;
		}
	}

	for (my $i = 0; $isSuccess and $i < scalar(@sequence); $i++) {
		my	$cmd = $sequence[$i];

		Log 'info', 'PHASE %s: type %s: command #%d: begin',
			$phaseName, $type, $i
		;

		if (ref($cmd) eq 'CODE') {
			$isSuccess = &{$cmd}();
		}
		elsif (ref($cmd) eq 'ARRAY') {
			$isSuccess = DoCommand(@{$cmd});
		}
		elsif (ref($cmd) eq '') {
			$isSuccess = DoCommand($cmd);
		}
		else {
			Log 'crit',
				'PHASE %s: type %s: command #%d: unsupported command type: %s',
				$phaseName, $type, $i, ref($cmd)
			;
			$isSuccess = 0;
		}

		unless ($isSuccess) {
			Log 'err', "PHASE %s: type %s: command #%d: failed",
				$phaseName, $type, $i
			;
		}
		Log 'info', "PHASE %s: type %s: command #%d: done",
			$phaseName, $type, $i
		;
	}

	return $isSuccess;
}

sub Do {
	my	($phaseName) = @_;
	my	%spec = %{$Phase{$phaseName}};
	my	$cdir = undef;
	my	$isSuccess = 1;

	Log 'info', 'PHASE %s: begin', $phaseName;

	if (exists $spec{workdir} and defined($spec{workdir})) {
		if (ref($spec{workdir}) eq 'CODE' or ref($spec{workdir}) eq '') {
			my	$wdir = ref($spec{workdir}) eq 'CODE'
				? &{$spec{workdir}}()
				: $spec{workdir};

			if (defined($wdir) and $wdir ne '') {
				$cdir = ChangeDirectory($wdir);
				$isSuccess = defined($cdir);
			}
			else {
				Log 'crit', 'PHASE %s: empty workdir', $phaseName;
			}
		}
		else {
			Log 'crit', 'PHASE %s: unsupported workdir type: %s',
				$phaseName, ref($spec{workdir})
			;
			$isSuccess = 0;
		}
	}

	if ($isSuccess) {
		$isSuccess = DoSeqence($phaseName, 'method');
		unless ($isSuccess) {
			Log 'err', 'PHASE %s: failed', $phaseName;
			DoSeqence($phaseName, 'rollback');
		}
	}

	ChangeDirectory($cdir)	if (defined($cdir));
	
	Log 'info', 'PHASE %s: end', $phaseName;
	return $isSuccess;
}

sub CheckSecurity {
	my	$ok = 1;

	if ($> != 0) {
		Log 'alert', 'effective user is not root: %d', $>;
		$ok = 0;
	}

	if (-f $0) {
		my	@st = stat $0;

		if ($st[2] & 04000) {
			Log 'alert', 'suid bit is added to this script. This is a big security hole.';
			$ok = 0;
		}
	}

	return $ok; 
}

#### Build-In Phase Specifier ####

# General
#	1. download gzip compressed tar file.
#	2. extract it.
#	3. do ./configure
#	4. do make
#	5. do make install
#	REQUIRED:
#		download file name format is xxxx-1.2.3.tar.gz
#		extracted directory name format is xxxx-1.2.3
#	SAMPLE CONFIGUREATION:
#		$Setting{version}->{latest}->{command} = <method of your software>;
#		$Setting{version}->{latest}->{regexp} = qr/regexp for above/;
#		$Setting{version}->{current}->{command} = <method of your software>;
#		$Setting{version}->{current}->{regexp} = qr/regexp for above/;
#		$Setting{option}->{src} = '<Base URL for your software>';
#		$Setting{option}->{name} = '<Name of your software>';
sub PhaseSpecifier4General {
	#### Initialization ####
	# Delete any from phase definitions.
	%Phase = ();

	#### Download Phase Definition ####
	# Add download to phase definitions.
	$Phase{download} = {};
	# working directory (not specified)
	$Phase{download}->{workdir} = undef;
	# command sequence
	$Phase{download}->{method} = [
		# curl --silent -o DST/NAME-VERSION.EXT SRC/NAME-VERSION.EXT
		['curl', '--silent', '-o',
			"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}",
			"$Setting{option}->{src}/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}"
		],
	];
	# rollback command sequence.
	$Phase{download}->{rollback} = [
		# rm -f DST/NAME-VERSION.EXT
		['rm', '-f',
			"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}",
		],
	];
	
	#### Extract Phase Definition ####
	# Add extract to phase definitions.
	$Phase{extract} = {};
	# working directory (not specified)
	$Phase{extract}->{workdir} = undef;
	# command sequence
	$Phase{extract}->{method} = [
		# tar xzf DST/NAME-VERSION.EXT
		['tar', 'xzf',
			"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}",
			'-C', $Setting{option}->{dst},
		],
	];
	# rollback command sequence
	$Phase{extract}->{rollback} = [
		# rm -rf DST/NAME-VERSION
		['rm', '-rf',
			"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion"
		],
	];

	#### Build Phase Definition ####
	# Add build to phase definitions.
	$Phase{build} = {};
	# working directory
	$Phase{build}->{workdir} =
		# DST/NAME-VERSION
		"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion"
	;
	# command sequence
	$Phase{build}->{method} = [
		[qw(./configure)],
		[qw(make)],
	];
	# rollback command sequence
	$Phase{build}->{rollback} = [
		[qw(make clean)],
	];
	
	#### Install Phase Definition ####
	# Add install to phase definitions.
	$Phase{install} = {};
	# working directory
	$Phase{install}->{workdir} =
		# DST/NAME-VERSION
		"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion"
	;
	# command sequence
	$Phase{install}->{method} = [
		[qw(make install)],
	];
	# rollback command sequence (nothing)
	$Phase{install}->{rollback} = [
	];

	1;
}

# ClamAV for ClamXav
#	1. download gzip compressed tar file.
#	2. extract it.
#	3. do ./configure --prefix=/usr/local/clamXav
#	4. do make
#	5. do make install
#	6. change owner, group and permission
sub PhaseSpecifier4ClamAV4ClamXav {
	#### Initialization ####
	# use PhaseSpecifier4General as base.
	PhaseSpecifier4General();

	#### Build Phase Customization ####
	# change command sequence
	$Phase{build}->{method} = [
		[qw(./configure --prefix=/usr/local/clamXav)],
		[qw(make)],
	];

	#### Install Phase Customization ####
	# add to command sequence
	push @{$Phase{install}->{method}}, (
		[qw(install -d -o root -g admin -m 0775 /usr/local/clamXav)],
		[qw(chown -R root:admin /usr/local/clamXav/etc)],
		[qw(find /usr/local/clamXav/etc -type d -exec chmod 0775 {} ;)],
		[qw(find /usr/local/clamXav/etc -type f -exec chmod 0664 {} ;)],
		[qw(chown -R root:admin /usr/local/clamXav/bin)],
		[qw(chmod -R 0755 /usr/local/clamXav/bin)],
		[qw(chown clamav /usr/local/clamXav/bin/freshclam)],
		[qw(chmod u+s /usr/local/clamXav/bin/freshclam)],
		[qw(chown -R clamav:clamav /usr/local/clamXav/share/clamav)],
		[qw(find /usr/local/clamXav/share/clamav -type d -exec chmod 0775 {} ;)],
		[qw(find /usr/local/clamXav/share/clamav -type f -exec chmod 0664 {} ;)],
		[qw(touch /usr/local/clamXav/share/clamav/freshclam.log)],
		[qw(chmod 0664 /usr/local/clamXav/share/clamav/freshclam.log)],
	);

	1;
}

# pkg on dmg file for Mac OS X
#	1. download dmg file.
#	2. attach it.
#	3. install from pkg file.
#	4. detach dmg fiel.
#	REQUIRED:
#		dmg file name format is xxxx-1.2.3.dmg
#		pkg file name format is xxxx-1.2.3.pkg
#	SAMPLE CONFIGUREATION:
#		$Setting{version}->{latest}->{command} = <method of your software>;
#		$Setting{version}->{latest}->{regexp} = qr/regexp for above/;
#		$Setting{version}->{current}->{command} = sub {
#			my  $newestVer;
#
#			opendir DFH, '/Library/Receipts' or die;
#	        while (my $fname = readdir DFH) {
#				if ($fname =~ m/^$Setting{option}->{name}-(\d+(?:\.\d+)*)\.pkg$/o) {
#					my  $foundVer = $1;
#
#					if (
#						!defined($newestVer)
#							or
#						&{$Setting{version}->{compare}}($newestVer, $foundVer) < 0
#					) {
#						$newestVer = $foundVer;
#					}
#				}
#			}
#			closedir DFH;
#
#			return $newestVer;
#		};
#		$Setting{version}->{current}->{regexp} = qr/^(\d+(?:\.\d+)*)\s*$/;
#		$Setting{environment}->{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
#		$Setting{option}->{src} = '<Base URL for your software>';
#		$Setting{option}->{name} = '<Name of your software>';
#		$Setting{option}->{ext} = 'dmg';
#		$Setting{phase}->{sequence} = [qw(download attach install detach)];
sub PhaseSpecifier4PkgOnDmg {
	PhaseSpecifier4General();
	delete $Phase{extract};
	delete $Phase{build};
	
	$Phase{attach} = {};
	$Phase{attach}->{workdir} = undef;
	$Phase{attach}->{method} = [
		['hdiutil', 'attach',
			'-mountpoint', $Setting{option}->{dst},
			"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}"
		],
	];
	$Phase{attach}->{rollback} = [
		['hdiutil', 'detach', '-force',
			$Setting{option}->{dst}
		],
	];

	$Phase{install}->{workdir} = undef;
	$Phase{install}->{method} = [
		['installer', '-pkg',
			"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion.pkg",
			'-target', '/'
		],
	];
	$Phase{install}->{rollback} = [
		['hdiutil', 'detach', '-force',
			$Setting{option}->{dst}
		],
	];

	$Phase{detach} = {};
	$Phase{detach}->{workdir} = undef;
	$Phase{detach}->{method} = [
		['hdiutil', 'detach', '-force',
			$Setting{option}->{dst}
		],
	];
	$Phase{detach}->{rollback} = [
	];

	1;
}

# clamav-update for ClamXav user
#	1. download gzip compressed tar file.
#	2. extract it.
#	3. copy files
#	SAMPLE CONFIGURATION:
#		$Setting{version}->{latest}->{command} = [qw(curl --silent http://clamav-update.sourceforge.jp/release/clamav-update-2/version.txt)];
#		$Setting{version}->{latest}->{regexp} = qr/^(\d+(?:\.\d+)*)\s*$/;
#		$Setting{version}->{current}->{command} = [qw(clamav-update.pl --version)];
#		$Setting{version}->{current}->{regexp} = qr/^(?s-im)clamav-update\.pl v(\d+(?:\.\d+)*)\s+.*$/;
#		$Setting{phase}->{sequence} = [qw(download extract install)]
#		$Setting{option}->{name} = 'clamav-update'.
#		$Setting{option}->{src} = <one of following URL.>
#   		'http://osdn.dl.sourceforge.jp/clamav-update'     - Tokyo Japan: OSDN Japan
#   		'http://keihanna.dl.sourceforge.jp/clamav-update' - Keihanna Japan: Kansai Science City Internet Community
#   		'http://qgpop.dl.sourceforge.jp/clamav-update'    - Fukuoka Japan: Kyushu GigaPOP Project
#		$Setting{option}->{limit}->{version} = '3';
sub PhaseSpecifier4ClamavUpdate4ClamXav {
	PhaseSpecifier4General();
	delete $Phase{build};

	$Phase{download}->{method} = [
		sub {
			my	$releaseID;
	
			$releaseID = ReadCommandResult(qw(curl --silent http://clamav-update.sourceforge.jp/release/clamav-update-2/release_id.txt));
			return 0	unless (defined($releaseID));
			chomp $releaseID;
			Log 'debug', 'release ID is %s', $releaseID;

			return DoCommand(
				'curl', '--silent', '-o',
				"$Setting{option}->{dst}/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}",
				"$Setting{option}->{src}/$releaseID/$Setting{option}->{name}-$LatestVersion.$Setting{option}->{ext}"
			);
		},
	];

	$Phase{install}->{method} = [
		[qw(install -Cbp -o root -g admin -m 0755 clamav-update.pl
			/usr/local/clamXav/bin/clamav-update.pl
		)],
		[qw(install -Cbp -o root -g admin -m 0664 clamav-update-update.conf
			/usr/local/clamXav/etc/clamav-update-update.conf
		)],
	];

	1;
}

# clamav-update
#	1. download gzip compressed tar file.
#	2. extract it.
#	3. copy files
#	SAMPLE CONFIGURATION:
#		$Setting{version}->{latest}->{command} = [qw(curl --silent http://clamav-update.sourceforge.jp/release/clamav-update-2/version.txt)];
#		$Setting{version}->{latest}->{regexp} = qr/^(\d+(?:\.\d+)*)\s*$/;
#		$Setting{version}->{current}->{command} = [qw(clamav-update.pl --version)];
#		$Setting{version}->{current}->{regexp} = qr/^(?s-im)clamav-update\.pl v(\d+(?:\.\d+)*)\s+.*$/;
#		$Setting{enveronment}->{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin:usr/local/bin';
#		$Setting{phase}->{sequence} = [qw(download extract install)]
#		$Setting{option}->{name} = 'clamav-update'.
#		$Setting{option}->{src} = <one of following URL.>
#   		'http://osdn.dl.sourceforge.jp/clamav-update'     - Tokyo Japan: OSDN Japan
#   		'http://keihanna.dl.sourceforge.jp/clamav-update' - Keihanna Japan: Kansai Science City Internet Community
#   		'http://qgpop.dl.sourceforge.jp/clamav-update'    - Fukuoka Japan: Kyushu GigaPOP Project
#		$Setting{option}->{limit}->{version} = '3';
sub PhaseSpecifier4ClamavUpdate {
	PhaseSpecifier4ClamavUpdate4ClamXav();

	$Phase{install}->{method} = [
		[qw(install -bp -o 0 -g 0 -m 0755 clamav-update.pl
			/usr/local/bin/clamav-update.pl
		)],
		[qw(install -bp -o 0 -g 0 -m 0664 clamav-update-update.conf
			/usr/local/etc/clamav-update-update.conf
		)],
	];

	1;
}


#-------------------------------------------------------------------------------
# グローバル変数初期値
#-------------------------------------------------------------------------------
$Version = 'clamav-update.pl v2.2.4';

%ErrorCode = (
	arguments		=> 1,
	prepare			=> 2,
	loadConfig		=> 3,
	version			=> 4,
	phaseSpecify	=> 5,
	fastPhase		=> 8,
);

$SyslogOpened = 0;
*Log = *_Fdlog;

%Setting = (
	logging	=> {
		setlogsock	=> \*STDERR,
		openlog	=> {
			ident		=> 'clamav-update',
			logopt		=> [qw()],
			facility	=> 'local6',
		},
		setlogmask	=> 'warning',
	},
	environment	=> {
		PATH	=> '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/clamXav/bin',
	},
	version	=> {
		latest	=> {
			command	=> [qw(host -t txt current.cvd.clamav.net)],
			regexp	=> qr/"(\d+(?:\.\d+)*):\d+:\d+:\d+:[^"]*"\s*$/,
		},
		current	=> {
			command	=> [qw(clamav-config --version)],
			regexp	=> qr/^(\d+(?:\.\d+)*)\s*$/,
		},
		compare	=> sub {
			my	($a, $b) = @_;
			my	@a = reverse(split(/\./, $a));
			my	@b = reverse(split(/\./, $b));

			while (0 < scalar(@a) and 0 < scalar(@b)) {
				$a = pop @a;
				$b = pop @b;
				return -1	if ($a < $b);
				return  1	if ($a > $b);
			}
			return  1 if (0 < scalar(@a));
			return -1 if (0 < scalar(@b));
			return  0;
		},
	},
	option	=> {
		src		=> 'http://jaist.dl.sourceforge.net/sourceforge/clamav',
		dst		=> tempdir(CLEANUP => 1),
		name	=> 'clamav',
		ext		=> 'tar.gz',
		force	=> 0,
		limit	=> {
			version	=> undef,
			action	=> undef,
		},
	},
	phase	=> {
		sequence	=> [qw(download extract build install)],
		specifier	=> undef,
	},
);

%Phase = ();

%Option	= (
	config	=> '/usr/local/clamXav/etc/clamav-update.conf',
	force	=> undef,
	src		=> undef,
	dst		=> undef,
	name	=> undef,
	ext		=> undef,
	limit	=> {
		version	=> undef,
		action	=> undef,
	},
);


#-------------------------------------------------------------------------------
# 引数解釈
#-------------------------------------------------------------------------------

=head1 OPTIONS

=over 4

=item -h|--help

Show manual of B<clamav-update.pl> to standard output.
You can use perldoc instead of -h option to see manual.
We recommend perldoc's one.

=item -V|--version

Show version of B<clamav-update.pl> to standard output

=item -c I<filepath> | --config I<filepath>
| -c=I<filepath> | --config=I<filepath>

Read config_file as configuration file.
Default value is F</usr/local/clamXav/etc/clamav-update.conf>.

=item -f|--force

Force to install. Change string of current version of the software to empty.

This is for forcing to install but is depends of how to get latest version info
and definition of comparing version function.
This correspond to C<$Setting{option}-E<gt>{force}>.

=item -s I<download_from> | --src I<download_from>
-s=I<download_from> | --src=I<download_from>

Specify base URL as I<download_from> for downloading target software.
Default value is F<http://osdn.dl.sourceforge.jp/clamav-update>.

B<clamav-update.pl> DO NOT use this value directly.
This value is used in some defined phase.
This correspond to C<$Setting{option}-E<gt>{src}>.

=item -d I<download_to> | --dst I<download_to>
-d=I<download_to> | --dst=I<download_to>

Specify a temporary directory to save download file as I<download_to>.
Default value is some alphanumeric strings at random
in your system temporary directory.
Default directory will be removed before clamav-update.pl finish.

B<clamav-update.pl> DO NOT use this value directly.
This value is used in some defined phase. 
This correspond to C<$Setting{option}-E<gt>{dst}>.

=item -n I<package_name> | --name I<package_name>
 -n=I<package_name> | --name=I<package_name>

Specify I<package_name> as package name of software.
Default value is clamav.

B<clamav-update.pl> DO NOT use this value directly.
This value is used in some defined phase.
This correspond to C<$Setting{option}-E<gt>{name}>.

=item -e I<package_extention> | --ext I<package_extention>
 -e=I<package_extention> | --ext=I<package_extention>
 
Specify I<package_extention> as suffix of an archive of the software.
Default value is tar.gz .

B<clamav-update.pl> DO NOT use this value directly.
This value is used in some defined phase.
This correspond to C<$Setting{option}-E<gt>{ext}>.

=item --limit-version I<version_number> | --limit-version=I<version_number>

Specify upper limit of version I<version_number>.
B<clamav-update.pl> doesn't update I<version_number> or higher.
Default value is undefined. It means unlimited.

This correspond to C<$Setting{option}-E<gt>{limit}-E<gt>{version}>.

=item --limit-action I<phase_name> | --limit-action=I<phase_name>

If B<clamav-update.pl> don't update software because of
its upper limit of version, B<clamav-update.pl> execute phase.
Default value is undefined. It means no action.

=item --option I<name>=I<value> | --option=I<name>=I<value>

Change value of C<$Setting{option}-E<gt>{I<name>}> to I<value>.
This treats as changing configuration file temporally.
If slash "/" character is included in name,
name is treated as path.
For example:

  clamav-update.pl --option one/two/three=four ...

It means that

  $Setting{option}->{one}->{two}->{three} = 'four';

This option configuration is overwrote by other directly option.
For example, if you execute following command line:

  clamav-update.pl --src download_from1 --option src=download_from2 ...

B<clamav-update.pl> download from I<download_from1>.
Because this is equiv to situation that
you have bellow line in configuration file:

  $Setting{option}->{src} = 'download_from2';

and execute following command line:

  clamav-update.pl --src download_from1 ...

=back

=cut

GetOptions(
	'h|help'		=> sub {
		PrepareLogging();
		pod2usage(-verbose => 2, -exitval => 0)
	},
	'V|version'		=> sub {
		PrepareLogging();
		print <<EOF;
$Version
Copyright (C) 2006-2007 OKAMURA Yuji, All rights reserved.
This is free software; see the source for copying conditions. There is NO 
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
		exit 0;
	},
	'c|config=s'		=> \$Option{config},
	'f|force'			=> \$Option{force},
	's|src=s'			=> \$Option{src},
	'd|dst=s'			=> \$Option{dst},
	'n|name=s'			=> \$Option{name},
	'e|ext=s'			=> \$Option{ext},
	'limit-version=s'	=> \$Option{limit}->{version},
	'limit-action=s'	=> \$Option{limit}->{action},
	'option=s'			=> sub {
		my	($namePath, $value) = split(/=/, $_[1], 2);
		my	@namePath = split(/\//, $namePath);
		my	$cur = $Setting{option};
		my	$last = pop @namePath;
		my	$history = '$Setting{option}';

		foreach my $name (@namePath) {
			$history .= '->{\''.$name.'\'}';
			$cur->{$name} = {}	unless (exists $cur->{$name});
			unless (ref($cur->{$name}) eq 'HASH') {
				printf STDERR "%s isn't a hash reference.\n", $history;
				exit $ErrorCode{arguments};
			}
			$cur = $cur->{$name};
		}
		$cur->{$last} = $value;
	},
) or pod2usage($ErrorCode{arguments});


#-------------------------------------------------------------------------------
# メイン
#-------------------------------------------------------------------------------
# 初期化
Prepare();
unless (CheckSecurity()) {
	exit $ErrorCode{prepare};
}

# 最新バージョン取得
$LatestVersion = LatestVersion();
unless (defined($LatestVersion)) {
	Log 'err', 'Can\'t check latest version.';
	exit $ErrorCode{version};
}

# カレントバージョン取得
$CurrentVersion = CurrentVersion();
unless (defined($CurrentVersion)) {
	Log 'err', 'Can\'t check current version.';
	exit $ErrorCode{version};
}

# アップデート
if (&{$Setting{version}->{compare}}($CurrentVersion, $LatestVersion) < 0) {
	# フェーズ定義
	if (defined($Setting{phase}->{specifier})) {
		if (ref($Setting{phase}->{specifier}) eq 'CODE') {
			&{$Setting{phase}->{specifier}}();
		}
		else {
			Log 'crit', 'unsupported phase specifier type: %s',
				ref($Setting{phase}->{specifier}) eq ''
					? 'ATOM'
					: ref($Setting{phase}->{specifier})
			;
			exit $ErrorCode{phaseSpecify};
		}
	}
	else {
		PhaseSpecifier4ClamAV4ClamXav();
	}

	my	$limit = GetOptionValue(qw(limit version));

	# バージョンアップ制限検査
	if (
		defined($limit)
			and
		&{$Setting{version}->{compare}}($limit, $LatestVersion) <= 0
	) {
		# 制限時指定フェーズ実行
		Log 'warning', 'auto update is limited: %s <= %s',
			$limit, $LatestVersion
		;
		$limit = GetOptionValue(qw(limit action));
		if (defined($limit)) {
			unless (Do($limit)) {
				exit($ErrorCode{fastPhase});
			}
		}
	}
	else {
		# フェーズ実行
		if (defined($Setting{phase}->{sequence})) {
			if (ref($Setting{phase}->{sequence}) eq 'ARRAY') {
				for (
					my $i = 0;
					$i < scalar(@{$Setting{phase}->{sequence}});
					$i++
				) {
					unless (Do($Setting{phase}->{sequence}->[$i])) {
						exit($ErrorCode{fastPhase} + $i);
					}
				}
			}
			else {
				Log 'crit', 'unsupported phase sequence type: %s',
					ref($Setting{phase}->{sequence}) eq ''
						? 'ATOM'
						: ref($Setting{phase}->{sequence})
				;
				exit $ErrorCode{phaseSpecify};
			}
		}
		else {
			Log 'warning', 'phase sequence not defined';
		}
	}
}


# 終了処理
END {
	Log 'info', 'Finish clamav-update.pl --config %s', GetOptionValue('config');
	CloseLog();
}

#===============================================================================

=head1 SEE ALSO

Reference of Configuration: L<http://clamav-update.sourceforge.jp/docs/clamav-update-2/chapter2.html#B-3>

=head1 CHANGE LOG

=over 4

=item v2.2.4

 * The function PhaseSpecifier4ClamavUpdate is improved for FreeBSD 6.2.
 * The function PhaseSpecifier4PkgOnDmg is improved to attach dmg file to unique
   mount point.

=item v2.2.3

 * Update POD based fu7mu4's translation.
 * Update english comments based fu7mu4's translation.

=item v2.2.2

 * The problem that warnning messages of Perl are outputted
   when the version of SYSLOG is new is fixed.

=item v2.2.1

 * When log destination is GLOB, restrain the buffering.
 * When log destination is GLOB, close it explicitly.
 * Argument of find command in the install phase of
   PhaseSpecifier4ClamAV4ClamXav was fixed.

=item v2.2

 * Add option named "option"

=item v2.1.1

 * install.sh problems are fixed.
 * All v1.x setting description are changed for v2.x

=item v2.1

 * Add new feature "update limitting".
 * Add new optinos description to POD.
 * Improve build-in phase specifier functions.
   Now you can use PhaseSpecifier4ClamavUpdate on the system which use GNU
   install command as standard install comannd.

=item v2.0.1

 * Add -b option to install command in built-in phase specifier.
 * Handle the error of getting latest version, and terminate if it occurs.
 * Allocate 4 as exit-status code for error in getting latest version.
 * Reallocate 5 as exit-status code for error in phase specifing.
 * workdir of the phase install is corrected to undef in PhaseSpecifier4PkgOnDmg.
 * Add rollback of the phase install in PhaseSpecifier4PkgOnDmg.

=item v2.0

 * Each phase sepecification is re-designed for general purpose.
   + You can sepcify phase sequence.
   + Each phase is defined as a key-value of %Phase.
   + "Phase" is defined as tupple of working directory, method and rollback
     method.
 * Version comparation are defined in setting.
 * String, array and function reference are supported for all command
   definitions.
 * $LatestVersion and $CurrentVersion variables are supported for comman
   definition in the phase sepecification.

=item v1.1

 * function reference support for $Setting{download}->{src}.

=item v1.0.1

 * Improve output contents of log.
 * Fix miss spelling in clamav-update.conf.

=item v1.0

Initial version.

=back

=head1 AUTHOR

OKAMURA Yuji E<lt>okamura@users.sourceforge.jpE<gt>

Manual translator: fu7mu4

=cut

__END__;
