1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
<?php
/* --------------------------------------------------------------
AbstractApiV2Controller.inc.php 2016-10-27
Gambio GmbH
http://www.gambio.de
Copyright (c) 2016 Gambio GmbH
Released under the GNU General Public License (Version 2)
[http://www.gnu.org/licenses/gpl-2.0.html]
--------------------------------------------------------------
*/
/**
* Class AbstractApiV2Controller
*
* This class defines the inner core functionality of a ApiV2Controller. It contains the
* initialization and request validation functionality that every controller must have.
*
* The functionality of this class is mark as private because child controllers must not alter
* the state at this point but rather adjust to it. This will force them to follow the same
* principles and methodologies.
*
* Child API controllers can use the "init" method to initialize their common dependencies.
*
* @category System
* @package ApiV2Controllers
*/
abstract class AbstractApiV2Controller
{
/**
* Defines the default page offset for responses that return multiple items.
*
* @var int
*/
const DEFAULT_PAGE_ITEMS = 50;
/**
* Default controller to be loaded when no resource was selected.
*
* @var string
*/
const DEFAULT_CONTROLLER_NAME = 'DefaultApiV2Controller';
/**
* Defines the maximum request limit for an authorized client.
*
* @var int
*/
const DEFAULT_RATE_LIMIT = 5000;
/**
* Defines the duration of an API session in minutes.
*
* @var int
*/
const DEFAULT_RATE_RESET_PERIOD = 15;
/**
* Slim Framework instance is used to manipulate the request or response data.
*
* @var \Slim\Slim
*/
protected $api;
/**
* Contains the request URI segments after the root api version segment.
*
* Example:
* URI - api.php/v2/customers/73/addresses
* CODE - $this->uri[1]; // will return '73'
*
* @var array
*/
protected $uri;
/**
* AbstractApiV2Controller Constructor
*
* Call this constructor from every child controller class in order to set the
* Slim instance and the request routes arguments to the class.
*
* @param \Slim\Slim $api Slim framework instance, used for request/response manipulation.
* @param array $uri This array contains all the segments of the current request, starting from the resource.
*
* @deprecated The "__initialize" method will is deprecated and will be removed in a future version. Please use
* the new "init" for bootstrapping your child API controllers.
*
* @throws HttpApiV2Exception Through _validateRequest
*/
public function __construct(\Slim\Slim $api, array $uri)
{
$this->api = $api;
$this->uri = $uri;
if(method_exists($this, '__initialize')) // Method for child-controller initialization stuff (deprecated).
{
$this->__initialize();
}
if(method_exists($this, 'init')) // Method for child-controller initialization stuff (new method).
{
$this->init();
}
$this->_validateRequest();
$this->_prepareResponse();
}
/**
* [PRIVATE] Validate request before proceeding with response.
*
* This method will validate the request headers, user authentication and other parameters
* before the controller proceeds with the response.
*
* Not available to child-controllers (private method).
*
* @throws HttpApiV2Exception If validation fails - 415 Unsupported media type.
*/
private function _validateRequest()
{
$requestMethod = $this->api->request->getMethod();
$contentType = $this->api->request->headers->get('Content-Type');
if(($requestMethod === 'POST' || $requestMethod === 'PUT' || $requestMethod === 'PATCH')
&& empty($_FILES)
&& $contentType !== 'application/json'
)
{
throw new HttpApiV2Exception('Unsupported Media Type HTTP', 415);
}
$this->_authorize();
$this->_setRateLimitHeader();
}
/**
* [PRIVATE] Prepare response headers.
*
* This method will prepare default attributes of the API responses. Further response
* settings must be set explicitly from each controller method separately.
*
* Not available to child-controllers (private method).
*/
private function _prepareResponse()
{
$this->api->response->setStatus(200);
$this->api->response->headers->set('Content-Type', 'application/json; charset=utf-8');
$this->api->response->headers->set('X-API-Version', 'v' . $this->api->config('version'));
$this->api->response->headers->set('X-Shop-Version', 'v' . gm_get_conf('INSTALLED_VERSION'));
}
/**
* [PRIVATE] Authorize request with HTTP Basic Authorization
*
* Call this method in every API operation that needs to be authorized with the HTTP Basic
* Authorization technique.
*
* @link http://php.net/manual/en/features.http-auth.php
*
* Not available to child-controllers (private method).
*
* @throws HttpApiV2Exception If request does not provide the "Authorization" header or if the
* credentials are invalid.
*
* @throws InvalidArgumentException If the username or password values are invalid.
*/
private function _authorize()
{
if(!isset($_SERVER['PHP_AUTH_USER']))
{
$this->api->response->headers->set('WWW-Authenticate', 'Basic realm="Gambio GX3 APIv2 Login"');
throw new HttpApiV2Exception('Unauthorized', 401);
}
$authService = StaticGXCoreLoader::getService('Auth');
$credentials = MainFactory::create('UsernamePasswordCredentials',
new NonEmptyStringType($_SERVER['PHP_AUTH_USER']),
new StringType($_SERVER['PHP_AUTH_PW']));
$db = StaticGXCoreLoader::getDatabaseQueryBuilder();
$isAdmin = (bool)$db->get_where('customers', [
'customers_email_address' => $_SERVER['PHP_AUTH_USER'],
'customers_status' => '0'
])->num_rows();
if(!$authService->authUser($credentials) || !$isAdmin)
{
throw new HttpApiV2Exception('Invalid Credentials', 401);
}
// Credentials were correct, continue execution ...
}
/**
* [PRIVATE] Handle rate limit headers.
*
* There is a cache file that will store each user session and provide a security
* mechanism that will protect the shop from DOS attacks or service overuse. Each
* session will use the hashed "Authorization header" to identify the client. When
* the limit is reached a "HTTP/1.1 429 Too Many Requests" will be returned.
*
* Headers:
* X-Rate-Limit-Limit >> Max number of requests allowed.
* X-Rate-Limit-Remaining >> Number of requests remaining.
* X-Rate-Limit-Reset >> UTC epoch seconds until the limit is reset.
*
* Important: This method will be executed in every API call and it might slow the
* response time due to filesystem operations. If the difference is significant
* then it should be optimized.
*
* Not available to child-controllers (private method).
*
* @throws HttpApiV2Exception If request limit exceed - 429 Too Many Requests
*/
private function _setRateLimitHeader()
{
// Load or create cache file.
$cacheFilePath = DIR_FS_CATALOG . 'cache/gxapi_v2_sessions_' . FileLog::get_secure_token();
if(!file_exists($cacheFilePath))
{
touch($cacheFilePath);
$sessions = array();
}
else
{
$sessions = unserialize(file_get_contents($cacheFilePath));
}
// Clear expired sessions.
foreach($sessions as $index => $session)
{
if($session['reset'] < time())
{
unset($sessions[$index]);
}
}
// Get session identifier from request.
$identifier = md5($this->api->request->headers->get('Authorization'));
if(empty($identifier))
{
throw new HttpApiV2Exception('Remote address value was not provided.', 400);
}
// Check session entry, if not found create one.
if(!isset($sessions[$identifier]))
{
$sessions[$identifier] = array(
'limit' => self::DEFAULT_RATE_LIMIT,
'remaining' => self::DEFAULT_RATE_LIMIT,
'reset' => time() + (self::DEFAULT_RATE_RESET_PERIOD * 60)
);
}
else if($sessions[$identifier]['remaining'] <= 0)
{
throw new HttpApiV2Exception('Request limit was reached.', 429);
}
// Set rate limiting headers to response.
$sessions[$identifier]['remaining']--;
$this->api->response->headers->set('X-Rate-Limit-Limit', $sessions[$identifier]['limit']);
$this->api->response->headers->set('X-Rate-Limit-Remaining', $sessions[$identifier]['remaining']);
$this->api->response->headers->set('X-Rate-Limit-Reset', $sessions[$identifier]['reset']);
file_put_contents($cacheFilePath, serialize($sessions));
}
}