PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / vendor / wpfluent / framework / src / WPFluent / Foundation / Async.php

Async.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.0, at vendor/wpfluent/framework/src/WPFluent/Foundation/Async.php

462 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Framework\Foundation;
4
5 use Exception;
6 use FluentCommunity\Framework\Support\Arr;
7 use FluentCommunity\Framework\Support\Helper;
8 use InvalidArgumentException;
9
10 /**
11 * @property \FluentCommunity\Framework\Foundation\Config $config
12 */
13 class Async
14 {
15 /**
16 * The dispatched handlers to stop recursion.
17 *
18 * @var array
19 */
20 private $dispatched = [];
21
22 /**
23 * The application instance
24 *
25 * @var \FluentCommunity\Framework\Foundation\Application
26 */
27 private static $app = null;
28
29 /**
30 * Self instance
31 *
32 * @var self
33 */
34 private static $instance = null;
35
36 /**
37 * The array of async action handlers
38 *
39 * @var array
40 */
41 private static $handlers = [];
42
43 /**
44 * The array of async action handlers in queue
45 *
46 * @var array
47 */
48 private static $queue = [
49 'default' => []
50 ];
51
52 /**
53 * Creates the instance
54 *
55 * @return self
56 */
57 public static function init($app = null)
58 {
59 $app = $app ?: App::make();
60
61 if (is_null(self::$instance)) {
62 self::$app = $app;
63 self::$instance = new static;
64 }
65
66 $action = self::$instance->makeAsyncHookAction();
67
68 self::$app->addAction(
69 "admin_post_{$action}", [self::$instance, 'handle']
70 );
71
72 self::$app->addAction(
73 "admin_post_nopriv_{$action}", [self::$instance, 'handle']
74 );
75
76 return self::$instance;
77 }
78
79 /**
80 * Makes the async hook action name
81 *
82 * @return string
83 */
84 public function makeAsyncHookAction()
85 {
86 $slug = self::$app->config->get('app.slug');
87
88 return "wpfluent_async_hook_{$slug}";
89 }
90
91 /**
92 * Handles the incoming async request
93 *
94 * @return void
95 */
96 public function handle()
97 {
98 $post = self::$app->request->post();
99
100 $this->verifyRequest(self::$app, $post);
101
102 $handlers = Arr::get($post, 'handlers', []);
103
104 foreach ($handlers as $handler) {
105 try {
106 [$class, $action] = $this->resolveHandler($handler);
107
108 $this->execute(self::$app, $class, $action['params'] ?? []);
109
110 } catch (Exception $e) {
111 error_log($e->getMessage());
112 }
113 }
114 }
115
116 /**
117 * Verify the request by checking the nonce.
118 *
119 * @param \FluentCommunity\Framework\Foundation\Application $app
120 * @param array $data
121 * @return void
122 */
123 protected function verifyRequest($app, $data)
124 {
125 if (!isset($data['wpfluent_async_nonce'])) {
126 exit;
127 }
128
129 !wp_verify_nonce(
130 $data['wpfluent_async_nonce'],
131 $app->config->get('app.slug')
132 ) && exit;
133 }
134
135 /**
136 * Resolve the action handler.
137 *
138 * @param array $action
139 * @return array
140 */
141 protected function resolveHandler($action)
142 {
143 if (json_last_error() !== JSON_ERROR_NONE) {
144 throw new InvalidArgumentException("Invalid action.");
145 }
146
147 $handler = base64_decode($action['handler']);
148
149 [$class, $method] = explode('@', $handler);
150
151 if (!class_exists($class)) {
152 throw new InvalidArgumentException(
153 "Handler {$class} does not exist."
154 );
155 }
156
157 return [$class.'@'.$method, $action];
158 }
159
160 /**
161 * Execute the action handler.
162 *
163 * @param \FluentCommunity\Framework\Foundation\Application $app
164 * @param string $class
165 * @param array $params
166 * @return void
167 */
168 protected function execute($app, $class, $params = [])
169 {
170 set_time_limit(0);
171 ignore_user_abort(true);
172 [$class, $method] = explode('@', $class);
173 $app->make($class)->{$method}($app, $params);
174 }
175
176 /**
177 * Add the async handler and register the shutdown handler
178 * All the handlers will be dispatched in a separate request
179 *
180 * @param string $handler (Class@handler or with __invoke method) $handler
181 * @return self
182 * @throws \InvalidArgumentException
183 */
184 public static function call($handler, array $params = [])
185 {
186 if (!self::$instance) {
187 static::init();
188 }
189
190 self::$handlers[] = self::$instance->validate(
191 $handler, $params, static::sign(debug_backtrace(false, 1)[0])
192 );
193
194 return self::$instance->maybeRegisterShutDownHandler();
195 }
196
197 /**
198 * Queue an async handler to be executed during shutdown.
199 *
200 * Queued handlers are grouped by queue name and dispatched
201 * together in a single async HTTP request.
202 *
203 * @param string $handler The handler 'Class@method'|invokable class.
204 * @param array|string $params Array of args or the queue name if a string.
205 * @param string $name The name of the queue (default is 'default').
206 * @return self
207 *
208 * @throws \InvalidArgumentException
209 */
210 public static function queue(
211 $handler, $params = [], $name = 'default'
212 ) {
213 if (!self::$instance) {
214 static::init();
215 }
216
217 if (is_string($params)) {
218 $name = $params;
219 $params = [];
220 }
221
222 self::$queue[$name][] = self::$instance->validate(
223 $handler, $params, static::sign(debug_backtrace(false, 1)[0])
224 );
225
226 return self::$instance->maybeRegisterShutDownHandler();
227 }
228
229 /**
230 * Sign the handler to mark as dispatched.
231 *
232 * @param array $handler
233 * @return string
234 */
235 protected static function sign($handler)
236 {
237 return md5($handler['file'] . $handler['line']);
238 }
239
240 /**
241 * Validate the handler and add a sign to mark as dispatched.
242 *
243 * @param string $handler (Class@handler or with __invoke method) $handler
244 * @return array
245 * @throws \InvalidArgumentException
246 */
247 public function validate($handler, $params, $sign)
248 {
249 $method = '__invoke';
250
251 if (is_array($handler)) {
252 if (is_object($handler[0])) {
253 $handler[0] = get_class($handler[0]);
254 }
255 $handler = $handler[0] . '@' . $handler[1];
256 }
257
258 if (str_contains($handler, '@')) {
259 [$handler, $method] = explode('@', $handler);
260 }
261
262 if (!class_exists($handler)) {
263 throw new InvalidArgumentException(
264 "Class {$handler} not found."
265 );
266 }
267
268 if (!method_exists($handler, $method)) {
269 throw new InvalidArgumentException(
270 "Class {$handler} must implement __invoke or specify method."
271 );
272 }
273
274 $handler = $handler.'@'.$method;
275
276 return [
277 'sign' => $sign,
278 'params' => $params,
279 'handler' => base64_encode($handler),
280 ];
281 }
282
283 /**
284 * Register the shutdown handler
285 *
286 * @return self
287 */
288 protected function maybeRegisterShutDownHandler()
289 {
290 $handler = [self::$instance, 'dispatch'];
291
292 if (!self::$app->hasAction('shutdown', $handler)) {
293 self::$app->addAction('shutdown', $handler);
294 }
295
296 return self::$instance;
297 }
298
299 /**
300 * Dispatches the async request
301 *
302 * @return void
303 */
304 public function dispatch()
305 {
306 $stacks = array_filter([
307 array_filter(self::$queue),
308 array_filter(self::$handlers),
309 ]);
310
311 // At first we need to mark all the handlers from all
312 // the stacks as dispatched before sending any request.
313 foreach ($stacks as $key => $stack) {
314 $stacks[$key] = $this->getDispatchables($stack);
315 }
316
317 // Now we can dispatch them all
318 foreach ($stacks as $stack) {
319 foreach ($stack as $handler) {
320 $this->sendAsyncRequest($this->wrap($handler));
321 }
322 }
323 }
324
325 /**
326 * Filter the handlers to be dispatched.
327 *
328 * @param array $stack
329 * @return array|null
330 */
331 protected function getDispatchables($stack)
332 {
333 // If the stack is an array of associtive arrays we
334 // need to get the first one because queued handlers
335 // will containn one associtive array in the stack.
336 $stack = !isset($stack[0]) ? reset($stack) : $stack;
337
338 return array_filter($stack, function ($handler) {
339 if (isset($handler['sign'])) {
340 $isDispatched = in_array(
341 $handler['sign'],
342 self::$app->request->post('dispatched', [])
343 );
344
345 if (!$isDispatched) {
346 $this->dispatched[] = $handler['sign'];
347 return !$isDispatched;
348 }
349 }
350 });
351 }
352
353 /**
354 * Wrap with an array if necessary. Only used for separate
355 * handlers because queued handlers will be an array of
356 * associative arrays and we treat all the stacks same.
357 *
358 * @param array $handlers
359 * @return array of array(s)
360 */
361 protected function wrap($handlers)
362 {
363 return isset($handlers[0]) ? $handlers : [$handlers];
364 }
365
366 /**
367 * Send the real async request
368 *
369 * @param array $handler
370 * @return mixed
371 */
372 public function sendAsyncRequest(array $handler)
373 {
374 Helper::retry(3, function () use ($handler) {
375 return $this->sendRequest(
376 $this->url(),
377 $this->data($handler),
378 ['cookie' => $this->getCookie()]
379 );
380 }, 2000, function ($e) {
381 return str_contains($e->getMessage(), 'cURL');
382 });
383 }
384
385 /**
386 * Prepare the request body/POST data.
387 *
388 * @param string|array $handler
389 * @return array
390 */
391 protected function data($handler)
392 {
393 $post = self::$app->request->post();
394
395 $data = [
396 'handlers' => $handler,
397 'wpfluent_async_nonce' => wp_create_nonce(
398 self::$app->config->get('app.slug')
399 ),
400 'dispatched' => array_unique(array_merge(
401 Arr::get($post, 'dispatched', []),
402 $this->dispatched,
403 )),
404 ];
405
406 return array_merge($data, Arr::except($post, [
407 'handlers', 'wpfluent_async_nonce', 'dispatched'
408 ]));
409 }
410
411 /**
412 * Build the request url.
413 *
414 * @return string
415 */
416 protected function url()
417 {
418 return admin_url('admin-post.php') . '?' . http_build_query(
419 array_merge(
420 self::$app->request->query(),
421 ['action' => $this->makeAsyncHookAction()]
422 )
423 );
424 }
425
426 /**
427 * Send the non-blocking request.
428 *
429 * @param string $url
430 * @param array $body
431 * @param array $headers
432 * @return mixed
433 */
434 protected function sendRequest($url, $body = [], $headers = [])
435 {
436 return wp_remote_post($url, [
437 'timeout' => 0.01,
438 'blocking' => false,
439 'sslverify' => false,
440 'body' => $body,
441 'headers' => $headers,
442 ]);
443 }
444
445 /**
446 * Get the cookie to send with the request
447 * @return string Cookie string
448 */
449 protected function getCookie()
450 {
451 $cookies = [];
452
453 foreach ($_COOKIE as $name => $value) {
454 $cookies[] = "$name=" . urlencode(
455 is_array($value) ? serialize($value) : $value
456 );
457 }
458
459 return implode('; ', $cookies);
460 }
461 }
462