Issues (10)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Docblock.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of the Docblock package.
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 *
7
 * @license MIT License
8
 */
9
10
namespace gossi\docblock;
11
12
use gossi\docblock\tags\AbstractTag;
13
use gossi\docblock\tags\TagFactory;
14
use InvalidArgumentException;
15
use LogicException;
16
use phootwork\collection\ArrayList;
17
use phootwork\collection\Map;
18
use phootwork\lang\Comparator;
19
use ReflectionClass;
20
use ReflectionFunctionAbstract;
21
use ReflectionProperty;
22
23
class Docblock implements \Stringable {
24
	protected string $shortDescription;
0 ignored issues
show
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
25
	protected string $longDescription;
26
	protected ArrayList $tags;
27
	protected ?Comparator $comparator = null;
28
29
	const REGEX_TAGNAME = '[\w\-\_\\\\]+';
30
31
	/**
32
	 * Static docblock factory
33
	 * 
34
	 * @param ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock a docblock to parse
35
	 *
36
	 * @return $this
37
	 */
38 1
	public static function create(ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock = ''): self {
39 1
		return new static($docblock);
40
	}
41
42
	/**
43
	 * Creates a new docblock instance and parses the initial string or reflector object if given
44
	 * 
45
	 * @param ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock a docblock to parse
46
	 */
47 13
	final public function __construct(ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock = '') {
48 13
		$this->tags = new ArrayList();
49 13
		$this->parse($docblock);
50 12
	}
51
52
	/**
53
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
54
	 *
55
	 * @param ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock
56
	 *
57
	 * @throws InvalidArgumentException if there is no getDocComment() method available
58
	 */
59 13
	protected function parse(ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock): void {
60 13
		$docblock = is_object($docblock) ? $docblock->getDocComment() : $docblock;
61 13
		$docblock = $this->cleanInput($docblock);
62
63 13
		[$short, $long, $tags] = $this->splitDocBlock($docblock);
64 13
		$this->shortDescription = $short;
65 13
		$this->longDescription = $long;
66 13
		$this->parseTags($tags);
67 12
	}
68
69
	/**
70
	 * Strips the asterisks from the DocBlock comment.
71
	 * 
72
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
73
	 *
74
	 * @param string $comment String containing the comment text.
75
	 *
76
	 * @return string
77
	 */
78 13
	protected function cleanInput(string $comment): string {
79 13
		$comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment));
80
81
		// reg ex above is not able to remove */ from a single line docblock
82 13
		if (substr($comment, -2) == '*/') {
83 1
			$comment = trim(substr($comment, 0, -2));
84
		}
85
86
		// normalize strings
87 13
		$comment = str_replace(["\r\n", "\r"], "\n", $comment);
88
89 13
		return $comment;
90
	}
91
92
	/**
93
	 * Splits the Docblock into a short description, long description and
94
	 * block of tags.
95
	 * 
96
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
97
	 *
98
	 * @param string $comment Comment to split into the sub-parts.
99
	 *
100
	 * @author RichardJ Special thanks to RichardJ for the regex responsible
101
	 *     for the split.
102
	 *
103
	 * @return string[] containing the short-, long description and an element
104
	 *     containing the tags.
105
	 */
106 12
	protected function splitDocBlock(string $comment): array {
107 12
		$matches = [];
108
109 12
		if (str_starts_with($comment, '@')) {
110 3
			$matches = ['', '', $comment];
111
		} else {
112
			// clears all extra horizontal whitespace from the line endings
113
			// to prevent parsing issues
114 9
			$comment = preg_replace('/\h*$/Sum', '', $comment);
115
116
			/*
117
			 * Splits the docblock into a short description, long description and
118
			 * tags section
119
			 * - The short description is started from the first character until
120
			 *   a dot is encountered followed by a newline OR
121
			 *   two consecutive newlines (horizontal whitespace is taken into
122
			 *   account to consider spacing errors)
123
			 * - The long description, any character until a new line is
124
			 *   encountered followed by an @ and word characters (a tag).
125
			 *   This is optional.
126
			 * - Tags; the remaining characters
127
			 *
128
			 * Big thanks to RichardJ for contributing this Regular Expression
129
			 */
130 9
			preg_match(
131 9
				'/
132
		        \A (
133
		          [^\n.]+
134
		          (?:
135
		            (?! \. \n | \n{2} ) # disallow the first seperator here
136
		            [\n.] (?! [ \t]* @\pL ) # disallow second seperator
137
		            [^\n.]+
138
		          )*
139
		          \.?
140
		        )
141
		        (?:
142
		          \s* # first seperator (actually newlines but it\'s all whitespace)
143
		          (?! @\pL ) # disallow the rest, to make sure this one doesn\'t match,
144
		          #if it doesn\'t exist
145
		          (
146
		            [^\n]+
147
		            (?: \n+
148
		              (?! [ \t]* @\pL ) # disallow second seperator (@param)
149
		              [^\n]+
150
		            )*
151
		          )
152
		        )?
153
		        (\s+ [\s\S]*)? # everything that follows
154
		        /ux',
155
				$comment,
156
				$matches
157
			);
158 9
			array_shift($matches);
159
		}
160
161 12
		while (count($matches) < 3) {
162 7
			$matches[] = '';
163
		}
164
165 12
		return $matches;
166
	}
