seacms-api/SeacmsApi.php
2023-03-16 23:03:14 +01:00

314 lines
9.9 KiB
PHP

<?php
// SPDX-License-Identifier: EUPL-1.2
// Authors: see README.md
use SeaCMS\Api\ApiAware;
use SeaCMS\Api\BadMethodException;
use SeaCMS\Api\Cookies;
use SeaCMS\Api\JsonResponse;
use SeaCMS\Api\LateApiAware;
use SeaCMS\Api\NotFoundRouteException;
use SeaCMS\Api\SpecialOutputException;
/**
* An api plugin for Pico 3.
*/
class SeacmsApi extends AbstractPicoPlugin implements ApiAware
{
/**
* Pico API version.
* @var int
*/
const API_VERSION = 4;
/**
* api routes
* @var array
*/
protected $routes ;
/**
* cookies to send
* @var Cookies
*/
protected $cookies;
/**
* Constructs a new instance of a Pico plugin
*
* @param Pico $pico current instance of Pico
*/
public function __construct(Pico $pico)
{
parent::__construct($pico);
$this->routes = [];
$this->cookies= new Cookies();
}
/**
* return $cookies
* @return Cookies
*/
public function getCookies(): Cookies
{
return $this->cookies;
}
/**
* return api routes
* @return array
*/
public function registerApiRoutes():array
{
return [
'POST test' => 'api',
'GET test/cookies/(.*)' => 'apiTestCookie',
'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]);
}
/**
* method to test cookie via api
* @param string $text
* @return JsonResponse
*/
public function apiTestCookie(string $text): JsonResponse
{
if (empty($text)){
$text= '';
}
$this->cookies->addCookie(
'Test-Cookie',
$text,
time()+3,
!empty($_SERVER['SCRIPT_NAME']) ? dirname($_SERVER['SCRIPT_NAME']) : '/',
!empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''
);
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,false];
}
}
}
if ($plugin instanceof LateApiAware){
$routes = $plugin->registerLateApiRoutes();
if (is_array($routes)){
foreach($routes as $route => $methodName){
if (is_string($methodName) && method_exists($plugin,$methodName)){
$this->routes[$route] = [$plugin,$methodName,true];
}
}
}
}
}
}
}
/**
* trig api
* Triggered before Pico loads its theme
* use this event because could be reach in 15 ms instead of 60 ms after rendering
*
* @see Pico::loadTheme()
* @see DummyPlugin::onThemeLoaded()
*
* @param string &$theme name of current theme
*/
public function onThemeLoading(&$theme)
{
$output = null;
if ($this->resolveApi($output)){
if (!($output instanceof JsonResponse)){
throw new Exception("Return of resolveApi should be JsonResponse at this point", 1);
} else {
throw new SpecialOutputException($output);
}
}
}
/**
* send cookies of not already sent.
*
* Triggered after Pico has rendered the page
*
* @param string &$output contents which will be sent to the user
* @return void
*/
public function onPageRendered(&$output)
{
if ($this->resolveApi($output,true)){
$output = $output->send();
}
if (JsonResponse::canSendHeaders()){
$this->getCookies()->sendCookiesOnce();
}
}
/**
* resolve api
* @param null|string|JsonResponse $output
* @param bool $isLate
* @return bool $outputChanged
*/
protected function resolveApi(&$output, bool $isLate = false): 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);
if (empty($route)){
$output = new JsonResponse(404,['code'=>404,'reason'=>'Empty api route'],[],$this->cookies);
} elseif (!preg_match('/^[A-Za-z0-9_\-.\/]+$/',$route)) {
$output = new JsonResponse(404,['code'=>404,'reason'=>"Route '$route' use forbidden characters !"],[],$this->cookies);
} else {
ob_start();
$response = null;
try {
$data = $this->searchCorrespondingRoute($route);
if ($data['isLate'] !== $isLate){
if ($isLate){
return new Exception('Calling an api route but catch onPageRedered whereas should be caught onThemeLoading !');
} else {
ob_end_clean();
return false;
}
}
$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'");
} else {
$response->setCookies($this->cookies);
}
} 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,[],$this->cookies));
} elseif (!empty($rawOutput)) {
$response->mergeInContent(compact(['rawOutput']));
}
$output = $response;
}
$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,
'isLate' => $data[2]
];
} 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('');
}
}