Issues (387)

Branch: develop

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/phpDocumentor/Compiler/Linker/Linker.php (4 issues)

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
2
declare(strict_types=1);
3
4
/**
5
 * This file is part of phpDocumentor.
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @author    Mike van Riel <[email protected]>
11
 * @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com)
12
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
13
 * @link      http://phpdoc.org
14
 */
15
16
namespace phpDocumentor\Compiler\Linker;
17
18
use phpDocumentor\Compiler\CompilerPassInterface;
19
use phpDocumentor\Descriptor\ClassDescriptor;
20
use phpDocumentor\Descriptor\DescriptorAbstract;
21
use phpDocumentor\Descriptor\FileDescriptor;
22
use phpDocumentor\Descriptor\InterfaceDescriptor;
23
use phpDocumentor\Descriptor\Interfaces\ProjectInterface;
24
use phpDocumentor\Descriptor\NamespaceDescriptor;
25
use phpDocumentor\Descriptor\ProjectDescriptor;
26
use phpDocumentor\Descriptor\TraitDescriptor;
27
use phpDocumentor\Descriptor\Type\UnknownTypeDescriptor;
28
use Traversable;
29
30
/**
31
 * The linker contains all rules to replace FQSENs in the ProjectDescriptor with aliases to objects.
32
 *
33
 * This object contains a list of class FQCNs for Descriptors and their associated linker rules.
34
 *
35
 * An example scenario should be:
36
 *
37
 *     The Descriptor ``\phpDocumentor\Descriptor\ClassDescriptor`` has a *Substitute* rule determining that the
38
 *     contents of the ``Parent`` field should be substituted with another ClassDescriptor with the FQCN
39
 *     represented by the value of the Parent field. In addition (second element) it has an *Analyse* rule
40
 *     specifying that the contents of the ``Methods`` field should be interpreted by the linker. Because that field
41
 *     contains an array or Descriptor Collection will each element be analysed by the linker.
42
 *
43
 * As can be seen in the above example is it possible to analyse a tree structure and substitute FQSENs where
44
 * encountered.
45
 */
46
class Linker implements CompilerPassInterface
47
{
48
    const COMPILER_PRIORITY = 10000;
49
50
    const CONTEXT_MARKER = '@context';
51
52
    /** @var DescriptorAbstract[] */
53
    protected $elementList = [];
54
55
    /** @var string[][] */
56
    protected $substitutions = [];
57
58
    /** @var string[] Prevent cycles by tracking which objects have been analyzed */
59
    protected $processedObjects = [];
60
61 1
    public function getDescription(): string
62
    {
63 1
        return 'Replace textual FQCNs with object aliases';
64
    }
65
66
    /**
67
     * Initializes the linker with a series of Descriptors to link to.
68
     *
69
     * @param string[][] $substitutions
70
     */
71 4
    public function __construct(array $substitutions)
72
    {
73 4
        $this->substitutions = $substitutions;
74 4
    }
75
76 1
    public function execute(ProjectDescriptor $project): void
77
    {
78 1
        $this->setObjectAliasesList($project->getIndexes()->elements->getAll());
0 ignored issues
show
The property elements does not exist on object<phpDocumentor\Descriptor\Collection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
79 1
        $this->substitute($project);
80 1
    }
81
82
    /**
83
     * Returns the list of substitutions for the linker.
84
     *
85
     * @return string[][]
86
     */
87 1
    public function getSubstitutions(): array
88
    {
89 1
        return $this->substitutions;
90
    }
91
92
    /**
93
     * Sets the list of object aliases to resolve the FQSENs with.
94
     *
95
     * @param DescriptorAbstract[] $elementList
96
     */
97 3
    public function setObjectAliasesList(array $elementList): void
98
    {
99 3
        $this->elementList = $elementList;
100 3
    }
101
102
    /**
103
     * Substitutes the given item or its children's FQCN with an object alias.
104
     *
105
     * This method may do either of the following depending on the item's type
106
     *
107
     * String
108
     *     If the given item is a string then this method will attempt to find an appropriate Class, Interface or
109
     *     TraitDescriptor object and return that. See {@see findAlias()} for more information on the normalization
110
     *     of these strings.
111
     *
112
     * Array or Traversable
113
     *     Iterate through each item, pass each key's contents to a new call to substitute and replace the key's
114
     *     contents if the contents is not an object (objects automatically update and saves performance).
115
     *
116
     * Object
117
     *     Determines all eligible substitutions using the substitutions property, construct a getter and retrieve
118
     *     the field's contents. Pass these contents to a new call of substitute and use a setter to replace the field's
119
     *     contents if anything other than null is returned.
120
     *
121
     * This method will return null if no substitution was possible and all of the above should not update the parent
122
     * item when null is passed.
123
     *
124
     * @param string|object|Traversable|array $item
125
     * @param DescriptorAbstract|null         $container A descriptor that acts as container for all elements
126
     *                                        underneath or null if there is no current container.
127
     *
128
     * @return null|string|DescriptorAbstract
129
     */
130 7
    public function substitute($item, $container = null)
131
    {
132 7
        $result = null;
133
134 7
        if (is_string($item)) {
135 5
            $result = $this->findAlias($item, $container);
136 7
        } elseif (is_array($item) || ($item instanceof Traversable && ! $item instanceof ProjectInterface)) {
137 2
            $isModified = false;
138 2
            foreach ($item as $key => $element) {
139 2
                $isModified = true;
140
141 2
                $element = $this->substitute($element, $container);
142 2
                if ($element !== null) {
143 2
                    $item[$key] = $element;
144
                }
145
            }
146
147 2
            if ($isModified) {
148 2
                $result = $item;
149
            }
150 6
        } elseif (is_object($item) && $item instanceof UnknownTypeDescriptor) {
151 1
            $alias = $this->findAlias($item->getName());
152 1
            $result = $alias ?: $item;
153 5
        } elseif (is_object($item)) {
154 5
            $hash = spl_object_hash($item);
155 5
            if (isset($this->processedObjects[$hash])) {
156
                // if analyzed; just return
157 1
                return null;
158
            }
159
160 5
            $newContainer = ($this->isDescriptorContainer($item)) ? $item : $container;
161
162 5
            $this->processedObjects[$hash] = $hash;
163
164 5
            $objectClassName = get_class($item);
165 5
            $fieldNames = $this->substitutions[$objectClassName] ?? [];
166
167 5
            foreach ($fieldNames as $fieldName) {
168 4
                $fieldValue = $this->findFieldValue($item, $fieldName);
169 4
                $response = $this->substitute($fieldValue, $newContainer);
0 ignored issues
show
It seems like $newContainer defined by $this->isDescriptorConta...m) ? $item : $container on line 160 can also be of type object; however, phpDocumentor\Compiler\Linker\Linker::substitute() does only seem to accept object<phpDocumentor\Des...escriptorAbstract>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
170
171
                // if the returned response is not an object it must be grafted on the calling object
172 4
                if ($response !== null) {
173 4
                    $setter = 'set' . ucfirst($fieldName);
174 4
                    $item->{$setter}($response);
175
                }
176
            }
177
        }
178
179 7
        return $result;
180
    }