167
168
	/**
169
	 * Parses the tags
170
	 * 
171
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
172
	 *
173
	 * @param string $tags
174
	 *
175
	 * @throws LogicException
176
	 * @throws InvalidArgumentException
177
	 */
178 13
	protected function parseTags(string $tags): void {
179 13
		$tags = trim($tags);
180 13
		if ($tags !== '') {
181
182
			// sanitize lines
183 6
			$result = [];
184 6
			foreach (explode("\n", $tags) as $line) {
185 6
				if ($this->isTagLine($line) || count($result) == 0) {
186 6
					$result[] = $line;
187 2
				} elseif ($line !== '') {
188 1
					$result[count($result) - 1] .= "\n" . $line;
189
				}
190
			}
191
192
			// create proper Tag objects
193 6
			if (count($result)) {
194 6
				$this->tags->clear();
195 6
				foreach ($result as $line) {
196 6
					$this->tags->add($this->parseTag($line));
197
				}
198
			}
199
		}
200 12
	}
201
202
	/**
203
	 * Checks whether the given line is a tag line (= starts with @) or not
204
	 * 
205
	 * @param string $line
206
	 *
207
	 * @return bool
208
	 */
209 6
	protected function isTagLine(string $line): bool {
210 6
		return str_starts_with($line, '@');
211
	}
212
213
	/**
214
	 * Parses an individual tag line
215
	 * 
216
	 * @param string $line
217
	 *
218
	 * @throws InvalidArgumentException
219
	 *
220
	 * @return AbstractTag
221
	 */
222 6
	protected function parseTag(string $line): AbstractTag {
223 6
		$matches = [];
224 6
		if (!preg_match('/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us', $line, $matches)) {
225 1
			throw new InvalidArgumentException('Invalid tag line detected: ' . $line);
226
		}
227
228 5
		$tagName = $matches[1];
229 5
		$content = $matches[2] ?? '';
230
231 5
		return TagFactory::create($tagName, $content);
232
	}
233
234
	/**
235
	 * Returns the short description
236
	 * 
237
	 * @return string the short description
238
	 */
239 3
	public function getShortDescription(): string {
240 3
		return $this->shortDescription;
241
	}
242
243
	/**
244
	 * Sets the short description
245
	 * 
246
	 * @param string $description the new description     
247
	 *
248
	 * @return $this   	
249
	 */
250 2
	public function setShortDescription(string $description = ''): self {
251 2
		$this->shortDescription = $description;
252
253 2
		return $this;
254
	}
255
256
	/**
257
	 * Returns the long description
258
	 *
259
	 * @return string the long description
260
	 */
261 2
	public function getLongDescription(): string {
262 2
		return $this->longDescription;
263
	}
264
265
	/**
266
	 * Sets the long description
267
	 * 
268
	 * @param string $description the new description
269
	 *
270
	 * @return $this
271
	 */
272 2
	public function setLongDescription(string $description = ''): self {
273 2
		$this->longDescription = $description;
274
275 2
		return $this;
276
	}
277
278
	/**
279
	 * Adds a tag to this docblock
280
	 * 
281
	 * @param AbstractTag $tag
282
	 *
283
	 * @return $this
284
	 */
285 4
	public function appendTag(AbstractTag $tag): self {
286 4
		$this->tags->add($tag);
287
288 4
		return $this;
289
	}
290
291
	/**
292
	 * Removes tags (by tag name)
293
	 *
294
	 * @param string $tagName
295
	 */
296 1
	public function removeTags(string $tagName = ''): void {
297 1
		$this->tags = $this->tags->filter(function (AbstractTag $tag) use ($tagName): bool {
298 1
			return $tagName !== $tag->getTagName();
299 1
		});
300 1
	}
