includes/debugger/debugConsole.class.php
author Dan
Fri, 05 Oct 2007 01:57:00 -0400
changeset 162 e1a22031b5bd
parent 1 fe660c52c48f
permissions -rw-r--r--
Major revamps to the template parser. Fixed a few security holes that could allow PHP to be injected in untimely places in TPL code. Improved Ux for XSS attempt in tplWikiFormat. Documented many functions. Backported much cleaner parser from 2.0 branch. Beautified a lot of code in the depths of the template class. Pretty much a small-scale Extreme Makeover.

<?php
/**
 * debugConsole class
 *
 * This class allows opening an external JavaScript
 * window for debugging purposes.
 *
 * @author Andreas Demmer <info@debugconsole.de>
 * @see <http://www.debugconsole.de>
 * @version 1.2.1
 * @package debugConsole_1.2.1
 */
class debugConsole {
	/**
	 * events which are shown in debug console
	 *
	 * @var array
	 */
	protected $filters;
	
	/**
	 * all watched variables with their current content
	 *
	 * @var array
	 */
	protected $watches;
	
	/**
	 * debugConsole configuration values
	 *
	 * @var array
	 */
	protected $config;
	
	/**
	 * URL where template can be found
	 *
	 * @var string
	 */
	protected $template;

	/**
	 * javascripts to control popup
	 *
	 * @var array
	 */
	protected $javascripts;

	/**
	 * html for popup
	 *
	 * @var array
	 */
	protected $html;

	/**
	 * time of debugrun start in milliseconds
	 *
	 * @var string
	 */
	protected $starttime;

	/**
	 * time of timer start in milliseconds
	 *
	 * @var array
	 */
	protected $timers;

	/**
	 * constructor, opens popup window
	 */
	public function __construct () {
		/* initialize class vars */
		$this->starttime = $this->getMicrotime();
		$this->watches = array ();
		$this->config = $GLOBALS['_debugConsoleConfig'];
		$this->html = $this->config['html'];
		$this->html['header'] = str_replace("\n\r", NULL, $this->html['header']);
		$this->html['header'] = str_replace("\n", NULL, $this->html['header']);
		$this->javascripts = $this->config['javascripts'];
		
		/* replace PHP's errorhandler */
		$errorhandler = array (
			$this,
			'errorHandlerCallback'
		);
		
		set_error_handler($errorhandler);
		
		/* open popup */
		$popupOptions = "', 'debugConsole', 'width=" . $this->config['dimensions']['width'] . ",height=" . $this->config['dimensions']['height'] . ',scrollbars=yes';
		
		$this->sendCommand('openPopup', $popupOptions);
		$this->sendCommand('write', $this->html['header']);
		
		$this->startDebugRun();
	}
	
	/**
	 * destructor, shows runtime and finishes html document in popup window
	 */
	public function __destruct () {
		$runtime = $this->getMicrotime() - $this->starttime;
		$runtime = number_format((float)$runtime, 4, '.', NULL);
		
		$info = '<p class="runtime">This debug-run took ' . $runtime . ' seconds to complete.</p>';

		$this->sendCommand('write', $info);
		$this->sendCommand('write', '</div>');
		$this->sendCommand('scroll', "0','100000");
		$this->sendCommand('write', $this->html['footer']);
		
		if ($this->config['focus']) {
			$this->sendCommand('focus');
		}
	}
	
	/**
	 * show new debug run header in console
	 */
	
	protected function startDebugRun () {
		$info = '<h1>new debug-run (' . date('H:i') . ' hours)</h1>';
		$this->sendCommand('write', '<div>');
		$this->sendCommand('write', $info);
	}

	/**
	 * adds a variable to the watchlist
	 * 
	 * Watched variables must be in a declare(ticks=n)
	 * block so that every n ticks the watched variables
	 * are checked for changes. If any changes were made,
	 * the new value of the variable is shown in the
	 * debugConsole with additional information where the
	 * changes happened.
	 *
	 * @param string $variableName
	 */
	public function watchVariable ($variableName) {
		if (count($this->watches) === 0) {
			$watchMethod = array (
				$this,
				'watchesCallback'
			);
			
			register_tick_function($watchMethod);
		}
		
		if (isset($GLOBALS[$variableName])) {
			$this->watches[$variableName] = $GLOBALS[$variableName];
		} else {
			$this->watches[$variableName] = NULL;
		}
	}
	
