ViewReview::mapPhpColors()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
1
<?php
2
3
/**
4
 * View Review
5
 *
6
 * PHP Version 5.3.0
7
 *
8
 * Copyright (c) 2007-2010, Mayflower GmbH
9
 * All rights reserved.
10
 *
11
 * Redistribution and use in source and binary forms, with or without
12
 * modification, are permitted provided that the following conditions
13
 * are met:
14
 *
15
 *   * Redistributions of source code must retain the above copyright
16
 *     notice, this list of conditions and the following disclaimer.
17
 *
18
 *   * Redistributions in binary form must reproduce the above copyright
19
 *     notice, this list of conditions and the following disclaimer in
20
 *     the documentation and/or other materials provided with the
21
 *     distribution.
22
 *
23
 *   * Neither the name of Mayflower GmbH nor the names of his
24
 *     contributors may be used to endorse or promote products derived
25
 *     from this software without specific prior written permission.
26
 *
27
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
30
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
31
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
32
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
33
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
34
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
36
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
37
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38
 * POSSIBILITY OF SUCH DAMAGE.
39
 *
40
 * @category PHP_CodeBrowser
41
 *
42
 * @author Elger Thiele <[email protected]>
43
 * @author Jan Mergler <[email protected]>
44
 * @author Simon Kohlmeyer <[email protected]>
45
 *
46
 * @copyright 2007-2010 Mayflower GmbH
47
 *
48
 * @license http://www.opensource.org/licenses/bsd-license.php  BSD License
49
 *
50
 * @version SVN: $Id$
51
 *
52
 * @link http://www.phpunit.de/
53
 *
54
 * @since File available since  0.1.0
55
 */
56
57
namespace PHPCodeBrowser\View;
58
59
use DOMDocument;
60
use DOMElement;
61
use DOMText;
62
use DOMXPath;
63
use PHPCodeBrowser\Helper\IOHelper;
64
65
/**
66
 * ViewReview
67
 *
68
 * This class is generating the highlighted and formatted html view for file.
69
 *
70
 * @category PHP_CodeBrowser
71
 *
72
 * @author Elger Thiele <[email protected]>
73
 * @author Jan Mergler <[email protected]>
74
 * @author Simon Kohlmeyer <[email protected]>
75
 *
76
 * @copyright 2007-2010 Mayflower GmbH
77
 *
78
 * @license http://www.opensource.org/licenses/bsd-license.php  BSD License
79
 *
80
 * @version Release: @package_version@
81
 *
82
 * @link http://www.phpunit.de/
83
 *
84
 * @since Class available since  0.1.0
85
 */
