<?php
/*
**	$Id$
**
**	Copyright (C) 1995-2007 by the ITools Authors.
**	This file is part of ITools - the Internet Tools Library
**
**	ITools is free software; you can redistribute it and/or modify
**	it under the terms of the GNU General Public License as published by
**	the Free Software Foundation; either version 3 of the License, or
**	(at your option) any later version.
**
**	ITools is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**	GNU General Public License for more details.
**
**	You should have received a copy of the GNU General Public License
**	along with this program.  If not, see <http://www.gnu.org/licenses/>.
**
**	it.class - static functions
*/

class it
{

/**
 * Create config class with static members initialized (e.g. $home).
 * NOTE: PHP5 ONLY
 * @param $p Static members to be generated in newly created class
 *      service: Class name of created class (default: from caller path)
 *      home: Home directory of application (default: from caller path)
 *      site: Domain name of application (default: from caller path)
 *      db: Database of application (default: from caller path)
 */
function createconfig($p = array())
{
	$stack = debug_backtrace();
	$filename = $stack[0]['file'];
	preg_match('!/www/((\w+)[^/]+)!', $filename, $parts);

	$p += array(
		'home' => $parts[0],
		'site' => $parts[1],
		'db' => strtr($parts[1], ".-", "__"),
		'service' => $parts[2],
	);

	if (class_exists($p['service'] . "_tools"))
		$extends = "extends {$p['service']}_tools ";

	$code = array("class {$p['service']} $extends{");

	foreach ($p as $name => $value)
		$code[] = "static \$$name = " . var_export($value, true) . ";";

	$code[] = "}";
	eval(join("\n", $code));
}


/**
 * Clone an object and return copy, works for all PHP versions
 */
function &cloneobj(&$object)
{
	$result = (is_object($object) && version_compare(zend_version(), 2, '>=')) ? clone($object) : $object;

	return $result;	# PHP internals need a tmp var to return by ref
}


/**
 * Append a line to a logfile in log/. Date will be added to filename and line
 * @param $name Name of logfile
 * @param $line1 Line to append (varargs)
 */
function log($name /* ... */)
{
	$args = func_get_args();
	$line = date("Y-m-d H:i:s") . "\t" . implode("\t", array_slice($args, 1)) . "\n";
	$fn = $GLOBALS['ULTRAHOME'] . "/log/$name-" . date('Ymd');

	$existed = file_exists($fn);

	if (isset($GLOBALS['ULTRAHOME']) && ($fh = fopen($fn, "a")))
	{
		fputs($fh, $line);
		fclose($fh);

		if (!$existed)
		{
			@chgrp($fn, "www");
			@unlink($GLOBALS['ULTRAHOME'] . "/log/$name");
			@symlink($fn, $GLOBALS['ULTRAHOME'] . "/log/$name");
		}
	}	
}


/**
 * Store timings for appending to log/timer_log-* in auto_append.php
 */
function timerlog($label = '')
{
	if ($GLOBALS['debug_timerlog'])
	{
		$s = $GLOBALS['ULTRATIME'];
		$e = gettimeofday();
		$msec= ($e['sec'] - $s['sec']) * 1000 + ($e['usec'] - $s['usec']) / 1000;
		$GLOBALS['ULTRATIMERLOG'] .= sprintf(" %s:%d", $label, $msec);
	}
}


/**
 * Send verbose error report to browser or (if display_errors is off) by email to .diffnotice gods, file owner or SERVER_ADMIN
 * All params optional. Single string parameter means 'title'.
 * @parma $p['title'] error title, one line
 * @param $p['body'] error body, multiline
 * @param $p['to'] comma separated recipient list
 * @param $p['id'] identifier of error. if given, only subsequent errors of same id will trigger message
 * @param $p['graceperiod'] number of seconds within which additional errors are ignored if id is set
 * @param $p['timewindow'] number of seconds after graceperiod within which the second error must occur if id is set
 * @param $p['backtraceskip'] number of stack levels to drop
 * @param $p['blockmail'] number of seconds to block mails after having sent a mail [3600]
 */
function error($p = array(), $body = null, $to = null) # $body and $to deprecated
{
	if (!is_array($p))
		$p = array('title' => $p, 'body' => $body, 'to' => $to);

	if ($_SERVER['REMOTE_ADDR'])
		$url = ($_SERVER['HTTPS'] ? "https://" : "http://") . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
	else
		$url = $_SERVER['SCRIPT_NAME'];

	$gods = strtr(trim(@file_get_contents($GLOBALS['ULTRAHOME'] . "/.diffnotice")), array("\n"=>', '));
	if (!$p['to'])
		unset($p['to']); # allow defaults to kick in
	$p += array(
		'title' => "it::error",
		'to' => $gods ? $gods : (get_current_user() ? get_current_user() : $_SERVER['SERVER_ADMIN']),
		'graceperiod' => 60, 
		'timewindow' => 25*3600,
		'backtraceskip' => 0,
		'blockmail' => 3600,
	);

	@mkdir("/tmp/alertdata");
	@chmod("/tmp/alertdata", 0777);		

	$toscreen = ini_get('display_errors') || (defined("STDOUT") && posix_isatty(STDOUT)) || EDC('astwin') || EDC('asdevel');
	if (!$toscreen) # this error can only be sent by mail: find out if we want to suppress it
	{
		if (!$p['id'])
			$sendmail = true;
		else
		{
			$errstampfn = "/tmp/alertdata/errstamp_" . urlencode($p['id']);
			$errstampage = time() - @filemtime($errstampfn);
			$sendmail = $errstampage >= $p['graceperiod'] && $errstampage <= $p['graceperiod'] + $p['timewindow'];
			if ($errstampage > $p['graceperiod'])
			{
				@unlink($errstampfn);
				@touch($errstampfn);
			}
		}

		if ($sendmail)
		{
			$lastsentfn = "/tmp/alertdata/lastsent_" . getmyuid() . "." . md5($p['to']);
			$now = time();
			clearstatcache();
			$lastsenttime = @filemtime($lastsentfn);
			$sendmail = $now - $lastsenttime > $p['blockmail'];
			$lastsentdebug = "Lastsent: lastsentfn=$lastsentfn now=$now lastsenttime=$lastsenttime blockmail={$p['blockmail']} sendmail=$sendmail\n\n";
			if ($sendmail)
			{
				@unlink($lastsentfn);
				@touch($lastsentfn);
			}
		}
	}

	if ($toscreen || $sendmail)
	{
		$trace = it_debug::backtrace($p['backtraceskip']); # moved in here for performance in mass error case

		$body = ($p['body'] ? trim($p['body'])."\n\n" : "") . ($url && !$toscreen? "{$p['title']}\n\nUrl: $url\n\n" : "") . ($trace ? ($sendmail?"" :"  ")."Trace: $trace\n\n" : "");

		if ($sendmail) # we're mailing: send maximum info
		{
			$p['title'] = "Alert: " . $p['title'] . " (on " . getenv('HOSTNAME') . ")";
			unset ($p['locals']['GLOBALS'], $p['locals']['_GET'], $p['locals']['_POST'], $p['locals']['_COOKIE']);
			$locals = print_r($p['locals'], true);

			if ($trace && ($fulltrace = array_slice(debug_backtrace(), $p['backtraceskip'])))
				while (strlen(print_r($fulltrace, true)) > 100000)
					array_pop($fulltrace);

			$body .= "Host: " . getenv('HOSTNAME') . "\n\n";
			$body .= $p['locals'] && strlen($locals) < 100000 ? "Locals: $locals\n\n" : "";
			$body .= $p['id']     ? "Filter: graceperiod={$p['graceperiod']} timewindow={$p['timewindow']}\n\n" : "";
			$body .= $lastsentdebug;
			$body .= $_GET        ? "\$_GET: "    . print_r($_GET, true) . "\n\n" : "";
			$body .= $_POST       ? "\$_POST: "   . print_r($_POST, true) . "\n\n" : "";
			$body .= $_COOKIE     ? "\$_COOKIE: " . print_r($_COOKIE, true) . "\n\n" : "";
			$body .= $_SERVER     ? "\$_SERVER: " . print_r($_SERVER, true) . "\n\n" : "";
			$body .= $fulltrace   ? "Stack: "     . print_r($fulltrace, true) . "\n\n" : "";
			$body  = it::replace(array('(pw|passw|password|secret)\] => .*' => '$1] => ********'), $body);

			it::mail(array('To' => $p['to'], 'Subject' => substr($p['title'], 0, 80), 'Body' => $body) + (($cc = $GLOBALS['it_defaultconfig']['error_cc']) ? array('Cc' => $cc) : array()));
		}
		else if ($_SERVER['REMOTE_ADDR']) # toscreen mode: web
			echo "<pre>{$p['title']}\n".rtrim($body)."</pre>";
		else  # toscreen mode: shell (outputs to stderr)
			error_log("it::error: " . $p['title'] . " in " . ($trace ? $trace : "{$p['file']}:{$p['line']}") . " Url: $url " . (EDC('verbose') ? D($p['locals']) : ""));
	}

	if (($fh = fopen("/tmp/alertdata/alert.log", "a")))
	{
		fputs($fh, it::date() . " " . $p['title'] . " in " . ($trace ? $trace : "{$p['file']}:{$p['line']}") . " Url: $url\n");
		fclose($fh);
		@chmod("/tmp/alertdata/alert.log", 0777);		
	}
}


/**
 * Same as it::error(), plus exit
 * @see error()
 */
function fatal($title='', $body='', $to='')
{
	$p = is_array($title) ? $title : array('title' => $title, 'body' => $body, 'to' => $to);
	$p['backtraceskip']++;
	it::error($p);
	exit(1);
}


/**
 * Print message to stderr and exit with error code
 */
function bail($message = "Bailed.\n")
{
	fputs(STDERR, $message);
	exit(1);
}


/**
 * Convert a string to ASCII-only chars, map/strip ISO-8859-1 accents
 * @param $text Text to convert
 * @return mapped/stripped version of $text contains only chars [0..127]
 */
function toascii($text)
{
	return strtr(strtr($text,
		'�����������������������������������������������',
		'CeaaaceeeiiiAEoouuyooaiounNAAAaAEEEIIIOOoOUUUyY'),
		array('�' => 'ae', '�' => 'oe', '�' => 'ue', '�' => 'Ae', '�' => 'Oe', '�' => 'Ue', '�' => 'ae', '�' => 'Ae', '�' => 'ss'));
}


/**
 * Convert regex for preg (adds and escapes delimiter, adds modifiers)
 * @param $pattern Regex to convert
 * @param $p['casesensitive'] Regex is case sensitive (omit modifier i)
 * @param $p['multiline'] add modifier m: ^ and $ match \n
 * @param $p['singleline'] add modifier s: . matches \n
 * @param $p['utf8'] add modifier u
 * @param $p['extended'] add modifier x
 * @return converted regex to use with preg
 */
function convertregex($pattern, $p = array())
{
	$pattern = preg_replace('|/|', '\/', $pattern); 
	$modifiers = '';

	if (!$p['casesensitive'])
		$modifiers .= 'i';

	foreach (array(
			'multiline'  => 'm',
			'singleline' => 's',
			'utf8'       => 'u',
			'extended'   => 'x',
		) as $key => $mod)
	{
		if ($p[$key])
			$modifiers .= $mod;
	}

	return  "/$pattern/$modifiers";
}

/**
 * Try to match string against regex. Case insensitive by default.
 * @param $pattern Regex to match against
 * @param $string String to match
 * @param $p['offset_capture'] Set flag preg_offset_capture (returns offsets with the matches).
 * @param $p['all'] Return every match as array instead of first match.
 * @param $p['locale'] Use given locale (default: de_CH), mainly affects handling of iso-latin chars
 * @param $p contains pattern modifiers, @see convertregex()
 * @return Matched string or false 
 */
function match($pattern, $string, $p = array())
{
	$flags = 0;
	$p += array('locale' => 'de_CH');

	if($p['offset_capture'])
		$flags |= PREG_OFFSET_CAPTURE;

	$oldlocale = setlocale( LC_CTYPE, 0 );
	setlocale(LC_CTYPE, $p['locale']);

	if ($p['all'])
		$r = preg_match_all(it::convertregex($pattern, $p), $string, $m, $flags | PREG_PATTERN_ORDER, $p['offset']);
	else
		$r = preg_match(it::convertregex($pattern, $p), $string, $m, $flags, $p['offset']);

	setlocale(LC_CTYPE, $oldlocale);

	if (!$r)	# no match
		$result = $p['all'] ? array() : null;
	else if (count($m) == 1)	# no capture
		$result = $m[0];
	else if (count($m) == 2)	# one capture
		$result = $m[1];
	else if ($p['all'] && !$p['pattern_order'])	# captures, reorder pattern_order to set_order but without first element
		$result = call_user_func_array('array_map', array_merge(array(null), array_slice($m, 1)));
	else	# captures, don't return first element (matched string)
		$result = array_slice($m, 1);

	return $result;
}

/**
 * Replace parts of a string matched by a pattern with according replacement string. See convertregex for named parameters.
 * @param $replacementes Array with patterns as keys and replacement strings as values.
 * @param $string String to change.
 * @return New string.
 */
function replace($replacements, $string, $p = array())
{
	$patterns = array();

	foreach (array_keys($replacements) as $pattern)
		$patterns[] = it::convertregex($pattern, $p);

	$oldlocale = setlocale(LC_CTYPE, 0);
	setlocale(LC_CTYPE, 'de_CH');
	$result = preg_replace($patterns, array_values($replacements), $string, isset($p['limit']) ? $p['limit'] : -1);
	setlocale(LC_CTYPE, $oldlocale);

	return $result;
}

/**
 * Extract key => value pairs from assoc array by key
 * @param $array array to filter
 * @param $keys array or comma separated list of keys to keep
 */
function filter_keys($array, $keys)
{
	$result = array();
	$keep = array_flip(is_string($keys) ? explode(",", $keys) : (array)$keys);

	foreach ($array as $key => $val)
		if (isset($keep[$key]))
			$result[$key] = $val;

	return $result;
}


/**
 * Construct shell command, log it, execute it and return output as string.
 * Keywords {keyword} are replace a la ET(), {-opts} takes an array and
 * inserts options a la it_html attributes (value, true, false or null)
 * @param $cmd Format string with {keywords} replace a la ET()
 * @param $values (zero, one or more arrays can be passed)
 * @return output of command. shell errors not detectable, see error_log in /www/server/logs
 */
function exec(/* $cmd, $values1 = array(), ... */)
{
	$args = func_get_args();
	$cmd = array_shift($args);
	$values = array();

	# Merge values into one array
	foreach ($args as $arg)
		$values += (array)$arg;

	while (list($tag, $option, $key) = it::match('({(-?)(\w+)})', $cmd))
	{
		$parts = array();

		if ($option)
		{
			foreach ((array)$values["-$key"] as $key => $value)
			{
				if ($value === true || $value === false || $value === null)
					$parts[] = $value ? $key : "";
				else foreach ((array)$value as $val)
					$parts[] = "$key " . it::_exec_quotevalue($val);
			}
		}
		else
		{
			foreach ((array)$values[$key] as $value)
				$parts[] = it::_exec_quotevalue($value);
		}

		$cmd = str_replace($tag, join(" ", $parts), $cmd);
	}

	$s = gettimeofday();
	$result = EDC('noexec') ? "" : (string)shell_exec($cmd);
	$e = gettimeofday();
	$msec= intval(($e['sec'] - $s['sec']) * 1000 + ($e['usec'] - $s['usec']) / 1000);

	it::log('exec', "$msec\t$cmd");

	return $result;
}

function _exec_quotevalue($value)
{
	$result = strval($value);

	if (it::match('^-', $result))
		it::fatal("leading - in value");

	return escapeshellarg($result);
}


/**
 * Convert an image to a given size and type (ensures input is an image)
 * @param $p['in']	Input filename (mandatory)
 * @param $p['out']	Output filename (mandatory)
 * @param $p['size']	Width x height of resulting image, e.g. "160x60"
 * @param $p['type']	Output file type, e.g. "jpg"
 * @param $p['types']	Comma separated list of accepted input types, default "gif,jpg,png,bmp,tif,jp2"
 * @return Success of convert as true/false
 */
function imageconvert($p)
{
	$result = false;
	$imagetype = @exif_imagetype($p['in']);

	if (!function_exists("image_type_to_extension") || !($type = it::replace(array("jpeg" => "jpg", "tiff" => "tif"), image_type_to_extension($imagetype, false))))
	{
		# Fallback for PHP4
		$knowntypes = array(IMAGETYPE_GIF => "gif", IMAGETYPE_JPEG => "jpg", IMAGETYPE_PNG => "png", IMAGETYPE_BMP => "bmp");
		$type = $knowntypes[$imagetype];
	}

	$p += array('type' => $type, 'types' => "gif,jpg,png,bmp,tif,jp2");

	if (it::match(",$type,", ",{$p['types']},"))	# Valid type?
		$result = it::exec("convert 2>&1 -flatten -quality 75 {-opts} {in} {type}:{out}", array('-opts' => array('-thumbnail' => $p['size'])), $p) === "";

	return $result;
}


/**
 * Parse command line options with Usage given as template and return assoc array. Example: (like grep --help)
 * Usage: myprogram.php [OPTIONS] PATTERN
 *  -s, --short      Use short ouput
 *  -f, --file=FILE  Use FILE for input
 *  -x EXTENSION     Ignore EXTENSION
 * Mandatory arguments from the Usage: line are returned under their (lowercased!) name.
 * All non-option arguments are returned in 'args'
 * Option text must be indented; if long and short option present, value is stored in long option
 * Options without arguments store true or false under their key
 * Options -h and --help will be handled internally by printing usage and exiting
 * When printing, the usage will be de-indented so the first line starts in the first column
 * Two or more blanks must be in front of option explanation
 * @param $helplines Usage parsed to determine options
 * @return Associative array of options
 */
function getopt($helplines)
{
	$GLOBALS['it_stdin'] = array(
		'fd' => null,
		'args' => array(),
		'filename' => "",
		'line' => 0,
	);

	$result = array('args' => array());

	if ($indentation = it::match('^\s+', $helplines))
		$helplines = it::replace(array($indentation => "\n"), $helplines);

	foreach(explode("\n", trim($helplines)) as $helpline)
	{
		$shortoptname = $shortoptarg = $longoptname = $longoptarg = "";
		foreach (explode(',', $helpline) as $optdesc)
		{
			$optdesc = trim($optdesc);
			if ($matches = (array)it::match('^--(\w[\w-]*)(=[A-Z])?', $optdesc))
				list($longoptname, $longoptarg) = $matches;
			elseif ($matches = (array)it::match('^-(\w)( [A-Z])?', $optdesc))
				list($shortoptname, $shortoptarg) = $matches;
		}	

		if ($longoptname || $shortoptname)
		{
			if ($longoptname && $shortoptname)
				$alias[$shortoptname] = $longoptname;

			$witharg[$longoptname ? $longoptname : $shortoptname] = $longoptarg || $shortoptarg;
		}
	}
	$witharg['debug'] = true;

	$mandatoryargs = array();
	if ($tmp = trim(it::replace(array("\n.*" => "", "^\S+\s+\S+\s*" => "", "\[.*?\]\s*" => ""), trim($helplines))))
		$mandatoryargs = preg_split('/\s+/', $tmp);

	foreach (array_slice($_SERVER['argv'], 1) as $arg)
	{
		if ($eat)
		{
			if (it::match('^--?\w', $arg))	# Already next option => Missing argument?
				$err = true;
			else
				$result[array_shift($eat)] = $arg;
		}
		elseif ($matches = (array)it::match('^--(\w[\w-]*)(=.*)?', $arg))
		{
			list($optname, $val) = $matches;
			if (!isset($witharg[$optname]) || isset($val) && !$witharg[$optname])
				$err = true;
			else if($witharg[$optname] && !$val)
				$eat[] = $optname;
			else
				$result[$optname] = $val ? substr($val, 1) : true;
		}
		else if ($letters = it::match('^-(\w+)', $arg))
		{
			foreach (explode("\n", trim(chunk_split($letters, 1, "\n"))) as $letter)
			{
				$optname = $alias[$letter] ? $alias[$letter] : $letter;
				if ($witharg[$optname])
					$eat[] = $optname;
				else if (!isset($witharg[$optname]))
					$err = true;
				else
					$result[$optname] = true;
			}
		}
		elseif($mandatoryargs)
			$result[strtolower(array_shift($mandatoryargs))] = $arg;
		else
			$result['args'][] = $arg;
	}

	if ($err || $eat || $result['h'] || $result['help'] || $mandatoryargs)
	{
		fputs(($result['h'] || $result['help'] ? STDOUT : STDERR), trim($helplines) . "\n");
		exit(1);
	}

	if ($result['debug'])    
	{
		foreach (split('[.,]', $result['debug']) as $ultrad)
		{
			$ultravar = split('[-=:]', $ultrad);
			$GLOBALS["debug_$ultravar[0]"] = isset($ultravar[1]) ? $ultravar[1] : 1;
		}
	}

	$GLOBALS['it_stdin']['args'] = $result['args'] ? $result['args'] : array("-");
	it::_stdin_next();

	return $result;
}

function _stdin_next()
{
	if ($result = $GLOBALS['it_stdin']['args'])
	{
		$GLOBALS['it_stdin']['filename'] = array_shift($GLOBALS['it_stdin']['args']);
		$GLOBALS['it_stdin']['fd'] = ($GLOBALS['it_stdin']['filename'] == "-") ? STDIN : @fopen($GLOBALS['it_stdin']['filename'], "r");
		$GLOBALS['it_stdin']['line'] = 0;
	}

	return $result;
}

/**
 * Get one line from stdin (or files given on command line) a la Perl <>.
 * Note: You need to call getopt() before using this function.
 * @return Line (including newline) or false on eof
 */
function gets()
{
	do {
		$result = fgets($GLOBALS['it_stdin']['fd']);
	} while (($result === false) && it::_stdin_next());

	$GLOBALS['it_stdin']['line']++;
	return $result;
}

/**
 * Output formatted and localized date
 * @param format optional format (default is 2007-01-02 03:04:05).
 *        Other formats are "date", "datetime", "time".
 *        Formats can be qualified with language, e.g. "date:en"
 * @param stamp optional unix timestamp (default is now).
 *        If it contains nondigits, it is fed to strtotime
 */
function date($format = "", $stamp = null)
{
	list($name, $language) = explode(":", $format);

	if ($format && !$language)
		$language = T_lang();

	$formats = array(
		"" => "Y-m-d H:i:s",
		"date" => "d.m.Y",
		"datetime" => "d.m.Y H:i",
		"time" => "H:i",
		"date:en" => "m/d/Y",
		"datetime:en" => "m/d/Y h:ia",
		"time:en" => "h:ia",
	);

	if (!($formatstring = $formats["$name:$language"]) && !($formatstring = $formats[$name]))
		$formatstring = $format;

	$stamp = !isset($stamp) ? time() : (it::match('^\d+$', $stamp) ? $stamp : strtotime($stamp));

	return date($formatstring, $stamp);
}

/**
 * Iterate over an array, replacing every element by expression
 * @param $expression The expression to apply, may contain $k for keys and $v for values
 * @param $array The array to iterate over
 */
function map($expression, $array)
{
	static $cache = array();

	if (!($func = $cache[$expression]))
		$func = $cache[$expression] = create_function('$k,$v', "return $expression;");

	foreach ($array as $k => $v)
		$result[$k] = $func($k, $v);

	return (array)$result;
}

/**
 * Send a mail. Expects array for Header => Content pairs with Body => for the mail body
 */
function mail($p)
{
	$body = $p['Body'];
	unset($p['Body']);
	$mail = new it_mail($p);
	$mail->add_body($body);

	return $mail->send();
}

}

?>