219 lines
7.0 KiB
PHP
219 lines
7.0 KiB
PHP
<?php
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
// Authors: see README.md
|
|
|
|
use Pico;
|
|
use SeaCMS\Api\ApiAware;
|
|
use SeaCMS\Api\BadMethodException;
|
|
use SeaCMS\Api\JsonResponse;
|
|
use SeaCMS\Api\NotFoundRouteException;
|
|
|
|
/**
|
|
* An api plugin for Pico 3.
|
|
*/
|
|
class SeacmsApi extends AbstractPicoPlugin implements ApiAware
|
|
{
|
|
/**
|
|
* Pico API version.
|
|
* @var int
|
|
*/
|
|
const API_VERSION = 3;
|
|
|
|
/**
|
|
* api routes
|
|
* @var array
|
|
*/
|
|
protected $routes ;
|
|
|
|
/**
|
|
* return api routes
|
|
* @return array
|
|
*/
|
|
public function registerApiRoutes():array
|
|
{
|
|
return [
|
|
'POST test' => 'api',
|
|
'GET test/(.*)' => 'apiWithText',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* method for api
|
|
* @return JsonResponse
|
|
*/
|
|
public function api(): JsonResponse
|
|
{
|
|
return new JsonResponse(200,['test'=>'OK']);
|
|
}
|
|
|
|
/**
|
|
* method for api
|
|
* @param string $text
|
|
* @return JsonResponse
|
|
*/
|
|
public function apiWithText(string $text): JsonResponse
|
|
{
|
|
return new JsonResponse(200,['text'=>$text]);
|
|
}
|
|
|
|
/**
|
|
* Triggered after Pico has loaded all available plugins
|
|
*
|
|
* This event is triggered nevertheless the plugin is enabled or not.
|
|
* It is NOT guaranteed that plugin dependencies are fulfilled!
|
|
*
|
|
*
|
|
* @param object[] $plugins loaded plugin instances
|
|
*/
|
|
public function onPluginsLoaded(array $plugins)
|
|
{
|
|
$this->routes = [];
|
|
foreach($plugins as $plugin){
|
|
if ($plugin instanceof ApiAware){
|
|
$routes = $plugin->registerApiRoutes();
|
|
if (is_array($routes)){
|
|
foreach($routes as $route => $methodName){
|
|
if (is_string($methodName) && method_exists($plugin,$methodName)){
|
|
$this->routes[$route] = [$plugin,$methodName];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the call is a save query, save the edited regions and output the JSON response.
|
|
*
|
|
* Triggered after Pico has rendered the page
|
|
*
|
|
* @param string &$output contents which will be sent to the user
|
|
* @return void
|
|
*/
|
|
public function onPageRendered(&$output)
|
|
{
|
|
$this->resolveApi($output);
|
|
}
|
|
|
|
/**
|
|
* resolve api
|
|
* @param string $$output
|
|
* @return bool $outputChanged
|
|
*/
|
|
protected function resolveApi(string &$output): bool
|
|
{
|
|
$outputChanged = false;
|
|
if (isset($_GET['api'])){
|
|
$route = $this->getPico()->getUrlParameter(
|
|
'api',
|
|
FILTER_UNSAFE_RAW,
|
|
[
|
|
'default' => ''
|
|
],
|
|
[
|
|
FILTER_FLAG_STRIP_LOW,
|
|
FILTER_FLAG_STRIP_HIGH
|
|
]
|
|
);
|
|
$route = trim($route);
|
|
$callable = function() {
|
|
$this->getPico()->triggerEvent('sendCookies');
|
|
};
|
|
if (empty($route)){
|
|
$output = (new JsonResponse(404,['code'=>404,'reason'=>'Empty api route'],[],$callable))->send();
|
|
} elseif (!preg_match('/^[A-Za-z0-9_\-.\/]+$/',$route)) {
|
|
$output = (new JsonResponse(404,['code'=>404,'reason'=>"Route '$route' use forbidden characters !"],[],$callable))->send();
|
|
} else {
|
|
ob_start();
|
|
$response = null;
|
|
try {
|
|
$data = $this->searchCorrespondingRoute($route);
|
|
$response = call_user_func_array([$data['plugin'],$data['methodName']],$data['params']);
|
|
if (!($response instanceof JsonResponse)){
|
|
$response = null;
|
|
throw new Exception("Return of '{$data['methodName']}' should be instanceof of 'JsonResponse'");
|
|
}
|
|
} catch (BadMethodException $th) {
|
|
$code = 405;
|
|
$content = ['reason'=>$th->getMessage()];
|
|
} catch (NotFoundRouteException $th) {
|
|
$code = 404;
|
|
$content = ['reason'=>"Route '$route' not found !"];
|
|
} catch (Throwable $th) {
|
|
$code = 500;
|
|
$content = ['reason'=>$th->__toString()];
|
|
}
|
|
$rawOutput = ob_get_contents();
|
|
ob_end_clean();
|
|
if (empty($response)){
|
|
if (!empty($rawOutput)){
|
|
$content['rawOutput'] = $rawOutput;
|
|
}
|
|
$content = array_merge(['code'=>$code],$content);
|
|
$response = (new JsonResponse($code,$content,[],$callable));
|
|
} elseif (!empty($rawOutput)) {
|
|
$response->mergeInContent(compact(['rawOutput']));
|
|
}
|
|
$output = $response->send();
|
|
}
|
|
$outputChanged = true;
|
|
}
|
|
return $outputChanged;
|
|
}
|
|
|
|
/**
|
|
* search corresponding route
|
|
* @param string $route
|
|
* @return array ['plugin'=>$plugin,'methodName'=>string,'params'=>array]
|
|
* @throws BadMethodException
|
|
* @throws NotFoundRouteException
|
|
*/
|
|
protected function searchCorrespondingRoute(string $route): array
|
|
{
|
|
if (empty($_SERVER['REQUEST_METHOD'])){
|
|
throw new BadMethodException('Method not defined');
|
|
}
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
if (!in_array($method,['GET','POST'],true)){
|
|
throw new BadMethodException('Not allowed method');
|
|
}
|
|
$splittedRoute = explode('/',$route);
|
|
|
|
$nb = count($splittedRoute);
|
|
$badMethod = false;
|
|
for ($i=0; $i < 2**$nb; $i++) {
|
|
$params = [];
|
|
$splittedRouteFiltered = [];
|
|
foreach ($splittedRoute as $idx => $value) {
|
|
$currentPower = $nb - $idx - 1 ;
|
|
if ((2**$currentPower & $i) > 0){
|
|
$splittedRouteFiltered[] = '(.*)';
|
|
$params[] = $value;
|
|
} else {
|
|
$splittedRouteFiltered[] = $value;
|
|
}
|
|
}
|
|
$searchingRoute = implode('/',$splittedRouteFiltered);
|
|
$data = [];
|
|
if (array_key_exists("$method $searchingRoute",$this->routes)){
|
|
$data = $this->routes["$method $searchingRoute"];
|
|
} elseif (array_key_exists("$searchingRoute",$this->routes)){
|
|
$data = $this->routes["$searchingRoute"];
|
|
}
|
|
if (!empty($data)){
|
|
return [
|
|
'plugin' => $data[0],
|
|
'methodName' => $data[1],
|
|
'params' => $params
|
|
];
|
|
} elseif (!$badMethod && array_key_exists((($method == 'GET') ? 'POST' : 'GET' )." $searchingRoute",$this->routes)){
|
|
$badMethod = true;
|
|
}
|
|
}
|
|
if ($badMethod){
|
|
throw new BadMethodException('Not allowed method');
|
|
}
|
|
throw new NotFoundRouteException('');
|
|
}
|
|
}
|