181
182
    /**
183
     * Attempts to find a Descriptor object alias with the FQSEN of the element it represents.
184
     *
185
     * This method will try to fetch an element after normalizing the provided FQSEN. The FQSEN may contain references
186
     * (bindings) that can only be resolved during linking (such as `self`) or it may contain a context marker
187
     * {@see CONTEXT_MARKER}.
188
     *
189
     * If there is a context marker then this method will see if a child of the given container exists that matches the
190
     * element following the marker. If such a child does not exist in the current container then the namespace is
191
     * queried if a child exists there that matches.
192
     *
193
     * For example:
194
     *
195
     *     Given the Fqsen `@context::myFunction()` and the lastContainer `\My\Class` will this method first check
196
     *     to see if `\My\Class::myFunction()` exists; if it doesn't it will then check if `\My\myFunction()` exists.
197
     *
198
     * If neither element exists then this method assumes it is an undocumented class/trait/interface and change the
199
     * given FQSEN by returning the namespaced element name (thus in the example above that would be
200
     * `\My\myFunction()`). The calling method {@see substitute()} will then replace the value of the field containing
201
     * the context marker with this normalized string.
202
     *
203
     * @param DescriptorAbstract|null $container
204
     *
205
     * @return DescriptorAbstract|string|null
206
     */
207 5
    public function findAlias(string $fqsen, $container = null)
