<?php
/*
**	Copyright (C) 1995-2021 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/>.
**
**	debug.class - Debug Functionality for ITools
*/

/**
 * Debug functions
 */
class it_debug
{
	var $level;
	static $force_html;

/**
 * Constructor
 * @param $level Debug level. All debug() calls with a level <= $level are shown.
 */
function __construct($level=0)
{
	$this->level = isset($GLOBALS['debug_level']) ? $GLOBALS['debug_level'] : $level;
}


/**
 * Output a message if the global debug level is higher or the same as $level
 * @param $text Message to display
 * @param $level Debug level
 */
function debug($text, $level = 0)
{
	if ($this->level >= $level)
	{
		echo (isset($_SERVER['REMOTE_ADDR']) ? nl2br("$level $text\n") : "$level $text")."\n";
		flush();
	}
}

static function echo($args)
{
	if (getenv('IT_ED_STDERR'))
		fputs(STDERR, self::dump($args));
	else
		echo self::dump($args);
}

static function debug_backtrace($p = []) {
	if (!is_array($p))
		$p = array('skiplevels' => $p);

	$p += array('levels' => 0, 'skiplevels'=> 0, 'skipfiles' => "###");

	foreach ($p['trace'] ?: debug_backtrace(@constant('DEBUG_BACKTRACE_IGNORE_ARGS')) as $call)
	{
		if (!$result && it::match('it_debug\.class$', $call['file']))
			continue;
		if (!$result && it::match($p['skipfiles'], $call['file']))
			continue;
		if (!$result && $p['skiplevels']-- > 0)
			continue;
		$result[] = $call;
		if (--$p['levels'] == 0)
			break;
	}
	return (array)$result;
}

/**
 * Get source line of grandparent calling this function
 * @param $stacksoffs go up an extra $stacksoffs levels
 */

static function srcline($p = array())
{
	$stack = self::debug_backtrace($p);
	$line = $stack[1]['line'];
	$file = $stack[1]['file'];

	if (!isset($GLOBALS['it_debug::dump source'][$file]))
		$GLOBALS['it_debug::dump source'][$file] = @it::file($file);

	return $GLOBALS['it_debug::dump source'][$file][$line-1];
}

/**
 * Prepare for debug output being echoed
 */
static function setup()
{
	$GLOBALS['debug_noredir'] = 1;

	if (ob_get_level() == 0 && $_SERVER['REMOTE_ADDR'])
		ob_start(); # prevent later 'headers already sent' error

	if ($_SERVER['REMOTE_ADDR'] && !EDC('edplain') && it::match('(?:^|,)\s*text/html\s*(?:,|$)', $_SERVER['HTTP_ACCEPT']))
		self::$force_html = true;
}



/**
 * Backend for functions D(), ED() and EDX() in functions.php
 * @param Origargs Array containing original arguments do ED() etc
 * @param $p['color']  Allow color [true]
 * @param $p['html']   Allow html [default based on REMOTE_ADDR]
 * @param $p['short']  Omit variable names
 * @return String representation of dump
 */
static function dump($args, $p = [])
{
	$p += ['html' => !EDC('edplain') && (self::$force_html || $_SERVER['REMOTE_ADDR']), 'color' => true];
	$htmlspecialchars = array("&" => "&amp;", '<' => "&lt;", '>' => "&gt;");
	$args = count($args) ? $args : [it_debug::backtrace(['skiplevels' => 1])];

	if ((preg_match('/csv|txt|gif|jpg/', $_SERVER['PHP_SELF']) || preg_grep('#text/(calendar|css|javascript|json|plain|rfc822|xml)|application/#', headers_list()) || $GLOBALS['debug_edplain']) && !$GLOBALS['debug_edhtml'])
		;
	else if ($p['html'])
		list($blue, $noblue) = $p['color'] ? ["<span style='color:#00c'>", "</span>"] : [];
	else if (defined('STDOUT') && posix_isatty(STDOUT) && $p['color'])
		list($blue, $noblue, $red, $nored, $ansiok) = getenv('IT_ED_BRIGHT') ? array("\033[34m", "\033[m", "\033[33m", "\033[m", 1) : array("\033[34m", "\033[m", "\033[31m", "\033[m", 1);

	$src = it_debug::srcline();
	list($dummy, $function, $paramlist) = preg_match('#\b(D|ED|EDC|EDX)\s*\((.*)#i', $src, $parts) ? $parts : array();
	$paramtokens = token_get_all("<?php $paramlist");
	array_shift($paramtokens);
	$param = "";
	$argnames = array();
	$paramnesting = 0;
	foreach ($paramtokens as $token)
	{
		if ($token == "(")
			$paramnesting++;
		else if ($token == ")")
		{
			if (!$paramnesting--)	# Found closing parens
			{
				$argnames[] = trim($param);
				break;
			}
		}

		if (($token == ",") && !$paramnesting)
		{
			$argnames[] = trim($param);
			$param = "";
		}
		else
			$param .= is_array($token) ? $token[1] : $token;
	}

	if (preg_match('#^EDC$#i', $function))	# First argument was removed by EDC
		array_shift($argnames);

	foreach ($args as $arg)
	{
		$var = array_shift($argnames);
		$item = gettype($arg) == 'resource' || is_array($arg) && isset($arg['GLOBALS']) || is_object($arg) && is_a($arg, "SimpleXMLElement") ? trim(print_r($arg, true)) : trim(var_export($arg, true));

		# Replace PHP 5+ var_export object representation by old readable style
		while (preg_match("#(.*\b)(\w+)::__set_state\(array\(([^()]+)\)\)(.*)#s", $item, $regs) && ++$iterations < 100)
		{
			list (, $head, $classname, $values, $tail) = $regs;
			$classname = strtolower($classname);
			$values = preg_replace("#'(\w+)' =>\s*([^\n]+),#", 'var \$$1 = $2;', $values);
			$item = rtrim($head, '\\') . "class $classname {" . $values . "}" . $tail;
		}

		$item = preg_replace_callback('/\b(1[2-9]\d\d\d\d\d\d\d\d)\b(.*)/', function($m) { return $m[1] . $m[2] . " # " . date('Y-m-d H:i:s', $m[1]); }, $item);
		$item = preg_replace("#(=>?)\s*\n\s*(array|class)#", '$1 $2', $item); # array( and class on same line as key
		$item = preg_replace_callback('#array \(\s+([^(){};]{1,100}),\s+\)#', function($m) { return "array (" . preg_replace("#\n\s+#", " ", $m[1]) . ")"; }, $item); # short arrays on 1 line
		$item = preg_replace('#class (\S+) \{\s+([^({,;]+;)?\s+\}#', 'class $1 { $2 }', $item); # 1-element objects on 1 line
		#$item = preg_replace('#\{\s*var \$attr#', '{ var $attr', $item); # move $attr on same line
		$item = preg_replace("#\\(\s*\n\s*\\)#", "()", $item);	   # empty arrays on 1 line
		$item = preg_replace("# =>\s*\n\s+\(object\) array\(\n#", " => (object) array(\n", $item);
		#$item = preg_replace('#\s+var \$_(.|\n)*?;\s*\n#', "", $item);
		$item = "$red$item$nored";

		if ($p['html'])
			$item = it::replace(['[\x00-\x08\x0b-\x0c\x0e-\x1f]' => ""], preg_replace("#(https?:)?//[^\s'\"]+#", "<a href='\$0'>\$0</a>", strtr($item, $htmlspecialchars)), ['utf8' => false]); # htmlspecialchars() does not like bad utf8

		if (preg_match('/^[\'"]/', $var))
			$r .= "$item ";
		else
		{
			$var = $p['html'] ? strtr($var, $htmlspecialchars) : $var;
			$var = $p['short'] ? "" : "$blue$var=$noblue";
			$r .= $previtem && preg_match("/\n/", $item . $previtem) ? "\n$var$item " : "$var$item ";
			$previtem = $item;
		}
	}

	if ($GLOBALS['debug_indent'])
		$r = str_repeat("  ", count(debug_backtrace(@constant('DEBUG_BACKTRACE_IGNORE_ARGS')))-3) . $r;

	# not it_html::Q(); we dont wanna load it_html in ultraclassloader debugging
	$title = htmlspecialchars(it_debug::backtrace(array('skiplevels' => 1)), ENT_COMPAT, "ISO-8859-1"); # skip this func as well as ED()/EDX()

	if ($p['html'])
		 return "<pre title='$title' style='color:#c00; text-align:left; background:white; border-bottom:1px solid #ddd; z-index:9999; position:relative; line-height:1'>$r</pre>\n";
	else
		 return $ansiok || $p['short'] || EDC('edplain') ? "$r\n" : "\u{2588}\u{258c}" . preg_replace("#\n#", "\n\u{2588} ", "$r") . "\n";
}

/**
 * Return short stackdump
 * @param $p['levels'] number of stack levels to return (default: 0 = all)
 * @param $p['skiplevels'] number of stack levels to omit
 * @param $p['skipfiles'] regular expression of filenames to omit
 * @param $p['trace'] Stack trace to compact
 */
static function backtrace($p = array())
{
	$frames = $p['trace'] ?? ($p['format'] ? array_slice(debug_backtrace(0), $p['skiplevels']) : null);

	if ($p['format'] == "long")
	{
		foreach ($frames as $frame)
			if ($tracesize < 100000 && ($tracesize += strlen(print_r($frame, true))) < 100000) # save mem
				$outframes[] = $frame;
		$result = $outframes ? print_r($outframes, true) . "\n" : null;
	}
	else if ($p['format'] == "medium")
	{
		foreach ($frames as $frame)
		{
			$locations[] = basename($frame['file']) . ":" . $frame['line'];

			$argstrings = [];
			foreach ((array)$frame['args'] as $arg)
			{
				$t = trim(it::replace(['^array \(\s*' => "[", ',\n\)$' => "]", '^array \( \)' => "[]", " +" => " ", "\n" => ""], var_export($arg, true)), " ,");
				$argstrings[] = strlen($t) < 60 ? $t : it::replace(["^(.{0,60} |.{0,60}).*" => '$1'], $t) . "..." . (is_array($arg) ? "]" : (is_object($arg) ? "}" :""));
			}

			$allargs = join(", ", $argstrings);
			if ($frame['type'] == "->" || $frame['type'] == "::")
				$funcs[] = $frame['class'] . $frame['type'] . $frame['function'] . "($allargs)";
			else
				$funcs[] = $frame['function'] ? $frame['function'] . "($allargs)" : "[inline]";

			$maxlen = max(it::map('strlen', $locations));
		}

		foreach ($locations as $idx => $location)
			$result .= str_pad($location, $maxlen + 2) . $funcs[$idx] . "\n";
	}
	else
	{
		foreach (self::debug_backtrace($p) as $frame)
		{
			$fn = $frame['file'];
			$fn = (it::match('auto_prepend', $fn) ? basename(dirname(dirname($fn))) . "/" : (it::match('/(cgi|bin)/', $fn) ? basename(dirname($fn)) . "/" : "")) . basename($fn);
			if ($fn)
				$result[] = $fn . ":" . $frame['line'];
		}

		$result = implode(" ", (array)$result);
	}

	return $result;
}

static function set($debug_string)
{
	foreach (preg_split('/[.,]/', $debug_string) as $ultrad)
	{
		$ultravar = preg_split('/[-=:]/', $ultrad, 2);
		$GLOBALS["debug_$ultravar[0]"] = isset($ultravar[1]) ? $ultravar[1] : 1;
	}

	# keep debug parameters in subrequests
	$_SERVER['ULTRAHOSTNAME'] = it::replace(['^\.+' => '', '([^.])\.*$' => '\1.'], $debug_string) . $_SERVER['ULTRAHOSTNAME'];
}

}
?>