Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
DeveloperToolbar
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
10 / 10
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addJsonToolbar
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
12
 addHtmlToolbar
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getResponseCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExitCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setResponseCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExitCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Miniframe\Toolbar\Response;
4
5use Miniframe\Core\Response;
6use Miniframe\Response\InternalServerErrorResponse;
7use Miniframe\Response\PhpResponse;
8use Miniframe\Toolbar\Service\DeveloperToolbar as DeveloperToolbarService;
9
10class DeveloperToolbar extends Response
11{
12    /**
13     * The parent response.
14     *
15     * @var Response
16     */
17    private $parentResponse;
18    /**
19     * URL for this debugging session.
20     *
21     * @var string
22     */
23    private $debugUrl;
24
25    /**
26     * Response that extends another response and adds the development toolbar to the output HTML
27     *
28     * @param Response $parentResponse The parent response.
29     * @param string   $debugUrl       URL for this debugging session.
30     */
31    public function __construct(Response $parentResponse, string $debugUrl)
32    {
33        $this->parentResponse = $parentResponse;
34        $this->debugUrl = $debugUrl;
35    }
36
37    /**
38     * Returns the response text
39     *
40     * @return string
41     */
42    public function render(): string
43    {
44        return $this->addJsonToolbar($this->addHtmlToolbar($this->parentResponse->render()));
45    }
46
47    /**
48     * Replaces the toolbar in existing json code
49     *
50     * @param string $jsonData The json code.
51     *
52     * @return string
53     */
54    private function addJsonToolbar(string $jsonData): string
55    {
56        // Validate if we're dealing with json data
57        if (
58            substr($jsonData, 0, 1) !== '{'
59            || substr($jsonData, -1) !== '}'
60            || !is_array(json_decode($jsonData, true, 255, JSON_INVALID_UTF8_IGNORE))
61        ) {
62            return $jsonData;
63        }
64        $service = DeveloperToolbarService::getInstance();
65
66        // Detect how the json is pretty-printed
67        preg_match('/^\{([\r\n]*)([\s]*)/', $jsonData, $matches);
68        $eol = $matches[1] ?? '';
69        $ws = $matches[2] ?? '';
70        $parts = preg_split('/[\s]*\}$/', $jsonData);
71
72        // Start adding a debugging element
73        $jsonData = $parts[0] . ',' . $eol;
74        $jsonData .= $ws . '"_debugging_' . array_reverse(explode('/', $this->debugUrl))[0] . '": {' . $eol;
75        $jsonData .= $ws . $ws . '"debug_url": "' . addslashes($this->debugUrl) . '"';
76
77        // Adding dumps
78        if (count($service->getDumps())) {
79            $jsonData .= ',' . $eol . $ws . $ws . '"dumps": [';
80            foreach ($service->getDumps() as $iterator => $dump) {
81                if ($iterator !== 0) {
82                    $jsonData .= ',';
83                }
84                $jsonData .= $eol . $ws . $ws . $ws . '{' . $eol;
85                $jsonData .= $ws . $ws . $ws . $ws . '"file": ' . json_encode($dump['file']) . ',' . $eol;
86                $jsonData .= $ws . $ws . $ws . $ws . '"line": ' . intval($dump['line']) . ',' . $eol;
87                $jsonData .= $ws . $ws . $ws . $ws . '"data": ' . json_encode($dump['data']) . $eol;
88                $jsonData .= $ws . $ws . $ws . '}';
89            }
90            $jsonData .= $eol . $ws . $ws . ']';
91        }
92
93        // Adding errors
94        if (count($service->getErrors())) {
95            $jsonData .= ',' . $eol . $ws . $ws . '"errors": [';
96            foreach ($service->getErrors() as $iterator => $error) {
97                if ($iterator !== 0) {
98                    $jsonData .= ',';
99                }
100                $jsonData .= $eol . $ws . $ws . $ws . '{' . $eol;
101                $jsonData .= $ws . $ws . $ws . $ws . '"errtype": "' . addslashes($error['errtype']) . '",' . $eol;
102                $jsonData .= $ws . $ws . $ws . $ws . '"file": "' . addslashes($error['file']) . '",' . $eol;
103                $jsonData .= $ws . $ws . $ws . $ws . '"line": ' . intval($error['line']) . ',' . $eol;
104                $jsonData .= $ws . $ws . $ws . $ws . '"message": ' . json_encode($error['message']) . $eol;
105                $jsonData .= $ws . $ws . $ws . '}';
106            }
107            $jsonData .= $eol . $ws . $ws . ']';
108        }
109
110        // Add backtrace if available
111        if ($this->parentResponse instanceof InternalServerErrorResponse || $this->parentResponse->getPrevious()) {
112            $exception = $this->parentResponse->getPrevious() ?? $this->parentResponse;
113            $jsonData .= ',' . $eol . $ws . $ws . '"exception": ' . json_encode(get_class($exception));
114            $jsonData .= ',' . $eol . $ws . $ws . '"message": ' . json_encode($exception->getMessage());
115            $jsonData .= ',' . $eol . $ws . $ws . '"code": ' . $exception->getCode();
116            $jsonData .= ',' . $eol . $ws . $ws . '"file": ' . json_encode($exception->getFile());
117            $jsonData .= ',' . $eol . $ws . $ws . '"line": ' . $exception->getLine();
118            $jsonData .= ',' . $eol . $ws . $ws . '"backtrace": ' . json_encode($exception->getTraceAsString());
119        }
120
121        // Close debugging element and json
122        $jsonData .= $eol . $ws . '}' . $eol . '}';
123
124        return $jsonData;
125    }
126
127    /**
128     * Replaces the toolbar in existing HTML code
129     *
130     * @param string $htmlData The HTML code.
131     *
132     * @return string
133     */
134    private function addHtmlToolbar(string $htmlData): string
135    {
136        // Look for HTML data, that contains a body
137        if (!preg_match('/<html.*>.*?<\/body>/is', $htmlData)) {
138            return $htmlData;
139        }
140
141        // Renders the toolbar HTML
142        $insertResponse = new PhpResponse(__DIR__ . '/../../templates/toolbar_insert.html.php', [
143            'debugUrl' => $this->debugUrl,
144            'bugiconUrl' => substr($this->debugUrl, 0, strrpos($this->debugUrl, '/')) . '/bugicon.svg',
145        ]);
146        $html = $insertResponse->render();
147
148        return preg_replace('/<\/body>/i', $html . '$0', $htmlData, 1);
149    }
150
151    /**
152     * Returns the current response code (used for HTTP responses)
153     *
154     * @return integer
155     */
156    public function getResponseCode(): int
157    {
158        return $this->parentResponse->getResponseCode();
159    }
160
161    /**
162     * Returns the current exit code (used for shell error codes)
163     *
164     * @return integer
165     */
166    public function getExitCode(): int
167    {
168        return $this->parentResponse->getExitCode();
169    }
170
171    /**
172     * Sets a new response code (used for HTTP responses)
173     *
174     * @param integer $responseCode The new response code.
175     *
176     * @return void
177     */
178    public function setResponseCode(int $responseCode): void
179    {
180        $this->parentResponse->setResponseCode($responseCode);
181    }
182
183    /**
184     * Adds a response header
185     *
186     * @param string  $header  The header string.
187     * @param boolean $replace When set to false, a header can be sent back multiple times.
188     *
189     * @return void
190     */
191    public function addHeader(string $header, bool $replace = true): void
192    {
193        $this->parentResponse->addHeader($header, $replace);
194    }
195
196    /**
197     * Sets a new exit code (used for shell error codes)
198     *
199     * @param integer $exitCode Error code.
200     *
201     * @return void
202     */
203    public function setExitCode(int $exitCode): void
204    {
205        $this->parentResponse->setExitCode($exitCode);
206    }
207
208    /**
209     * Returns a list of all headers to set (each record has two keys: 'header' and 'replace')
210     *
211     * @return array[]
212     */
213    public function getHeaders(): array
214    {
215        return $this->parentResponse->getHeaders();
216    }
217}