86
class ViewReview extends ViewAbstract
87
{
88
    /**
89
     * Highlight mapping.
90
     *
91
     * @var array
92
     */
93
    protected $phpHighlightColorMap;
94
95
    /**
96
     * Suffixes for php files.
97
     *
98
     * @var array
99
     */
100
    protected $phpSuffixes;
101
102
    /**
103
     * Default constructor
104
     *
105
     * Highlighting strings are set.
106
     *
107
     * @param string   $templateDir The directory containing the templates.
108
     * @param string   $outputDir   The directory where the reviews should be.
109
     * @param IOHelper $ioHelper    The IOHelper object to use for I/O.
110
     * @param array    $phpSuffixes The array with extensions of php files.
111
     */
112
    public function __construct(string $templateDir, string $outputDir, IOHelper $ioHelper, array $phpSuffixes = ['php'])
113
    {
114
        parent::__construct($templateDir, $outputDir, $ioHelper);
115
116
        $this->phpHighlightColorMap = [
117
            \ini_get('highlight.string')  => 'string',
118
            \ini_get('highlight.comment') => 'comment',
119
            \ini_get('highlight.keyword') => 'keyword',
120
            \ini_get('highlight.default') => 'default',
121
            \ini_get('highlight.html')    => 'html',
122
        ];
123
124
        $this->phpSuffixes = $phpSuffixes;
125
    }
126
127
    /**
128
     * Generating the Html code browser view for a given file.
129
     *
130
     * Issue list for each file will be marked in source code.
131
     * Source code is highlighted.
132
     * Generated Html source code is be saved as Html.
133
     *
134
     * @param array  $issueList        The issue list for given file
135
     * @param string $fileName
136
     * @param string $commonPathPrefix The prefix path all given files have
137
     *                                 in common
138
     * @param bool   $excludeOK
139
     *
140
     * @return void
141
     *
142
     * @see self::_formatIssues
143
     * @see self::_formatSourceCode
144
     * @see self::_generateJSCode
145
     */
146
    public function generate(array $issueList, string $fileName, string $commonPathPrefix, bool $excludeOK = false): void
147
    {
148
        $issues        = $this->formatIssues($issueList);
149
        $shortFilename = \substr($fileName, \strlen($commonPathPrefix));
150
151
        $data = [];
152
153
        $data['issues']   = $issueList;
154
        $data['filepath'] = $shortFilename;
155
        $data['source']   = $this->formatSourceCode($fileName, $issues);
156
157
        $depth           = \substr_count($shortFilename, DIRECTORY_SEPARATOR);
158
        $data['csspath'] = \str_repeat('../', \max($depth - 1, 0));
159
160
        //we want to exclude files without issues and there are no issues in this one
161
        if ($excludeOK && !$data['issues']) {
162
            return;
163
        }
164
165
        $this->ioHelper->createFile(
166
            $this->outputDir.$shortFilename.'.html',
167
            $this->render('review', $data)
168
        );
169
    }
170
171
    /**
172
     * Highlighter method for PHP source code
173
     *
174
     * The source code is highlighted by PHP native method.
175
     * Afterwords a DOMDocument will be generated with each
176
     * line in a separate node.
177
     *
178
     * @param string $sourceCode The PHP source code
179
     *
180
     * @return DOMDocument
181
     */
182
    protected function highlightPhpCode(string $sourceCode): DOMDocument
183
    {
184
        $code = \highlight_string($sourceCode, true);
185
186
        if (\extension_loaded('mbstring') && !\mb_check_encoding($code, 'UTF-8')) {
0 ignored issues
show
Bug introduced by
It seems like $code can also be of type true; however, parameter $value of mb_check_encoding() does only seem to accept array|null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

186
        if (\extension_loaded('mbstring') && !\mb_check_encoding(/** @scrutinizer ignore-type */ $code, 'UTF-8')) {
Loading history...
187
            $detectOrder   = (array) \mb_detect_order();
188
            $detectOrder[] = 'iso-8859-1';
189
190
            $encoding = \mb_detect_encoding($code, $detectOrder, true);
0 ignored issues
show
Bug introduced by
It seems like $code can also be of type true; however, parameter $string of mb_detect_encoding() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
            $encoding = \mb_detect_encoding(/** @scrutinizer ignore-type */ $code, $detectOrder, true);
Loading history...
191
192
            if (false === $encoding) {
193
                \error_log('Error detecting file encoding');
194
            }
195
196
            $code = \mb_convert_encoding(
197
                $code,
0 ignored issues
show
Bug introduced by
It seems like $code can also be of type true; however, parameter $string of mb_convert_encoding() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

197
                /** @scrutinizer ignore-type */ $code,
Loading history...
198
                'UTF-8',
199
                $encoding
200
            );
201
        }
202
203
        $sourceDom = new DOMDocument();
204
        $sourceDom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'.$code);
0 ignored issues
show
Bug introduced by
Are you sure $code of type array|string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
        $sourceDom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'./** @scrutinizer ignore-type */ $code);
Loading history...
205
206
        //fetch <code>-><span>->children from php generated html
207
        $sourceElements = $sourceDom->getElementsByTagName('code')->item(0)
208
            ->childNodes->item(0)->childNodes;
209
210
        //create target dom
211
        $targetDom  = new DOMDocument();
212
        $targetNode = $targetDom->createElement('ol');
213
        $targetNode->setAttribute('class', 'code');
214
        $targetDom->appendChild($targetNode);
215
216
        $liElement = $targetDom->createElement('li');
217
        $targetNode->appendChild($liElement);
218
219
        // iterate through all <span> elements
220
        foreach ($sourceElements as $sourceElement) {
221
            if ($sourceElement instanceof DOMText) {
222
                $span            = $targetDom->createElement('span');
223
                $span->nodeValue = \htmlspecialchars($sourceElement->wholeText, ENT_COMPAT);
224
                $liElement->appendChild($span);
225
            }
226
227
            if (!$sourceElement instanceof DOMElement) {
228
                continue;
229
            }
230
231
            if ('br' === $sourceElement->tagName) {
232
                // create new li and new line
233
                $liElement = $targetDom->createElement('li');
234
                $targetNode->appendChild($liElement);
235
236
                continue;
237
            }
238
239
            $elementClass = $this->mapPhpColors(
240
                $sourceElement->getAttribute('style')
241
            );
242
243
            foreach ($sourceElement->childNodes as $sourceChildElement) {
244
                if ($sourceChildElement instanceof DOMElement
245
                    && 'br' === $sourceChildElement->tagName
246
                ) {
247
                    // create new li and new line
248
                    $liElement = $targetDom->createElement('li');
249
                    $targetNode->appendChild($liElement);
250
                } else {
251
                    // append content to current li element
252
                    $span            = $targetDom->createElement('span');
253
                    $span->nodeValue = \htmlspecialchars($sourceChildElement->textContent, ENT_COMPAT);
254
                    $span->setAttribute('class', $elementClass);
255
                    $liElement->appendChild($span);
256
                }
257
            }
258
        }
259
260
        return $targetDom;
261
    }
262
263
    /**
264
     * Return colors defined in ini files.
265
     *
266
     * @param string $style The given style name, e.g. "comment"
267
     *
268
     * @return string
269
     */
270
    protected function mapPhpColors(string $style): string
271
    {
272
        $color = \substr($style, 7);
273
274
        return $this->phpHighlightColorMap[$color];
275
    }
276
277
    /**
278
     * Highlighting source code of given file.
279
     *
280
     * Php code is using native php highlighter.
281
     * If PEAR Text_Highlighter is installed all defined files in $highlightMap
282
     * will be highlighted as well.
283
     *
284
     * @param string $file The filename / real path to file
285
     *
286
     * @return DOMDocument Html representation of parsed source code
287
     */
288
    protected function highlightCode(string $file): DOMDocument
289
    {
290
        $sourceCode = $this->ioHelper->loadFile($file);
291
        $extension  = \pathinfo($file, PATHINFO_EXTENSION);
292
293
        if (\in_array($extension, $this->phpSuffixes, true)) {
294
            return $this->highlightPhpCode($sourceCode);
295
        }
296
297
        $sourceCode = \preg_replace(['/^.*$/m', '/ /'], ['<li>$0</li>', '&nbsp;'], \htmlentities($sourceCode, ENT_COMPAT));
298
        $sourceCode = '<div class="code"><ol class="code">'.$sourceCode.'</ol></div>';
299
        $sourceCode = $this->stripInvalidXml($sourceCode);
300
301
        $doc = new DOMDocument();
302
        $doc->loadHTML($sourceCode);
303
304
        return $doc;
305
    }
306
307
    /**
308
     * Source code is highlighted an formatted.
309
     *
310
     * Besides highlighting, whole lines will be marked with different colors
311
     * and JQuery functions (like tooltips) are integrated.
312
     *
313
     * @param string $filename     The file to format
314
     * @param array  $outputIssues Sorted issueList by line number
315
     *
316
     * @return string Html formatted string
317
     */
318
    private function formatSourceCode(string $filename, array $outputIssues): string
319
    {
320
        $sourceDom = $this->highlightCode($filename);
321
        $xpath     = new DOMXPath($sourceDom);
322
        $lines     = $xpath->query('//ol/li');
323
324
        // A shortcut to prevent possible trouble with log(0)
325
        // Note that this is exactly what will happen anyways.
326
        if ($lines->length === 0) {
327
            return $sourceDom->saveHTML();
328
        }
329
330
        $lineNumber = 0;
331
        $linePlaces = \floor(\log($lines->length, 10)) + 1;
332
333
        foreach ($lines as $line) {
334
            /**
335
             * @var DOMElement $line
336
             */
337
            $line = $line;
338
            ++$lineNumber;
339
            $line->setAttribute('id', 'line_'.$lineNumber);
340
341
            $lineClasses = [
342
                ($lineNumber % 2 === 0) ? 'odd' : 'even',
343
            ];
344
345
            if (isset($outputIssues[$lineNumber])) {
346
                $lineClasses[] = 'hasIssues';
347
                $message       = '|';
348
349
                foreach ($outputIssues[$lineNumber] as $issue) {
350
                    $message .= \sprintf(
351
                        '
352
                        <div class="tooltip">
353
                            <div class="title %s">%s</div>
354
                            <div class="text">%s</div>
355
                        </div>
356
                        ',
357
                        $issue->getFoundBy(),
358
                        $issue->getFoundBy(),
359
                        $issue->getDescription()
360
                    );
361
                }
362
363
                $line->setAttribute('title', $message);
364
            }
365
366
            // Add line number
367
            $nuSpan = $sourceDom->createElement('span');
368
            $nuSpan->setAttribute('class', 'lineNumber');
369
370
            for ($i = 0; $i < $linePlaces - \strlen((string) $lineNumber); ++$i) {
371
                $nuSpan->appendChild($sourceDom->createEntityReference('nbsp'));
372
            }
373
374
            $nuSpan->appendChild($sourceDom->createTextNode((string) $lineNumber));
375
            $nuSpan->appendChild($sourceDom->createEntityReference('nbsp'));
376
            $line->insertBefore($nuSpan, $line->firstChild);
377
378
            //create anchor for the new line
379
            $anchor = $sourceDom->createElement('a');
380
            $anchor->setAttribute('name', 'line_'.$lineNumber);
381
            $line->appendChild($anchor);
382
383
            $lineErrorCount = (isset($outputIssues[$lineNumber])
384
                               ? \count($outputIssues[$lineNumber])
385
                               : 0);
386
387
            // set li css class depending on line errors
388
            switch ($lineErrorCount) {
389
                case 0:
390
                    break;
391
                case 1:
392
                    $lineClasses[] = $outputIssues[$lineNumber][0]->getFoundBy();
393
394
                    break;
395
                case 1 < $lineErrorCount:
396
                    $lineClasses[] = 'moreErrors';
397
398
                    break;
399
400
                // This can't happen, count always returns >= 0
401
                // @codeCoverageIgnoreStart
402
                default:
403
                    break;
404
405
                // @codeCoverageIgnoreEnd
406
            }
407
408
            $line->setAttribute('class', \implode(' ', $lineClasses));
409
        }
410
411
        return $sourceDom->saveHTML();
412
    }
413
414
    /**
415
     * Sorting a list of issues combining issues matching same line number
416
     * for each file.
417
     *
418
     * @param array $issueList List of issues
419
     *
420
     * @return array
421
     */
422
    private function formatIssues(array $issueList): array
423
    {
424
        $outputIssues = [];
425
426
        foreach ($issueList as $issue) {
427
            for ($i = $issue->getLineStart(); $i <= $issue->getLineEnd(); ++$i) {
428
                $outputIssues[$i][] = $issue;
429
            }
430
        }
431
432
        return $outputIssues;
433
    }
434
435
    /**
436
     * Removes invalid XML
437
     *
438
     * @access private
439
     *
440
     * @param string $value
441
     *
442
     * @return string
443
     */
444
    private function stripInvalidXml(string $value): string
445
    {
446
        $ret = '';
447
448
        if (empty($value)) {
449
            return $ret;
450
        }
451
452
        $length = \strlen($value);
453
454
        for ($i = 0; $i < $length; ++$i) {
455
            $current = \ord($value[$i]);
456
457
            if ((0x9 === $current)
458
                || (0xA === $current)
459
                || (0xD === $current)
460
                || (($current >= 0x20) && ($current <= 0xD7FF))
461
                || (($current >= 0xE000) && ($current <= 0xFFFD))
462
                || (($current >= 0x10000) && ($current <= 0x10FFFF))
463
            ) {
464
                $ret .= \chr($current);
465
            } else {
466
                $ret .= ' ';
467
            }
468
        }
469
470
        return $ret;
471
    }
472
}
473