Skip to content

Commit

Permalink
pkp#9434 Allow plugin level API call
Browse files Browse the repository at this point in the history
  • Loading branch information
touhidurabir committed Oct 23, 2023
1 parent 34cf626 commit c9a8849
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 37 deletions.
139 changes: 117 additions & 22 deletions classes/core/APIRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/**
* @file classes/core/APIRouter.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Copyright (c) 2023 Simon Fraser University
* Copyright (c) 2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class APIRouter
Expand All @@ -23,32 +23,90 @@
use APP\core\Application;
use Exception;
use Illuminate\Http\Response;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
use PKP\handler\APIHandler;
use PKP\session\SessionManager;

class APIRouter extends PKPRouter
{
/**
* Define if the api call for a plugin implemented endpoint
*/
protected bool $_pluginApi = false;

/**
* Determines path info parts
*
*/
protected function getPathInfoParts(): array
{
return explode('/', trim($_SERVER['PATH_INFO'] ?? '', '/'));
}

/**
* Get the API controller file path
*/
protected function getSourceFilePath(): string
{
if ($this->_pluginApi) {
return sprintf(
'%s/%s/%s/api/%s/%s/index.php',
$this->getPluginApiPathPrefix(),
$this->getPluginCategory(),
$this->getPluginName(),
$this->getVersion(),
$this->getEntity()
);
}

return sprintf('api/%s/%s/index.php', $this->getVersion(), $this->getEntity());
}

/**
* Get the starting api url segment for plugin implemented API endpoint
*
* @example Considering an API endpoint such as
* http://BASE_URL/index.php/CONTEXT_PATH/plugins/PLUGIN_CATEGORY/PLUGIN_NAME/api/VERSION/ENTITY
* the plugin api uri prefix is `plugins` which start right after the CONTEXT_PATH
*/
public function getPluginApiPathPrefix(): string
{
return 'plugins';
}

/**
* Define if the target reqeust is for plugin implemented API routes
*/
public function isPluginApi(): bool
{
return $this->_pluginApi;
}

