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 => ' <i class="icon-unlock-alt icon-large"></i>', //public api 83 1 => ' <i class="icon-adjust icon-large"></i>', //hybrid api 84 2 => ' <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