feat(api): create
This commit is contained in:
parent
e20d695fc3
commit
e4cdba842c
174
SeacmsApi.php
174
SeacmsApi.php
@ -2,6 +2,10 @@
|
|||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
// Authors: see README.md
|
// Authors: see README.md
|
||||||
|
|
||||||
|
use SeaCMS\Api\BadMethodException;
|
||||||
|
use SeaCMS\Api\JsonResponse;
|
||||||
|
use SeaCMS\Api\NotFoundRouteException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An api plugin for Pico 3.
|
* An api plugin for Pico 3.
|
||||||
*/
|
*/
|
||||||
@ -13,6 +17,69 @@ class SeacmsApi extends AbstractPicoPlugin
|
|||||||
*/
|
*/
|
||||||
const API_VERSION = 3;
|
const API_VERSION = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* routes trigerred OnPageRendered
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $routesOnPageRendered ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return api routes
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function registerOnPageRenderedApiRoutes():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->routesOnPageRendered = [];
|
||||||
|
foreach($plugins as $plugin){
|
||||||
|
if (method_exists($plugin,'registerOnPageRenderedApiRoutes')){
|
||||||
|
$routes = $plugin->registerOnPageRenderedApiRoutes();
|
||||||
|
if (is_array($routes)){
|
||||||
|
foreach($routes as $route => $methodName){
|
||||||
|
if (is_string($methodName) && method_exists($plugin,$methodName)){
|
||||||
|
$this->routesOnPageRendered[$route] = [$plugin,$methodName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the call is a save query, save the edited regions and output the JSON response.
|
* If the call is a save query, save the edited regions and output the JSON response.
|
||||||
*
|
*
|
||||||
@ -23,6 +90,111 @@ class SeacmsApi extends AbstractPicoPlugin
|
|||||||
*/
|
*/
|
||||||
public function onPageRendered(&$output)
|
public function onPageRendered(&$output)
|
||||||
{
|
{
|
||||||
// TODO
|
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']))->send();
|
||||||
|
} elseif (!preg_match('/^[A-Za-z0-9_\-.\/]+$/',$route)) {
|
||||||
|
$output = (new JsonResponse(404,['code'=>404,'reason'=>"Route '$route' use forbidden characters !"]))->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));
|
||||||
|
} elseif (!empty($rawOutput)) {
|
||||||
|
$response->mergeInContent(compact(['rawOutput']));
|
||||||
|
}
|
||||||
|
$output = $response->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->routesOnPageRendered)){
|
||||||
|
$data = $this->routesOnPageRendered["$method $searchingRoute"];
|
||||||
|
} elseif (array_key_exists("$searchingRoute",$this->routesOnPageRendered)){
|
||||||
|
$data = $this->routesOnPageRendered["$searchingRoute"];
|
||||||
|
}
|
||||||
|
if (!empty($data)){
|
||||||
|
return [
|
||||||
|
'plugin' => $data[0],
|
||||||
|
'methodName' => $data[1],
|
||||||
|
'params' => $params
|
||||||
|
];
|
||||||
|
} elseif (!$badMethod && array_key_exists((($method == 'GET') ? 'POST' : 'GET' )." $searchingRoute",$this->routesOnPageRendered)){
|
||||||
|
$badMethod = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($badMethod){
|
||||||
|
throw new BadMethodException('Not allowed method');
|
||||||
|
}
|
||||||
|
throw new NotFoundRouteException('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,9 @@
|
|||||||
"php": "^8.0"
|
"php": "^8.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"SeaCMS\\Api\\": "src"
|
||||||
|
},
|
||||||
"classmap": [ "SeacmsApi.php" ]
|
"classmap": [ "SeacmsApi.php" ]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
13
src/BadMethodException.php
Normal file
13
src/BadMethodException.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
// Authors: see README.md
|
||||||
|
|
||||||
|
namespace SeaCMS\Api;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for bad http method
|
||||||
|
*/
|
||||||
|
class BadMethodException extends Exception
|
||||||
|
{}
|
206
src/JsonResponse.php
Normal file
206
src/JsonResponse.php
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
// Authors: see README.md
|
||||||
|
|
||||||
|
namespace SeaCMS\Api;
|
||||||
|
|
||||||
|
use JsonSerializable;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response for http response
|
||||||
|
*/
|
||||||
|
class JsonResponse implements JsonSerializable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Codes
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
const HTTP_CODES = [
|
||||||
|
200 => 'OK',
|
||||||
|
301 => 'Moved Permanently',
|
||||||
|
302 => 'Found',
|
||||||
|
304 => 'Not Modified',
|
||||||
|
307 => 'Temporary Redirect',
|
||||||
|
308 => 'Permanent Redirect',
|
||||||
|
400 => 'Bad Request',
|
||||||
|
401 => 'Unauthorized',
|
||||||
|
403 => 'Forbidden',
|
||||||
|
404 => 'Not Found',
|
||||||
|
405 => 'Method Not Allowed',
|
||||||
|
406 => 'Not Acceptable',
|
||||||
|
408 => 'Request Timeout',
|
||||||
|
500 => 'Internal Server Error',
|
||||||
|
501 => 'Not Implemented',
|
||||||
|
503 => 'Service Unavailable',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP CODE
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $code;
|
||||||
|
/**
|
||||||
|
* content as array
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $content;
|
||||||
|
/**
|
||||||
|
* headers
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $headers;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(int $code, array $content, array $headers = []){
|
||||||
|
$this->code = array_key_exists($code, self::HTTP_CODES) ? $code : 501; // default
|
||||||
|
$this->content = $content;
|
||||||
|
$this->headers = array_merge([
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Access-Control-Allow-Credentials' => 'true',
|
||||||
|
'Access-Control-Allow-Headers' => 'X-Requested-With, Location, Slug, Accept, Content-Type',
|
||||||
|
'Access-Control-Expose-Headers' => 'Location, Slug, Accept, Content-Type',
|
||||||
|
'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, DELETE, PUT, PATCH',
|
||||||
|
'Access-Control-Max-Age' => '86400'
|
||||||
|
], $headers);
|
||||||
|
$this->preparedOutput = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* merge in content
|
||||||
|
* @param array $newContent
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function mergeInContent(array $newContent){
|
||||||
|
$this->content = array_merge(
|
||||||
|
$this->content,
|
||||||
|
$newContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* prepend in content
|
||||||
|
* @param array $newContent
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
protected function prependInContent(array $newContent){
|
||||||
|
$this->content = array_merge(
|
||||||
|
$newContent,
|
||||||
|
$this->content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set code
|
||||||
|
* @param int
|
||||||
|
*/
|
||||||
|
public function setCode(int $code)
|
||||||
|
{
|
||||||
|
$this->code = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Getters === */
|
||||||
|
/**
|
||||||
|
* return code
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getCode(): int
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* return content
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getContent(): array
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === === */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends HTTP headers.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function sendHeaders(): JsonResponse
|
||||||
|
{
|
||||||
|
// headers have already been sent by the developer
|
||||||
|
if (!headers_sent()) {
|
||||||
|
|
||||||
|
// headers
|
||||||
|
foreach ($this->headers as $name => $value) {
|
||||||
|
header($name.': '.$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// status
|
||||||
|
$statusText = self::HTTP_CODES[$this->code];
|
||||||
|
header("HTTP/1.0 {$this->code} $statusText");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send headers and return output
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function send(): string
|
||||||
|
{
|
||||||
|
return $this->preparedOutput()->prepareStatusText()->sendHeaders()->returnContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if output is JSONSerializable
|
||||||
|
* @return JsonResponse $this
|
||||||
|
*/
|
||||||
|
protected function preparedOutput(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
json_encode($this->content);
|
||||||
|
} catch (Throwable $th) {
|
||||||
|
$this->code = 500;
|
||||||
|
$this->content = ['code' => 500, 'reason' =>"Not possible to JSONSerialize content"];
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prepare statusText
|
||||||
|
* @return JsonResponse $this
|
||||||
|
*/
|
||||||
|
protected function prepareStatusText(): JsonResponse
|
||||||
|
{
|
||||||
|
if (!array_key_exists($this->code,self::HTTP_CODES)){
|
||||||
|
$previousCode = intval($this->code);
|
||||||
|
$this->code = 501;
|
||||||
|
$this->prependInContent(['code' => 501, 'reason' =>"Wanted code ($previousCode) is not implemented !"]);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return content as String
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function returnContent(): string
|
||||||
|
{
|
||||||
|
return json_encode($this->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* export class as array
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function jsonSerialize(): mixed
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => $this->getCode(),
|
||||||
|
'content' => $this->getContent(),
|
||||||
|
'headers' => $this->headers,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
13
src/NotFoundRouteException.php
Normal file
13
src/NotFoundRouteException.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
// Authors: see README.md
|
||||||
|
|
||||||
|
namespace SeaCMS\Api;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception when route not found
|
||||||
|
*/
|
||||||
|
class NotFoundRouteException extends Exception
|
||||||
|
{}
|
Loading…
x
Reference in New Issue
Block a user