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