You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
11 KiB
443 lines
11 KiB
<?php declare(strict_types=1); |
|
/* |
|
* This file is part of sebastian/global-state. |
|
* |
|
* (c) Sebastian Bergmann <sebastian@phpunit.de> |
|
* |
|
* For the full copyright and license information, please view the LICENSE |
|
* file that was distributed with this source code. |
|
*/ |
|
namespace SebastianBergmann\GlobalState; |
|
|
|
use const PHP_VERSION_ID; |
|
use function array_keys; |
|
use function array_merge; |
|
use function array_reverse; |
|
use function func_get_args; |
|
use function get_declared_classes; |
|
use function get_declared_interfaces; |
|
use function get_declared_traits; |
|
use function get_defined_constants; |
|
use function get_defined_functions; |
|
use function get_included_files; |
|
use function in_array; |
|
use function ini_get_all; |
|
use function is_array; |
|
use function is_object; |
|
use function is_resource; |
|
use function is_scalar; |
|
use function serialize; |
|
use function unserialize; |
|
use ReflectionClass; |
|
use SebastianBergmann\ObjectReflector\ObjectReflector; |
|
use SebastianBergmann\RecursionContext\Context; |
|
use Throwable; |
|
|
|
/** |
|
* A snapshot of global state. |
|
*/ |
|
class Snapshot |
|
{ |
|
/** |
|
* @var ExcludeList |
|
*/ |
|
private $excludeList; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $globalVariables = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $superGlobalArrays = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $superGlobalVariables = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $staticAttributes = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $iniSettings = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $includedFiles = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $constants = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $functions = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $interfaces = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $classes = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private $traits = []; |
|
|
|
/** |
|
* Creates a snapshot of the current global state. |
|
*/ |
|
public function __construct(ExcludeList $excludeList = null, bool $includeGlobalVariables = true, bool $includeStaticAttributes = true, bool $includeConstants = true, bool $includeFunctions = true, bool $includeClasses = true, bool $includeInterfaces = true, bool $includeTraits = true, bool $includeIniSettings = true, bool $includeIncludedFiles = true) |
|
{ |
|
$this->excludeList = $excludeList ?: new ExcludeList; |
|
|
|
if ($includeConstants) { |
|
$this->snapshotConstants(); |
|
} |
|
|
|
if ($includeFunctions) { |
|
$this->snapshotFunctions(); |
|
} |
|
|
|
if ($includeClasses || $includeStaticAttributes) { |
|
$this->snapshotClasses(); |
|
} |
|
|
|
if ($includeInterfaces) { |
|
$this->snapshotInterfaces(); |
|
} |
|
|
|
if ($includeGlobalVariables) { |
|
$this->setupSuperGlobalArrays(); |
|
$this->snapshotGlobals(); |
|
} |
|
|
|
if ($includeStaticAttributes) { |
|
$this->snapshotStaticAttributes(); |
|
} |
|
|
|
if ($includeIniSettings) { |
|
$this->iniSettings = ini_get_all(null, false); |
|
} |
|
|
|
if ($includeIncludedFiles) { |
|
$this->includedFiles = get_included_files(); |
|
} |
|
|
|
if ($includeTraits) { |
|
$this->traits = get_declared_traits(); |
|
} |
|
} |
|
|
|
public function excludeList(): ExcludeList |
|
{ |
|
return $this->excludeList; |
|
} |
|
|
|
public function globalVariables(): array |
|
{ |
|
return $this->globalVariables; |
|
} |
|
|
|
public function superGlobalVariables(): array |
|
{ |
|
return $this->superGlobalVariables; |
|
} |
|
|
|
public function superGlobalArrays(): array |
|
{ |
|
return $this->superGlobalArrays; |
|
} |
|
|
|
public function staticAttributes(): array |
|
{ |
|
return $this->staticAttributes; |
|
} |
|
|
|
public function iniSettings(): array |
|
{ |
|
return $this->iniSettings; |
|
} |
|
|
|
public function includedFiles(): array |
|
{ |
|
return $this->includedFiles; |
|
} |
|
|
|
public function constants(): array |
|
{ |
|
return $this->constants; |
|
} |
|
|
|
public function functions(): array |
|
{ |
|
return $this->functions; |
|
} |
|
|
|
public function interfaces(): array |
|
{ |
|
return $this->interfaces; |
|
} |
|
|
|
public function classes(): array |
|
{ |
|
return $this->classes; |
|
} |
|
|
|
public function traits(): array |
|
{ |
|
return $this->traits; |
|
} |
|
|
|
/** |
|
* Creates a snapshot user-defined constants. |
|
*/ |
|
private function snapshotConstants(): void |
|
{ |
|
$constants = get_defined_constants(true); |
|
|
|
if (isset($constants['user'])) { |
|
$this->constants = $constants['user']; |
|
} |
|
} |
|
|
|
/** |
|
* Creates a snapshot user-defined functions. |
|
*/ |
|
private function snapshotFunctions(): void |
|
{ |
|
$functions = get_defined_functions(); |
|
|
|
$this->functions = $functions['user']; |
|
} |
|
|
|
/** |
|
* Creates a snapshot user-defined classes. |
|
*/ |
|
private function snapshotClasses(): void |
|
{ |
|
foreach (array_reverse(get_declared_classes()) as $className) { |
|
$class = new ReflectionClass($className); |
|
|
|
if (!$class->isUserDefined()) { |
|
break; |
|
} |
|
|
|
$this->classes[] = $className; |
|
} |
|
|
|
$this->classes = array_reverse($this->classes); |
|
} |
|
|
|
/** |
|
* Creates a snapshot user-defined interfaces. |
|
*/ |
|
private function snapshotInterfaces(): void |
|
{ |
|
foreach (array_reverse(get_declared_interfaces()) as $interfaceName) { |
|
$class = new ReflectionClass($interfaceName); |
|
|
|
if (!$class->isUserDefined()) { |
|
break; |
|
} |
|
|
|
$this->interfaces[] = $interfaceName; |
|
} |
|
|
|
$this->interfaces = array_reverse($this->interfaces); |
|
} |
|
|
|
/** |
|
* Creates a snapshot of all global and super-global variables. |
|
*/ |
|
private function snapshotGlobals(): void |
|
{ |
|
$superGlobalArrays = $this->superGlobalArrays(); |
|
|
|
foreach ($superGlobalArrays as $superGlobalArray) { |
|
$this->snapshotSuperGlobalArray($superGlobalArray); |
|
} |
|
|
|
foreach (array_keys($GLOBALS) as $key) { |
|
if ($key !== 'GLOBALS' && |
|
!in_array($key, $superGlobalArrays, true) && |
|
$this->canBeSerialized($GLOBALS[$key]) && |
|
!$this->excludeList->isGlobalVariableExcluded($key)) { |
|
/* @noinspection UnserializeExploitsInspection */ |
|
$this->globalVariables[$key] = unserialize(serialize($GLOBALS[$key])); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Creates a snapshot a super-global variable array. |
|
*/ |
|
private function snapshotSuperGlobalArray(string $superGlobalArray): void |
|
{ |
|
$this->superGlobalVariables[$superGlobalArray] = []; |
|
|
|
if (isset($GLOBALS[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray])) { |
|
foreach ($GLOBALS[$superGlobalArray] as $key => $value) { |
|
/* @noinspection UnserializeExploitsInspection */ |
|
$this->superGlobalVariables[$superGlobalArray][$key] = unserialize(serialize($value)); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Creates a snapshot of all static attributes in user-defined classes. |
|
*/ |
|
private function snapshotStaticAttributes(): void |
|
{ |
|
foreach ($this->classes as $className) { |
|
$class = new ReflectionClass($className); |
|
$snapshot = []; |
|
|
|
foreach ($class->getProperties() as $attribute) { |
|
if ($attribute->isStatic()) { |
|
$name = $attribute->getName(); |
|
|
|
if ($this->excludeList->isStaticAttributeExcluded($className, $name)) { |
|
continue; |
|
} |
|
|
|
$attribute->setAccessible(true); |
|
|
|
if (PHP_VERSION_ID >= 70400 && !$attribute->isInitialized()) { |
|
continue; |
|
} |
|
|
|
$value = $attribute->getValue(); |
|
|
|
if ($this->canBeSerialized($value)) { |
|
/* @noinspection UnserializeExploitsInspection */ |
|
$snapshot[$name] = unserialize(serialize($value)); |
|
} |
|
} |
|
} |
|
|
|
if (!empty($snapshot)) { |
|
$this->staticAttributes[$className] = $snapshot; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Returns a list of all super-global variable arrays. |
|
*/ |
|
private function setupSuperGlobalArrays(): void |
|
{ |
|
$this->superGlobalArrays = [ |
|
'_ENV', |
|
'_POST', |
|
'_GET', |
|
'_COOKIE', |
|
'_SERVER', |
|
'_FILES', |
|
'_REQUEST', |
|
]; |
|
} |
|
|
|
private function canBeSerialized($variable): bool |
|
{ |
|
if (is_scalar($variable) || $variable === null) { |
|
return true; |
|
} |
|
|
|
if (is_resource($variable)) { |
|
return false; |
|
} |
|
|
|
foreach ($this->enumerateObjectsAndResources($variable) as $value) { |
|
if (is_resource($value)) { |
|
return false; |
|
} |
|
|
|
if (is_object($value)) { |
|
$class = new ReflectionClass($value); |
|
|
|
if ($class->isAnonymous()) { |
|
return false; |
|
} |
|
|
|
try { |
|
@serialize($value); |
|
} catch (Throwable $t) { |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
private function enumerateObjectsAndResources($variable): array |
|
{ |
|
if (isset(func_get_args()[1])) { |
|
$processed = func_get_args()[1]; |
|
} else { |
|
$processed = new Context; |
|
} |
|
|
|
$result = []; |
|
|
|
if ($processed->contains($variable)) { |
|
return $result; |
|
} |
|
|
|
$array = $variable; |
|
$processed->add($variable); |
|
|
|
if (is_array($variable)) { |
|
foreach ($array as $element) { |
|
if (!is_array($element) && !is_object($element) && !is_resource($element)) { |
|
continue; |
|
} |
|
|
|
if (!is_resource($element)) { |
|
/** @noinspection SlowArrayOperationsInLoopInspection */ |
|
$result = array_merge( |
|
$result, |
|
$this->enumerateObjectsAndResources($element, $processed) |
|
); |
|
} else { |
|
$result[] = $element; |
|
} |
|
} |
|
} else { |
|
$result[] = $variable; |
|
|
|
foreach ((new ObjectReflector)->getAttributes($variable) as $value) { |
|
if (!is_array($value) && !is_object($value) && !is_resource($value)) { |
|
continue; |
|
} |
|
|
|
if (!is_resource($value)) { |
|
/** @noinspection SlowArrayOperationsInLoopInspection */ |
|
$result = array_merge( |
|
$result, |
|
$this->enumerateObjectsAndResources($value, $processed) |
|
); |
|
} else { |
|
$result[] = $value; |
|
} |
|
} |
|
} |
|
|
|
return $result; |
|
} |
|
}
|
|
|