PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.20
VikAppointments Services Booking Calendar v1.2.20
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / api / api.php
vikappointments / site / helpers / libraries / api Last commit date
implementors 1 month ago plugins 1 month ago api.php 1 month ago autoload.php 1 month ago error.php 1 month ago event.php 1 month ago index.html 1 month ago response.php 1 month ago user.php 1 month ago
api.php
835 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * VikAppointments API base framework.
16 * This class is used to run all the installed plugins in a given directory.
17 *
18 * All the events are runnable only if the user is correctly authenticated.
19 *
20 * @see VAPApiUser
21 * @see VAPApiResponse
22 * @see VAPApiError
23 * @see VAPApiEvent
24 *
25 * @since 1.7
26 */
27 abstract class VAPApi
28 {
29 /**
30 * The path of the folder containing all the available plugins.
31 *
32 * @var array
33 */
34 private $includePaths = array();
35
36 /**
37 * True if the API framework is enabled and accessible.
38 *
39 * @var boolean
40 */
41 private $enabled = true;
42
43 /**
44 * The instance of the user which is using the API framework.
45 *
46 * @var VAPApiUser
47 */
48 private $user = null;
49
50 /**
51 * The last error caught.
52 *
53 * @var VAPApiError
54 */
55 private $error = null;
56
57 /**
58 * The array that contains the configuration keys.
59 *
60 * @var array
61 */
62 private $config = array();
63
64 /**
65 * Flag used to avoid sending the headers while outputting the events data.
66 *
67 * @var boolean
68 */
69 protected $sendHeaders = true;
70
71 /**
72 * The instance of the API framework.
73 *
74 * @var VAPApi
75 */
76 protected static $instance = null;
77
78 /**
79 * Class constructor.
80 * @protected This class can be accessed only through the static getInstance() method.
81 *
82 * @param string $path The dir path containing all the plugins.
83 *
84 * @see getInstance()
85 */
86 protected function __construct($path = null)
87 {
88 if (empty($path))
89 {
90 // use default folder if not specified
91 $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'plugins';
92 }
93
94 // set include paths
95 $this->setIncludePaths($path);
96 }
97
98 /**
99 * Class cloner.
100 */
101 private function __clone()
102 {
103 // cloning function not accessible
104 }
105
106 /**
107 * Get the instance of the API object.
108 *
109 * @param string $path The dir path containing all the plugins.
110 *
111 * @return VAPApi The instance of the API framework.
112 */
113 public static function getInstance($path = null)
114 {
115 if (static::$instance === null)
116 {
117 static::$instance = new static($path);
118 }
119
120 return static::$instance;
121 }
122
123 /**
124 * Return true if the APIs framework is enabled and accessible.
125 *
126 * @return boolean True if enabled, otherwise false.
127 */
128 public function isEnabled()
129 {
130 return $this->enabled;
131 }
132
133 /**
134 * Enable the API framework.
135 *
136 * @return self This object to support chaining.
137 */
138 protected function enable()
139 {
140 $this->enabled = true;
141
142 return $this;
143 }
144
145 /**
146 * Disable the API framework.
147 *
148 * @return self This object to support chaining.
149 */
150 protected function disable()
151 {
152 $this->enabled = false;
153
154 return $this;
155 }
156
157 /**
158 * Return true if the user is correctly logged.
159 *
160 * @return boolean True if logged, otherwise false.
161 */
162 public function isConnected()
163 {
164 return $this->user !== null && $this->user->id();
165 }
166
167 /**
168 * Return the object of the logged user.
169 *
170 * @return VAPApiUser The object of the user connected, otherwise NULL.
171 */
172 public function getUser()
173 {
174 return $this->user;
175 }
176
177 /**
178 * Disconnect the user.
179 *
180 * @return self This object to support chaining.
181 */
182 public function disconnect()
183 {
184 $this->user = null;
185
186 return $this;
187 }
188
189 /**
190 * Get the path of the specified event.
191 *
192 * @return mixed The event path if exists, false otherwise.
193 */
194 public function getEventPath($event)
195 {
196 // get all include paths
197 $paths = $this->getIncludePaths();
198
199 // trim trailing .php from event name
200 $event = preg_replace("/\.php$/i", '', $event);
201
202 // iterate supported paths
203 foreach ($paths as $path)
204 {
205 // build event path
206 $tmp = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $event . '.php';
207
208 // make sure the file exists
209 if (is_file($tmp))
210 {
211 return $tmp;
212 }
213 }
214
215 return false;
216 }
217
218 /**
219 * Gets a list of supported include paths.
220 *
221 * @return array
222 */
223 public function getIncludePaths()
224 {
225 return $this->includePaths;
226 }
227
228 /**
229 * Adds one path to include in plugin search.
230 * Proxy of addIncludePaths().
231 *
232 * @param string $path The path to search for plugins.
233 *
234 * @return self This object to support chaining.
235 *
236 * @uses addIncludePaths()
237 */
238 public function addIncludePath($path)
239 {
240 return $this->addIncludePaths($path);
241 }
242
243 /**
244 * Adds one or more paths to include in plugin search.
245 *
246 * @param mixed $paths The path or array of paths to search for plugins.
247 *
248 * @return self This object to support chaining.
249 *
250 * @uses getIncludePaths()
251 * @uses setIncludePaths()
252 */
253 public function addIncludePaths($paths)
254 {
255 if (empty($paths))
256 {
257 return $this;
258 }
259
260 $includePaths = $this->getIncludePaths();
261
262 // in case the path is an array, merge all the paths and make sure we have no duplicated
263 if (is_array($paths))
264 {
265 $includePaths = array_unique(array_merge($includePaths, $paths));
266 }
267 // otherwise add the path as first element
268 else
269 {
270 $includePaths[] = $paths;
271 }
272
273 // update include paths
274 $this->setIncludePaths($includePaths);
275
276 return $this;
277 }
278
279 /**
280 * Sets the include paths to search for plugins.
281 *
282 * @param array $paths Array with paths to search in.
283 *
284 * @return self This object to support chaining.
285 */
286 public function setIncludePaths($paths)
287 {
288 $this->includePaths = (array) $paths;
289
290 return $this;
291 }
292
293 /**
294 * Connect the specified user to the API framework.
295 *
296 * In case the login fails, here is evaluated a permanent BAN.
297 * Otherwise the MANIFEST of the user is updated and the BAN is reset.
298 *
299 * This method can raise the following internal errors:
300 * - 100 = Authentication Error (Generic)
301 * - 101 = The username is empty or invalid
302 * - 102 = The password is empty or invalid
303 * - 104 = The account is blocked
304 *
305 * @param VAPApiUser $user The object to represent the user login.
306 *
307 * @return boolean True if the user is accepted, otherwise false.
308 */
309 public function connect(VAPApiUser $user)
310 {
311 // check if API framework is enabled
312 if (!$this->isEnabled())
313 {
314 // do not log anything and stop flow
315 return false;
316 }
317
318 // check if the user is banned
319 // and the user is connectable
320 // and the login connection returns a valid user ID
321 if (
322 !($banned = $this->isBanned($user))
323 && $user->isConnectable()
324 && ($id_user = $this->doConnection($user)) !== false
325 ) {
326 // setup the user and fill the ID
327 $this->user = $user;
328 $this->user->assign($id_user);
329
330 // update user manifest
331 $this->updateUserManifest();
332
333 $this->resetBan($this->user);
334
335 return true;
336 }
337
338 // login failed : if user is not yet banned, evaluate a ban
339 if (!$banned && $this->needBan($user))
340 {
341 // ban the user
342 $this->ban($user);
343 }
344
345 // only if the user is not banned
346 // register the failure of the login (no event is reported)
347 if (!$banned)
348 {
349 $credentials = $user->getCredentials();
350
351 $text = sprintf(
352 'Authentication Error! Authentication error for user {%s : %s}.',
353 $credentials->username,
354 $credentials->password
355 );
356
357 $this->registerEvent(null, new VAPApiResponse(0, $text));
358 }
359
360 if ($banned)
361 {
362 // set error : user banned
363 $this->setError(104, 'Authentication Error! This account is blocked.');
364 }
365 else if (!strlen($user->getUsername()))
366 {
367 // set error : username empty
368 $this->setError(101, 'Authentication Error! The username is empty or invalid.');
369 }
370 else if (!strlen($user->getPassword()))
371 {
372 // set error : password empty
373 $this->setError(102, 'Authentication Error! The password is empty or invalid.');
374 }
375 else if (!$this->hasError())
376 {
377 // no err specified yet : set a generic authentication error
378 $this->setError(100, 'Authentication Error!');
379 }
380
381 return false;
382 }
383
384 /**
385 * Trigger the specified event.
386 * Accessible only in case the user is correctly connected.
387 *
388 * This method can raise the following internal errors:
389 * - 100 = Authentication Error (Generic)
390 * - 201 = The event requested does not exist
391 * - 202 = The event requested is not valid
392 * - 203 = The event requested is not runnable
393 * - 204 = The event requested is not authorized
394 * - 500 = Internal error of the plugin executed
395 *
396 * The response of the plugin is always echoed.
397 *
398 * @param string $event The filename of the plugin to run.
399 * @param array $args The arguments to pass within the plugin.
400 * @param boolean $register True to register the response, otherwise false to skip it.
401 *
402 * @return boolean True if the plugin is executed without errors.
403 */
404 public function trigger($event, array $args = array(), $register = true)
405 {
406 // check if API framework is still enabled
407 if (!$this->isEnabled() || !$this->isConnected())
408 {
409 // this condition can be verified only when triggered manually
410 $this->setError(100, 'Authentication Error');
411 return false;
412 }
413
414 $obj = null;
415
416 $response = new VAPApiResponse();
417
418 if ($event)
419 {
420 // the event requested does not exist (?) : define response and error
421 $response->setStatus(0)->setContent('File Not Found! The event requested does not exist.');
422 $this->setError(201, $response->getContent());
423
424 $eventPath = $this->getEventPath($event);
425 }
426 else
427 {
428 // prevent fatal errors in case the event is missing
429 $response->setStatus(0)->setContent('Missing event.');
430 $this->setError(200, $response->getContent());
431
432 $eventPath = false;
433 }
434
435 if ($eventPath)
436 {
437 // COMMIT : the event exists
438
439 // the event is not valid (?) : define response and error
440 $response->setContent('Event Not Found! The event requested is not valid.');
441 $this->setError(202, $response->getContent());
442
443 $event_clazz = str_replace('_', ' ', $event);
444 $event_clazz = ucwords($event_clazz);
445 $event_clazz = str_replace(' ', '', $event_clazz);
446
447 $event_clazz = 'VAPApiEvent' . $event_clazz;
448
449 require_once $eventPath;
450
451 if (class_exists($event_clazz))
452 {
453 // COMMIT : the event is valid
454
455 // the event does not own a runnable method (?) : define response and error
456 $response->setContent('Run Method Not Accessible! The event requested is not runnable.');
457 $this->setError(203, $response->getContent());
458
459 // Invoke abstract method to load the configuration of the event.
460 // This way, the framework implementor can retrieve the preferences
461 // by using the preferred storage system.
462 $options = $this->loadEventConfig($event);
463
464 // instantiate runnable event
465 $obj = new $event_clazz($event, $options);
466
467 if ($obj instanceof VAPApiEvent)
468 {
469 // COMMIT : the event is runnable
470
471 // the user is not authorized to run the event (?) : define response and error
472 $response->setContent('Event Authorization Error! The event requested is not authorized.');
473 $this->setError(204, $response->getContent());
474
475 if ($this->user->authorise($obj))
476 {
477 // COMMIT : the user is authorized
478
479 // clear the response error
480 $response->clearContent();
481
482 try
483 {
484 // run the event, which is able to modify the response
485 $output = $obj->run($args, $response);
486 }
487 catch (Exception $e)
488 {
489 // Catch any exception that might have been thrown
490 // by the dispatched event. Generates an error
491 // according to the VAPApiError specifications.
492 $output = new VAPApiError($e->getCode(), $e->getMessage());
493 }
494
495 // invoke abstract method to save the configuration of the event
496 $this->saveEventConfig($obj);
497
498 // register request payload for extended logging
499 $response->setPayload($args);
500
501 if ($response->isVerified())
502 {
503 // call get error function to clean all
504 $this->getError();
505
506 // safely output the response fetched by the event
507 $this->output($output, $response->getContentType());
508 }
509 else
510 {
511 if ($output instanceof VAPApiError)
512 {
513 // set error retrieved from plugin
514 $this->setError($output);
515 }
516 else
517 {
518 // generic event error (500) : get details from response
519 $this->setError(500, $response->getContent());
520 }
521 }
522 }
523 }
524 }
525 }
526
527 // register event and response
528 if ($register)
529 {
530 $this->registerEvent($obj, $response);
531 }
532
533 return $response->isVerified();
534 }
535
536 /**
537 * Dispatch the specified event to catch the response echoed from the plugin.
538 * Accessible only in case the user is correctly connected.
539 *
540 * This method can raise the following internal errors:
541 * - 100 = Authentication Error (Generic)
542 * - 201 = The event requested does not exist
543 * - 202 = The event requested is not valid
544 * - 203 = The event requested is not runnable
545 * - 204 = The event requested is not authorized
546 * - 500 = Internal error of the plugin executed
547 *
548 * @param string $event The filename of the plugin to run.
549 * @param array $args The arguments to pass within the plugin.
550 * @param boolean $register True to register the response, otherwise false to skip it.
551 *
552 * @return string The response echoed from the plugin on success.
553 *
554 * @uses trigger() Trigger the event to catch the response.
555 *
556 * @throws Exception
557 */
558 public function dispatch($event, array $args = array(), $register = false)
559 {
560 // temporarily lock the headers
561 $headers = $this->sendHeaders;
562 $this->sendHeaders = false;
563
564 // start catching the response echoed
565 ob_start();
566 // trigger the plugin and get the verified status
567 $verified = $this->trigger($event, $args, $register);
568 // get the response echoed
569 $contents = ob_get_contents();
570 // stop catching
571 ob_end_clean();
572
573 // unlock the headers (by setting the previous value)
574 $this->sendHeaders = $headers;
575
576 if ($verified)
577 {
578 return $contents;
579 }
580
581 // get error
582 $err = $this->getError();
583 // raise exception
584 throw new Exception($err->error, $err->errcode);
585 }
586
587 /**
588 * Set the last error caught.
589 *
590 * @param mixed $code Either the error code identifier or the error instance.
591 * @param string $str A text description of the error.
592 *
593 * @return self This object to support chaining.
594 */
595 protected function setError($code, $str = '')
596 {
597 if ($code instanceof VAPApiError)
598 {
599 $this->error = $code;
600 }
601 else
602 {
603 $this->error = new VAPApiError($code, $str);
604 }
605
606 return $this;
607 }
608
609 /**
610 * Get the last error caught and clean it.
611 *
612 * @return VAPApiError The error object if exists, otherwise NULL.
613 */
614 public function getError()
615 {
616 $err = $this->error;
617 $this->error = null;
618 return $err;
619 }
620
621 /**
622 * Return true if an error has been raised.
623 *
624 * @return boolean True in case of error, otherwise false.
625 */
626 public function hasError()
627 {
628 return $this->error !== null;
629 }
630
631 /**
632 * Check if the specified key is set in the configuration.
633 *
634 * @param string $key The configuration key to check.
635 *
636 * @return boolean True if exists, otherwise false.
637 */
638 public function has($key)
639 {
640 return array_key_exists($key, $this->config);
641 }
642
643 /**
644 * Get the configuration value of the specified setting.
645 *
646 * @param string $key The key of the configuration value to get.
647 * @param mixed $def The default value if not exists.
648 *
649 * @return mixed The configuration value if exists, otherwise the default value.
650 *
651 * @uses has() Check if the setting exists.
652 */
653 public function get($key, $def = null)
654 {
655 if ($this->has($key))
656 {
657 return $this->config[$key];
658 }
659
660 return $def;
661 }
662
663 /**
664 * Set the configuration value for the specified setting.
665 *
666 * @param string $key The key of the configuration value to set.
667 * @param string $val The configuration value to set.
668 *
669 * @return self This object to support chaining.
670 */
671 public function set($key, $val)
672 {
673 $this->config[$key] = $val;
674
675 return $this;
676 }
677
678 /**
679 * Get the object of the given plugin name, otherwise return all the installed plugins if not specified.
680 *
681 * @param string $plg_name The name of the plugin to get.
682 * If not specified it will be replaced by "*" (all plugins).
683 *
684 * @return array A list of the plugins found.
685 */
686 public function getPluginsList($plg_name = '')
687 {
688 // if the plugin name is empty or NULL
689 if ($plg_name === null || empty($plg_name))
690 {
691 // get all the installed plugins
692 $plg_name = '*';
693 }
694
695 $paths = array();
696
697 foreach ($this->getIncludePaths() as $dir)
698 {
699 // retrieve all the plugin that match the query
700 $paths = array_merge($paths, glob(rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . "$plg_name.php"));
701 }
702
703 $plugins = array();
704
705 foreach ($paths as $p)
706 {
707 // require the plugin file
708 require_once $p;
709
710 // get the filename from full path
711 $event = substr($p, ($n = strrpos($p, DIRECTORY_SEPARATOR) + 1), strrpos($p, '.') - $n);
712
713 // convert the filename in classname
714 $event_clazz = str_replace('_', ' ', $event);
715 $event_clazz = ucwords($event_clazz);
716 $event_clazz = str_replace(' ', '', $event_clazz);
717
718 $event_clazz = 'VAPApiEvent' . $event_clazz;
719
720 if (class_exists($event_clazz))
721 {
722 $obj = new $event_clazz($event);
723
724 if ($obj instanceof VAPApiEvent)
725 {
726 $plugins[] = $obj;
727 }
728 }
729 }
730
731 // sort plugins by name since they might be located on different folders
732 usort($plugins, function($a, $b)
733 {
734 return strcmp($a->getName(), $b->getName());
735 });
736
737 return $plugins;
738 }
739
740 /**
741 * Authenticate the provided user and connect it on success.
742 *
743 * @param VAPApiUser $user The object of the user.
744 *
745 * @return integer The ID of the user on success, otherwise false.
746 */
747 abstract protected function doConnection(VAPApiUser $user);
748
749 /**
750 * Check if the provided user has been banned.
751 * This action is executed only before the authentication.
752 * The ban could be evaluated on the name of the user and on the IP origin.
753 *
754 * @param VAPApiUser $user The object of the user.
755 *
756 * @return boolean True is the user is banned, otherwise false.
757 */
758 abstract protected function isBanned(VAPApiUser $user);
759
760 /**
761 * Evaluates if the provided user needs to be banned.
762 * This action is executed only after a failed authentication.
763 *
764 * @param VAPApiUser $user The object of the user.
765 *
766 * @return boolean Return true if the user should be banned, otherwise false.
767 */
768 abstract protected function needBan(VAPApiUser $user);
769
770 /**
771 * Register a new ban for the provided user.
772 *
773 * @param VAPApiUser $user The object of the user.
774 */
775 abstract protected function ban(VAPApiUser $user);
776
777 /**
778 * Reset or remove the ban of the provided user.
779 *
780 * @param VAPApiUser $user The object of the user.
781 */
782 abstract protected function resetBan(VAPApiUser $user);
783
784 /**
785 * Register the provided event and response.
786 * This log should be visible only from the administrator.
787 *
788 * @param VAPApiEvent $event The event requested.
789 * @param VAPApiResponse $response The response caught or raised.
790 *
791 * @return boolean True if the event has been registered, otherwise false.
792 */
793 abstract protected function registerEvent(VAPApiEvent $event, VAPApiResponse $response);
794
795 /**
796 * Update the user manifest after a successful authentication.
797 *
798 * @return boolean True on success, otherwise false.
799 *
800 * @see getUser() to access the user object.
801 */
802 abstract protected function updateUserManifest();
803
804 /**
805 * Prepares the document to output the given data.
806 *
807 * @param mixed $data The data to output.
808 * @param mixed $type The content type.
809 *
810 * @return void
811 */
812 abstract public function output($data, $type = 'application/json');
813
814 /**
815 * Loads the configuration for the specified event and user.
816 * @userby APIs::trigger()
817 *
818 * @param string $eventName The name of the event.
819 * @param VAPApiUser $user The object of the user.
820 *
821 * @return mixed Either an array or an object.
822 */
823 abstract protected function loadEventConfig($eventName, ?VAPApiUser $user = null);
824
825 /**
826 * Saves the configuration for the specified event and user.
827 *
828 * @param VAPApiEvent $event The event requested.
829 * @param VAPApiUser $user The object of the user.
830 *
831 * @return boolean True on success, false otherwise.
832 */
833 abstract protected function saveEventConfig(VAPApiEvent $event, ?VAPApiUser $user = null);
834 }
835