koriym /
BEAR.Package
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
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
|
|||
| 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
|
|||
| 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
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
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 |
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.