<?php
/**
 * Debug_HackerConsole_Main: write debug messages into hidden console.
 * (C) 2005 Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * See http://www.gnu.org/copyleft/lesser.html

 * Console may be toggled using Shift+Ctrl+` (tilde) combination.
 * 
 * @version 1.x $Id: Main.php 168 2007-01-30 21:12:03Z dk $
 */
 
class Debug_HackerConsole_Main
{
	var $_hc_height = "400"; // height of the console (pixels)
	var $_hc_entries = array();
	var $TAB_SIZE = 4;
	var $disabled = false;

	/**
	 * constructor($autoAttachToHtmlOutput=true)
	 * Create new console. If $autoAttachToHtmlOutput, output buffering
	 * handler is set to automatically attach JavaScript showing code to 
	 * HTML page.
	 */
	function Debug_HackerConsole_Main($autoAttach=false)
	{
		if ($autoAttach) ob_start(array(&$this, '_obHandler'));
		$GLOBALS['Debug_HackerConsole_Main_LAST'] = &$this;
	}


	/**
	 * string attachToHtml(string $pageHtml)
	 * Attach the console to given HTML page.
	 */
	function attachToHtml($page)
	{
		if ($this->disabled) return $page;
		$js = file_get_contents(dirname(__FILE__).'/Hconsole.js');
		if (get_magic_quotes_runtime()) $js = stripslashes($js);
		$js = str_replace('{HEIGHT}', $this->_hc_height, $js);
		// We MUST use "hackerConsole" instead of "console" because of Safari.
		$code = "window.hackerConsole = window.hackerConsole || "
		."window.Debug_HackerConsole_Js && new window.Debug_HackerConsole_Js();\n";
		$code .= "if (window.hackerConsole) setTimeout(function() { with (window.hackerConsole) {\n";
		foreach ($this->_hc_entries as $gid=>$elements) {
			foreach ($elements as $e) {
				if ($e['tip'] === null) {
					$file = str_replace('\\', '/', $e['file']);
					if (isset($_SERVER['DOCUMENT_ROOT'])) {
						// Under IIS DOCUMENT_ROOT may not be available.
						$dr = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']);
						$file = preg_replace('{^'.preg_quote($dr, '{}').'}is', '~', $file);
					}
					$title = "at {$file} line {$e['line']}" . (!empty($e['function'])? ", {$e['function']}" : "");
				} else {
					$title = $e['tip'];
				}
				$text = $this->toPre($e['text']);
				if (!empty($e['color'])) $text = "<div style=\"color:{$e['color']}\">$text</div>";
				$code .= "  out(".$this->_toJs($text).", ".$this->_toJs($title).", ".$this->_toJs($gid).");\n";
			}
		}
		$code .= "}}, 200);";
		$html = '';
		// Dirty close opened tags. This is bad, but better than nothing...
		$lower = strtolower($page);
		if (strpos($lower, '</body>') === false) {
			foreach (array('script', 'xmp', 'pre') as $tag) {
				if (substr_count($lower, "<$tag") > substr_count($lower, "</$tag")) {
					$html .= "</$tag>";
				}
			}
		}
		$html .= "\n";
		$html .= "<!-- ##################### -->\n";
		$html .= "<!-- ### HackerConsole ### -->\n";
		$html .= "<!-- ##################### -->\n";
		$html .= "<script type=\"text/javascript\">//<![CDATA[\n{$js}\n{$code}\n//]]></script>\n";
		$page = preg_replace('{(?=</body[^>]*>|$)}si', preg_replace('/([\\\\$])/', '\\\\$1', $html), $page, 1);
		return $page;
	}


	/**
	 * void out(string $msg, string $group="message", $color=null, $tip=null)
	 * Add new message to the console.
	 * Messages may be grouped together using $group parameters for better view.
	 * By default messages are tipped with caller context (file, line).
	 * Contexts generated by call_user_func() are skipped!
	 */
	static function out($v, $group="message", $color=null, $tip=null)
	{
		// Have to work only with $obj, NOT $this!
		if (empty($this) || strtolower(get_class($this)) != 'debug_hackerconsole_main') {
			$obj =& $GLOBALS['Debug_HackerConsole_Main_LAST'];
		} else {
			$obj =& $this;
		}
		if (!$obj) return;
		
		// Detect caller if needed. Used in tip.
		$s = array();
		if ($tip === null) {
			// Find caller. Use call_user_func to get context of out() calling.
			$s = call_user_func(
				array(&$obj, 'debug_backtrace_smart'), 
				'call_user_func.*', // ignore indirect contexts
				true
			);
		}
		
		if (is_scalar($v)) $text = "$v\n";
		else $text = Debug_HackerConsole_Main::print_r($v, true);
		
		$obj->_hc_entries[$group][] = array(
			'file'	 => isset($s['file'])? $s['file'] : null,
			'line'	 => isset($s['line'])? $s['line'] : null,
			'function' => isset($s['function'])? $s['function'] : null,
			'text'	 => $text,
			'color'	=> $color,
			'tip'	  => $tip,
		);
	}
	
	
	/**
	 * void disable()
	 * Disable displaying of the console.
	 */
	static function disable()
	{
		// Work only with $obj, NOT $this!
		if (empty($this) || strtolower(get_class($this)) != 'debug_hackerconsole_main') {
			$obj =& $GLOBALS['Debug_HackerConsole_Main_LAST'];
		} else {
			$obj =& $this;
		}
		if ($obj) $obj->disabled = true;		
	}

