public static function get($for = '', $activeTrail = null) { if (empty(static::$tree)) { /** @var Restler $restler */ $restler = Scope::get('Restler'); if (static::$addExtension) { static::$extension = isset($restler->responseFormat) ? '.' . $restler->responseFormat->getExtension() : '.html'; } static::$url = $restler->getBaseUrl(); if (empty(static::$url)) { static::$url = ''; } static::$activeTrail = $activeTrail = empty($activeTrail) ? empty($restler->url) || $restler->url == 'index' ? static::$root : $restler->url : $activeTrail; if (static::$addExtension) { static::$extension = isset($restler->responseFormat) ? '.' . $restler->responseFormat->getExtension() : '.html'; } static::addUrls(static::$prepends); $map = Routes::findAll(static::$excludedPaths, array('POST', 'DELETE', 'PUT', 'PATCH'), $restler->getRequestedApiVersion()); foreach ($map as $path => $data) { foreach ($data as $item) { $access = $item['access']; $route = $item['route']; $url = $route['url']; if ($access && !Text::contains($url, '{')) { $label = Util::nestedValue($route, 'metadata', CommentParser::$embeddedDataName, 'label'); if (!empty($url)) { $url .= static::$extension; } static::add($url, $label); } } } static::addUrls(static::$appends); } elseif (empty($activeTrail)) { $activeTrail = static::$activeTrail; } $tree = static::$tree; $activeTrail = explode('/', $activeTrail); $nested =& static::nested($tree, $activeTrail); if (is_array($nested)) { $nested['active'] = true; } if (!empty($for)) { $for = explode('/', $for); $tree = static::nested($tree, $for)['children']; } return array_filter($tree); }
/** * Route the public and protected methods of an Api class * * @param string $className * @param string $resourcePath * @param int $version * * @throws RestException */ public static function addAPIClass($className, $resourcePath = '', $version = 1) { /* * Mapping Rules * ============= * * - Optional parameters should not be mapped to URL * - If a required parameter is of primitive type * - If one of the self::$prefixingParameterNames * - Map it to URL * - Else If request method is POST/PUT/PATCH * - Map it to body * - Else If request method is GET/DELETE * - Map it to body * - If a required parameter is not primitive type * - Do not include it in URL */ $class = new ReflectionClass($className); $dataName = CommentParser::$embeddedDataName; try { $classMetadata = CommentParser::parse($class->getDocComment()); } catch (Exception $e) { throw new RestException(500, "Error while parsing comments of `{$className}` class. " . $e->getMessage()); } $classMetadata['scope'] = $scope = static::scope($class); $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_PROTECTED); foreach ($methods as $method) { $methodUrl = strtolower($method->getName()); //method name should not begin with _ if ($methodUrl[0] == '_') { continue; } $doc = $method->getDocComment(); try { $metadata = CommentParser::parse($doc) + $classMetadata; } catch (Exception $e) { throw new RestException(500, "Error while parsing comments of `{$className}::{$method->getName()}` method. " . $e->getMessage()); } //@access should not be private if (isset($metadata['access']) && $metadata['access'] == 'private') { continue; } $arguments = array(); $defaults = array(); $params = $method->getParameters(); $position = 0; $pathParams = array(); $allowAmbiguity = isset($metadata['smart-auto-routing']) && $metadata['smart-auto-routing'] != 'true' || !Defaults::$smartAutoRouting; $metadata['resourcePath'] = trim($resourcePath, '/'); if (isset($classMetadata['description'])) { $metadata['classDescription'] = $classMetadata['description']; } if (isset($classMetadata['classLongDescription'])) { $metadata['classLongDescription'] = $classMetadata['longDescription']; } if (!isset($metadata['param'])) { $metadata['param'] = array(); } if (isset($metadata['return']['type'])) { if ($qualified = Scope::resolve($metadata['return']['type'], $scope)) { list($metadata['return']['type'], $metadata['return']['children']) = static::getTypeAndModel(new ReflectionClass($qualified), $scope); } } else { //assume return type is array $metadata['return']['type'] = 'array'; } foreach ($params as $param) { $children = array(); $type = $param->isArray() ? 'array' : $param->getClass(); $arguments[$param->getName()] = $position; $defaults[$position] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; if (!isset($metadata['param'][$position])) { $metadata['param'][$position] = array(); } $m =& $metadata['param'][$position]; $m['name'] = $param->getName(); if (!isset($m[$dataName])) { $m[$dataName] = array(); } $p =& $m[$dataName]; if (empty($m['label'])) { $m['label'] = Text::title($m['name']); } if (is_null($type) && isset($m['type'])) { $type = $m['type']; } if (isset(static::$fieldTypesByName[$m['name']]) && empty($p['type']) && $type == 'string') { $p['type'] = static::$fieldTypesByName[$m['name']]; } $m['default'] = $defaults[$position]; $m['required'] = !$param->isOptional(); $contentType = Util::nestedValue($p, 'type'); if ($type == 'array' && $contentType && ($qualified = Scope::resolve($contentType, $scope))) { list($p['type'], $children, $modelName) = static::getTypeAndModel(new ReflectionClass($qualified), $scope, $className . Text::title($methodUrl), $p); } if ($type instanceof ReflectionClass) { list($type, $children, $modelName) = static::getTypeAndModel($type, $scope, $className . Text::title($methodUrl), $p); } elseif ($type && is_string($type) && ($qualified = Scope::resolve($type, $scope))) { list($type, $children, $modelName) = static::getTypeAndModel(new ReflectionClass($qualified), $scope, $className . Text::title($methodUrl), $p); } if (isset($type)) { $m['type'] = $type; } $m['children'] = $children; if (isset($modelName)) { $m['model'] = $modelName; } if ($m['name'] == Defaults::$fullRequestDataName) { $from = 'body'; if (!isset($m['type'])) { $type = $m['type'] = 'array'; } } elseif (isset($p['from'])) { $from = $p['from']; } else { if (isset($type) && Util::isObjectOrArray($type)) { $from = 'body'; if (!isset($type)) { $type = $m['type'] = 'array'; } } elseif ($m['required'] && in_array($m['name'], static::$prefixingParameterNames)) { $from = 'path'; } else { $from = 'body'; } } $p['from'] = $from; if (!isset($m['type'])) { $type = $m['type'] = static::type($defaults[$position]); } if ($allowAmbiguity || $from == 'path') { $pathParams[] = $position; } $position++; } $accessLevel = 0; if ($method->isProtected()) { $accessLevel = 3; } elseif (isset($metadata['access'])) { if ($metadata['access'] == 'protected') { $accessLevel = 2; } elseif ($metadata['access'] == 'hybrid') { $accessLevel = 1; } } elseif (isset($metadata['protected'])) { $accessLevel = 2; } /* echo " access level $accessLevel for $className::" .$method->getName().$method->isProtected().PHP_EOL; */ // take note of the order $call = array('url' => null, 'className' => $className, 'path' => rtrim($resourcePath, '/'), 'methodName' => $method->getName(), 'arguments' => $arguments, 'defaults' => $defaults, 'metadata' => $metadata, 'accessLevel' => $accessLevel); // if manual route if (preg_match_all('/@url\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)' . '[ \\t]*\\/?(\\S*)/s', $doc, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $httpMethod = $match[1]; $url = rtrim($resourcePath . $match[2], '/'); //deep copy the call, as it may change for each @url $copy = unserialize(serialize($call)); foreach ($copy['metadata']['param'] as $i => $p) { $inPath = strpos($url, '{' . $p['name'] . '}') || strpos($url, ':' . $p['name']); if ($inPath) { $copy['metadata']['param'][$i][$dataName]['from'] = 'path'; } elseif ($httpMethod == 'GET' || $httpMethod == 'DELETE') { $copy['metadata']['param'][$i][$dataName]['from'] = 'query'; } elseif (empty($p[$dataName]['from']) || $p[$dataName]['from'] == 'path') { $copy['metadata']['param'][$i][$dataName]['from'] = 'body'; } } $url = preg_replace_callback('/{[^}]+}|:[^\\/]+/', function ($matches) use($copy) { $match = trim($matches[0], '{}:'); $index = $copy['arguments'][$match]; return '{' . Routes::typeChar(isset($copy['metadata']['param'][$index]['type']) ? $copy['metadata']['param'][$index]['type'] : null) . $index . '}'; }, $url); static::addPath($url, $copy, $httpMethod, $version); } //if auto route enabled, do so } elseif (Defaults::$autoRoutingEnabled) { // no configuration found so use convention if (preg_match_all('/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i', $methodUrl, $matches)) { $httpMethod = strtoupper($matches[0][0]); $methodUrl = substr($methodUrl, strlen($httpMethod)); } else { $httpMethod = 'GET'; } if ($methodUrl == 'index') { $methodUrl = ''; } $url = empty($methodUrl) ? rtrim($resourcePath, '/') : $resourcePath . $methodUrl; $lastPathParam = array_keys($pathParams); $lastPathParam = end($lastPathParam); for ($position = 0; $position < count($params); $position++) { $from = $metadata['param'][$position][$dataName]['from']; if ($from == 'body' && ($httpMethod == 'GET' || $httpMethod == 'DELETE')) { $call['metadata']['param'][$position][$dataName]['from'] = 'query'; } } if (empty($pathParams) || $allowAmbiguity) { static::addPath($url, $call, $httpMethod, $version); } foreach ($pathParams as $position) { if (!empty($url)) { $url .= '/'; } $url .= '{' . static::typeChar(isset($call['metadata']['param'][$position]['type']) ? $call['metadata']['param'][$position]['type'] : null) . $position . '}'; if ($allowAmbiguity || $position == $lastPathParam) { static::addPath($url, $call, $httpMethod, $version); } } } } }
private function apis($version = 1, $resource = false) { $map = Routes::findAll(static::$excludedPaths + array($this->base()), static::$excludedHttpMethods, $version); $r = array(); $a = array(); foreach ($map as $path => $data) { $route = $data[0]['route']; $access = $data[0]['access']; if ($access && !Text::contains($path, '{')) { $r[] = array('path' => empty($path) ? '/root' : "/{$path}"); } if (static::$hideProtected && !$access) { continue; } $grouper = array(); foreach ($data as $item) { $route = $item['route']; $access = $item['access']; if (static::$hideProtected && !$access) { continue; } $url = $route['url']; if (isset($grouper[$url])) { $grouper[$url]['operations'][] = $this->operation($route); } else { $api = array('path' => "/{$url}", 'description' => Util::nestedValue($route, 'metadata', 'classDescription') ?: '', 'operations' => array($this->operation($route))); static::$groupOperations ? $grouper[$url] = $api : ($a[$path][] = $api); } } if (!empty($grouper)) { $a[$path] = array_values($grouper); } else { $order = array('GET' => 1, 'POST' => 2, 'PUT' => 3, 'PATCH' => 4, 'DELETE' => 5); foreach ($a as &$b) { usort($b, function ($x, $y) use($order) { return $x['operations'][0]->method == $y['operations'][0]->method ? $x['path'] > $y['path'] : $order[$x['operations'][0]->method] > $order[$y['operations'][0]->method]; }); } } } if (false !== $resource) { if ($resource == 'root') { $resource = ''; } if (isset($a[$resource])) { return $a[$resource]; } } return $r; }
/** * @access hybrid * @return \stdClass */ public function index() { if (!static::$accessControlFunction && Defaults::$accessControlFunction) { static::$accessControlFunction = Defaults::$accessControlFunction; } $version = $this->restler->getRequestedApiVersion(); $allRoutes = Util::nestedValue(Routes::toArray(), "v{$version}"); $r = $this->_resourceListing(); $map = array(); if (isset($allRoutes['*'])) { $this->_mapResources($allRoutes['*'], $map, $version); unset($allRoutes['*']); } $this->_mapResources($allRoutes, $map, $version); foreach ($map as $path => $description) { if (!String::contains($path, '{')) { //add id $r->apis[] = array('path' => $path . $this->formatString, 'description' => $description); } } if (Defaults::$useUrlBasedVersioning && static::$listHigherVersions) { $nextVersion = $version + 1; if ($nextVersion <= $this->restler->getApiVersion()) { list($status, $data) = $this->_loadResource("/v{$nextVersion}/resources.json"); if ($status == 200) { $r->apis = array_merge($r->apis, $data->apis); $r->apiVersion = $data->apiVersion; } } } return $r; }
/** * Store the url map cache if needed */ public function __destruct() { if ($this->productionMode && !$this->cached) { $this->cache->set('routes', Routes::toArray() + array('apiVersionMap' => $this->apiVersionMap)); } }
/** * @access hybrid * @return \stdClass */ public function index() { $r = $this->_resourceListing(); $map = array(); $allRoutes = Routes::toArray(); if (isset($allRoutes['*'])) { $this->_mapResources($allRoutes['*'], $map); unset($allRoutes['*']); } $this->_mapResources($allRoutes, $map); foreach ($map as $path => $description) { if (false === strpos($path, '{')) { //add id $r->apis[] = array('path' => "/resources/{$path}{$this->formatString}", 'description' => $description); } } return $r; }
/** * Get the form * * @param string $method http method to submit the form * @param string $action relative path from the web root. When set to null * it uses the current api method's path * @param bool $dataOnly if you want to render the form yourself use this * option * @param string $prefix used for adjusting the spacing in front of * form elements * @param string $indent used for adjusting indentation * * @return array|T * * @throws \Luracast\Restler\RestException */ public static function get($method = 'POST', $action = null, $dataOnly = false, $prefix = '', $indent = ' ') { if (!static::$style) { static::$style = FormStyles::$html; } try { /** @var Restler $restler */ $restler = Scope::get('Restler'); if (is_null($action)) { $action = $restler->url; } $info = $restler->url == $action && Util::getRequestMethod() == $method ? $restler->apiMethodInfo : Routes::find(trim($action, '/'), $method, $restler->getRequestedApiVersion(), static::$preFill || $restler->requestMethod == $method && $restler->url == $action ? $restler->getRequestData() : array()); } catch (RestException $e) { //echo $e->getErrorMessage(); $info = false; } if (!$info) { throw new RestException(500, 'invalid action path for form `' . $method . ' ' . $action . '`'); } static::$info = $info; $m = $info->metadata; $r = static::fields($dataOnly); if ($method != 'GET' && $method != 'POST') { if (empty(Defaults::$httpMethodOverrideProperty)) { throw new RestException(500, 'Forms require `Defaults::\\$httpMethodOverrideProperty`' . "for supporting HTTP {$method}"); } if ($dataOnly) { $r[] = array('tag' => 'input', 'name' => Defaults::$httpMethodOverrideProperty, 'type' => 'hidden', 'value' => 'method'); } else { $r[] = T::input()->name(Defaults::$httpMethodOverrideProperty)->value($method)->type('hidden'); } $method = 'POST'; } if (session_id() != '') { $form_key = static::key($method, $action); if ($dataOnly) { $r[] = array('tag' => 'input', 'name' => static::FORM_KEY, 'type' => 'hidden', 'value' => 'hidden'); } else { $key = T::input()->name(static::FORM_KEY)->type('hidden')->value($form_key); $r[] = $key; } } $s = array('tag' => 'button', 'type' => 'submit', 'label' => Util::nestedValue($m, 'return', CommentParser::$embeddedDataName, 'label') ?: 'Submit'); if (!$dataOnly) { $s = Emmet::make(static::style('submit', $m), $s); } $r[] = $s; $t = array('action' => $restler->getBaseUrl() . '/' . rtrim($action, '/'), 'method' => $method); if (static::$fileUpload) { static::$fileUpload = false; $t['enctype'] = 'multipart/form-data'; } if (!$dataOnly) { $t = Emmet::make(static::style('form', $m), $t); $t->prefix = $prefix; $t->indent = $indent; $t[] = $r; } else { $t['fields'] = $r; } return $t; }
/** * Store the url map cache if needed */ protected function saveRoutesToCache() { if ($this->productionMode && !$this->cached && $this->enableRouteCache) { $this->cache->set('routes', Routes::toArray() + array('apiVersionMap' => $this->apiVersionMap)); } }
private function paths($version = 1) { $map = Routes::findAll(static::$excludedPaths + array($this->base()), static::$excludedHttpMethods, $version); $paths = array(); foreach ($map as $path => $data) { $access = $data[0]['access']; if (static::$hideProtected && !$access) { continue; } foreach ($data as $item) { $route = $item['route']; $access = $item['access']; if (static::$hideProtected && !$access) { continue; } $url = $route['url']; $paths["/{$url}"][strtolower($route['httpMethod'])] = $this->operation($route); } } return $paths; }
public static function get($for = '', $activeUrl = null) { if (!static::$accessControlFunction && Defaults::$accessControlFunction) { static::$accessControlFunction = Defaults::$accessControlFunction; } /** @var Restler $restler */ $restler = Scope::get('Restler'); if (static::$addExtension) { static::$extension = '.' . $restler->responseFormat->getExtension(); } if (is_null($activeUrl)) { $activeUrl = $restler->url; } $tree = array(); foreach (static::$prepends as $path => $text) { $url = null; if (is_array($text)) { if (isset($text['url'])) { $url = $text['url']; $text = $text['text']; } else { $url = current(array_keys($text)); $text = current($text); } } if (is_numeric($path)) { $path = $text; $text = null; } if (empty($for) || 0 === strpos($path, "{$for}/")) { static::build($tree, $path, $url, $text, $activeUrl); } } $routes = Routes::toArray(); $routes = $routes['v' . $restler->getRequestedApiVersion()]; foreach ($routes as $value) { foreach ($value as $httpMethod => $route) { if ($httpMethod != 'GET') { continue; } $path = $route['url']; if (false !== strpos($path, '{')) { continue; } if ($route['accessLevel'] > 1 && !Util::$restler->_authenticated) { continue; } foreach (static::$excludedPaths as $exclude) { if (empty($exclude)) { if (empty($path)) { continue 2; } } elseif (0 === strpos($path, $exclude)) { continue 2; } } if ($restler->_authenticated && static::$accessControlFunction && !call_user_func(static::$accessControlFunction, $route['metadata'])) { continue; } $text = Util::nestedValue($route, 'metadata', CommentParser::$embeddedDataName, 'label'); if (empty($for) || 0 === strpos($path, "{$for}/")) { static::build($tree, $path, null, $text, $activeUrl); } } } foreach (static::$appends as $path => $text) { $url = null; if (is_array($text)) { if (isset($text['url'])) { $url = $text['url']; $text = $text['text']; } else { $url = current(array_keys($text)); $text = current($text); } } if (is_numeric($path)) { $path = $text; $text = null; } if (empty($for) || 0 === strpos($path, "{$for}/")) { static::build($tree, $path, $url, $text, $activeUrl); } } if (!empty($for)) { $for = explode('/', $for); $p =& $tree; foreach ($for as $f) { if (isset($p[$f]['children'])) { $p =& $p[$f]['children']; } else { return array(); } } return $p; } return $tree; }
/** * Store the url map cache if needed */ public function __destruct() { if ($this->productionMode && !$this->cached) { $this->cache->set('routes', Routes::toArray()); } }