includes/debugger/debugConsole.class.php
changeset 1 fe660c52c48f
equal deleted inserted replaced
0:902822492a68 1:fe660c52c48f
       
     1 <?php
       
     2 /**
       
     3  * debugConsole class
       
     4  *
       
     5  * This class allows opening an external JavaScript
       
     6  * window for debugging purposes.
       
     7  *
       
     8  * @author Andreas Demmer <info@debugconsole.de>
       
     9  * @see <http://www.debugconsole.de>
       
    10  * @version 1.2.1
       
    11  * @package debugConsole_1.2.1
       
    12  */
       
    13 class debugConsole {
       
    14 	/**
       
    15 	 * events which are shown in debug console
       
    16 	 *
       
    17 	 * @var array
       
    18 	 */
       
    19 	protected $filters;
       
    20 	
       
    21 	/**
       
    22 	 * all watched variables with their current content
       
    23 	 *
       
    24 	 * @var array
       
    25 	 */
       
    26 	protected $watches;
       
    27 	
       
    28 	/**
       
    29 	 * debugConsole configuration values
       
    30 	 *
       
    31 	 * @var array
       
    32 	 */
       
    33 	protected $config;
       
    34 	
       
    35 	/**
       
    36 	 * URL where template can be found
       
    37 	 *
       
    38 	 * @var string
       
    39 	 */
       
    40 	protected $template;
       
    41 
       
    42 	/**
       
    43 	 * javascripts to control popup
       
    44 	 *
       
    45 	 * @var array
       
    46 	 */
       
    47 	protected $javascripts;
       
    48 
       
    49 	/**
       
    50 	 * html for popup
       
    51 	 *
       
    52 	 * @var array
       
    53 	 */
       
    54 	protected $html;
       
    55 
       
    56 	/**
       
    57 	 * time of debugrun start in milliseconds
       
    58 	 *
       
    59 	 * @var string
       
    60 	 */
       
    61 	protected $starttime;
       
    62 
       
    63 	/**
       
    64 	 * time of timer start in milliseconds
       
    65 	 *
       
    66 	 * @var array
       
    67 	 */
       
    68 	protected $timers;
       
    69 
       
    70 	/**
       
    71 	 * constructor, opens popup window
       
    72 	 */
       
    73 	public function __construct () {
       
    74 		/* initialize class vars */
       
    75 		$this->starttime = $this->getMicrotime();
       
    76 		$this->watches = array ();
       
    77 		$this->config = $GLOBALS['_debugConsoleConfig'];
       
    78 		$this->html = $this->config['html'];
       
    79 		$this->html['header'] = str_replace("\n\r", NULL, $this->html['header']);
       
    80 		$this->html['header'] = str_replace("\n", NULL, $this->html['header']);
       
    81 		$this->javascripts = $this->config['javascripts'];
       
    82 		
       
    83 		/* replace PHP's errorhandler */
       
    84 		$errorhandler = array (
       
    85 			$this,
       
    86 			'errorHandlerCallback'
       
    87 		);
       
    88 		
       
    89 		set_error_handler($errorhandler);
       
    90 		
       
    91 		/* open popup */
       
    92 		$popupOptions = "', 'debugConsole', 'width=" . $this->config['dimensions']['width'] . ",height=" . $this->config['dimensions']['height'] . ',scrollbars=yes';
       
    93 		
       
    94 		$this->sendCommand('openPopup', $popupOptions);
       
    95 		$this->sendCommand('write', $this->html['header']);
       
    96 		
       
    97 		$this->startDebugRun();
       
    98 	}
       
    99 	
       
   100 	/**
       
   101 	 * destructor, shows runtime and finishes html document in popup window
       
   102 	 */
       
   103 	public function __destruct () {
       
   104 		$runtime = $this->getMicrotime() - $this->starttime;
       
   105 		$runtime = number_format((float)$runtime, 4, '.', NULL);
       
   106 		
       
   107 		$info = '<p class="runtime">This debug-run took ' . $runtime . ' seconds to complete.</p>';
       
   108 
       
   109 		$this->sendCommand('write', $info);
       
   110 		$this->sendCommand('write', '</div>');
       
   111 		$this->sendCommand('scroll', "0','100000");
       
   112 		$this->sendCommand('write', $this->html['footer']);
       
   113 		
       
   114 		if ($this->config['focus']) {
       
   115 			$this->sendCommand('focus');
       
   116 		}
       
   117 	}
       
   118 	
       
   119 	/**
       
   120 	 * show new debug run header in console
       
   121 	 */
       
   122 	
       
   123 	protected function startDebugRun () {
       
   124 		$info = '<h1>new debug-run (' . date('H:i') . ' hours)</h1>';
       
   125 		$this->sendCommand('write', '<div>');
       
   126 		$this->sendCommand('write', $info);
       
   127 	}
       
   128 
       
   129 	/**
       
   130 	 * adds a variable to the watchlist
       
   131 	 * 
       
   132 	 * Watched variables must be in a declare(ticks=n)
       
   133 	 * block so that every n ticks the watched variables
       
   134 	 * are checked for changes. If any changes were made,
       
   135 	 * the new value of the variable is shown in the
       
   136 	 * debugConsole with additional information where the
       
   137 	 * changes happened.
       
   138 	 *
       
   139 	 * @param string $variableName
       
   140 	 */
       
   141 	public function watchVariable ($variableName) {
       
   142 		if (count($this->watches) === 0) {
       
   143 			$watchMethod = array (
       
   144 				$this,
       
   145 				'watchesCallback'
       
   146 			);
       
   147 			
       
   148 			register_tick_function($watchMethod);
       
   149 		}
       
   150 		
       
   151 		if (isset($GLOBALS[$variableName])) {
       
   152 			$this->watches[$variableName] = $GLOBALS[$variableName];
       
   153 		} else {
       
   154 			$this->watches[$variableName] = NULL;
       
   155 		}
       
   156 	}
       
   157 	
       
   158 	/**
       
   159 	 * tick callback: process watches and show changes
       
   160 	 */
       
   161 	public function watchesCallback () {
       
   162 		if ($this->config['filters']['watches']) {
       
   163 			foreach ($this->watches as $variableName => $variableValue) {
       
   164 				if ($GLOBALS[$variableName] !== $this->watches[$variableName]) {
       
   165 					$info = '<p class="watch"><strong>$' . $variableName;
       
   166 					$info .= '</strong> changed from "';
       
   167 					$info .= $this->watches[$variableName];
       
   168 					$info .= '" (' . gettype($this->watches[$variableName]) . ')';
       
   169 					$info .= ' to "' . $GLOBALS[$variableName] . '" (';
       
   170 					$info .= gettype($GLOBALS[$variableName]) . ')';
       
   171 					$info .= $this->getTraceback() . '</p>';
       
   172 					
       
   173 					$this->watches[$variableName] = $GLOBALS[$variableName];
       
   174 					$this->sendCommand('write', $info);
       
   175 				}
       
   176 			}
       
   177 		}
       
   178 	}
       
   179 	
       
   180 	/**
       
   181 	 * sends a javascript command to browser
       
   182 	 *
       
   183 	 * @param string $command
       
   184 	 * @param string $value
       
   185 	 */
       
   186 	protected function sendCommand ($command, $value = FALSE) {
       
   187     if($command == 'write') $value = '\'+unescape(\''.rawurlencode($value).'\')+\'';
       
   188 		$value = str_replace('\\', '\\\\', $value);
       
   189     $value = nl2br($value);
       
   190 		
       
   191 		if ((bool)$value) { 
       
   192 			/* write optionally logfile */
       
   193 			$this->writeLogfileEntry($command, $value);
       
   194 			
       
   195 			$command = $this->javascripts[$command] . "('" . $value . "');";
       
   196 		} else {
       
   197 			$command = $this->javascripts[$command] . ';';
       
   198 		}
       
   199 		
       
   200 		$command = str_replace("\n\r", NULL, $command);
       
   201 		$command = str_replace("\n", NULL, $command);
       
   202 		
       
   203 		if (!$this->config['logfile']['disablePopup']) {
       
   204 			echo $this->javascripts['openTag'], "\n";
       
   205 			echo $command, "\n";
       
   206 			echo $this->javascripts['closeTag'], "\n";
       
   207 		}
       
   208 		
       
   209 		flush();
       
   210 	}
       
   211 	
       
   212 	/**
       
   213 	 * writes html output as text entry into logfile
       
   214 	 *
       
   215 	 * @param string $command
       
   216 	 * @param string $value
       
   217 	 */
       
   218 	protected function writeLogfileEntry ($command, $value) {
       
   219 		if ($this->config['logfile']['enable']) {
       
   220 			$logfile = $this->config['logfile']['path'] . $this->config['logfile']['filename'];
       
   221 			/* log only useful entries, no html header and footer */
       
   222 			if (
       
   223 				$command === 'write'
       
   224 				&& !strpos($value, '<html>')
       
   225 				&&  !strpos($value, '</html>')
       
   226 			) {
       
   227 				/* convert html to text */
       
   228 				$value = html_entity_decode($value);
       
   229 				$value = str_replace('>', '> ', $value);
       
   230 				$value = strip_tags($value);
       
   231 				
       
   232 				$fp = fopen($logfile, 'a+');
       
   233 				fputs($fp, $value . "\n\n");
       
   234 				fclose($fp);
       
   235 			} elseif (strpos($value, '</html>')) {
       
   236 				$fp = fopen($logfile, 'a+');
       
   237 				fputs($fp, "-----------\n");
       
   238 				fclose($fp);
       
   239 			}
       
   240 		}
       
   241 	}
       
   242 
       
   243 	/**
       
   244 	 * shows in console that a checkpoint has been passed,
       
   245 	 * additional info is the file and line which triggered
       
   246 	 * the output
       
   247 	 *
       
   248 	 * @param string $message
       
   249 	 */	
       
   250 	public function passedCheckpoint ($message = NULL) {
       
   251 		if ($this->config['filters']['checkpoints']) {
       
   252 			$message = (bool)$message ? $message : 'Checkpoint passed!';
       
   253 	
       
   254 			$info = '<p class="checkpoint"><strong>' . $message . '</strong>';
       
   255 			$info .= $this->getTraceback() . '</p>';
       
   256 			
       
   257 			$this->sendCommand('write', $info);
       
   258 		}
       
   259 	}
       
   260 	
       
   261 	/**
       
   262 	 * returns microtime as float value
       
   263 	 *
       
   264 	 * @return float
       
   265 	 */
       
   266 	protected function getMicrotime () {
       
   267 		list($usec, $sec) = explode(' ', microtime()); 
       
   268     	return ((float)$usec + (float)$sec);
       
   269 	}
       
   270 	
       
   271 	/**
       
   272 	 * returns all possible filter events for debugConsole::setFilter() method
       
   273 	 *
       
   274 	 * @return array
       
   275 	 */
       
   276 	public function getFilters () {
       
   277 		$filters = array_keys($this->config['filters']);
       
   278 		
       
   279 		ksort($filters);
       
   280 		reset($filters);
       
   281 		
       
   282 		return $filters; 
       
   283 	}
       
   284 	
       
   285 	/**
       
   286 	 * shows or hides an event-type in debugConsole,
       
   287 	 * returns previous setting of the given event-type
       
   288 	 *
       
   289 	 * @param string $event
       
   290 	 * @param bool $isShown
       
   291 	 * @return bool
       
   292 	 */
       
   293 	public function setFilter ($event, $isShown) {
       
   294 		if (array_key_exists($event, $this->config['filters'])) {
       
   295 			$oldValue = $this->config['filters'][$event];
       
   296 			$this->config['filters'][$event] = $isShown;
       
   297 		} else {
       
   298 			throw new Exception ('debugConsole: unknown event "' . $event . '" in debugConsole::filter()');
       
   299 		}
       
   300 		
       
   301 		return $oldValue;
       
   302 	}
       
   303 	
       
   304 	/**
       
   305 	 * show debug info for variable in debugConsole,
       
   306 	 * added by custom text for documentation and hints
       
   307 	 *
       
   308 	 * @param mixed $variable
       
   309 	 * @param string $text
       
   310 	 */
       
   311 	public function dump ($variable, $text) {
       
   312 		if ($this->config['filters']['debug']) {
       
   313 			@ob_start();
       
   314 			
       
   315 			/* grab current ob content */
       
   316 			$obContents = ob_get_contents();
       
   317 			ob_clean();
       
   318 			
       
   319 			/* grap var dump from ob */
       
   320 			var_dump($variable);
       
   321 			$variableDebug = ob_get_contents();
       
   322 			ob_end_clean();
       
   323 			
       
   324 			/* restore previous ob content */
       
   325 			if ((bool)$obContents) echo $obContents;
       
   326 			
       
   327 			/* render debug */
       
   328 			$variableDebug = htmlspecialchars($variableDebug);			
       
   329 			$infos = '<p class="dump">' . $text . '<br />';
       
   330 			
       
   331 			if (is_array($variable)) {
       
   332 				$variableDebug = str_replace(' ', '&nbsp;', $variableDebug);
       
   333 				$infos .= '<span class="source">' . $variableDebug . '</span>';
       
   334 			} else {
       
   335 				$infos .= '<strong>' . $variableDebug . '</strong>';
       
   336 			}
       
   337 			
       
   338 			$infos .= $this->getTraceback() . '</p>';
       
   339 			$this->sendCommand('write', $infos);
       
   340 		}
       
   341 	}
       
   342 	
       
   343 	/**
       
   344 	 * callback method for PHP errorhandling
       
   345 	 * 
       
   346 	 * @todo implement more errorlevels
       
   347 	 */
       
   348 	public function errorHandlerCallback () {
       
   349 		$details = func_get_args();
       
   350 		$details[1] = str_replace("'", '"', $details[1]);
       
   351 		$details[1] = str_replace('href="function.', 'target="_blank" href="http://www.php.net/', $details[1]);
       
   352 		
       
   353 		
       
   354 		/* determine error level */
       
   355 		switch ($details[0]) {
       
   356 			case 2:
       
   357 				if (!$this->config['filters']['php_warnings']) return;
       
   358 				$errorlevel = 'warning';
       
   359 				break;
       
   360 			case 8:
       
   361 				if (!$this->config['filters']['php_notices']) return;
       
   362 				$errorlevel = 'notice';
       
   363 				break;
       
   364 			case 2048:
       
   365 				if (!$this->config['filters']['php_suggestions']) return;
       
   366 				$errorlevel = 'suggestion';
       
   367 				break;
       
   368 		}
       
   369 
       
   370 		$file = $this->cropScriptPath($details[2]);
       
   371 		
       
   372 		$infos = '<p class="' . $errorlevel . '"><strong>';
       
   373 		$infos .= 'PHP ' . strtoupper($errorlevel) . '</strong>';
       
   374 		$infos .= $details[1] . '<span class="backtrace">';
       
   375 		$infos .= $file . ' on line ';
       
   376 		$infos .= $details[3] . '</span></p>';		
       
   377 		
       
   378 		$this->sendCommand('write', $infos);
       
   379 	}
       
   380 	
       
   381 	/**
       
   382 	 * start timer clock, returns timer handle
       
   383 	 * 
       
   384 	 * @return mixed
       
   385 	 * @param string $comment
       
   386 	 */
       
   387 	public function startTimer ($comment) {
       
   388 		if ($this->config['filters']['timers']) {
       
   389 			$timerHandle = md5(microtime());
       
   390 			
       
   391 			$this->timers[$timerHandle] = array (
       
   392 				'starttime' => $this->getMicrotime(),
       
   393 				'comment' => $comment
       
   394 			);
       
   395 		} else {		
       
   396 			$timerHandle = FALSE;		
       
   397 		}
       
   398 		
       
   399 		return $timerHandle;
       
   400 	}
       
   401 	
       
   402 	/**
       
   403 	 * stop timer clock
       
   404 	 * 
       
   405 	 * @return bool
       
   406 	 * @param string $timerHandle
       
   407 	 */
       
   408 	public function stopTimer ($timerHandle) {
       
   409 		if ($this->config['filters']['timers']) {
       
   410 			if (array_key_exists($timerHandle, $this->timers)) {
       
   411 				$timerExists = TRUE;
       
   412 				$timespan = $this->getMicrotime() - $this->timers[$timerHandle]['starttime'];
       
   413 			
       
   414 				$info = '<p class="timer"><strong>' . $this->timers[$timerHandle]['comment'];
       
   415 				$info .= '</strong><br />The timer ran ';
       
   416 				$info .= '<strong>' . number_format ($timespan, 4, '.', NULL) . '</strong>';
       
   417 				$info .= ' seconds.' . $this->getTraceback() . '</p>';
       
   418 			
       
   419 				$this->sendCommand('write', $info);
       
   420 			} else {
       
   421 				$timerExists = FALSE;
       
   422 			}
       
   423 		} else {
       
   424 			$timerExists = FALSE;
       
   425 		}
       
   426 		
       
   427 		return $timerExists;
       
   428 	}
       
   429 	
       
   430 	/**
       
   431 	 * returns a formatted traceback string
       
   432 	 *
       
   433 	 * @return string
       
   434 	 */
       
   435 	public function getTraceback () {
       
   436 		$callStack = debug_backtrace();
       
   437 
       
   438 		$debugConsoleFiles = array(
       
   439 			'debugConsole.class.php',
       
   440 			'debugConsole.functions.php'
       
   441 		);
       
   442 		
       
   443 		$call = array (
       
   444 			'file' => 'debugConsole.class.php'
       
   445 		);
       
   446 		
       
   447 		while(in_array(basename($call['file']), $debugConsoleFiles)) {
       
   448 			$call = array_shift($callStack);
       
   449 		}
       
   450 
       
   451 		$call['file'] = $this->cropScriptPath($call['file']);
       
   452 		
       
   453 		$traceback = '<span class="backtrace">';
       
   454 		$traceback .= $call['file'] . ' on line ';
       
   455 		$traceback .= $call['line'] . '</span>';
       
   456 		
       
   457 		return $traceback;
       
   458 	}
       
   459 	
       
   460 	/**
       
   461 	 * crops long script path, shows only the last $maxLength chars
       
   462 	 *
       
   463 	 * @param string $path
       
   464 	 * @param int $maxLength
       
   465 	 * @return string
       
   466 	 */
       
   467 	protected function cropScriptPath ($path, $maxLength = 30) {
       
   468 		if (strlen($path) > $maxLength) {
       
   469 			$startPos = strlen($path) - $maxLength - 2;
       
   470 			
       
   471 			if ($startPos > 0) {
       
   472 				$path = '...' . substr($path, $startPos);
       
   473 			}
       
   474 		}
       
   475 
       
   476 		return $path;
       
   477 	}
       
   478 }
       
   479 ?>