PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.0.1
Defender Security – Malware Scanner, Login Security & Firewall v6.0.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / lib / packages / DI / Compiler / Compiler.php

Compiler.php in Defender Security – Malware Scanner, Login Security & Firewall 6.0.1, at lib/packages/DI/Compiler/Compiler.php

403 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace WPMU_DEV\Defender\Vendor\DI\Compiler;
6
7 use function chmod;
8 use WPMU_DEV\Defender\Vendor\DI\Definition\ArrayDefinition;
9 use WPMU_DEV\Defender\Vendor\DI\Definition\DecoratorDefinition;
10 use WPMU_DEV\Defender\Vendor\DI\Definition\Definition;
11 use WPMU_DEV\Defender\Vendor\DI\Definition\EnvironmentVariableDefinition;
12 use WPMU_DEV\Defender\Vendor\DI\Definition\Exception\InvalidDefinition;
13 use WPMU_DEV\Defender\Vendor\DI\Definition\FactoryDefinition;
14 use WPMU_DEV\Defender\Vendor\DI\Definition\ObjectDefinition;
15 use WPMU_DEV\Defender\Vendor\DI\Definition\Reference;
16 use WPMU_DEV\Defender\Vendor\DI\Definition\Source\DefinitionSource;
17 use WPMU_DEV\Defender\Vendor\DI\Definition\StringDefinition;
18 use WPMU_DEV\Defender\Vendor\DI\Definition\ValueDefinition;
19 use WPMU_DEV\Defender\Vendor\DI\DependencyException;
20 use WPMU_DEV\Defender\Vendor\DI\Proxy\ProxyFactoryInterface;
21 use function dirname;
22 use function file_put_contents;
23 use InvalidArgumentException;
24 use WPMU_DEV\Defender\Vendor\Laravel\SerializableClosure\Support\ReflectionClosure;
25 use function rename;
26 use function sprintf;
27 use function tempnam;
28 use function unlink;
29
30 /**
31 * Compiles the container into PHP code much more optimized for performances.
32 *
33 * @author Matthieu Napoli <[email protected]>
34 */
35 class Compiler
36 {
37 private string $containerClass;
38
39 private string $containerParentClass;
40
41 /**
42 * Definitions indexed by the entry name. The value can be null if the definition needs to be fetched.
43 *
44 * Keys are strings, values are `Definition` objects or null.
45 */
46 private \ArrayIterator $entriesToCompile;
47
48 /**
49 * Progressive counter for definitions.
50 *
51 * Each key in $entriesToCompile is defined as 'SubEntry' + counter
52 * and each definition has always the same key in the CompiledContainer
53 * if PHP-WPMU_DEV\Defender\Vendor\DI configuration does not change.
54 */
55 private int $subEntryCounter = 0;
56
57 /**
58 * Progressive counter for CompiledContainer get methods.
59 *
60 * Each CompiledContainer method name is defined as 'get' + counter
61 * and remains the same after each recompilation
62 * if PHP-WPMU_DEV\Defender\Vendor\DI configuration does not change.
63 */
64 private int $methodMappingCounter = 0;
65
66 /**
67 * Map of entry names to method names.
68 *
69 * @var string[]
70 */
71 private array $entryToMethodMapping = [];
72
73 /**
74 * @var string[]
75 */
76 private array $methods = [];
77
78 private bool $autowiringEnabled;
79
80 public function __construct(
81 private ProxyFactoryInterface $proxyFactory,
82 ) {
83 }
84
85 public function getProxyFactory() : ProxyFactoryInterface
86 {
87 return $this->proxyFactory;
88 }
89
90 /**
91 * Compile the container.
92 *
93 * @return string The compiled container file name.
94 */
95 public function compile(
96 DefinitionSource $definitionSource,
97 string $directory,
98 string $className,
99 string $parentClassName,
100 bool $autowiringEnabled,
101 ) : string {
102 $fileName = rtrim($directory, '/') . '/' . $className . '.php';
103
104 if (file_exists($fileName)) {
105 // The container is already compiled
106 return $fileName;
107 }
108
109 $this->autowiringEnabled = $autowiringEnabled;
110
111 // Validate that a valid class name was provided
112 $validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $className);
113 if (!$validClassName) {
114 throw new InvalidArgumentException("The container cannot be compiled: `$className` is not a valid PHP class name");
115 }
116
117 $this->entriesToCompile = new \ArrayIterator($definitionSource->getDefinitions());
118
119 // We use an ArrayIterator so that we can keep adding new items to the list while we compile entries
120 foreach ($this->entriesToCompile as $entryName => $definition) {
121 $silenceErrors = false;
122 // This is an entry found by reference during autowiring
123 if (!$definition) {
124 $definition = $definitionSource->getDefinition($entryName);
125 // We silence errors for those entries because type-hints may reference interfaces/abstract classes
126 // which could later be defined, or even not used (we don't want to block the compilation for those)
127 $silenceErrors = true;
128 }
129 if (!$definition) {
130 // We do not throw a `NotFound` exception here because the dependency
131 // could be defined at runtime
132 continue;
133 }
134 // Check that the definition can be compiled
135 $errorMessage = $this->isCompilable($definition);
136 if ($errorMessage !== true) {
137 continue;
138 }
139 try {
140 $this->compileDefinition($entryName, $definition);
141 } catch (InvalidDefinition $e) {
142 if ($silenceErrors) {
143 // forget the entry
144 unset($this->entryToMethodMapping[$entryName]);
145 } else {
146 throw $e;
147 }
148 }
149 }
150
151 $this->containerClass = $className;
152 $this->containerParentClass = $parentClassName;
153
154 ob_start();
155 require __DIR__ . '/Template.php';
156 $fileContent = ob_get_clean();
157
158 $fileContent = "<?php\n" . $fileContent;
159
160 $this->createCompilationDirectory(dirname($fileName));
161 $this->writeFileAtomic($fileName, $fileContent);
162
163 return $fileName;
164 }
165
166 private function writeFileAtomic(string $fileName, string $content) : void
167 {
168 $tmpFile = @tempnam(dirname($fileName), 'swap-compile');
169 if ($tmpFile === false) {
170 throw new InvalidArgumentException(
171 sprintf('Error while creating temporary file in %s', dirname($fileName))
172 );
173 }
174 @chmod($tmpFile, 0666);
175
176 $written = file_put_contents($tmpFile, $content);
177 if ($written === false) {
178 @unlink($tmpFile);
179
180 throw new InvalidArgumentException(sprintf('Error while writing to %s', $tmpFile));
181 }
182
183 @chmod($tmpFile, 0666);
184 $renamed = @rename($tmpFile, $fileName);
185 if (!$renamed) {
186 @unlink($tmpFile);
187
188 throw new InvalidArgumentException(sprintf('Error while renaming %s to %s', $tmpFile, $fileName));
189 }
190 }
191
192 /**
193 * @return string The method name
194 * @throws DependencyException
195 * @throws InvalidDefinition
196 */
197 private function compileDefinition(string $entryName, Definition $definition) : string
198 {
199 // Generate a unique method name
200 $methodName = 'get' . (++$this->methodMappingCounter);
201 $this->entryToMethodMapping[$entryName] = $methodName;
202
203 switch (true) {
204 case $definition instanceof ValueDefinition:
205 $value = $definition->getValue();
206 $code = 'return ' . $this->compileValue($value) . ';';
207 break;
208 case $definition instanceof Reference:
209 $targetEntryName = $definition->getTargetEntryName();
210 $code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');';
211 // If this method is not yet compiled we store it for compilation
212 if (!isset($this->entriesToCompile[$targetEntryName])) {
213 $this->entriesToCompile[$targetEntryName] = null;
214 }
215 break;
216 case $definition instanceof StringDefinition:
217 $entryName = $this->compileValue($definition->getName());
218 $expression = $this->compileValue($definition->getExpression());
219 $code = 'return \WPMU_DEV\Defender\Vendor\DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);';
220 break;
221 case $definition instanceof EnvironmentVariableDefinition:
222 $variableName = $this->compileValue($definition->getVariableName());
223 $isOptional = $this->compileValue($definition->isOptional());
224 $defaultValue = $this->compileValue($definition->getDefaultValue());
225 $code = <<<PHP
226 \$value = \$_ENV[$variableName] ?? \$_SERVER[$variableName] ?? getenv($variableName);
227 if (false !== \$value) return \$value;
228 if (!$isOptional) {
229 throw new \WPMU_DEV\Defender\Vendor\DI\Definition\Exception\InvalidDefinition("The environment variable '{$definition->getVariableName()}' has not been defined");
230 }
231 return $defaultValue;
232 PHP;
233 break;
234 case $definition instanceof ArrayDefinition:
235 try {
236 $code = 'return ' . $this->compileValue($definition->getValues()) . ';';
237 } catch (\Exception $e) {
238 throw new DependencyException(sprintf(
239 'Error while compiling %s. %s',
240 $definition->getName(),
241 $e->getMessage()
242 ), 0, $e);
243 }
244 break;
245 case $definition instanceof ObjectDefinition:
246 $compiler = new ObjectCreationCompiler($this);
247 $code = $compiler->compile($definition);
248 $code .= "\n return \$object;";
249 break;
250 case $definition instanceof DecoratorDefinition:
251 $decoratedDefinition = $definition->getDecoratedDefinition();
252 if (! $decoratedDefinition instanceof Definition) {
253 if (! $definition->getName()) {
254 throw new InvalidDefinition('Decorators cannot be nested in another definition');
255 }
256 throw new InvalidDefinition(sprintf(
257 'Entry "%s" decorates nothing: no previous definition with the same name was found',
258 $definition->getName()
259 ));
260 }
261 $code = sprintf(
262 'return call_user_func(%s, %s, $this->delegateContainer);',
263 $this->compileValue($definition->getCallable()),
264 $this->compileValue($decoratedDefinition)
265 );
266 break;
267 case $definition instanceof FactoryDefinition:
268 $value = $definition->getCallable();
269
270 // Custom error message to help debugging
271 $isInvokableClass = is_string($value) && class_exists($value) && method_exists($value, '__invoke');
272 if ($isInvokableClass && !$this->autowiringEnabled) {
273 throw new InvalidDefinition(sprintf(
274 'Entry "%s" cannot be compiled. Invokable classes cannot be automatically resolved if autowiring is disabled on the container, you need to enable autowiring or define the entry manually.',
275 $entryName
276 ));
277 }
278
279 $definitionParameters = '';
280 if (!empty($definition->getParameters())) {
281 $definitionParameters = ', ' . $this->compileValue($definition->getParameters());
282 }
283
284 $code = sprintf(
285 'return $this->resolveFactory(%s, %s%s);',
286 $this->compileValue($value),
287 var_export($entryName, true),
288 $definitionParameters
289 );
290
291 break;
292 default:
293 // This case should not happen (so it cannot be tested)
294 throw new \Exception('Cannot compile definition of type ' . $definition::class);
295 }
296
297 $this->methods[$methodName] = $code;
298
299 return $methodName;
300 }
301
302 public function compileValue(mixed $value) : string
303 {
304 // Check that the value can be compiled
305 $errorMessage = $this->isCompilable($value);
306 if ($errorMessage !== true) {
307 throw new InvalidDefinition($errorMessage);
308 }
309
310 if ($value instanceof Definition) {
311 // Give it an arbitrary unique name
312 $subEntryName = 'subEntry' . (++$this->subEntryCounter);
313 // Compile the sub-definition in another method
314 $methodName = $this->compileDefinition($subEntryName, $value);
315
316 // The value is now a method call to that method (which returns the value)
317 return "\$this->$methodName()";
318 }
319
320 if (is_array($value)) {
321 $value = array_map(function ($value, $key) {
322 $compiledValue = $this->compileValue($value);
323 $key = var_export($key, true);
324
325 return " $key => $compiledValue,\n";
326 }, $value, array_keys($value));
327 $value = implode('', $value);
328
329 return "[\n$value ]";
330 }
331
332 if ($value instanceof \Closure) {
333 return $this->compileClosure($value);
334 }
335
336 return var_export($value, true);
337 }
338
339 private function createCompilationDirectory(string $directory) : void
340 {
341 if (!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory)) {
342 throw new InvalidArgumentException(sprintf('Compilation directory does not exist and cannot be created: %s.', $directory));
343 }
344 if (!is_writable($directory)) {
345 throw new InvalidArgumentException(sprintf('Compilation directory is not writable: %s.', $directory));
346 }
347 }
348
349 /**
350 * @return string|true If true is returned that means that the value is compilable.
351 */
352 private function isCompilable($value) : string|bool
353 {
354 if ($value instanceof ValueDefinition) {
355 return $this->isCompilable($value->getValue());
356 }
357 if (($value instanceof DecoratorDefinition) && empty($value->getName())) {
358 return 'Decorators cannot be nested in another definition';
359 }
360 // All other definitions are compilable
361 if ($value instanceof Definition) {
362 return true;
363 }
364 if ($value instanceof \Closure) {
365 return true;
366 }
367 /** @psalm-suppress UndefinedClass */
368 if ((\PHP_VERSION_ID >= 80100) && ($value instanceof \UnitEnum)) {
369 return true;
370 }
371 if (is_object($value)) {
372 return 'An object was found but objects cannot be compiled';
373 }
374 if (is_resource($value)) {
375 return 'A resource was found but resources cannot be compiled';
376 }
377
378 return true;
379 }
380
381 /**
382 * @throws InvalidDefinition
383 */
384 private function compileClosure(\Closure $closure) : string
385 {
386 $reflector = new ReflectionClosure($closure);
387
388 if ($reflector->getUseVariables()) {
389 throw new InvalidDefinition('Cannot compile closures which import variables using the `use` keyword');
390 }
391
392 if ($reflector->isBindingRequired() || $reflector->isScopeRequired()) {
393 throw new InvalidDefinition('Cannot compile closures which use $this or self/static/parent references');
394 }
395
396 // Force all closures to be static (add the `static` keyword), i.e. they can't use
397 // $this, which makes sense since their code is copied into another class.
398 $code = ($reflector->isStatic() ? '' : 'static ') . $reflector->getCode();
399
400 return trim($code, "\t\n\r;");
401 }
402 }
403