301
302
	/**
303
	 * Checks whether a tag is present
304
	 * 
305
	 * @param string $tagName
306
	 *
307
	 * @return bool
308
	 */
309 2
	public function hasTag(string $tagName): bool {
310 2
		return $this->tags->search($tagName,
311 2
			fn (AbstractTag $tag, string $query): bool => $tag->getTagName() === $query
312 2
		);
313
	}
314
315
	/**
316
	 * Gets tags (by tag name)
317
	 * 
318
	 * @param string $tagName
319
	 *
320
	 * @return ArrayList the tags
321
	 */
322 3
	public function getTags(string $tagName = ''): ArrayList {
323 3
		return $tagName === '' ? $this->tags : $this->tags->filter(
324 1
			fn (AbstractTag $tag): bool => $tag->getTagName() === $tagName
325 3
		);
326
	}
327
328
	/**
329
	 * A list of tags sorted by tag-name
330
	 * 
331
	 * @return ArrayList
332
	 */
333 6
	public function getSortedTags(): ArrayList {
334 6
		$this->comparator = $this->comparator ?? new TagNameComparator();
335
336
		// 1) group by tag name
337 6
		$group = new Map();
338
		/** @var AbstractTag $tag */
339 6
		foreach ($this->tags->toArray() as $tag) {
340 5
			if (!$group->has($tag->getTagName())) {
341 5
				$group->set($tag->getTagName(), new ArrayList());
342
			}
343
344
			/** @var ArrayList $list */
345 5
			$list = $group->get($tag->getTagName());
346 5
			$list->add($tag);
347
		}
348
349
		// 2) Sort the group by tag name
350 6
		$group->sortKeys(new TagNameComparator());
351
352
		// 3) flatten the group
353 6
		$sorted = new ArrayList();
354
		/** @var array $tags */
355 6
		foreach ($group->values()->toArray() as $tags) {
356 5
			$sorted->add(...$tags);
357
		}
358
359 6
		return $sorted;
360
	}
361
362
	/**
363
	 * Returns true when there is no content in the docblock
364
	 *  
365
	 * @return bool
366
	 */
367 1
	public function isEmpty(): bool {
368 1
		return $this->shortDescription === ''
369 1
				&& $this->longDescription === ''
370 1
				&& $this->tags->size() === 0;
371
	}
372
373
	/**
374
	 * Returns the string version of the docblock
375
	 * 
376
	 * @return string
377
	 */
378 5
	public function toString(): string {
379 5
		$docblock = "/**\n";
380
381
		// short description
382 5
		$short = trim($this->shortDescription);
383 5
		if ($short !== '') {
384 3
			$docblock .= $this->writeLines(explode("\n", $short));
385
		}
386
387
		// short description
388 5
		$long = trim($this->longDescription);
389 5
		if ($long !== '') {
390 2
			$docblock .= $this->writeLines(explode("\n", $long), !empty($short));
391
		}
392
393
		// tags
394 5
		$tags = $this->getSortedTags()->map(function (AbstractTag $tag): string {
395 4
			return (string) $tag;
396 5
		});
397
398 5
		if (!$tags->isEmpty()) {
399
			/** @psalm-suppress MixedArgumentTypeCoercion */
400 4
			$docblock .= $this->writeLines($tags->toArray(), $short !== '' || $long !== '');
401
		}
402
403 5
		$docblock .= ' */';
404
405 5
		return $docblock;
406
	}
407
408
	/**
409
	 * Writes multiple lines with ' * ' prefixed for docblock
410
	 * 
411
	 * @param string[] $lines the lines to be written
412
	 * @param bool $newline if a new line should be added before
413
	 *
414
	 * @return string the lines as string
415
	 */
416 5
	protected function writeLines(array $lines, bool $newline = false): string {
417 5
		$docblock = '';
418 5
		if ($newline) {
419 3
			$docblock .= " *\n";
420
		}
421
422 5
		foreach ($lines as $line) {
423 5
			if (str_contains($line, "\n")) {
424 1
				$sublines = explode("\n", $line);
425 1
				$line = array_shift($sublines);
426 1
				$docblock .= " * $line\n";
427 1
				$docblock .= $this->writeLines($sublines);
428
			} else {
429 5
				$docblock .= " * $line\n";
430
			}
431
		}
432
433 5
		return $docblock;
434
	}
435
436
	/**
437
	 * Magic toString() method
438
	 * 
439
	 * @return string
440
	 */
441 1
	public function __toString(): string {
442 1
		return $this->toString();
443
	}
444
}
445