PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.41
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.41
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Foundation / Async.php

Async.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.41, at vendor/wpfluent/framework/src/WPFluent/Foundation/Async.php

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