Issues (45)

Security Analysis    not enabled

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/Compiler.php (7 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
3
declare(strict_types=1);
4
5
namespace BEAR\Package;
6
7
use BEAR\AppMeta\AbstractAppMeta;
8
use BEAR\AppMeta\Meta;
9
use BEAR\Package\Provide\Error\NullPage;
10
use BEAR\Resource\Exception\ParameterException;
11
use BEAR\Resource\NamedParameterInterface;
12
use BEAR\Resource\Uri;
13
use BEAR\Sunday\Extension\Application\AppInterface;
14
use Composer\Autoload\ClassLoader;
15
use Doctrine\Common\Annotations\Reader;
16
use Ray\Di\AbstractModule;
17
use Ray\Di\Exception\Unbound;
18
use Ray\Di\InjectorInterface;
19
use Ray\ObjectGrapher\ObjectGrapher;
20
use ReflectionClass;
21
use RuntimeException;
22
use Throwable;
23
24
use function array_keys;
25
use function assert;
26
use function class_exists;
27
use function count;
28
use function file_exists;
29
use function file_put_contents;
30
use function get_class;
31
use function in_array;
32
use function interface_exists;
33
use function is_callable;
34
use function is_float;
35
use function is_int;
36
use function memory_get_peak_usage;
37
use function microtime;
38
use function number_format;
39
use function preg_quote;
40
use function preg_replace;
41
use function printf;
42
use function property_exists;
43
use function realpath;
44
use function sort;
45
use function spl_autoload_register;
46
use function sprintf;
47
use function strpos;
48
use function substr;
49
use function trait_exists;
50
51
use const PHP_EOL;
52
53
final class Compiler
54
{
55
    /** @var string[] */
56
    private $classes = [];
57
58
    /** @var InjectorInterface */
59
    private $injector;
60
61
    /** @var string */
62
    private $context;
63
64
    /** @var string */
65
    private $appDir;
66
67
    /** @var Meta */
68
    private $appMeta;
69
70
    /** @var array<int, string> */
71
    private $compiled = [];
72
73
    /** @var array<string, string> */
74
    private $failed = [];
75
76
    /** @var list<string> */
77
    private $overwritten = [];
78
79
    /**
80
     * @param string $appName application name "MyVendor|MyProject"
81
     * @param string $context application context "prod-app"
82
     * @param string $appDir  application path
83
     */
84
    public function __construct(string $appName, string $context, string $appDir)
85
    {
86
        $this->registerLoader($appDir);
87
        $this->hookNullObjectClass($appDir);
88
        $this->context = $context;
89
        $this->appDir = $appDir;
90
        $this->appMeta = new Meta($appName, $context, $appDir);
91
        /** @psalm-suppress MixedAssignment */
92
        $this->injector = Injector::getInstance($appName, $context, $appDir);
93
    }
94
95
    /**
96
     * Compile application
97
     *
98
     * @return 0|1 exit code
0 ignored issues
show
The doc-type 0|1 could not be parsed: Unknown type name "0" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
99
     */
100
    public function compile(): int
101
    {
102
        $preload = $this->compilePreload($this->appMeta, $this->context);
103
        $module = (new Module())($this->appMeta, $this->context);
104
        $this->compileSrc($module);
105
        echo PHP_EOL;
106
        $this->compileDiScripts($this->appMeta);
107
        $dot = $this->failed ? '' : $this->compileObjectGraphDotFile($module);
108
        $start = $_SERVER['REQUEST_TIME_FLOAT'];
109
        assert(is_float($start));
110
        $time = number_format(microtime(true) - $start, 2);
111
        $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3);
112
        echo PHP_EOL;
113
        printf("Compilation (1/2) took %f seconds and used %fMB of memory\n", $time, $memory);
114
        printf("Success: %d Failed: %d\n", count($this->compiled), count($this->failed));
115
        printf("preload.php: %s\n", $this->getFileInfo($preload));
116
        printf("module.dot: %s\n", $dot ? $this->getFileInfo($dot) : 'n/a');
117
118
        foreach ($this->failed as $depedencyIndex => $error) {
119
            printf("UNBOUND: %s for %s \n", $error, $depedencyIndex);
120
        }
121
122
        return $this->failed ? 1 : 0;
123
    }
124
125
    public function dumpAutoload(): int
126
    {
127
        echo PHP_EOL;
128
        $this->invokeTypicalRequest();
129
        $paths = $this->getPaths($this->classes);
130
        $autolaod = $this->saveAutoloadFile($this->appMeta->appDir, $paths);
131
        $start = $_SERVER['REQUEST_TIME_FLOAT'];
132
        assert(is_float($start));
133
        $time = number_format(microtime(true) - $start, 2);
134
        $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3);
135
        printf("Compilation (2/2) took %f seconds and used %fMB of memory\n", $time, $memory);
136
        printf("autoload.php: %s\n", $this->getFileInfo($autolaod));
137
138
        return 0;
139
    }