	/**
	 * string toPre($text)
	 * Format plaintext like <pre> tag does, but with <br> at the line tails
	 * and &nbsp; in line prefixes. 
	 */
	function toPre($text, $tabSize=null)
	{
		$text = htmlspecialchars($text);
		// Expand tabulators.
		if ($tabSize === null) {
			if (isset($GLOBALS['Debug_HackerConsole_Main_LAST'])) 
				$tabSize = $GLOBALS['Debug_HackerConsole_Main_LAST']->TAB_SIZE;
			else 
				$tabSize = 4;
		}
		$text = Debug_HackerConsole_Main::expandTabs($text, $tabSize);
		$text = str_replace(' ', '&nbsp;', $text);
		$text = nl2br($text);
		return $text;
	}
	

	/**
	 *  We need manual custom print_r() to use it in OB handlers
	 * (original print_r() cannot work inside OB handler).
	 */  
	function print_r($obj, $no_print=0, $level=0)
	{
		if ($level < 10) {
			if (is_array($obj)) {
				$type = "Array[".count($obj)."]"; 
			} elseif (is_object($obj)) {
				$type = "Object";
			} elseif (gettype($obj) == "boolean") {
				$type = $obj? "TRUE" : "FALSE";
			} elseif ($obj === null) {
				$type = "NULL";
			} else {
				$type = preg_replace("/\r?\n/", "\\n", $obj);
			}
			$buf = $type;
			if (is_array($obj) || is_object($obj)) { 
				$leftSp = str_repeat("	", $level+1);
				for (reset($obj); list($k, $v) = each($obj); ) {
					if ($k === "GLOBALS") continue;
					$buf .= "\n{$leftSp}[$k] => ".Debug_HackerConsole_Main::print_r($v, $no_print, $level+1);
				}
			}
		} else {
			$buf = "*RECURSION*";
		}
		$buf = str_replace("\x00", " ", $buf); // PHP5 private methods contain \x00 in names
		if ($no_print) return $buf;
		else echo $buf;
		return null;
	}
	
	
	/**
	 * string expandTabs($text, $tabSize=4)
	 * Correctly convert tabulators to spaces.
	 */
	function expandTabs($text, $tabSize=4)
	{
		$GLOBALS['expandTabs_tabSize'] = $tabSize;
		while (1) {
			$old = $text;
			$text = preg_replace_callback(
				'/^([^\t\r\n]*)\t(\t*)/m',
				array('Debug_HackerConsole_Main','expandTabs_callback'),
				$text
			);
			if ($old === $text) return $text;
		}
	}
	
	static function expandTabs_callback($m)
	{
		$tabSize = $GLOBALS['expandTabs_tabSize'];
		$n = 
			intval((strlen($m[1]) + $tabSize) / $tabSize) * $tabSize 
			- strlen($m[1]) 
			+ strlen($m[2]) * $tabSize;
		return $m[1] . str_repeat(' ', $n);
	}


	/**
	 * Internal methods.
	 */
	
	function _obHandler($s)
	{
		return $this->attachToHtml($s);
	}

	
	function _toJs($a)
	{
		$a = addslashes($a);
		$a = str_replace(array("\n", "\r", ">", "<"), array('\n', '\r', "'+'>", "<'+'"), $a);
		return "'$a'";				   
	}
	
	
	/**
	 * array debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
	 * 
	 * Return stacktrace. Correctly work with call_user_func*
	 * (totally skip them correcting caller references).
	 * If $returnCaller is true, return only first matched caller,
	 * not all stacktrace.
	 * 
	 * @version 2.03
	 */
	function debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
	{
		if (!is_callable($tracer = 'debug_backtrace')) return array();
		$trace = $tracer();
		
		if ($ignoresRe !== null) $ignoresRe = "/^(?>{$ignoresRe})$/six";
		$smart = array();
		$framesSeen = 0;
		for ($i=0, $n=count($trace); $i<$n; $i++) {
			$t = $trace[$i];
			if (!$t) continue;
				
			// Next frame.
			$next = isset($trace[$i+1])? $trace[$i+1] : null;
			
			// Dummy frame before call_user_func* frames.
			if (!isset($t['file'])) {
				$t['over_function'] = $trace[$i+1]['function'];
				$t = $t + $trace[$i+1];
				$trace[$i+1] = null; // skip call_user_func on next iteration
			}
			
			// Skip myself frame.
			if (++$framesSeen < 2) continue;

			// 'class' and 'function' field of next frame define where
			// this frame function situated. Skip frames for functions
			// situated in ignored places.
			if ($ignoresRe && $next) {
				// Name of function "inside which" frame was generated.
				$frameCaller = (isset($next['class'])? $next['class'].'::' : '')
				. (isset($next['function'])? $next['function'] : '');
				if (preg_match($ignoresRe, $frameCaller)) continue;
			}
			
			// On each iteration we consider ability to add PREVIOUS frame
			// to $smart stack.
			if ($returnCaller) return $t;
			$smart[] = $t;
		}
		return $smart;
	}
}


/**
 * Last created console.
 */
$GLOBALS['Debug_HackerConsole_Main_LAST'] = null;

