PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.7.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.7.0
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Dependencies / SuperClosure / Serializer.php

Serializer.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.7.0, at includes/Dependencies/SuperClosure/Serializer.php

223 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php namespace WPDeveloper\BetterDocs\Dependencies\SuperClosure;
2 // phpcs:ignoreFile -- Bundled third-party (Mozart) dependency; exempt from plugin coding standards.
3
4 use WPDeveloper\BetterDocs\Dependencies\SuperClosure\Analyzer\AstAnalyzer as DefaultAnalyzer;
5 use WPDeveloper\BetterDocs\Dependencies\SuperClosure\Analyzer\ClosureAnalyzer;
6 use WPDeveloper\BetterDocs\Dependencies\SuperClosure\Exception\ClosureSerializationException;
7 use WPDeveloper\BetterDocs\Dependencies\SuperClosure\Exception\ClosureUnserializationException;
8
9 /**
10 * This is the serializer class used for serializing Closure objects.
11 *
12 * We're abstracting away all the details, impossibilities, and scary things
13 * that happen within.
14 */
15 class Serializer implements SerializerInterface
16 {
17 /**
18 * The special value marking a recursive reference to a closure.
19 *
20 * @var string
21 */
22 const RECURSION = "{{RECURSION}}";
23
24 /**
25 * The keys of closure data required for serialization.
26 *
27 * @var array
28 */
29 private static $dataToKeep = [
30 'code' => true,
31 'context' => true,
32 'binding' => true,
33 'scope' => true,
34 'isStatic' => true,
35 ];
36
37 /**
38 * The closure analyzer instance.
39 *
40 * @var ClosureAnalyzer
41 */
42 private $analyzer;
43
44 /**
45 * The HMAC key to sign serialized closures.
46 *
47 * @var string
48 */
49 private $signingKey;
50
51 /**
52 * Create a new serializer instance.
53 *
54 * @param ClosureAnalyzer|null $analyzer Closure analyzer instance.
55 * @param string|null $signingKey HMAC key to sign closure data.
56 */
57 public function __construct(
58 ?ClosureAnalyzer $analyzer = null,
59 $signingKey = null
60 ) {
61 $this->analyzer = $analyzer ?: new DefaultAnalyzer;
62 $this->signingKey = $signingKey;
63 }
64
65 /**
66 * @inheritDoc
67 */
68 public function serialize(\Closure $closure)
69 {
70 $serialized = serialize(new SerializableClosure($closure, $this));
71
72 if ($serialized === null) {
73 throw new ClosureSerializationException(
74 'The closure could not be serialized.'
75 );
76 }
77
78 if ($this->signingKey) {
79 $signature = $this->calculateSignature($serialized);
80 $serialized = '%' . base64_encode($signature) . $serialized;
81 }
82
83 return $serialized;
84 }
85
86 /**
87 * @inheritDoc
88 */
89 public function unserialize($serialized)
90 {
91 // Strip off the signature from the front of the string.
92 $signature = null;
93 if ($serialized[0] === '%') {
94 $signature = base64_decode(substr($serialized, 1, 44));
95 $serialized = substr($serialized, 45);
96 }
97
98 // If a key was provided, then verify the signature.
99 if ($this->signingKey) {
100 $this->verifySignature($signature, $serialized);
101 }
102
103 set_error_handler(function () {});
104 $unserialized = unserialize($serialized);
105 restore_error_handler();
106 if ($unserialized === false) {
107 throw new ClosureUnserializationException(
108 'The closure could not be unserialized.'
109 );
110 } elseif (!$unserialized instanceof SerializableClosure) {
111 throw new ClosureUnserializationException(
112 'The closure did not unserialize to a SuperClosure.'
113 );
114 }
115
116 return $unserialized->getClosure();
117 }
118
119 /**
120 * @inheritDoc
121 */
122 public function getData(\Closure $closure, $forSerialization = false)
123 {
124 // Use the closure analyzer to get data about the closure.
125 $data = $this->analyzer->analyze($closure);
126
127 // If the closure data is getting retrieved solely for the purpose of
128 // serializing the closure, then make some modifications to the data.
129 if ($forSerialization) {
130 // If there is no reference to the binding, don't serialize it.
131 if (!$data['hasThis']) {
132 $data['binding'] = null;
133 }
134
135 // Remove data about the closure that does not get serialized.
136 $data = array_intersect_key($data, self::$dataToKeep);
137
138 // Wrap any other closures within the context.
139 foreach ($data['context'] as &$value) {
140 if ($value instanceof \Closure) {
141 $value = ($value === $closure)
142 ? self::RECURSION
143 : new SerializableClosure($value, $this);
144 }
145 }
146 }
147
148 return $data;
149 }
150
151 /**
152 * Recursively traverses and wraps all Closure objects within the value.
153 *
154 * NOTE: THIS MAY NOT WORK IN ALL USE CASES, SO USE AT YOUR OWN RISK.
155 *
156 * @param mixed $data Any variable that contains closures.
157 * @param SerializerInterface $serializer The serializer to use.
158 */
159 public static function wrapClosures(&$data, SerializerInterface $serializer)
160 {
161 if ($data instanceof \Closure) {
162 // Handle and wrap closure objects.
163 $reflection = new \ReflectionFunction($data);
164 if ($binding = $reflection->getClosureThis()) {
165 self::wrapClosures($binding, $serializer);
166 $scope = $reflection->getClosureScopeClass();
167 $scope = $scope ? $scope->getName() : 'static';
168 $data = $data->bindTo($binding, $scope);
169 }
170 $data = new SerializableClosure($data, $serializer);
171 } elseif (is_array($data) || $data instanceof \stdClass || $data instanceof \Traversable) {
172 // Handle members of traversable values.
173 foreach ($data as &$value) {
174 self::wrapClosures($value, $serializer);
175 }
176 } elseif (is_object($data) && !$data instanceof \Serializable) {
177 // Handle objects that are not already explicitly serializable.
178 $reflection = new \ReflectionObject($data);
179 if (!$reflection->hasMethod('__sleep')) {
180 foreach ($reflection->getProperties() as $property) {
181 if ($property->isPrivate() || $property->isProtected()) {
182 $property->setAccessible(true);
183 }
184 $value = $property->getValue($data);
185 self::wrapClosures($value, $serializer);
186 $property->setValue($data, $value);
187 }
188 }
189 }
190 }
191
192 /**
193 * Calculates a signature for a closure's serialized data.
194 *
195 * @param string $data Serialized closure data.
196 *
197 * @return string Signature of the closure's data.
198 */
199 private function calculateSignature($data)
200 {
201 return hash_hmac('sha256', $data, $this->signingKey, true);
202 }
203
204 /**
205 * Verifies the signature for a closure's serialized data.
206 *
207 * @param string $signature The provided signature of the data.
208 * @param string $data The data for which to verify the signature.
209 *
210 * @throws ClosureUnserializationException if the signature is invalid.
211 */
212 private function verifySignature($signature, $data)
213 {
214 // Verify that the provided signature matches the calculated signature.
215 if (!hash_equals($signature, $this->calculateSignature($data))) {
216 throw new ClosureUnserializationException('The signature of the'
217 . ' closure\'s data is invalid, which means the serialized '
218 . 'closure has been modified and is unsafe to unserialize.'
219 );
220 }
221 }
222 }
223