/**
* Determines whether this router can route the given request.
*
* @param PKPRequest $request
*
* @return bool true, if the router supports this request, otherwise false
*/
public function supports($request): bool
public function supports(PKPRequest $request): bool
{
$pathInfoParts = $this->getPathInfoParts();

if (!is_null($pathInfoParts) && count($pathInfoParts) >= 2 && $pathInfoParts[1] == 'api') {
// Context-specific API requests: [index.php]/{contextPath}/api
if (is_null($pathInfoParts)) {
return false;
}

if (count($pathInfoParts) < 2) {
return false;
}

// Context-specific API requests: [index.php]/{contextPath}/api
if ($pathInfoParts[1] == 'api') {
return true;
}

// plugin specific API request [index.php]/{contextPath}/plugins/{category}/{pluginName}/api
if ($pathInfoParts[1] == $this->getPluginApiPathPrefix() && $pathInfoParts[4] == 'api') {
$this->_pluginApi = true;
return true;
}

Expand All @@ -57,35 +115,69 @@ public function supports($request): bool

/**
* Get the API version
*
* @return string
*/
public function getVersion()
public function getVersion(): string
{
$pathInfoParts = $this->getPathInfoParts();

if ($this->isPluginApi()) {
return Core::cleanFileVar($pathInfoParts[5] ?? '');
}

return Core::cleanFileVar($pathInfoParts[2] ?? '');
}

/**
* Get the entity being requested
*
* @return string
*/
public function getEntity()
public function getEntity(): string
{
$pathInfoParts = $this->getPathInfoParts();

if ($this->isPluginApi()) {
return Core::cleanFileVar($pathInfoParts[6] ?? '');
}

return Core::cleanFileVar($pathInfoParts[3] ?? '');
}

/**
* Get the plugin name if the api endpoint is implemented at plugin level
*/
public function getPluginName(): string
{
if (!$this->isPluginApi()) {
return '';
}

$pathInfoParts = $this->getPathInfoParts();

return Core::cleanFileVar($pathInfoParts[3]);
}

/**
* Get the plugin category if the api endpoint is implemented at plugin level
*/
public function getPluginCategory(): string
{
if (!$this->isPluginApi()) {
return '';
}

$pathInfoParts = $this->getPathInfoParts();

return Core::cleanFileVar($pathInfoParts[2]);
}

//
// Implement template methods from PKPRouter
//
/**
* @copydoc PKPRouter::route()
* @copydoc \PKP\core\PKPRouter::route()
*/
public function route($request)
{
$sourceFile = sprintf('api/%s/%s/index.php', $this->getVersion(), $this->getEntity());
$sourceFile = $this->getSourceFilePath();

if (!file_exists($sourceFile)) {
response()->json([
Expand All @@ -96,22 +188,25 @@ public function route($request)
}

if (!SessionManager::isDisabled()) {
// Initialize session
SessionManager::getManager();
SessionManager::getManager(); // Initialize session
}

$handler = require('./' . $sourceFile); /** @var \PKP\handler\APIHandler|\PKP\core\PKPBaseController $handler */

if ($handler instanceof PKPBaseController) {
$handler = new APIHandler($handler); /** @var \PKP\handler\APIHandler $handler */
}

$handler = require('./' . $sourceFile);
$this->setHandler($handler);
$handler->runRoutes();
}

/**
* Get the requested operation
*
* @param PKPRequest $request
*
* @return string
*/
public function getRequestedOp($request)
public function getRequestedOp(PKPRequest $request)
{
if ($routeActionName = PKPBaseController::getRouteActionName()) {
return $routeActionName;
Expand Down
3 changes: 0 additions & 3 deletions classes/core/PKPBaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ abstract class PKPBaseController extends Controller
* The unique endpoint string for the APIs that will be served through controller.
*
* This is equivalent to property \PKP\handler\APIHandler::_handlerPath
* which will be passed through \PKP\handler\APIHandler\PKPApiRoutingHandler
*/
abstract public function getHandlerPath(): string;

Expand Down Expand Up @@ -149,7 +148,6 @@ public static function roleAuthorizer(array $roles): string
* The endpoint pattern for the APIs that will be served through controller.
*
* This is equivalent to property \PKP\handler\APIHandler::_pathPattern
* which will be passed through \PKP\handler\APIHandler\PKPApiRoutingHandler
*/
public function getPathPattern(): ?string
{
Expand All @@ -160,7 +158,6 @@ public function getPathPattern(): ?string
* Define if all the path building for admin api use rather than at context level
*
* This is equivalent to property \PKP\handler\APIHandler::_apiForAdmin
* which will be passed through \PKP\handler\APIHandler\PKPApiRoutingHandler
*/
public function isSiteWide(): bool
{
Expand Down
80 changes: 68 additions & 12 deletions classes/handler/APIHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,38 @@

namespace PKP\handler;

use APP\core\Application;
use Illuminate\Http\Response;
use Illuminate\Routing\Pipeline;
use PKP\core\PKPBaseController;
use PKP\core\PKPContainer;
use PKP\core\PKPRoutingProvider;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

class APIHandler extends PKPHandler
{
/** @var string The endpoint pattern for this handler */
protected $_pathPattern;
/**
* The endpoint pattern for this handler
*/
protected ?string $_pathPattern = null;

/** @var string The unique endpoint string for this handler */
protected $_handlerPath = null;
/**
* The unique endpoint string for this handler
*/
protected ?string $_handlerPath = null;

/** @var bool Define if all the path building for admin api */
protected $_apiForAdmin = false;
/**
* Define if all the path building for admin api
*/
protected bool $_apiForAdmin = false;

/**
* The API routing controller class
*/
protected PKPBaseController $apiController;

/**
* Constructor
Expand All @@ -42,6 +56,10 @@ public function __construct(PKPBaseController $controller)
{
parent::__construct();

Hook::call('APIHandler::endpoints', [&$controller, $this]);

$this->apiController = $controller;

$this->_pathPattern = $controller->getPathPattern();
$this->_handlerPath = $controller->getHandlerPath();
$this->_apiForAdmin = $controller->isSiteWide();
Expand All @@ -50,9 +68,41 @@ public function __construct(PKPBaseController $controller)
'prefix' => $this->getEndpointPattern(),
'middleware' => $controller->getRouteGroupMiddleware(),
], $controller->getGroupRoutes(...));
}

/**
* Get the API controller for current running route
*/
public function getApiController(): PKPBaseController
{
return $this->apiController;
}

/**
* Run the API routes
*/
public function runRoutes(): mixed
{
if(app('router')->getRoutes()->count() === 0) {
return;
return response()->json([
'error' => __('api.400.routeNotDefined')
], Response::HTTP_BAD_REQUEST)->send();
}

$router = $this->apiController->getRequest()->getRouter(); /** @var \PKP\core\APIRouter $router */

if ($router->isPluginApi()) {
$contextId = $this->apiController->getRequest()->getContext()?->getId() ?? Application::CONTEXT_SITE;

// load the plugin only for current running context or site if no context available
$plugin = PluginRegistry::loadPlugin($router->getPluginCategory(), $router->getPluginName(), $contextId);

// Will only allow api call only from enable plugins
if (!$plugin->getEnabled($contextId)) {
return response()->json([
'error' => __('api.400.pluginNotEnabled')
], Response::HTTP_BAD_REQUEST)->send();
}
}

try {
Expand Down Expand Up @@ -105,21 +155,27 @@ public function __construct(PKPBaseController $controller)
*
* Compiles the URI path pattern from the context, api version and the
* unique string for the this handler.
*
* @return string
*/
public function getEndpointPattern()
public function getEndpointPattern(): string
{
if (isset($this->_pathPattern)) {
return $this->_pathPattern;
}

$router = $this->apiController->getRequest()->getRouter(); /** @var \PKP\core\APIRouter $router */

if ($this->_apiForAdmin) {
$this->_pathPattern = '/index/api/{version}/' . $this->_handlerPath;
$this->_pathPattern = $router->isPluginApi()
? "/index/{$router->getPluginApiPathPrefix()}/{$router->getPluginCategory()}/{$router->getPluginName()}/api/{version}/{$this->_handlerPath}"
: "/index/api/{version}/{$this->_handlerPath}";

return $this->_pathPattern;
}

$this->_pathPattern = '/{contextPath}/api/{version}/' . $this->_handlerPath;
$this->_pathPattern = $router->isPluginApi()
? "/{contextPath}/{$router->getPluginApiPathPrefix()}/{$router->getPluginCategory()}/{$router->getPluginName()}/api/{version}/{$this->_handlerPath}"
: "/{contextPath}/api/{version}/{$this->_handlerPath}";

return $this->_pathPattern;
}
}
Expand Down
6 changes: 6 additions & 0 deletions locale/en/api.po
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ msgstr "Unable to process API request."
msgid "api.417.routeResponseIsNull"
msgstr "Unable to receive any expected response."

msgid "api.400.pluginNotEnabled"
msgstr "The API request can not be processed as the plugin responsible to handle the API routing is not enable."

msgid "api.400.routeNotDefined"
msgstr "No API route was provided."

msgid "api.400.paramNotSupported"
msgstr "The {$param} parameter is not supported."

Expand Down

0 comments on commit c9a8849

Please sign in to comment.