140
141
    public function registerLoader(string $appDir): void
142
    {
143
        $loaderFile = $appDir . '/vendor/autoload.php';
144
        if (! file_exists($loaderFile)) {
145
            throw new RuntimeException('no loader');
146
        }
147
148
        $loader = require $loaderFile;
149
        assert($loader instanceof ClassLoader);
150
        spl_autoload_register(
151
            /** @var class-string $class */
0 ignored issues
show
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
152
            function (string $class) use ($loader): void {
153
                $loader->loadClass($class);
154
                if ($class !== NullPage::class) {
155
                    $this->classes[] = $class;
156
                }
157
            }
158
        );
159
    }
160
161
    public function compileDiScripts(AbstractAppMeta $appMeta): void
162
    {
163
        $reader = $this->injector->getInstance(Reader::class);
164
        assert($reader instanceof Reader);
165
        $namedParams = $this->injector->getInstance(NamedParameterInterface::class);
166
        assert($namedParams instanceof NamedParameterInterface);
167
        // create DI factory class and AOP compiled class for all resources and save $app cache.
168
        $app = $this->injector->getInstance(AppInterface::class);
169
        assert($app instanceof AppInterface);
170
171
        // check resource injection and create annotation cache
172
        $metas = $appMeta->getResourceListGenerator();
173
        /** @var array{0: string, 1:string} $meta */
174
        foreach ($metas as $meta) {
175
            [$className] = $meta;
0 ignored issues
show
The variable $className does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
176
            assert(class_exists($className));
177
            $this->scanClass($reader, $namedParams, $className);
178
        }
179
    }
180
181
    public function compileSrc(AbstractModule $module): AbstractModule
182
    {
183
        $container = $module->getContainer()->getContainer();
184
        $dependencies = array_keys($container);
185
        sort($dependencies);
186
        foreach ($dependencies as $dependencyIndex) {
187
            $pos = strpos((string) $dependencyIndex, '-');
188
            assert(is_int($pos));
189
            $interface = substr((string) $dependencyIndex, 0, $pos);
190
            $name = substr((string) $dependencyIndex, $pos + 1);
191
            $this->getInstance($interface, $name);
192
        }
193
194
        return $module;
195
    }
196
197
    private function getFileInfo(string $filename): string
198
    {
199
        if (in_array($filename, $this->overwritten, true)) {
200
            return $filename . ' (overwritten)';
201
        }
202
203
        return $filename;
204
    }
205
206
    /**
207
     * @param array<string> $paths
208
     */
209
    private function saveAutoloadFile(string $appDir, array $paths): string
