-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from batopa/feat/api-proxy-trait
Add API proxy trait
- Loading branch information
Showing
4 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* BEdita, API-first content management framework | ||
* Copyright 2020 ChannelWeb Srl, Chialab Srl | ||
* | ||
* This file is part of BEdita: you can redistribute it and/or modify | ||
* it under the terms of the GNU Lesser General Public License as published | ||
* by the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details. | ||
*/ | ||
namespace BEdita\WebTools\Controller; | ||
|
||
use BEdita\SDK\BEditaClientException; | ||
use BEdita\WebTools\ApiClientProvider; | ||
use Cake\Http\Exception\MethodNotAllowedException; | ||
use Cake\Routing\Router; | ||
use Cake\Utility\Hash; | ||
use Cake\View\ViewVarsTrait; | ||
|
||
/** | ||
* Use this Trait in a controller to directly proxy requests to BE4 API. | ||
* The response will be the same of the API itself with links masked. | ||
* | ||
* You need also to define routing rules configured as (for ApiController) | ||
* | ||
* ``` | ||
* $builder->scope('/api', ['_namePrefix' => 'api:'], function (RouteBuilder $builder) { | ||
* $builder->get('/**', ['controller' => 'Api', 'action' => 'get'], 'get'); | ||
* $builder->post('/**', ['controller' => 'Api', 'action' => 'post'], 'post'); | ||
* // and so on for patch, delete if you want to use it | ||
* }); | ||
* ``` | ||
*/ | ||
trait ApiProxyTrait | ||
{ | ||
use ViewVarsTrait; | ||
|
||
/** | ||
* An instance of a \Cake\Http\ServerRequest object that contains information about the current request. | ||
* | ||
* @var \Cake\Http\ServerRequest | ||
*/ | ||
protected $request; | ||
|
||
/** | ||
* An instance of a Response object that contains information about the impending response. | ||
* | ||
* @var \Cake\Http\Response | ||
*/ | ||
protected $response; | ||
|
||
/** | ||
* BEdita4 API client | ||
* | ||
* @var \BEdita\SDK\BEditaClient | ||
*/ | ||
protected $apiClient = null; | ||
|
||
/** | ||
* Base URL used for mask links. | ||
* | ||
* @var string | ||
*/ | ||
protected $baseUrl = ''; | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function initialize(): void | ||
{ | ||
parent::initialize(); | ||
|
||
if ($this->apiClient === null) { | ||
$this->apiClient = ApiClientProvider::getApiClient(); | ||
} | ||
|
||
$this->viewBuilder() | ||
->setClassName('Json') | ||
->setOption('serialize', true); | ||
} | ||
|
||
/** | ||
* Set base URL used for mask links removing trailing slashes. | ||
* | ||
* @param string $path The path on which build base URL | ||
* @return void | ||
*/ | ||
protected function setBaseUrl($path): void | ||
{ | ||
$requestPath = $this->request->getPath(); | ||
$basePath = substr($requestPath, 0, strpos($requestPath, $path)); | ||
$this->baseUrl = Router::url(rtrim($basePath, '/'), true); | ||
} | ||
|
||
/** | ||
* Proxy for GET requests to BEdita4 API | ||
* | ||
* @param string $path The path for API request | ||
* @return void | ||
*/ | ||
public function get($path = '') | ||
{ | ||
$this->setBaseUrl($path); | ||
$this->apiRequest([ | ||
'method' => 'get', | ||
'path' => $path, | ||
'query' => $this->request->getQueryParams(), | ||
]); | ||
} | ||
|
||
/** | ||
* Routes a request to the API handling response and errors. | ||
* | ||
* `$options` are: | ||
* - method => the HTTP request method | ||
* - path => a string representing the complete endpoint path | ||
* - query => an array of query strings | ||
* - body => the body sent | ||
* - headers => an array of headers | ||
* | ||
* @param array $options The request options | ||
* @return void | ||
*/ | ||
protected function apiRequest(array $options): void | ||
{ | ||
$options += [ | ||
'method' => '', | ||
'path' => '', | ||
'query' => null, | ||
'body' => null, | ||
'headers' => null, | ||
]; | ||
|
||
try { | ||
switch (strtolower($options['method'])) { | ||
case 'get': | ||
$response = $this->apiClient->get($options['path'], $options['query'], $options['headers']); | ||
break; | ||
// case 'post': | ||
// $response = $this->apiClient->post($options['path'], $options['body'], $options['headers']); | ||
// break; | ||
// case 'patch': | ||
// $response = $this->apiClient->patch($options['path'], $options['body'], $options['headers']); | ||
// break; | ||
// case 'delete': | ||
// $response = $this->apiClient->delete($options['path'], $options['body'], $options['headers']); | ||
// break; | ||
default: | ||
throw new MethodNotAllowedException(); | ||
} | ||
|
||
if (empty($response) || !is_array($response)) { | ||
return; | ||
} | ||
|
||
$response = $this->maskResponseLinks($response); | ||
$this->set($response); | ||
} catch (\Throwable $e) { | ||
$this->handleError($e); | ||
} | ||
} | ||
|
||
/** | ||
* Handle error. | ||
* Set error var for view. | ||
* | ||
* @param \Throwable $error The error thrown. | ||
* @return void | ||
*/ | ||
protected function handleError(\Throwable $error): void | ||
{ | ||
$status = $error->getCode(); | ||
if ($status < 100 || $status > 599) { | ||
$status = 500; | ||
} | ||
$this->response = $this->response->withStatus($status); | ||
$errorData = [ | ||
'status' => (string)$status, | ||
'title' => $error->getMessage(), | ||
]; | ||
$this->set('error', $errorData); | ||
|
||
if (!$error instanceof BEditaClientException) { | ||
return; | ||
} | ||
|
||
$errorAttributes = $error->getAttributes(); | ||
if (!empty($errorAttributes)) { | ||
$this->set('error', $errorAttributes); | ||
} | ||
} | ||
|
||
/** | ||
* Mask links of response to not expose API URL. | ||
* | ||
* @param array $response The response from API | ||
* @return array | ||
*/ | ||
protected function maskResponseLinks(array $response): array | ||
{ | ||
$response = $this->maskLinks($response, '$id'); | ||
$response = $this->maskLinks($response, 'links'); | ||
$response = $this->maskLinks($response, 'meta.schema'); | ||
|
||
if (!empty($response['meta']['resources'])) { | ||
$response = $this->maskMultiLinks($response, 'meta.resources', 'href'); | ||
} | ||
|
||
$data = (array)Hash::get($response, 'data'); | ||
if (empty($data)) { | ||
return $response; | ||
} | ||
|
||
if (Hash::numeric(array_keys($data))) { | ||
foreach ($data as &$item) { | ||
$item = $this->maskLinks($item, 'links'); | ||
$item = $this->maskMultiLinks($item); | ||
} | ||
$response['data'] = $data; | ||
} else { | ||
$response['data']['relationships'] = $this->maskMultiLinks($data); | ||
} | ||
|
||
return (array)$response; | ||
} | ||
|
||
/** | ||
* Mask links across multidimensional array. | ||
* By default search for `relationships` and mask their `links`. | ||
* | ||
* @param array $data The data with links to mask | ||
* @param string $path The path to search for | ||
* @param string $key The key on which are the links | ||
* @return array | ||
*/ | ||
protected function maskMultiLinks(array $data, string $path = 'relationships', string $key = 'links'): array | ||
{ | ||
$relationships = Hash::get($data, $path, []); | ||
foreach ($relationships as &$rel) { | ||
$rel = $this->maskLinks($rel, $key); | ||
} | ||
|
||
return Hash::insert($data, $path, $relationships); | ||
} | ||
|
||
/** | ||
* Mask links found in `$path` | ||
* | ||
* @param array $data The data with links to mask | ||
* @param string $path The path to search for | ||
* @return array | ||
*/ | ||
protected function maskLinks(array $data, string $path): array | ||
{ | ||
$links = Hash::get($data, $path, []); | ||
if (empty($links)) { | ||
return $data; | ||
} | ||
|
||
if (is_string($links)) { | ||
$links = str_replace($this->apiClient->getApiBaseUrl(), $this->baseUrl, $links); | ||
|
||
return Hash::insert($data, $path, $links); | ||
} | ||
|
||
foreach ($links as &$link) { | ||
$link = str_replace($this->apiClient->getApiBaseUrl(), $this->baseUrl, $link); | ||
} | ||
|
||
return Hash::insert($data, $path, $links); | ||
} | ||
} |
Oops, something went wrong.