	/**
	 * tick callback: process watches and show changes
	 */
	public function watchesCallback () {
		if ($this->config['filters']['watches']) {
			foreach ($this->watches as $variableName => $variableValue) {
				if ($GLOBALS[$variableName] !== $this->watches[$variableName]) {
					$info = '<p class="watch"><strong>$' . $variableName;
					$info .= '</strong> changed from "';
					$info .= $this->watches[$variableName];
					$info .= '" (' . gettype($this->watches[$variableName]) . ')';
					$info .= ' to "' . $GLOBALS[$variableName] . '" (';
					$info .= gettype($GLOBALS[$variableName]) . ')';
					$info .= $this->getTraceback() . '</p>';
					
					$this->watches[$variableName] = $GLOBALS[$variableName];
					$this->sendCommand('write', $info);
				}
			}
		}
	}
	
	/**
	 * sends a javascript command to browser
	 *
	 * @param string $command
	 * @param string $value
	 */
	protected function sendCommand ($command, $value = FALSE) {
    if($command == 'write') $value = '\'+unescape(\''.rawurlencode($value).'\')+\'';
		$value = str_replace('\\', '\\\\', $value);
    $value = nl2br($value);
		
		if ((bool)$value) { 
			/* write optionally logfile */
			$this->writeLogfileEntry($command, $value);
			
			$command = $this->javascripts[$command] . "('" . $value . "');";
		} else {
			$command = $this->javascripts[$command] . ';';
		}
		
		$command = str_replace("\n\r", NULL, $command);
		$command = str_replace("\n", NULL, $command);
		
		if (!$this->config['logfile']['disablePopup']) {
			echo $this->javascripts['openTag'], "\n";
			echo $command, "\n";
			echo $this->javascripts['closeTag'], "\n";
		}
		
		flush();
	}
	
	/**
	 * writes html output as text entry into logfile
	 *
	 * @param string $command
	 * @param string $value
	 */
	protected function writeLogfileEntry ($command, $value) {
		if ($this->config['logfile']['enable']) {
			$logfile = $this->config['logfile']['path'] . $this->config['logfile']['filename'];
			/* log only useful entries, no html header and footer */
			if (
				$command === 'write'
				&& !strpos($value, '<html>')
				&&  !strpos($value, '</html>')
			) {
				/* convert html to text */
				$value = html_entity_decode($value);
				$value = str_replace('>', '> ', $value);
				$value = strip_tags($value);
				
				$fp = fopen($logfile, 'a+');
				fputs($fp, $value . "\n\n");
				fclose($fp);
			} elseif (strpos($value, '</html>')) {
				$fp = fopen($logfile, 'a+');
				fputs($fp, "-----------\n");
				fclose($fp);
			}
		}
	}

	/**
	 * shows in console that a checkpoint has been passed,
	 * additional info is the file and line which triggered
	 * the output
	 *
	 * @param string $message
	 */	
	public function passedCheckpoint ($message = NULL) {
		if ($this->config['filters']['checkpoints']) {
			$message = (bool)$message ? $message : 'Checkpoint passed!';
	
			$info = '<p class="checkpoint"><strong>' . $message . '</strong>';
			$info .= $this->getTraceback() . '</p>';
			
			$this->sendCommand('write', $info);
		}
	}
	
	/**
	 * returns microtime as float value
	 *
	 * @return float
	 */
	protected function getMicrotime () {
		list($usec, $sec) = explode(' ', microtime()); 
    	return ((float)$usec + (float)$sec);
	}
	
	/**
	 * returns all possible filter events for debugConsole::setFilter() method
	 *
	 * @return array
	 */
	public function getFilters () {
		$filters = array_keys($this->config['filters']);
		
		ksort($filters);
		reset($filters);
		
		return $filters; 
	}
	
	/**
	 * shows or hides an event-type in debugConsole,
	 * returns previous setting of the given event-type
	 *
	 * @param string $event
	 * @param bool $isShown
	 * @return bool
	 */
	public function setFilter ($event, $isShown) {
		if (array_key_exists($event, $this->config['filters'])) {
			$oldValue = $this->config['filters'][$event];
			$this->config['filters'][$event] = $isShown;
		} else {
			throw new Exception ('debugConsole: unknown event "' . $event . '" in debugConsole::filter()');
		}
		
		return $oldValue;
	}
	
