1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Luracast\Restler; |
4
|
|
|
|
5
|
|
|
use Luracast\Restler\Data\Text; |
6
|
|
|
use Luracast\Restler\Scope; |
7
|
|
|
use stdClass; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* API Class to create Swagger Spec 1.1 compatible id and operation |
11
|
|
|
* listing |
12
|
|
|
* |
13
|
|
|
* @category Framework |
14
|
|
|
* @package Restler |
15
|
|
|
* @author R.Arul Kumaran <[email protected]> |
16
|
|
|
* @copyright 2010 Luracast |
17
|
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
18
|
|
|
* @link http://luracast.com/products/restler/ |
19
|
|
|
* |
20
|
|
|
*/ |
21
|
|
|
class Resources implements iUseAuthentication, iProvideMultiVersionApi |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* @var bool should protected resources be shown to unauthenticated users? |
25
|
|
|
*/ |
26
|
|
|
public static $hideProtected = true; |
27
|
|
|
/** |
28
|
|
|
* @var bool should we use format as extension? |
29
|
|
|
*/ |
30
|
|
|
public static $useFormatAsExtension = true; |
31
|
|
|
/** |
32
|
|
|
* @var bool should we include newer apis in the list? works only when |
33
|
|
|
* Defaults::$useUrlBasedVersioning is set to true; |
34
|
|
|
*/ |
35
|
|
|
public static $listHigherVersions = true; |
36
|
|
|
/** |
37
|
|
|
* @var array all http methods specified here will be excluded from |
38
|
|
|
* documentation |
39
|
|
|
*/ |
40
|
|
|
public static $excludedHttpMethods = array('OPTIONS'); |
41
|
|
|
/** |
42
|
|
|
* @var array all paths beginning with any of the following will be excluded |
43
|
|
|
* from documentation |
44
|
|
|
*/ |
45
|
|
|
public static $excludedPaths = array(); |
46
|
|
|
/** |
47
|
|
|
* @var bool |
48
|
|
|
*/ |
49
|
|
|
public static $placeFormatExtensionBeforeDynamicParts = true; |
50
|
|
|
/** |
51
|
|
|
* @var bool should we group all the operations with the same url or not |
52
|
|
|
*/ |
53
|
|
|
public static $groupOperations = false; |
54
|
|
|
/** |
55
|
|
|
* @var null|callable if the api methods are under access control mechanism |
56
|
|
|
* you can attach a function here that returns true or false to determine |
57
|
|
|
* visibility of a protected api method. this function will receive method |
58
|
|
|
* info as the only parameter. |
59
|
|
|
*/ |
60
|
|
|
public static $accessControlFunction = null; |
61
|
|
|
/** |
62
|
|
|
* @var array type mapping for converting data types to javascript / swagger |
63
|
|
|
*/ |
64
|
|
|
public static $dataTypeAlias = array( |
65
|
|
|
'string' => 'string', |
66
|
|
|
'int' => 'int', |
67
|
|
|
'number' => 'float', |
68
|
|
|
'float' => 'float', |
69
|
|
|
'bool' => 'boolean', |
70
|
|
|
'boolean' => 'boolean', |
71
|
|
|
'NULL' => 'null', |
72
|
|
|
'array' => 'Array', |
73
|
|
|
'object' => 'Object', |
74
|
|
|
'stdClass' => 'Object', |
75
|
|
|
'mixed' => 'string', |
76
|
|
|
'DateTime' => 'Date' |
77
|
|
|
); |
78
|
|
|
/** |
79
|
|
|
* @var array configurable symbols to differentiate public, hybrid and |
80
|
|
|
* protected api |
81
|
|
|
*/ |
82
|
|
|
public static $apiDescriptionSuffixSymbols = array( |
83
|
|
|
0 => ' <i class="icon-unlock-alt icon-large"></i>', //public api |
84
|
|
|
1 => ' <i class="icon-adjust icon-large"></i>', //hybrid api |
85
|
|
|
2 => ' <i class="icon-lock icon-large"></i>', //protected api |
86
|
|
|
); |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Injected at runtime |
90
|
|
|
* |
91
|
|
|
* @var Restler instance of restler |
92
|
|
|
*/ |
93
|
|
|
public $restler; |
94
|
|
|
/** |
95
|
|
|
* @var string when format is not used as the extension this property is |
96
|
|
|
* used to set the extension manually |
97
|
|
|
*/ |
98
|
|
|
public $formatString = ''; |
99
|
|
|
protected $_models; |
100
|
|
|
protected $_bodyParam; |
101
|
|
|
/** |
102
|
|
|
* @var bool|stdClass |
103
|
|
|
*/ |
104
|
|
|
protected $_fullDataRequested = false; |
105
|
|
|
protected $crud = array( |
106
|
|
|
'POST' => 'create', |
107
|
|
|
'GET' => 'retrieve', |
108
|
|
|
'PUT' => 'update', |
109
|
|
|
'DELETE' => 'delete', |
110
|
|
|
'PATCH' => 'partial update' |
111
|
|
|
); |
112
|
|
|
protected static $prefixes = array( |
113
|
|
|
'get' => 'retrieve', |
114
|
|
|
'index' => 'list', |
115
|
|
|
'post' => 'create', |
116
|
|
|
'put' => 'update', |
117
|
|
|
'patch' => 'modify', |
118
|
|
|
'delete' => 'remove', |
119
|
|
|
); |
120
|
|
|
protected $_authenticated = false; |
121
|
|
|
protected $cacheName = ''; |
122
|
|
|
|
123
|
|
|
public function __construct() |
124
|
|
|
{ |
125
|
|
|
if (static::$useFormatAsExtension) { |
126
|
|
|
$this->formatString = '.{format}'; |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* This method will be called first for filter classes and api classes so |
132
|
|
|
* that they can respond accordingly for filer method call and api method |
133
|
|
|
* calls |
134
|
|
|
* |
135
|
|
|
* |
136
|
|
|
* @param bool $isAuthenticated passes true when the authentication is |
137
|
|
|
* done, false otherwise |
138
|
|
|
* |
139
|
|
|
* @return mixed |
140
|
|
|
*/ |
141
|
|
|
public function __setAuthenticationStatus($isAuthenticated = false) |
142
|
|
|
{ |
143
|
|
|
$this->_authenticated = $isAuthenticated; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* pre call for get($id) |
148
|
|
|
* |
149
|
|
|
* if cache is present, use cache |
150
|
|
|
*/ |
151
|
|
|
public function _pre_get_json($id) |
152
|
|
|
{ |
153
|
|
|
$userClass = Defaults::$userIdentifierClass; |
154
|
|
|
$this->cacheName = $userClass::getCacheIdentifier() . '_resources_' . $id; |
155
|
|
|
if ( |
156
|
|
|
$this->restler->getProductionMode() |
157
|
|
|
&& !$this->restler->refreshCache |
158
|
|
|
&& $this->restler->cache->isCached($this->cacheName) |
159
|
|
|
) { |
160
|
|
|
//by pass call, compose, postCall stages and directly send response |
161
|
|
|
$this->restler->composeHeaders(); |
162
|
|
|
die($this->restler->cache->get($this->cacheName)); |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* post call for get($id) |
168
|
|
|
* |
169
|
|
|
* create cache if in production mode |
170
|
|
|
* |
171
|
|
|
* @param $responseData |
172
|
|
|
* |
173
|
|
|
* @internal param string $data composed json output |
174
|
|
|
* |
175
|
|
|
* @return string |
176
|
|
|
*/ |
177
|
|
|
public function _post_get_json($responseData) |
178
|
|
|
{ |
179
|
|
|
if ($this->restler->getProductionMode()) { |
180
|
|
|
$this->restler->cache->set($this->cacheName, $responseData); |
181
|
|
|
} |
182
|
|
|
return $responseData; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @access hybrid |
187
|
|
|
* |
188
|
|
|
* @param string $id |
189
|
|
|
* |
190
|
|
|
* @throws RestException |
191
|
|
|
* @return null|stdClass |
192
|
|
|
* |
193
|
|
|
* @url GET {id} |
194
|
|
|
*/ |
195
|
|
|
public function get($id = '') |
196
|
|
|
{ |
197
|
|
|
$version = $this->restler->getRequestedApiVersion(); |
198
|
|
|
if (empty($id)) { |
199
|
|
|
//do nothing |
200
|
|
|
} elseif (false !== ($pos = strpos($id, '-v'))) { |
201
|
|
|
//$version = intval(substr($id, $pos + 2)); |
202
|
|
|
$id = substr($id, 0, $pos); |
203
|
|
|
} elseif ($id[0] == 'v' && is_numeric($v = substr($id, 1))) { |
204
|
|
|
$id = ''; |
205
|
|
|
//$version = $v; |
206
|
|
|
} elseif ($id == 'root' || $id == 'index') { |
207
|
|
|
$id = ''; |
208
|
|
|
} |
209
|
|
|
$this->_models = new stdClass(); |
210
|
|
|
$r = null; |
211
|
|
|
$count = 0; |
212
|
|
|
|
213
|
|
|
$tSlash = !empty($id); |
214
|
|
|
$target = empty($id) ? '' : $id; |
215
|
|
|
$tLen = strlen($target); |
216
|
|
|
|
217
|
|
|
$filter = array(); |
218
|
|
|
|
219
|
|
|
$routes |
220
|
|
|
= Util::nestedValue(Routes::toArray(), "v$version") |
221
|
|
|
? : array(); |
222
|
|
|
|
223
|
|
|
$prefix = Defaults::$useUrlBasedVersioning ? "/v$version" : ''; |
224
|
|
|
|
225
|
|
|
foreach ($routes as $value) { |
226
|
|
|
foreach ($value as $httpMethod => $route) { |
227
|
|
|
if (in_array($httpMethod, static::$excludedHttpMethods)) { |
228
|
|
|
continue; |
229
|
|
|
} |
230
|
|
|
$fullPath = $route['url']; |
231
|
|
|
if ($fullPath !== $target && !Text::beginsWith($fullPath, $target)) { |
232
|
|
|
continue; |
233
|
|
|
} |
234
|
|
|
$fLen = strlen($fullPath); |
235
|
|
|
if ($tSlash) { |
236
|
|
|
if ($fLen != $tLen && !Text::beginsWith($fullPath, $target . '/')) |
237
|
|
|
continue; |
238
|
|
|
} elseif ($fLen > $tLen + 1 && $fullPath[$tLen + 1] != '{' && !Text::beginsWith($fullPath, '{')) { |
239
|
|
|
//when mapped to root exclude paths that have static parts |
240
|
|
|
//they are listed else where under that static part name |
241
|
|
|
continue; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
if (!static::verifyAccess($route)) { |
245
|
|
|
continue; |
246
|
|
|
} |
247
|
|
|
foreach (static::$excludedPaths as $exclude) { |
248
|
|
|
if (empty($exclude)) { |
249
|
|
|
if ($fullPath == $exclude) |
250
|
|
|
continue 2; |
251
|
|
|
} elseif (Text::beginsWith($fullPath, $exclude)) { |
252
|
|
|
continue 2; |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
$m = $route['metadata']; |
256
|
|
|
if ($id == '' && $m['resourcePath'] != '') { |
257
|
|
|
continue; |
258
|
|
|
} |
259
|
|
|
if (isset($filter[$httpMethod][$fullPath])) { |
260
|
|
|
continue; |
261
|
|
|
} |
262
|
|
|
$filter[$httpMethod][$fullPath] = true; |
263
|
|
|
// reset body params |
264
|
|
|
$this->_bodyParam = array( |
265
|
|
|
'required' => false, |
266
|
|
|
'description' => array() |
267
|
|
|
); |
268
|
|
|
$count++; |
269
|
|
|
$className = $this->_noNamespace($route['className']); |
270
|
|
|
if (!$r) { |
271
|
|
|
$resourcePath = '/' |
272
|
|
|
. trim($m['resourcePath'], '/'); |
273
|
|
|
$r = $this->_operationListing($resourcePath); |
274
|
|
|
} |
275
|
|
|
$parts = explode('/', $fullPath); |
276
|
|
|
$pos = count($parts) - 1; |
277
|
|
|
if (count($parts) == 1 && $httpMethod == 'GET') { |
278
|
|
|
} else { |
279
|
|
|
for ($i = 0; $i < count($parts); $i++) { |
280
|
|
|
if (strlen($parts[$i]) && $parts[$i][0] == '{') { |
281
|
|
|
$pos = $i - 1; |
282
|
|
|
break; |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
$nickname = $this->_nickname($route); |
287
|
|
|
$index = static::$placeFormatExtensionBeforeDynamicParts && $pos > 0 ? $pos : 0; |
288
|
|
|
if (!empty($parts[$index])) |
289
|
|
|
$parts[$index] .= $this->formatString; |
290
|
|
|
|
291
|
|
|
$fullPath = implode('/', $parts); |
292
|
|
|
$description = isset( |
293
|
|
|
$m['classDescription']) |
294
|
|
|
? $m['classDescription'] |
295
|
|
|
: $className . ' API'; |
296
|
|
|
if (empty($m['description'])) { |
297
|
|
|
$m['description'] = $this->restler->getProductionMode() |
298
|
|
|
? '' |
299
|
|
|
: 'routes to <mark>' |
300
|
|
|
. $route['className'] |
301
|
|
|
. '::' |
302
|
|
|
. $route['methodName'] . '();</mark>'; |
303
|
|
|
} |
304
|
|
|
if (empty($m['longDescription'])) { |
305
|
|
|
$m['longDescription'] = $this->restler->getProductionMode() |
306
|
|
|
? '' |
307
|
|
|
: 'Add PHPDoc long description to ' |
308
|
|
|
. "<mark>$className::" |
309
|
|
|
. $route['methodName'] . '();</mark>' |
310
|
|
|
. ' (the api method) to write here'; |
311
|
|
|
} |
312
|
|
|
$operation = $this->_operation( |
313
|
|
|
$route, |
314
|
|
|
$nickname, |
315
|
|
|
$httpMethod, |
316
|
|
|
$m['description'], |
317
|
|
|
$m['longDescription'] |
318
|
|
|
); |
319
|
|
|
if (isset($m['throws'])) { |
320
|
|
|
foreach ($m['throws'] as $exception) { |
321
|
|
|
$operation->errorResponses[] = array( |
322
|
|
|
'reason' => $exception['message'], |
323
|
|
|
'code' => $exception['code']); |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
if (isset($m['param'])) { |
327
|
|
|
foreach ($m['param'] as $param) { |
328
|
|
|
//combine body params as one |
329
|
|
|
$p = $this->_parameter($param); |
330
|
|
|
if ($p->paramType == 'body') { |
331
|
|
|
$this->_appendToBody($p); |
332
|
|
|
} else { |
333
|
|
|
$operation->parameters[] = $p; |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
} |
337
|
|
|
if ( |
338
|
|
|
count($this->_bodyParam['description']) || |
339
|
|
|
( |
340
|
|
|
$this->_fullDataRequested && |
341
|
|
|
$httpMethod != 'GET' && |
342
|
|
|
$httpMethod != 'DELETE' |
343
|
|
|
) |
344
|
|
|
) { |
345
|
|
|
$operation->parameters[] = $this->_getBody(); |
346
|
|
|
} |
347
|
|
|
if (isset($m['return']['type'])) { |
348
|
|
|
$responseClass = $m['return']['type']; |
349
|
|
|
if (is_string($responseClass)) { |
350
|
|
|
if (class_exists($responseClass)) { |
351
|
|
|
$this->_model($responseClass); |
352
|
|
|
$operation->responseClass |
353
|
|
|
= $this->_noNamespace($responseClass); |
354
|
|
|
} elseif (strtolower($responseClass) == 'array') { |
355
|
|
|
$operation->responseClass = 'Array'; |
356
|
|
|
$rt = $m['return']; |
357
|
|
|
if ( |
358
|
|
|
isset( |
359
|
|
|
$rt[CommentParser::$embeddedDataName]['type']) |
360
|
|
|
) { |
361
|
|
|
$rt = $rt[CommentParser::$embeddedDataName] |
362
|
|
|
['type']; |
363
|
|
|
if (class_exists($rt)) { |
364
|
|
|
$this->_model($rt); |
365
|
|
|
$operation->responseClass .= '[' . |
366
|
|
|
$this->_noNamespace($rt) . ']'; |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
} |
371
|
|
|
} |
372
|
|
|
$api = false; |
373
|
|
|
|
374
|
|
|
if (static::$groupOperations) { |
375
|
|
|
foreach ($r->apis as $a) { |
376
|
|
|
if ($a->path == "$prefix/$fullPath") { |
377
|
|
|
$api = $a; |
378
|
|
|
break; |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
if (!$api) { |
384
|
|
|
$api = $this->_api("$prefix/$fullPath", $description); |
385
|
|
|
$r->apis[] = $api; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
$api->operations[] = $operation; |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
if (!$count) { |
392
|
|
|
throw new RestException(404); |
393
|
|
|
} |
394
|
|
|
if (!is_null($r)) |
395
|
|
|
$r->models = $this->_models; |
396
|
|
|
usort( |
397
|
|
|
$r->apis, |
398
|
|
|
function ($a, $b) { |
399
|
|
|
$order = array( |
400
|
|
|
'GET' => 1, |
401
|
|
|
'POST' => 2, |
402
|
|
|
'PUT' => 3, |
403
|
|
|
'PATCH' => 4, |
404
|
|
|
'DELETE' => 5 |
405
|
|
|
); |
406
|
|
|
return |
407
|
|
|
$a->operations[0]->httpMethod == |
408
|
|
|
$b->operations[0]->httpMethod |
409
|
|
|
? $a->path > $b->path |
410
|
|
|
: $order[$a->operations[0]->httpMethod] > |
411
|
|
|
$order[$b->operations[0]->httpMethod]; |
412
|
|
|
} |
413
|
|
|
); |
414
|
|
|
return $r; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
protected function _nickname(array $route) |
418
|
|
|
{ |
419
|
|
|
static $hash = array(); |
420
|
|
|
$method = $route['methodName']; |
421
|
|
|
if (isset(static::$prefixes[$method])) { |
422
|
|
|
$method = static::$prefixes[$method]; |
423
|
|
|
} else { |
424
|
|
|
$method = str_replace( |
425
|
|
|
array_keys(static::$prefixes), |
426
|
|
|
array_values(static::$prefixes), |
427
|
|
|
$method |
428
|
|
|
); |
429
|
|
|
} |
430
|
|
|
while (isset($hash[$method]) && $route['url'] != $hash[$method]) { |
431
|
|
|
//create another one |
432
|
|
|
$method .= '_'; |
433
|
|
|
} |
434
|
|
|
$hash[$method] = $route['url']; |
435
|
|
|
return $method; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
protected function _noNamespace($className) |
439
|
|
|
{ |
440
|
|
|
$className = explode('\\', $className); |
441
|
|
|
return end($className); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
protected function _operationListing($resourcePath = '/') |
445
|
|
|
{ |
446
|
|
|
$r = $this->_resourceListing(); |
447
|
|
|
$r->resourcePath = $resourcePath; |
448
|
|
|
$r->models = new stdClass(); |
449
|
|
|
return $r; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
protected function _resourceListing() |
453
|
|
|
{ |
454
|
|
|
$r = new stdClass(); |
455
|
|
|
$r->apiVersion = (string)$this->restler->_requestedApiVersion; |
456
|
|
|
$r->swaggerVersion = "1.1"; |
457
|
|
|
$r->basePath = $this->restler->getBaseUrl(); |
458
|
|
|
$r->produces = $this->restler->getWritableMimeTypes(); |
459
|
|
|
$r->consumes = $this->restler->getReadableMimeTypes(); |
460
|
|
|
$r->apis = array(); |
461
|
|
|
return $r; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
protected function _api($path, $description = '') |
465
|
|
|
{ |
466
|
|
|
$r = new stdClass(); |
467
|
|
|
$r->path = $path; |
468
|
|
|
$r->description = |
469
|
|
|
empty($description) && $this->restler->getProductionMode() |
470
|
|
|
? 'Use PHPDoc comment to describe here' |
471
|
|
|
: $description; |
472
|
|
|
$r->operations = array(); |
473
|
|
|
return $r; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
protected function _operation( |
477
|
|
|
$route, |
478
|
|
|
$nickname, |
479
|
|
|
$httpMethod = 'GET', |
480
|
|
|
$summary = 'description', |
481
|
|
|
$notes = 'long description', |
482
|
|
|
$responseClass = 'void' |
483
|
|
|
) { |
484
|
|
|
//reset body params |
485
|
|
|
$this->_bodyParam = array( |
486
|
|
|
'required' => false, |
487
|
|
|
'description' => array() |
488
|
|
|
); |
489
|
|
|
|
490
|
|
|
$r = new stdClass(); |
491
|
|
|
$r->httpMethod = $httpMethod; |
492
|
|
|
$r->nickname = $nickname; |
493
|
|
|
$r->responseClass = $responseClass; |
494
|
|
|
|
495
|
|
|
$r->parameters = array(); |
496
|
|
|
|
497
|
|
|
$r->summary = $summary . ($route['accessLevel'] > 2 |
498
|
|
|
? static::$apiDescriptionSuffixSymbols[2] |
499
|
|
|
: static::$apiDescriptionSuffixSymbols[$route['accessLevel']] |
500
|
|
|
); |
501
|
|
|
$r->notes = $notes; |
502
|
|
|
|
503
|
|
|
$r->errorResponses = array(); |
504
|
|
|
return $r; |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
protected function _parameter($param) |
508
|
|
|
{ |
509
|
|
|
$r = new stdClass(); |
510
|
|
|
$r->name = $param['name']; |
511
|
|
|
$r->description = !empty($param['description']) |
512
|
|
|
? $param['description'] . '.' |
513
|
|
|
: ($this->restler->getProductionMode() |
514
|
|
|
? '' |
515
|
|
|
: 'add <mark>@param {type} $' . $r->name |
516
|
|
|
. ' {comment}</mark> to describe here'); |
517
|
|
|
//paramType can be path or query or body or header |
518
|
|
|
$r->paramType = Util::nestedValue($param, CommentParser::$embeddedDataName, 'from') ? : 'query'; |
519
|
|
|
$r->required = isset($param['required']) && $param['required']; |
520
|
|
|
if (isset($param['default'])) { |
521
|
|
|
$r->defaultValue = $param['default']; |
522
|
|
|
} elseif (isset($param[CommentParser::$embeddedDataName]['example'])) { |
523
|
|
|
$r->defaultValue |
524
|
|
|
= $param[CommentParser::$embeddedDataName]['example']; |
525
|
|
|
} |
526
|
|
|
$r->allowMultiple = false; |
527
|
|
|
$type = 'string'; |
528
|
|
|
if (isset($param['type'])) { |
529
|
|
|
$type = $param['type']; |
530
|
|
|
if (is_array($type)) { |
531
|
|
|
$type = array_shift($type); |
532
|
|
|
} |
533
|
|
|
if ($type == 'array') { |
534
|
|
|
$contentType = Util::nestedValue( |
535
|
|
|
$param, |
536
|
|
|
CommentParser::$embeddedDataName, |
537
|
|
|
'type' |
538
|
|
|
); |
539
|
|
|
if ($contentType) { |
540
|
|
|
if ($contentType == 'indexed') { |
541
|
|
|
$type = 'Array'; |
542
|
|
|
} elseif ($contentType == 'associative') { |
543
|
|
|
$type = 'Object'; |
544
|
|
|
} else { |
545
|
|
|
$type = "Array[$contentType]"; |
546
|
|
|
} |
547
|
|
|
if (Util::isObjectOrArray($contentType)) { |
548
|
|
|
$this->_model($contentType); |
549
|
|
|
} |
550
|
|
|
} elseif (isset(static::$dataTypeAlias[$type])) { |
551
|
|
|
$type = static::$dataTypeAlias[$type]; |
552
|
|
|
} |
553
|
|
|
} elseif (Util::isObjectOrArray($type)) { |
554
|
|
|
$this->_model($type); |
555
|
|
|
} elseif (isset(static::$dataTypeAlias[$type])) { |
556
|
|
|
$type = static::$dataTypeAlias[$type]; |
557
|
|
|
} |
558
|
|
|
} |
559
|
|
|
$r->dataType = $type; |
560
|
|
|
if (isset($param[CommentParser::$embeddedDataName])) { |
561
|
|
|
$p = $param[CommentParser::$embeddedDataName]; |
562
|
|
|
if (isset($p['min']) && isset($p['max'])) { |
563
|
|
|
$r->allowableValues = array( |
564
|
|
|
'valueType' => 'RANGE', |
565
|
|
|
'min' => $p['min'], |
566
|
|
|
'max' => $p['max'], |
567
|
|
|
); |
568
|
|
|
} elseif (isset($p['choice'])) { |
569
|
|
|
$r->allowableValues = array( |
570
|
|
|
'valueType' => 'LIST', |
571
|
|
|
'values' => $p['choice'] |
572
|
|
|
); |
573
|
|
|
} |
574
|
|
|
} |
575
|
|
|
return $r; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
protected function _appendToBody($p) |
579
|
|
|
{ |
580
|
|
|
if ($p->name === Defaults::$fullRequestDataName) { |
581
|
|
|
$this->_fullDataRequested = $p; |
582
|
|
|
unset($this->_bodyParam['names'][Defaults::$fullRequestDataName]); |
583
|
|
|
return; |
584
|
|
|
} |
585
|
|
|
$this->_bodyParam['description'][$p->name] |
586
|
|
|
= "$p->name" |
587
|
|
|
. ' : <tag>' . $p->dataType . '</tag> ' |
588
|
|
|
. ($p->required ? ' <i>(required)</i> - ' : ' - ') |
589
|
|
|
. $p->description; |
590
|
|
|
$this->_bodyParam['required'] = $p->required |
591
|
|
|
|| $this->_bodyParam['required']; |
592
|
|
|
$this->_bodyParam['names'][$p->name] = $p; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
protected function _getBody() |
596
|
|
|
{ |
597
|
|
|
$r = new stdClass(); |
598
|
|
|
$n = isset($this->_bodyParam['names']) |
599
|
|
|
? array_values($this->_bodyParam['names']) |
600
|
|
|
: array(); |
601
|
|
|
if (count($n) == 1) { |
602
|
|
|
if (isset($this->_models->{$n[0]->dataType})) { |
603
|
|
|
// ============ custom class =================== |
604
|
|
|
$r = $n[0]; |
605
|
|
|
$c = $this->_models->{$r->dataType}; |
606
|
|
|
$a = $c->properties; |
607
|
|
|
$r->description = "Paste JSON data here"; |
608
|
|
|
if (count($a)) { |
609
|
|
|
$r->description .= " with the following" |
610
|
|
|
. (count($a) > 1 ? ' properties.' : ' property.'); |
611
|
|
|
foreach ($a as $k => $v) { |
612
|
|
|
$r->description .= "<hr/>$k : <tag>" |
613
|
|
|
. $v['type'] . '</tag> ' |
614
|
|
|
. (isset($v['required']) ? '(required)' : '') |
615
|
|
|
. ' - ' . $v['description']; |
616
|
|
|
} |
617
|
|
|
} |
618
|
|
|
$r->defaultValue = "{\n \"" |
619
|
|
|
. implode( |
620
|
|
|
"\": \"\",\n \"", |
621
|
|
|
array_keys($c->properties)) |
622
|
|
|
. "\": \"\"\n}"; |
623
|
|
|
return $r; |
624
|
|
|
} elseif (false !== ($p = strpos($n[0]->dataType, '['))) { |
625
|
|
|
// ============ array of custom class =============== |
626
|
|
|
$r = $n[0]; |
627
|
|
|
$t = substr($r->dataType, $p + 1, -1); |
628
|
|
|
if ($c = Util::nestedValue($this->_models, $t)) { |
629
|
|
|
$a = $c->properties; |
630
|
|
|
$r->description = "Paste JSON data here"; |
631
|
|
|
if (count($a)) { |
632
|
|
|
$r->description .= " with an array of objects with the following" |
633
|
|
|
. (count($a) > 1 ? ' properties.' : ' property.'); |
634
|
|
|
foreach ($a as $k => $v) { |
635
|
|
|
$r->description .= "<hr/>$k : <tag>" |
636
|
|
|
. $v['type'] . '</tag> ' |
637
|
|
|
. (isset($v['required']) ? '(required)' : '') |
638
|
|
|
. ' - ' . $v['description']; |
639
|
|
|
} |
640
|
|
|
} |
641
|
|
|
$r->defaultValue = "[\n {\n \"" |
642
|
|
|
. implode( |
643
|
|
|
"\": \"\",\n \"", |
644
|
|
|
array_keys($c->properties)) |
645
|
|
|
. "\": \"\"\n }\n]"; |
646
|
|
|
return $r; |
647
|
|
|
} else { |
648
|
|
|
$r->description = "Paste JSON data here with an array of $t values."; |
649
|
|
|
$r->defaultValue = "[ ]"; |
650
|
|
|
return $r; |
651
|
|
|
} |
652
|
|
|
} elseif ($n[0]->dataType == 'Array') { |
653
|
|
|
// ============ array =============================== |
654
|
|
|
$r = $n[0]; |
655
|
|
|
$r->description = "Paste JSON array data here" |
656
|
|
|
. ($r->required ? ' (required) . ' : '. ') |
657
|
|
|
. "<br/>$r->description"; |
658
|
|
|
$r->defaultValue = "[\n {\n \"" |
659
|
|
|
. "property\" : \"\"\n }\n]"; |
660
|
|
|
return $r; |
661
|
|
|
} elseif ($n[0]->dataType == 'Object') { |
662
|
|
|
// ============ object ============================== |
663
|
|
|
$r = $n[0]; |
664
|
|
|
$r->description = "Paste JSON object data here" |
665
|
|
|
. ($r->required ? ' (required) . ' : '. ') |
666
|
|
|
. "<br/>$r->description"; |
667
|
|
|
$r->defaultValue = "{\n \"" |
668
|
|
|
. "property\" : \"\"\n}"; |
669
|
|
|
return $r; |
670
|
|
|
} |
671
|
|
|
} |
672
|
|
|
$p = array_values($this->_bodyParam['description']); |
673
|
|
|
$r->name = 'REQUEST_BODY'; |
674
|
|
|
$r->description = "Paste JSON data here"; |
675
|
|
|
if (count($p) == 0 && $this->_fullDataRequested) { |
676
|
|
|
$r->required = $this->_fullDataRequested->required; |
677
|
|
|
$r->defaultValue = "{\n \"property\" : \"\"\n}"; |
678
|
|
|
} else { |
679
|
|
|
$r->description .= " with the following" |
680
|
|
|
. (count($p) > 1 ? ' properties.' : ' property.') |
681
|
|
|
. '<hr/>' |
682
|
|
|
. implode("<hr/>", $p); |
683
|
|
|
$r->required = $this->_bodyParam['required']; |
684
|
|
|
// Create default object that includes parameters to be submitted |
685
|
|
|
$defaultObject = new \StdClass(); |
686
|
|
|
foreach ($this->_bodyParam['names'] as $name => $values) { |
687
|
|
|
if (!$values->required) |
688
|
|
|
continue; |
689
|
|
|
if (class_exists($values->dataType)) { |
690
|
|
|
$myClassName = $values->dataType; |
691
|
|
|
$defaultObject->$name = new $myClassName(); |
692
|
|
|
} else { |
693
|
|
|
$defaultObject->$name = ''; |
694
|
|
|
} |
695
|
|
|
} |
696
|
|
|
$r->defaultValue = Scope::get('JsonFormat')->encode($defaultObject, true); |
697
|
|
|
} |
698
|
|
|
$r->paramType = 'body'; |
699
|
|
|
$r->allowMultiple = false; |
700
|
|
|
$r->dataType = 'Object'; |
701
|
|
|
return $r; |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
protected function _model($className, $instance = null) |
705
|
|
|
{ |
706
|
|
|
$id = $this->_noNamespace($className); |
707
|
|
|
if (isset($this->_models->{$id})) { |
708
|
|
|
return; |
709
|
|
|
} |
710
|
|
|
$properties = array(); |
711
|
|
|
if (!$instance) { |
712
|
|
|
if (!class_exists($className)) |
713
|
|
|
return; |
714
|
|
|
$instance = new $className(); |
715
|
|
|
} |
716
|
|
|
$data = get_object_vars($instance); |
717
|
|
|
$reflectionClass = new \ReflectionClass($className); |
718
|
|
|
foreach ($data as $key => $value) { |
719
|
|
|
$propertyMetaData = null; |
720
|
|
|
|
721
|
|
|
try { |
722
|
|
|
$property = $reflectionClass->getProperty($key); |
723
|
|
|
if ($c = $property->getDocComment()) { |
724
|
|
|
$propertyMetaData = Util::nestedValue( |
725
|
|
|
CommentParser::parse($c), |
726
|
|
|
'var' |
727
|
|
|
); |
728
|
|
|
} |
729
|
|
|
} catch (\ReflectionException $e) { |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
if (is_null($propertyMetaData)) { |
733
|
|
|
$type = $this->getType($value, true); |
734
|
|
|
$description = ''; |
735
|
|
|
} else { |
736
|
|
|
$type = Util::nestedValue( |
737
|
|
|
$propertyMetaData, |
738
|
|
|
'type' |
739
|
|
|
) ? : $this->getType($value, true); |
740
|
|
|
$description = Util::nestedValue( |
741
|
|
|
$propertyMetaData, |
742
|
|
|
'description' |
743
|
|
|
) ? : ''; |
744
|
|
|
|
745
|
|
|
if (class_exists($type)) { |
746
|
|
|
$this->_model($type); |
747
|
|
|
$type = $this->_noNamespace($type); |
748
|
|
|
} |
749
|
|
|
} |
750
|
|
|
|
751
|
|
|
if (isset(static::$dataTypeAlias[$type])) { |
752
|
|
|
$type = static::$dataTypeAlias[$type]; |
753
|
|
|
} |
754
|
|
|
$properties[$key] = array( |
755
|
|
|
'type' => $type, |
756
|
|
|
'description' => $description |
757
|
|
|
); |
758
|
|
|
if ( |
759
|
|
|
Util::nestedValue( |
760
|
|
|
$propertyMetaData, |
761
|
|
|
CommentParser::$embeddedDataName, |
762
|
|
|
'required' |
763
|
|
|
) |
764
|
|
|
) { |
765
|
|
|
$properties[$key]['required'] = true; |
766
|
|
|
} |
767
|
|
|
if ($type == 'Array') { |
768
|
|
|
$itemType = Util::nestedValue( |
769
|
|
|
$propertyMetaData, |
770
|
|
|
CommentParser::$embeddedDataName, |
771
|
|
|
'type' |
772
|
|
|
) ? : |
773
|
|
|
(count($value) |
774
|
|
|
? $this->getType(end($value), true) |
775
|
|
|
: 'string'); |
776
|
|
|
if (class_exists($itemType)) { |
777
|
|
|
$this->_model($itemType); |
778
|
|
|
$itemType = $this->_noNamespace($itemType); |
779
|
|
|
} |
780
|
|
|
$properties[$key]['items'] = array( |
781
|
|
|
'type' => $itemType, |
782
|
|
|
/*'description' => '' */ //TODO: add description |
783
|
|
|
); |
784
|
|
|
} else if (preg_match('/^Array\[(.+)\]$/', $type, $matches)) { |
785
|
|
|
$itemType = $matches[1]; |
786
|
|
|
$properties[$key]['type'] = 'Array'; |
787
|
|
|
$properties[$key]['items']['type'] = $this->_noNamespace($itemType); |
788
|
|
|
|
789
|
|
|
if (class_exists($itemType)) { |
790
|
|
|
$this->_model($itemType); |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
} |
794
|
|
|
if (!empty($properties)) { |
795
|
|
|
$model = new stdClass(); |
796
|
|
|
$model->id = $id; |
797
|
|
|
$model->properties = $properties; |
798
|
|
|
$this->_models->{$id} = $model; |
799
|
|
|
} |
800
|
|
|
} |
801
|
|
|
|
802
|
|
|
/** |
803
|
|
|
* Find the data type of the given value. |
804
|
|
|
* |
805
|
|
|
* |
806
|
|
|
* @param mixed $o given value for finding type |
807
|
|
|
* |
808
|
|
|
* @param bool $appendToModels if an object is found should we append to |
809
|
|
|
* our models list? |
810
|
|
|
* |
811
|
|
|
* @return string |
812
|
|
|
* |
813
|
|
|
* @access private |
814
|
|
|
*/ |
815
|
|
|
public function getType($o, $appendToModels = false) |
816
|
|
|
{ |
817
|
|
|
if (is_object($o)) { |
818
|
|
|
$oc = get_class($o); |
819
|
|
|
if ($appendToModels) { |
820
|
|
|
$this->_model($oc, $o); |
821
|
|
|
} |
822
|
|
|
return $this->_noNamespace($oc); |
823
|
|
|
} |
824
|
|
|
if (is_array($o)) { |
825
|
|
|
if (count($o)) { |
826
|
|
|
$child = end($o); |
827
|
|
|
if (Util::isObjectOrArray($child)) { |
828
|
|
|
$childType = $this->getType($child, $appendToModels); |
829
|
|
|
return "Array[$childType]"; |
830
|
|
|
} |
831
|
|
|
} |
832
|
|
|
return 'array'; |
833
|
|
|
} |
834
|
|
|
if (is_bool($o)) return 'boolean'; |
835
|
|
|
if (is_numeric($o)) return is_float($o) ? 'float' : 'int'; |
836
|
|
|
return 'string'; |
837
|
|
|
} |
838
|
|
|
|
839
|
|
|
/** |
840
|
|
|
* pre call for index() |
841
|
|
|
* |
842
|
|
|
* if cache is present, use cache |
843
|
|
|
*/ |
844
|
|
|
public function _pre_index_json() |
845
|
|
|
{ |
846
|
|
|
$userClass = Defaults::$userIdentifierClass; |
847
|
|
|
$this->cacheName = $userClass::getCacheIdentifier() |
848
|
|
|
. '_resources-v' |
849
|
|
|
. $this->restler->_requestedApiVersion; |
850
|
|
|
if ( |
851
|
|
|
$this->restler->getProductionMode() |
852
|
|
|
&& !$this->restler->refreshCache |
853
|
|
|
&& $this->restler->cache->isCached($this->cacheName) |
854
|
|
|
) { |
855
|
|
|
//by pass call, compose, postCall stages and directly send response |
856
|
|
|
$this->restler->composeHeaders(); |
857
|
|
|
die($this->restler->cache->get($this->cacheName)); |
858
|
|
|
} |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
/** |
862
|
|
|
* post call for index() |
863
|
|
|
* |
864
|
|
|
* create cache if in production mode |
865
|
|
|
* |
866
|
|
|
* @param $responseData |
867
|
|
|
* |
868
|
|
|
* @internal param string $data composed json output |
869
|
|
|
* |
870
|
|
|
* @return string |
871
|
|
|
*/ |
872
|
|
|
public function _post_index_json($responseData) |
873
|
|
|
{ |
874
|
|
|
if ($this->restler->getProductionMode()) { |
875
|
|
|
$this->restler->cache->set($this->cacheName, $responseData); |
876
|
|
|
} |
877
|
|
|
return $responseData; |
878
|
|
|
} |
879
|
|
|
|
880
|
|
|
/** |
881
|
|
|
* @access hybrid |
882
|
|
|
* @return \stdClass |
883
|
|
|
*/ |
884
|
|
|
public function index() |
885
|
|
|
{ |
886
|
|
|
if (!static::$accessControlFunction && Defaults::$accessControlFunction) |
887
|
|
|
static::$accessControlFunction = Defaults::$accessControlFunction; |
888
|
|
|
$version = $this->restler->getRequestedApiVersion(); |
889
|
|
|
$allRoutes = Util::nestedValue(Routes::toArray(), "v$version"); |
890
|
|
|
$r = $this->_resourceListing(); |
891
|
|
|
$map = array(); |
892
|
|
|
if (isset($allRoutes['*'])) { |
893
|
|
|
$this->_mapResources($allRoutes['*'], $map, $version); |
894
|
|
|
unset($allRoutes['*']); |
895
|
|
|
} |
896
|
|
|
$this->_mapResources($allRoutes, $map, $version); |
897
|
|
|
foreach ($map as $path => $description) { |
898
|
|
|
if (!Text::contains($path, '{')) { |
899
|
|
|
//add id |
900
|
|
|
$r->apis[] = array( |
901
|
|
|
'path' => $path . $this->formatString, |
902
|
|
|
'description' => $description |
903
|
|
|
); |
904
|
|
|
} |
905
|
|
|
} |
906
|
|
|
if (Defaults::$useUrlBasedVersioning && static::$listHigherVersions) { |
907
|
|
|
$nextVersion = $version + 1; |
908
|
|
|
if ($nextVersion <= $this->restler->getApiVersion()) { |
909
|
|
|
list($status, $data) = $this->_loadResource("/v$nextVersion/resources.json"); |
910
|
|
|
if ($status == 200) { |
911
|
|
|
$r->apis = array_merge($r->apis, $data->apis); |
912
|
|
|
$r->apiVersion = $data->apiVersion; |
913
|
|
|
} |
914
|
|
|
} |
915
|
|
|
} |
916
|
|
|
return $r; |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
protected function _loadResource($url) |
920
|
|
|
{ |
921
|
|
|
$ch = curl_init($this->restler->getBaseUrl() . $url |
922
|
|
|
. (empty($_GET) ? '' : '?' . http_build_query($_GET))); |
923
|
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); |
924
|
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15); |
925
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
926
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array( |
927
|
|
|
'Accept:application/json', |
928
|
|
|
)); |
929
|
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); |
930
|
|
|
$result = json_decode(curl_exec($ch)); |
931
|
|
|
$http_status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); |
932
|
|
|
return array($http_status, $result); |
933
|
|
|
} |
934
|
|
|
|
935
|
|
|
protected function _mapResources(array $allRoutes, array &$map, $version = 1) |
936
|
|
|
{ |
937
|
|
|
foreach ($allRoutes as $fullPath => $routes) { |
938
|
|
|
$path = explode('/', $fullPath); |
939
|
|
|
$resource = isset($path[0]) ? $path[0] : ''; |
940
|
|
|
if ($resource == 'resources' || Text::endsWith($resource, 'index')) |
941
|
|
|
continue; |
942
|
|
|
foreach ($routes as $httpMethod => $route) { |
943
|
|
|
if (in_array($httpMethod, static::$excludedHttpMethods)) { |
944
|
|
|
continue; |
945
|
|
|
} |
946
|
|
|
if (!static::verifyAccess($route)) { |
947
|
|
|
continue; |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
foreach (static::$excludedPaths as $exclude) { |
951
|
|
|
if (empty($exclude)) { |
952
|
|
|
if ($fullPath == $exclude) |
953
|
|
|
continue 2; |
954
|
|
|
} elseif (Text::beginsWith($fullPath, $exclude)) { |
955
|
|
|
continue 2; |
956
|
|
|
} |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
$res = $resource |
960
|
|
|
? ($version == 1 ? "/resources/$resource" : "/v$version/resources/$resource-v$version") |
961
|
|
|
: ($version == 1 ? "/resources/root" : "/v$version/resources/root-v$version"); |
962
|
|
|
|
963
|
|
|
if (empty($map[$res])) { |
964
|
|
|
$map[$res] = isset( |
965
|
|
|
$route['metadata']['classDescription']) |
966
|
|
|
? $route['metadata']['classDescription'] : ''; |
967
|
|
|
} |
968
|
|
|
} |
969
|
|
|
} |
970
|
|
|
} |
971
|
|
|
|
972
|
|
|
/** |
973
|
|
|
* Maximum api version supported by the api class |
974
|
|
|
* @return int |
975
|
|
|
*/ |
976
|
|
|
public static function __getMaximumSupportedVersion() |
977
|
|
|
{ |
978
|
|
|
return Scope::get('Restler')->getApiVersion(); |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
/** |
982
|
|
|
* Verifies that the requesting user is allowed to view the docs for this API |
983
|
|
|
* |
984
|
|
|
* @param $route |
985
|
|
|
* |
986
|
|
|
* @return boolean True if the user should be able to view this API's docs |
987
|
|
|
*/ |
988
|
|
|
protected function verifyAccess($route) |
989
|
|
|
{ |
990
|
|
|
if ($route['accessLevel'] < 2) { |
991
|
|
|
return true; |
992
|
|
|
} |
993
|
|
|
if ( |
994
|
|
|
static::$hideProtected |
995
|
|
|
&& !$this->_authenticated |
996
|
|
|
&& $route['accessLevel'] > 1 |
997
|
|
|
) { |
998
|
|
|
return false; |
999
|
|
|
} |
1000
|
|
|
if ( |
1001
|
|
|
$this->_authenticated |
1002
|
|
|
&& static::$accessControlFunction |
1003
|
|
|
&& (!call_user_func( |
1004
|
|
|
static::$accessControlFunction, $route['metadata'])) |
1005
|
|
|
) { |
1006
|
|
|
return false; |
1007
|
|
|
} |
1008
|
|
|
return true; |
1009
|
|
|
} |
1010
|
|
|
} |
1011
|
|
|
|