Newer
Older
framework / system / Debug / Toolbar.php
@Jim Parry Jim Parry on 1 Dec 2018 13 KB Release 4.0.0-alpha.3
<?php namespace CodeIgniter\Debug;

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2018 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2018 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

use Config\App;
use Config\Services;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Format\XMLFormatter;

/**
 * Debug Toolbar
 *
 * Displays a toolbar with bits of stats to aid a developer in debugging.
 *
 * Inspiration: http://prophiler.fabfuel.de
 *
 * @package CodeIgniter\Debug
 */
class Toolbar
{

	/**
	 * Collectors to be used and displayed.
	 *
	 * @var array
	 */
	protected $collectors = [];

	/**
	 * Incoming Request
	 *
	 * @var \CodeIgniter\HTTP\IncomingRequest
	 */
	protected static $request;

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * @param BaseConfig $config
	 */
	public function __construct(BaseConfig $config)
	{
		foreach ($config->toolbarCollectors as $collector)
		{
			if (! class_exists($collector))
			{
				log_message('critical', 'Toolbar collector does not exists(' . $collector . ').' .
										'please check $toolbarCollectors in the Config\App.php file.');
				continue;
			}

			$this->collectors[] = new $collector();
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Returns all the data required by Debug Bar
	 *
	 * @param float                               $startTime App start time
	 * @param float                               $totalTime
	 * @param \CodeIgniter\HTTP\RequestInterface  $request
	 * @param \CodeIgniter\HTTP\ResponseInterface $response
	 *
	 * @return string JSON encoded data
	 */
	public function run($startTime, $totalTime, $request, $response): string
	{
		// Data items used within the view.
		$data['url']             = current_url();
		$data['method']          = $request->getMethod(true);
		$data['isAJAX']          = $request->isAJAX();
		$data['startTime']       = $startTime;
		$data['totalTime']       = $totalTime * 1000;
		$data['totalMemory']     = number_format((memory_get_peak_usage()) / 1024 / 1024, 3);
		$data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7, 5);
		$data['segmentCount']    = (int)ceil($data['totalTime'] / $data['segmentDuration']);
		$data['CI_VERSION']      = \CodeIgniter\CodeIgniter::CI_VERSION;
		$data['collectors']      = [];

		foreach ($this->collectors as $collector)
		{
			$data['collectors'][] = [
				'title'           => $collector->getTitle(),
				'titleSafe'       => $collector->getTitle(true),
				'titleDetails'    => $collector->getTitleDetails(),
				'display'         => $collector->display(),
				'badgeValue'      => $collector->getBadgeValue(),
				'isEmpty'         => $collector->isEmpty(),
				'hasTabContent'   => $collector->hasTabContent(),
				'hasLabel'        => $collector->hasLabel(),
				'icon'            => $collector->icon(),
				'hasTimelineData' => $collector->hasTimelineData(),
				'timelineData'    => $collector->timelineData(),
			];
		}

		foreach ($this->collectVarData() as $heading => $items)
		{
			$vardata = [];

			if (is_array($items))
			{
				foreach ($items as $key => $value)
				{
					$vardata[esc($key)] = is_string($value) ? esc($value) : print_r($value, true);
				}
			}

			$data['vars']['varData'][esc($heading)] = $vardata;
		}

		if (! empty($_SESSION))
		{
			foreach ($_SESSION as $key => $value)
			{
				$data['vars']['session'][esc($key)] = is_string($value) ? esc($value) : print_r($value, true);
			}
		}

		foreach ($request->getGet() as $name => $value)
		{
			$data['vars']['get'][esc($name)] = is_array($value) ? esc(print_r($value, true)) : esc($value);
		}

		foreach ($request->getPost() as $name => $value)
		{
			$data['vars']['post'][esc($name)] = is_array($value) ? esc(print_r($value, true)) : esc($value);
		}

		foreach ($request->getHeaders() as $header => $value)
		{
			if (empty($value))
			{
				continue;
			}

			if (! is_array($value))
			{
				$value = [$value];
			}

			foreach ($value as $h)
			{
				$data['vars']['headers'][esc($h->getName())] = esc($h->getValueLine());
			}
		}

		foreach ($request->getCookie() as $name => $value)
		{
			$data['vars']['cookies'][esc($name)] = esc($value);
		}

		$data['vars']['request'] = ($request->isSecure() ? 'HTTPS' : 'HTTP') . '/' . $request->getProtocolVersion();

		$data['vars']['response'] = [
			'statusCode'  => $response->getStatusCode(),
			'reason'      => esc($response->getReason()),
			'contentType' => esc($response->getHeaderLine('content-type')),
		];

		$data['config'] = \CodeIgniter\Debug\Toolbar\Collectors\Config::display();

		if ($response->CSP !== null)
		{
			$response->CSP->addImageSrc( 'data:' );
		}

		return json_encode($data);
	}

	//--------------------------------------------------------------------

	/**
	 * Format output
	 *
	 * @param string $data   JSON encoded Toolbar data
	 * @param string $format html, json, xml
	 *
	 * @return string
	 */
	protected static function format(string $data, string $format = 'html')
	{
		$data = json_decode($data, true);

		// History must be loaded on the fly
		$filenames = glob(WRITEPATH . 'debugbar/debugbar_*');
		$total     = count($filenames);
		rsort($filenames);

		$files = [];

		$current = static::$request->getGet('debugbar_time');
		$app     = config(App::class);

		for ($i = 0; $i < $total; $i++)
		{
						// Oldest files will be deleted
			if ($app->toolbarMaxHistory >= 0 && $i + 1 > $app->toolbarMaxHistory)
			{
				unlink($filenames[$i]);
				continue;
			}

			// Get the contents of this specific history request
			ob_start();
			include($filenames[$i]);
			$contents = ob_get_contents();
			ob_end_clean();

			$file = json_decode($contents, true);

			// Debugbar files shown in History Collector
			$files[] = [
				'time'        => (int)$time = substr($filenames[$i], -10),
				'datetime'    => date('Y-m-d H:i:s', $time),
				'active'      => (int)($time === $current),
				'status'      => $file['vars']['response']['statusCode'],
				'method'      => $file['method'],
				'url'         => $file['url'],
				'isAJAX'      => $file['isAJAX'] ? 'Yes' : 'No',
				'contentType' => $file['vars']['response']['contentType'],
			];
		}

		// Set the History here. Class is not necessary
		$data['collectors'][]           = [
			'title'           => 'History',
			'titleSafe'       => 'history',
			'titleDetails'    => '',
			'display'         => ['files' => $files],
			'badgeValue'      => $count = count($files),
			'isEmpty'         => ! (bool)$count,
			'hasTabContent'   => true,
			'hasLabel'        => true,
			'icon'            => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJySURBVEhL3ZU7aJNhGIVTpV6i4qCIgkIHxcXLErS4FBwUFNwiCKGhuTYJGaIgnRoo4qRu6iCiiIuIXXTTIkIpuqoFwaGgonUQlC5KafU5ycmNP0lTdPLA4fu+8573/a4/f6hXpFKpwUwmc9fDfweKbk+n07fgEv33TLSbtt/hvwNFT1PsG/zdTE0Gp+GFfD6/2fbVIxqNrqPIRbjg4t/hY8aztcngfDabHXbKyiiXy2vcrcPH8oDCry2FKDrA+Ar6L01E/ypyXzXaARjDGGcoeNxSDZXE0dHRA5VRE5LJ5CFy5jzJuOX2wHRHRnjbklZ6isQ3tIctBaAd4vlK3jLtkOVWqABBXd47jGHLmjTmSScttQV5J+SjfcUweFQEbsjAas5aqoCLXutJl7vtQsAzpRowYqkBinyCC8Vicb2lOih8zoldd0F8RD7qTFiqAnGrAy8stUAvi/hbqDM+YzkAFrLPdR5ZqoLXsd+Bh5YCIH7JniVdquUWxOPxDfboHhrI5XJ7HHhiqQXox+APe/Qk64+gGYVCYZs8cMpSFQj9JOoFzVqqo7k4HIvFYpscCoAjOmLffUsNUGRaQUwDlmofUa34ecsdgXdcXo4wbakBgiUFafXJV8A4DJ/2UrxUKm3E95H8RbjLcgOJRGILhnmCP+FBy5XvwN2uIPcy1AJvWgqC4xm2aU4Xb3lF4I+Tpyf8hRe5w3J7YLymSeA8Z3nSclv4WLRyFdfOjzrUFX0klJUEtZtntCNc+F69cz/FiDzEPtjzmcUMOr83kDQEX6pAJxJfpL3OX22n01YN7SZCoQnaSdoZ+Jz+PZihH3wt/xlCoT9M6nEtmRSPCQAAAABJRU5ErkJggg==',
			'hasTimelineData' => false,
			'timelineData'    => [],
		];

		$output = '';

		switch ($format)
		{
			case 'html':
				$data['styles'] = [];
				extract($data);
				$parser = Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null, false);
				ob_start();
				include(__DIR__ . '/Toolbar/Views/toolbar.tpl.php');
				$output = ob_get_contents();
				ob_end_clean();
				break;
			case 'json':
				$output = json_encode($data);
				break;
			case 'xml':
				$formatter = new XMLFormatter;
				$output    = $formatter->format($data);
				break;
		}

		return $output;
	}

