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