	/**
	 * show debug info for variable in debugConsole,
	 * added by custom text for documentation and hints
	 *
	 * @param mixed $variable
	 * @param string $text
	 */
	public function dump ($variable, $text) {
		if ($this->config['filters']['debug']) {
			@ob_start();
			
			/* grab current ob content */
			$obContents = ob_get_contents();
			ob_clean();
			
			/* grap var dump from ob */
			var_dump($variable);
			$variableDebug = ob_get_contents();
			ob_end_clean();
			
			/* restore previous ob content */
			if ((bool)$obContents) echo $obContents;
			
			/* render debug */
			$variableDebug = htmlspecialchars($variableDebug);			
			$infos = '<p class="dump">' . $text . '<br />';
			
			if (is_array($variable)) {
				$variableDebug = str_replace(' ', '&nbsp;', $variableDebug);
				$infos .= '<span class="source">' . $variableDebug . '</span>';
			} else {
				$infos .= '<strong>' . $variableDebug . '</strong>';
			}
			
			$infos .= $this->getTraceback() . '</p>';
			$this->sendCommand('write', $infos);
		}
	}
	
	/**
	 * callback method for PHP errorhandling
	 * 
	 * @todo implement more errorlevels
	 */
	public function errorHandlerCallback () {
		$details = func_get_args();
		$details[1] = str_replace("'", '"', $details[1]);
		$details[1] = str_replace('href="function.', 'target="_blank" href="http://www.php.net/', $details[1]);
		
		
		/* determine error level */
		switch ($details[0]) {
			case 2:
				if (!$this->config['filters']['php_warnings']) return;
				$errorlevel = 'warning';
				break;
			case 8:
				if (!$this->config['filters']['php_notices']) return;
				$errorlevel = 'notice';
				break;
			case 2048:
				if (!$this->config['filters']['php_suggestions']) return;
				$errorlevel = 'suggestion';
				break;
		}

		$file = $this->cropScriptPath($details[2]);
		
		$infos = '<p class="' . $errorlevel . '"><strong>';
		$infos .= 'PHP ' . strtoupper($errorlevel) . '</strong>';
		$infos .= $details[1] . '<span class="backtrace">';
		$infos .= $file . ' on line ';
		$infos .= $details[3] . '</span></p>';		
		
		$this->sendCommand('write', $infos);
	}
	
	/**
	 * start timer clock, returns timer handle
	 * 
	 * @return mixed
	 * @param string $comment
	 */
	public function startTimer ($comment) {
		if ($this->config['filters']['timers']) {
			$timerHandle = md5(microtime());
			
			$this->timers[$timerHandle] = array (
				'starttime' => $this->getMicrotime(),
				'comment' => $comment
			);
		} else {		
			$timerHandle = FALSE;		
		}
		
		return $timerHandle;
	}
	
	/**
	 * stop timer clock
	 * 
	 * @return bool
	 * @param string $timerHandle
	 */
	public function stopTimer ($timerHandle) {
		if ($this->config['filters']['timers']) {
			if (array_key_exists($timerHandle, $this->timers)) {
				$timerExists = TRUE;
				$timespan = $this->getMicrotime() - $this->timers[$timerHandle]['starttime'];
			
				$info = '<p class="timer"><strong>' . $this->timers[$timerHandle]['comment'];
				$info .= '</strong><br />The timer ran ';
				$info .= '<strong>' . number_format ($timespan, 4, '.', NULL) . '</strong>';
				$info .= ' seconds.' . $this->getTraceback() . '</p>';
			
				$this->sendCommand('write', $info);
			} else {
				$timerExists = FALSE;
			}
		} else {
			$timerExists = FALSE;
		}
		
		return $timerExists;
	}
	
	/**
	 * returns a formatted traceback string
	 *
	 * @return string
	 */
	public function getTraceback () {
		$callStack = debug_backtrace();

		$debugConsoleFiles = array(
			'debugConsole.class.php',
			'debugConsole.functions.php'
		);
		
		$call = array (
			'file' => 'debugConsole.class.php'
		);
		
		while(in_array(basename($call['file']), $debugConsoleFiles)) {
			$call = array_shift($callStack);
		}

		$call['file'] = $this->cropScriptPath($call['file']);
		
		$traceback = '<span class="backtrace">';
		$traceback .= $call['file'] . ' on line ';
		$traceback .= $call['line'] . '</span>';
		
		return $traceback;
	}
	
	/**
	 * crops long script path, shows only the last $maxLength chars
	 *
	 * @param string $path
	 * @param int $maxLength
	 * @return string
	 */
	protected function cropScriptPath ($path, $maxLength = 30) {
		if (strlen($path) > $maxLength) {
			$startPos = strlen($path) - $maxLength - 2;
			
			if ($startPos > 0) {
				$path = '...' . substr($path, $startPos);
			}
		}

		return $path;
	}
}
?>