1 <?php
2 /**
3 * Copyright (C) 2014 MyAllocator
4 *
5 * A copy of the LICENSE can be found in the LICENSE file within
6 * the root directory of this library.
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included
16 * in all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24 * IN THE SOFTWARE.
25 */
26
27 namespace MyAllocator\phpsdk\src\Api;
28 use MyAllocator\phpsdk\src\MaBaseClass;
29 use MyAllocator\phpsdk\src\Object\Auth;
30 use MyAllocator\phpsdk\src\Util\Requestor;
31 use MyAllocator\phpsdk\src\Util\Common;
32 use MyAllocator\phpsdk\src\Exception\ApiException;
33 use MyAllocator\phpsdk\src\Exception\ApiAuthenticationException;
34
35 /**
36 * The Base API class.
37 */
38 class MaApi extends MaBaseClass
39 {
40 /**
41 * @var boolean Whether or not the API is currently enabled/supported.
42 */
43 protected $enabled = true;
44
45 /**
46 * @var string The api method.
47 */
48 protected $id = 'MaApi';
49
50 /**
51 * @var \MyAllocator\Object\Auth Authentication object for requester.
52 */
53 private $auth = null;
54
55 /**
56 * @var array API request parameters to be included in API request.
57 */
58 private $params = null;
59
60 /**
61 * @var mixed The response from the last request.
62 */
63 private $lastApiResponse = null;
64
65 /**
66 * @var array Array of required and optional authentication and argument
67 * keys (string) for API method.
68 */
69 protected $keys = array(
70 'auth' => array(
71 'req' => array(),
72 'opt' => array()
73 ),
74 'args' => array(
75 'req' => array(),
76 'opt' => array()
77 )
78 );
79
80 /**
81 * Class contructor attempts to assign authentication parameters
82 * from $cfg argument. Authentication parameters may be configured
83 * via Auth object or array. The parent constructor handles
84 * the included configuration parameters.
85 *
86 * @param mixed $cfg API configuration potentially containing an
87 * 'auth' key with authentication parameters/object or a
88 * 'cfg' key containing configurations to overwrite Config/Config.php.
89 */
90 public function __construct($cfg = null)
91 {
92 parent::__construct($cfg);
93
94 // Load auth information if provided
95 if (isset($cfg) && isset($cfg['auth'])) {
96 if (is_object($cfg['auth']) &&
97 is_a($cfg['auth'], 'MyAllocator\phpsdk\src\Object\Auth')
98 ) {
99 $this->auth = $cfg['auth'];
100 } else if (is_array($cfg['auth'])) {
101 $auth = new Auth();
102 $auth_refl = new \ReflectionClass($auth);
103 $props = $auth_refl->getProperties(\ReflectionProperty::IS_PUBLIC);
104
105 foreach ($props as $prop) {
106 $name = $prop->getName();
107 if (isset($cfg['auth'][$name])) {
108 $auth->$name = $cfg['auth'][$name];
109 }
110 }
111 $this->auth = $auth;
112 }
113 }
114 }
115
116 /**
117 * Set the parameters to be used in the API request. Parameters may
118 * also be set at the time of API call via callApiWithParams().
119 *
120 * @param array $params API request parameters.
121 */
122 public function setParams($params)
123 {
124 $this->params = $params;
125 }
126
127 /**
128 * Call the API using previously set parameters (if any).
129 *
130 * @return mixed API response.
131 */
132 public function callApi()
133 {
134 return $this->processRequest($this->params);
135 }
136
137 /**
138 * Call the API using provided parameters (if any).
139 *
140 * @param array $params API request parameters.
141 * @return mixed API response.
142 */
143 public function callApiWithParams($params = null)
144 {
145 return $this->processRequest($params);
146 }
147
148 /**
149 * Get the authentication object.
150 *
151 * @param string $errorOnNull If true, throw an exception if auth null.
152 *
153 * @return MyAllocator\phpsdk\src\Object\Auth API Authentication object.
154 *
155 * @throws MyAllocator\phpsdk\src\Exception\ApiException
156 */
157 public function getAuth($errorOnNull = false)
158 {
159 if ($errorOnNull && !$this->auth) {
160 $msg = 'No Auth object provided. (HINT: Set your Auth data using '
161 . '"$API->setAuth(Auth $auth)" or $API\' constructor. '
162 . 'See https://TODO for details.';
163 throw new ApiException($msg);
164 }
165
166 return $this->auth;
167 }
168
169 /**
170 * Set the authentication object for the API.
171 *
172 * @param MyAllocator\phpsdk\src\Object\Auth API Authentication object.
173 */
174 public function setAuth(Auth $auth)
175 {
176 $this->auth = $auth;
177 }
178
179 /**
180 * Determine if the API is enabled.
181 *
182 * @return booleam True if the API is enabled.
183 */
184 public function isEnabled()
185 {
186 return $this->enabled;
187 }
188
189 /**
190 * Get the last API response as array($rbody, $rcode).
191 *
192 * @return mixed The last API response.
193 */
194 public function getLastApiResponse()
195 {
196 return $this->lastApiResponse;
197 }
198
199 /**
200 * Validate and process/send the API request.
201 *
202 * @param array $params API request parameters.
203 * @return mixed API response.
204 */
205 private function processRequest($params = null)
206 {
207 // Ensure this api is currently enabled/supported
208 $this->assertEnabled();
209
210 // Instantiate requester
211 $requestor = new Requestor($this->config);
212
213 switch ($this->config['dataFormat']) {
214 case 'xml':
215 // Do nothing special for XML
216 break;
217 case 'json':
218 // Validate and sanitize parameters (json decode/encode)
219 if ($this->config['paramValidationEnabled']) {
220 $params_decoded = json_decode($params, TRUE);
221 $params_decoded = $this->validateApiParameters($this->keys, $params_decoded);
222 // Add URI method and version to payload
223 $params['_method'] = $this->id;
224 $params['_version'] = $requestor->version;
225 $params = json_encode($params_decoded);
226 }
227 break;
228 case 'array':
229 // Validate and sanitize parameters
230 if ($this->config['paramValidationEnabled']) {
231 $params = $this->validateApiParameters($this->keys, $params);
232 } else {
233 $params = $this->setAuthenticationParametersNoValidation($params);
234 }
235 // Add URI method and version to payload
236 $params['_method'] = $this->id;
237 $params['_version'] = $requestor->version;
238 break;
239 default:
240 throw new ApiException(
241 'Invalid dataFormat: '.$this->config['dataFormat']
242 );
243 }
244
245 // Send request
246 $response = $requestor->request('post', $this->id, $params);
247
248 // Return result
249 $this->lastApiResponse = $response;
250 return $response;
251 }
252
253 /**
254 * Assert the API is enabled.
255 */
256 private function assertEnabled()
257 {
258 if (!$this->enabled) {
259 $msg = 'This API is not currently enabled/supported.';
260 throw new ApiException($msg);
261 }
262 }
263
264 /**
265 * Validate authentication and argument parameters for an API.
266 *
267 * @param array $keys Array of required and optional
268 * authentication and argument keys (string) for API method.
269 * @param array $params API specific parameters.
270 *
271 * @return array Validated API parameters.
272 */
273 private function validateApiParameters($keys = null, $params = null)
274 {
275 // Assert API has defined an id/endpoint
276 $this->assertApiId();
277
278 // Assert API keys array structure is valid
279 $this->assertKeysArrayValid($keys);
280
281 // Assert keys array has minimum required optional parameters
282 $this->assertKeysHasMinOptParams($keys, $params);
283
284 // Assert and set authentication parameters from Auth object
285 $params = $this->setAuthenticationParameters($keys, $params, 'req');
286 $params = $this->setAuthenticationParameters($keys, $params, 'opt');
287
288 // Assert required argument parameters exist (non-authentication)
289 $this->assertReqParameters($keys, $params);
290
291 // Remove extra parameters not defined in keys array
292 $this->removeUnknownParameters($keys, $params);
293
294 return $params;
295 }
296
297 /**
298 * Assert the API id is set by the API class.
299 */
300 private function assertApiId()
301 {
302 // Assert minimum number of optional args exist if requirement exists
303 if (!$this->id) {
304 $msg = 'The API id has not be set in the API class.';
305 throw new ApiException($msg);
306 }
307 }
308
309 /**
310 * Assert required API keys exist and are valid.
311 *
312 * @param array $keys Array of required and optional
313 * authentication and argument keys (string) for API method.
314 */
315 private function assertKeysArrayValid($keys = null)
316 {
317 if ((!$keys) ||
318 (!is_array($keys)) ||
319 (!isset($keys['auth'])) ||
320 (!is_array($keys['auth'])) ||
321 (!isset($keys['auth']['req'])) ||
322 (!is_array($keys['auth']['req'])) ||
323 (!isset($keys['auth']['opt'])) ||
324 (!is_array($keys['auth']['opt'])) ||
325 (!isset($keys['args'])) ||
326 (!is_array($keys['args'])) ||
327 (!isset($keys['args']['req'])) ||
328 (!is_array($keys['auth']['req'])) ||
329 (!isset($keys['args']['opt'])) ||
330 (!is_array($keys['auth']['opt']))
331 ) {
332 $msg = 'Invalid API keys provided. (HINT: Each '
333 . 'API class must define a $keys array with '
334 . 'specific key requirements. (HINT: View an /Api/[file] '
335 . 'for an example.)';
336 throw new ApiException($msg);
337 }
338 }
339
340 /**
341 * Assert parameters include minimum number of optional
342 * parameters as configured/defined by the API.
343 *
344 * @param array $keys Array of required and optional
345 * authentication and argument keys (string) for API method.
346 * @param array $params API specific parameters.
347 *
348 * @throws MyAllocator\phpsdk\src\Exception\ApiException
349 */
350 private function assertKeysHasMinOptParams($keys, $params)
351 {
352 // Assert minimum number of optional args exist if requirement exists
353 if ((isset($keys['args']['optMin'])) &&
354 (!$params || count($params) < $keys['args']['optMin'])
355 ) {
356 $msg = 'API requires at least '.$keys['args']['optMin'].' optional '
357 . 'parameter(s). (HINT: Reference the $keys '
358 . 'property at the top of the API class file for '
359 . 'required and optional parameters.)';
360 throw new ApiException($msg);
361 }
362 }
363
364 /**
365 * Validate and set required authentication parameters from Auth object.
366 *
367 * @param array $keys Array of required and optional
368 * authentication and argument keys (string) for API method.
369 * @param array $params API specific parameters.
370 * @param string $type The type of authentication parameters to
371 * process (optional or required).
372 *
373 * @return array Paramters with authentication parameters of $type set.
374 *
375 * @throws MyAllocator\phpsdk\src\Exception\ApiAuthenticationException
376 */
377 private function setAuthenticationParameters(
378 $keys = null,
379 $params = null,
380 $type = 'req'
381 ) {
382 if (!empty($keys['auth'][$type])) {
383 if ($this->auth == null) {
384 $msg = 'No Auth object provided. (HINT: Set your Auth data using '
385 . '"$API->setAuth(Auth $auth)" or $API\' constructor. '
386 . 'See https://TODO for details.';
387 throw new ApiAuthenticationException($msg);
388 }
389
390 // Set authentication parameters
391 $auth_group = false;
392 foreach ($keys['auth'][$type] as $k) {
393 if (is_array($k) && !empty($k)) {
394 /*
395 * Different auth key groups may be required.
396 * In these situations, must assert that each
397 * key within an auth key group exists. Exits
398 * once the first auth key group is validated.
399 */
400
401 if ($auth_group && $auth_group_validated) {
402 /*
403 * At this point an authentication group has been satisfied
404 * and we don't need to process additional groups.
405 */
406 continue;
407 }
408
409 $auth_group = true;
410 $auth_group_validated = true;
411 foreach ($k as $g) {
412 if (!isset($params[$g])) {
413 $v = $this->auth->getAuthKeyVar($g);
414 if (!$v) {
415 $auth_group_validated = false;
416 break;
417 }
418 $params[$g] = $v;
419 }
420 }
421 } else {
422 if (!isset($params[$k])) {
423 $v = $this->auth->getAuthKeyVar($k);
424 if (!$v) {
425 if ($type == 'req') {
426 $msg = 'Authentication key `'.$k.'` is required. '
427 . 'HINT: Set your Auth data using "$API->'
428 . 'setAuth(Auth $auth)" or $API\' constructor. '
429 . 'See https://TODO for details.';
430 throw new ApiAuthenticationException($msg);
431 } else {
432 // optional
433 continue;
434 }
435 }
436 $params[$k] = $v;
437 }
438 }
439 }
440
441 // If keys configured with authentication groups, verify one was validated
442 if ($auth_group && !$auth_group_validated) {
443 $msg = 'A required authentication key group was not satisfied. '
444 . '(HINT: Reference the $keys '
445 . 'property at the top of the API class file for '
446 . 'required and optional parameters.)';
447 throw new ApiAuthenticationException($msg);
448 }
449 }
450
451 return $params;
452 }
453
454 /**
455 * Set authentication parameters if authentication property set.
456 * This only runs if parameter validation is disabled and does
457 * not validate $keys.
458 *
459 * @param array $params API specific parameters.
460 *
461 * @return array Paramters with authentication parameters set.
462 */
463 private function setAuthenticationParametersNoValidation($params = null)
464 {
465 // Return if authentication property not set
466 if ($this->auth == null) {
467 return $params;
468 }
469
470 // Set parameters for previously configured auth properties
471 // Get property list from auth class
472 $auth_refl = new \ReflectionClass($this->auth);
473 $props = $auth_refl->getProperties(\ReflectionProperty::IS_PUBLIC);
474
475 /*
476 * Loop through property names to determine if configured in auth object.
477 * Add to parameters if set and does not already exist.
478 */
479 foreach ($props as $prop) {
480 $name = $prop->getName();
481 if (isset($this->auth->$name)) {
482 // Do not overwrite if parameter already included
483 $key = $this->auth->getAuthKeyByVar($name);
484 if (!isset($params[$key])) {
485 $params[$key] = $this->auth->$name;
486 }
487 }
488 }
489
490 return $params;
491 }
492
493 /**
494 * Validate required parameters for API.
495 *
496 * @param array $keys Array of required and optional
497 * authentication and argument keys (string) for API method.
498 * @param array $params API specific parameters.
499 *
500 * @throws MyAllocator\phpsdk\src\Exception\ApiException
501 */
502 private function assertReqParameters($keys, $params = null)
503 {
504 if (!empty($keys['args']['req'])) {
505 if (!$params) {
506 $msg = 'No parameters provided. (HINT: Reference the $keys '
507 . 'property at the top of the API class file for '
508 . 'required and optional parameters.)';
509 throw new ApiException($msg);
510 }
511
512 foreach ($keys['args']['req'] as $k) {
513 if (!isset($params[$k])) {
514 $msg = 'Required parameter `'.$k.'` not provided. '
515 . '(HINT: Reference the $keys '
516 . 'property at the top of the API class file for '
517 . 'required and optional parameters.)';
518 throw new ApiException($msg);
519 }
520 }
521 }
522 }
523
524 /**
525 * Strip parameters not defined in API keys array.
526 *
527 * @param array $keys Array of required and optional
528 * authentication and argument keys (string) for API method.
529 * @param array $params API specific parameters.
530 *
531 * @return array API parameters with unknown parameters
532 * removed.
533 */
534 private function removeUnknownParameters($keys, $params)
535 {
536 $valid_keys = array_merge(
537 $keys['auth']['req'],
538 $keys['auth']['opt'],
539 $keys['args']['req'],
540 $keys['args']['opt']
541 );
542
543 foreach ($params as $k => $v) {
544 if (!in_array($k, $valid_keys)) {
545 unset($params[$k]);
546 }
547 }
548
549 return $params;
550 }
551 }
552