210
    {
211
        $requiredFile = '';
212
        foreach ($paths as $path) {
213
            $requiredFile .= sprintf(
214
                "require %s';\n",
215
                $this->getRelativePath($appDir, $path)
216
            );
217
        }
218
219
        $autoloadFile = sprintf("<?php
220
221
// %s autoload
222
223
%s
224
require __DIR__ . '/vendor/autoload.php';
225
", $this->context, $requiredFile);
226
        $fileName = realpath($appDir) . '/autoload.php';
227
        $this->putFileContents($fileName, $autoloadFile);
228
229
        return $fileName;
230
    }
231
232
    private function compilePreload(AbstractAppMeta $appMeta, string $context): string
233
    {
234
        $this->loadResources($appMeta->name, $context, $appMeta->appDir);
235
        $paths = $this->getPaths($this->classes);
236
        $requiredOnceFile = '';
237
        foreach ($paths as $path) {
238
            $requiredOnceFile .= sprintf(
239
                "require_once %s';\n",
240
                $this->getRelativePath($appMeta->appDir, $path)
241
            );
242
        }
243
244
        $preloadFile = sprintf("<?php
245
246
// %s preload
247
248
require __DIR__ . '/vendor/autoload.php';
249
250
%s", $this->context, $requiredOnceFile);
251
        $fileName = realpath($appMeta->appDir) . '/preload.php';
252
        $this->putFileContents($fileName, $preloadFile);
253
254
        return $fileName;
255
    }
256
257
    private function getRelativePath(string $rootDir, string $file): string
258
    {
259
        $dir = (string) realpath($rootDir);
260
        if (strpos($file, $dir) !== false) {
261
            return (string) preg_replace('#^' . preg_quote($dir, '#') . '#', "__DIR__ . '", $file);
262
        }
263
264
        return $file;
265
    }
266
267
    /**
268
     * @psalm-suppress MixedFunctionCall
269
     * @psalm-suppress NoInterfaceProperties
270
     */
271
    private function invokeTypicalRequest(): void
272
    {
273
        $app = $this->injector->getInstance(AppInterface::class);
274
        assert($app instanceof AppInterface);
275
        assert(property_exists($app, 'resource'));
276
        $ro = new NullPage();
277
        $ro->uri = new Uri('app://self/');
278
        /** @psalm-suppress MixedMethodCall */
279
        $app->resource->get->object($ro)();
0 ignored issues
show
Accessing resource on the interface BEAR\Sunday\Extension\Application\AppInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
280
    }
281
282
    /**
283
     * Save annotation and method meta information
284
     *
285
     * @param class-string<T> $className
0 ignored issues
show
The doc-type class-string<T> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
286
     *
287
     * @template T
288
     */
289
    private function scanClass(Reader $reader, NamedParameterInterface $namedParams, string $className): void
290
    {
291
        $class = new ReflectionClass($className);
292
        $instance = $class->newInstanceWithoutConstructor();
293
        if (! $instance instanceof $className) {
294
            return; // @codeCoverageIgnore
295
        }
296
297
        $reader->getClassAnnotations($class);
298
        $methods = $class->getMethods();
299
        $log = sprintf('M %s:', $className);
300
        foreach ($methods as $method) {
301
            $methodName = $method->getName();
0 ignored issues
show
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
302
            if ($this->isMagicMethod($methodName)) {
303
                continue;
304
            }
305
306
            if (substr($methodName, 0, 2) === 'on') {
307
                $log .= sprintf(' %s', $methodName);
308
                $this->saveNamedParam($namedParams, $instance, $methodName);
309
            }
310
311
            // method annotation
312
            $reader->getMethodAnnotations($method);
313
            $log .= sprintf('@ %s', $methodName);
314
        }
315
316
//        echo $log . PHP_EOL;
317
    }
318
319
    private function isMagicMethod(string $method): bool
320
    {
321
        return in_array($method, ['__sleep', '__wakeup', 'offsetGet', 'offsetSet', 'offsetExists', 'offsetUnset', 'count', 'ksort', 'asort', 'jsonSerialize'], true);
322
    }
323
324
    private function saveNamedParam(NamedParameterInterface $namedParameter, object $instance, string $method): void
325
    {
326
        // named parameter
327
        if (! in_array($method, ['onGet', 'onPost', 'onPut', 'onPatch', 'onDelete', 'onHead'], true)) {
328
            return;  // @codeCoverageIgnore
329
        }
330
331
        $callable = [$instance, $method];
332
        if (! is_callable($callable)) {
333
            return;  // @codeCoverageIgnore
334
        }
335
336
        try {
337
            $namedParameter->getParameters($callable, []);
338
        } catch (ParameterException $e) {
339
            return;
340
        }
341
    }
342
343
    /**
344
     * @param array<string> $classes
345
     *
346
     * @return array<string>
347
     */
348
    private function getPaths(array $classes): array
349
    {
350
        $paths = [];
351
        foreach ($classes as $class) {
352
            // could be phpdoc tag by annotation loader
353
            if ($this->isNotAutoloadble($class)) {
354
                continue;
355
            }
356
357
            /** @var class-string $class */
0 ignored issues
show
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
358
            $filePath = (string) (new ReflectionClass($class))->getFileName(); // @phpstan-ignore-line
359
            if (! $this->isNotCompileFile($filePath)) {
360
                continue; // @codeCoverageIgnore
361
            }
362
363
            $paths[] = $this->getRelativePath($this->appDir, $filePath);
364
        }
365
366
        return $paths;
367
    }
368
369
    private function isNotCompileFile(string $filePath): bool
370
    {
371
        return file_exists($filePath) || is_int(strpos($filePath, 'phar'));
372
    }
373
374
    private function isNotAutoloadble(string $class): bool
375
    {
376
        return ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false);
377
    }
378
379
    private function loadResources(string $appName, string $context, string $appDir): void
380
    {
381
        $meta = new Meta($appName, $context, $appDir);
382
383
        $resMetas = $meta->getGenerator('*');
384
        foreach ($resMetas as $resMeta) {
385
            $this->getInstance($resMeta->class);
386
        }
387
    }
388
389
    private function getInstance(string $interface, string $name = ''): void
390
    {
391
        $dependencyIndex = $interface . '-' . $name;
392
        if (in_array($dependencyIndex, $this->compiled, true)) {
393
            // @codeCoverageIgnoreStart
394
            printf("S %s:%s\n", $interface, $name);
395
396
            return;
397
398
            // @codeCoverageIgnoreEnd
399
        }
400
401
        try {
402
            $this->injector->getInstance($interface, $name);
403
            $this->compiled[] = $dependencyIndex;
404
            $this->progress('.');
405
        } catch (Unbound $e) {
406
            if ($dependencyIndex === 'Ray\Aop\MethodInvocation-') {
407
                return;
408
            }
409
410
            $this->failed[$dependencyIndex] = $e->getMessage();
411
            $this->progress('F');
412
            // @codeCoverageIgnoreStart
413
        } catch (Throwable $e) {
414
            $this->failed[$dependencyIndex] = sprintf('%s: %s', get_class($e), $e->getMessage());
415
            $this->progress('F');
416
            // @codeCoverageIgnoreEnd
417
        }
418
    }
419
420
    private function progress(string $char): void
421
    {
422
        /**
423
         * @var int
424
         */
425
        static $cnt = 0;
426
427
        echo $char;
428
        $cnt++;
429
        if ($cnt === 60) {
430
            $cnt = 0;
431
            echo PHP_EOL;
432
        }
433
    }
434
435
    private function hookNullObjectClass(string $appDir): void
436
    {
437
        $compileScript = realpath($appDir) . '/.compile.php';
438
        if (file_exists($compileScript)) {
439
            require $compileScript;
440
        }
441
    }
442
443
    private function putFileContents(string $fileName, string $content): void
444
    {
445
        if (file_exists($fileName)) {
446
            $this->overwritten[] = $fileName;
447
        }
448
449
        file_put_contents($fileName, $content);
450
    }
451
452
    private function compileObjectGraphDotFile(AbstractModule $module): string
453
    {
454
        $dotFile = sprintf('%s/module.dot', $this->appDir);
455
        $this->putFileContents($dotFile, (new ObjectGrapher())($module));
456
457
        return $dotFile;
458
    }
459
}
460