208
    {
209 5
        $fqsen = $this->replacePseudoTypes($fqsen, $container);
210
211 5
        if ($this->isContextMarkerInFqsen($fqsen) && $container instanceof DescriptorAbstract) {
212
            // first exchange `@context::element` for `\My\Class::element` and if it exists, return that
213 4
            $classMember = $this->fetchElementByFqsen($this->getTypeWithClassAsContext($fqsen, $container));
214 4
            if ($classMember) {
215 1
                return $classMember;
216
            }
217
218
            // otherwise exchange `@context::element` for `\My\element` and if it exists, return that
219 3
            $namespaceContext = $this->getTypeWithNamespaceAsContext($fqsen, $container);
220 3
            $namespaceMember = $this->fetchElementByFqsen($namespaceContext);
0 ignored issues
show
Are you sure the assignment to $namespaceMember is correct as $this->fetchElementByFqsen($namespaceContext) (which targets phpDocumentor\Compiler\L...::fetchElementByFqsen()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
221 3
            if ($namespaceMember) {
222 1
                return $namespaceMember;
223
            }
224
225
            // otherwise check if the element exists in the global namespace and if it exists, return that
226 2
            $globalNamespaceContext = $this->getTypeWithGlobalNamespaceAsContext($fqsen);
227 2
            $globalNamespaceMember = $this->fetchElementByFqsen($globalNamespaceContext);
0 ignored issues
show
Are you sure the assignment to $globalNamespaceMember is correct as $this->fetchElementByFqs...globalNamespaceContext) (which targets phpDocumentor\Compiler\L...::fetchElementByFqsen()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
228 2
            if ($globalNamespaceMember) {
229 1
                return $globalNamespaceMember;
230
            }
231
232
            // Otherwise we assume it is an undocumented class/interface/trait and return `\My\element` so
233
            // that the name containing the marker may be replaced by the class reference as string
234 1
            return $namespaceContext;
235
        }
236
237 1
        return $this->fetchElementByFqsen($fqsen);
238
    }
239
240
    /**
241
     * Returns the value of a field in the given object.
242
     *
243
     * @param object $object
244
     * @return string|object
245
     */
246 1
    public function findFieldValue($object, string $fieldName)
247
    {
248 1
        $getter = 'get' . ucfirst($fieldName);
249
250 1
        return $object->{$getter}();
251
    }
252
253
    /**
254
     * Returns true if the given Descriptor is a container type.
255
     *
256
     * @param DescriptorAbstract|mixed $item
257
     */
258 1
    protected function isDescriptorContainer($item): bool
259
    {
260 1
        return $item instanceof FileDescriptor
261 1
            || $item instanceof NamespaceDescriptor
262 1
            || $item instanceof ClassDescriptor
263 1
            || $item instanceof TraitDescriptor
264 1
            || $item instanceof InterfaceDescriptor;
265
    }
266
267
    /**
268
     * Replaces pseudo-types, such as `self`, into a normalized version based on the last container that was
269
     * encountered.
270
     *
271
     * @param DescriptorAbstract|null $container
272
     */
273 2
    protected function replacePseudoTypes(string $fqsen, $container): string
274
    {
275 2
        $pseudoTypes = ['self', '$this'];
276 2
        foreach ($pseudoTypes as $pseudoType) {
277 2
            if ((strpos($fqsen, $pseudoType . '::') === 0 || $fqsen === $pseudoType) && $container) {
278 2
                $fqsen = $container->getFullyQualifiedStructuralElementName()
279 2
                    . substr($fqsen, strlen($pseudoType));
280
            }
281
        }
282
283 2
        return $fqsen;
284
    }
285
286
    /**
287
     * Returns true if the context marker is found in the given FQSEN.
288
     */
289 4
    protected function isContextMarkerInFqsen(string $fqsen): bool
290
    {
291 4
        return strpos($fqsen, self::CONTEXT_MARKER) !== false;
292
    }
293
294
    /**
295
     * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent.
296
     */
297 2
    protected function getTypeWithClassAsContext(string $fqsen, DescriptorAbstract $container): string
298
    {
299 2
        if (!$container instanceof ClassDescriptor
300 2
            && !$container instanceof InterfaceDescriptor
301 2
            && !$container instanceof TraitDescriptor
302
        ) {
303 1
            return $fqsen;
304
        }
305
306 1
        $containerFqsen = $container->getFullyQualifiedStructuralElementName();
307
308 1
        return str_replace(self::CONTEXT_MARKER . '::', $containerFqsen . '::', $fqsen);
309
    }
310
311
    /**
312
     * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent.
313
     */
314 2
    protected function getTypeWithNamespaceAsContext(string $fqsen, DescriptorAbstract $container): string
315
    {
316 2
        $namespace = $container instanceof NamespaceDescriptor ? $container : $container->getNamespace();
317 2
        $fqnn = $namespace instanceof NamespaceDescriptor
318 1
            ? $namespace->getFullyQualifiedStructuralElementName()
319 2
            : $namespace;
320
321 2
        return str_replace(self::CONTEXT_MARKER . '::', $fqnn . '\\', $fqsen);
322
    }
323
324
    /**
325
     * Normalizes the given FQSEN as if the context marker represents the global namespace as parent.
326
     */
327 2
    protected function getTypeWithGlobalNamespaceAsContext(string $fqsen): string
328
    {
329 2
        return str_replace(self::CONTEXT_MARKER . '::', '\\', $fqsen);
330
    }
331
332
    /**
333
     * Attempts to find an element with the given Fqsen in the list of elements for this project and returns null if
334
     * it cannot find it.
335
     *
336
     * @return DescriptorAbstract|null
337
     */
338 4
    protected function fetchElementByFqsen(string $fqsen)
339
    {
340 4
        return $this->elementList[$fqsen] ?? null;
341
    }
342
}
343