	//--------------------------------------------------------------------

	/**
	 * Called within the view to display the timeline itself.
	 *
	 * @param array   $collectors
	 * @param float   $startTime
	 * @param integer $segmentCount
	 * @param integer $segmentDuration
	 *
	 * @return string
	 */
	protected static function renderTimeline(array $collectors, $startTime, int $segmentCount, int $segmentDuration, array& $styles): string
	{
		$displayTime = $segmentCount * $segmentDuration;
		$rows        = static::collectTimelineData($collectors);
		$output      = '';
		$styleCount  = 0;

		foreach ($rows as $row)
		{
			$output .= '<tr>';
			$output .= "<td>{$row['name']}</td>";
			$output .= "<td>{$row['component']}</td>";
			$output .= "<td class='debug-bar-alignRight'>" . number_format($row['duration'] * 1000, 2) . ' ms</td>';
			$output .= "<td class='debug-bar-noverflow' colspan='{$segmentCount}'>";

			$offset = ((($row['start'] - $startTime) * 1000) / $displayTime) * 100;
			$length = (($row['duration'] * 1000) / $displayTime) * 100;

			$styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;";
			$output                                     .= "<span class='timer debug-bar-timeline-{$styleCount}' title='" . number_format($length,
					2) . "%'></span>";
			$output                                     .= '</td>';
			$output                                     .= '</tr>';

			$styleCount++;
		}

		return $output;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns a sorted array of timeline data arrays from the collectors.
	 *
	 * @return array
	 */
	protected static function collectTimelineData($collectors): array
	{
		$data = [];

		// Collect it
		foreach ($collectors as $collector)
		{
			if (! $collector['hasTimelineData'])
			{
				continue;
			}

			$data = array_merge($data, $collector['timelineData']);
		}

		// Sort it

		return $data;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns an array of data from all of the modules
	 * that should be displayed in the 'Vars' tab.
	 *
	 * @return array
	 */
	protected function collectVarData()// : array
	{
		$data = [];

		foreach ($this->collectors as $collector)
		{
			if (! $collector->hasVarData())
			{
				continue;
			}

			$data = array_merge($data, $collector->getVarData());
		}

		return $data;
	}

	//--------------------------------------------------------------------

	/**
	 * Rounds a number to the nearest incremental value.
	 *
	 * @param float   $number
	 * @param integer $increments
	 *
	 * @return float
	 */
	protected function roundTo($number, $increments = 5)
	{
		$increments = 1 / $increments;

		return (ceil($number * $increments) / $increments);
	}

	//--------------------------------------------------------------------

	/**
	 *
	 */
	public static function eventHandler()
	{
		static::$request = Services::request();

		if (ENVIRONMENT === 'testing')
		{
			return;
		}

		// If the request contains '?debugbar then we're
		// simply returning the loading script
		if (static::$request->getGet('debugbar') !== null)
		{
			// Let the browser know that we are sending javascript
			header('Content-Type: application/javascript');

			ob_start();
			include(BASEPATH . 'Debug/Toolbar/toolbarloader.js.php');
			$output = ob_get_contents();
			@ob_end_clean();

			exit($output);
		}

		// Otherwise, if it includes ?debugbar_time, then
		// we should return the entire debugbar.
		if (static::$request->getGet('debugbar_time'))
		{
			helper('security');

			// Negotiate the content-type to format the output
			$format = static::$request->negotiate('media', [
				'text/html',
				'application/json',
				'application/xml',
			]);
			$format = explode('/', $format)[1];

			$file     = sanitize_filename('debugbar_' . static::$request->getGet('debugbar_time'));
			$filename = WRITEPATH . 'debugbar/' . $file;

			// Show the toolbar
			if (is_file($filename))
			{
				$contents = static::format(file_get_contents($filename), $format);
				exit($contents);
			}

			// File was not written or do not exists
			http_response_code(404);
			exit(); // Exit here is needed to avoid load the index page
		}
	}
}