From f6c93525a6c3f0ff727f12648e725e4ebede98a9 Mon Sep 17 00:00:00 2001 From: jfefe Date: Fri, 1 May 2015 14:03:32 +0200 Subject: [PATCH 01/66] Require restler/framework into composer.json --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f0de86ce7ea..ed75d3ca61d 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "require": { "php": ">=5.3.0", "ext-gd": "*", - "ext-curl": "*" + "ext-curl": "*", + "restler/framework": "3.0.*" }, "suggest": { "ext-mysqli": "*", From b503b16f07065592fe733968b3dbba02c39c85a2 Mon Sep 17 00:00:00 2001 From: jfefe Date: Fri, 1 May 2015 15:42:05 +0200 Subject: [PATCH 02/66] NEW : add restler framework First step to build REST API into Dolibarr --- htdocs/includes/restler/ApcCache.php | 114 ++ htdocs/includes/restler/AutoLoader.php | 438 +++++ htdocs/includes/restler/CommentParser.php | 466 ++++++ htdocs/includes/restler/Compose.php | 71 + .../includes/restler/Data/ApiMethodInfo.php | 55 + htdocs/includes/restler/Data/Arr.php | 33 + htdocs/includes/restler/Data/Invalid.php | 20 + htdocs/includes/restler/Data/Object.php | 157 ++ htdocs/includes/restler/Data/String.php | 84 + .../includes/restler/Data/ValidationInfo.php | 273 ++++ htdocs/includes/restler/Data/Validator.php | 626 +++++++ htdocs/includes/restler/Data/ValueObject.php | 61 + htdocs/includes/restler/Data/iValidate.php | 32 + htdocs/includes/restler/Data/iValueObject.php | 39 + htdocs/includes/restler/Defaults.php | 360 ++++ htdocs/includes/restler/EventDispatcher.php | 98 ++ htdocs/includes/restler/Filter/RateLimit.php | 178 ++ htdocs/includes/restler/Flash.php | 146 ++ htdocs/includes/restler/Format/AmfFormat.php | 45 + htdocs/includes/restler/Format/CsvFormat.php | 181 ++ htdocs/includes/restler/Format/Format.php | 140 ++ htdocs/includes/restler/Format/HtmlFormat.php | 485 ++++++ htdocs/includes/restler/Format/JsFormat.php | 48 + htdocs/includes/restler/Format/JsonFormat.php | 210 +++ .../includes/restler/Format/MultiFormat.php | 144 ++ .../includes/restler/Format/PlistFormat.php | 91 ++ htdocs/includes/restler/Format/TsvFormat.php | 24 + .../includes/restler/Format/UploadFormat.php | 145 ++ .../restler/Format/UrlEncodedFormat.php | 58 + htdocs/includes/restler/Format/XmlFormat.php | 348 ++++ htdocs/includes/restler/Format/YamlFormat.php | 36 + .../includes/restler/Format/iDecodeStream.php | 30 + htdocs/includes/restler/Format/iFormat.php | 109 ++ .../includes/restler/HumanReadableCache.php | 129 ++ htdocs/includes/restler/README.md | 8 + htdocs/includes/restler/Redirect.php | 53 + htdocs/includes/restler/Resources.php | 1006 ++++++++++++ htdocs/includes/restler/RestException.php | 138 ++ htdocs/includes/restler/Restler.php | 1451 +++++++++++++++++ htdocs/includes/restler/Routes.php | 696 ++++++++ htdocs/includes/restler/Scope.php | 190 +++ htdocs/includes/restler/UI/Emmet.php | 383 +++++ htdocs/includes/restler/UI/FormStyles.php | 59 + htdocs/includes/restler/UI/Forms.php | 434 +++++ htdocs/includes/restler/UI/Nav.php | 208 +++ htdocs/includes/restler/UI/Tags.php | 282 ++++ htdocs/includes/restler/User.php | 100 ++ htdocs/includes/restler/Util.php | 201 +++ .../restler/compatibility/iAuthenticate.php | 10 + .../restler/compatibility/restler1.php | 14 + .../restler/compatibility/restler2.php | 40 + htdocs/includes/restler/composer.json | 70 + htdocs/includes/restler/composer.lock | 23 + htdocs/includes/restler/iAuthenticate.php | 25 + htdocs/includes/restler/iCache.php | 63 + htdocs/includes/restler/iCompose.php | 36 + htdocs/includes/restler/iFilter.php | 30 + htdocs/includes/restler/iIdentifyUser.php | 63 + .../restler/iProvideMultiVersionApi.php | 11 + .../includes/restler/iUseAuthentication.php | 32 + htdocs/includes/restler/vendor/autoload.php | 7 + .../restler/vendor/composer/ClassLoader.php | 413 +++++ .../vendor/composer/autoload_classmap.php | 9 + .../vendor/composer/autoload_namespaces.php | 10 + .../restler/vendor/composer/autoload_psr4.php | 9 + .../restler/vendor/composer/autoload_real.php | 70 + .../restler/vendor/composer/installed.json | 1 + htdocs/includes/restler/views/debug.css | 441 +++++ htdocs/includes/restler/views/debug.php | 169 ++ 69 files changed, 12229 insertions(+) create mode 100644 htdocs/includes/restler/ApcCache.php create mode 100644 htdocs/includes/restler/AutoLoader.php create mode 100644 htdocs/includes/restler/CommentParser.php create mode 100644 htdocs/includes/restler/Compose.php create mode 100644 htdocs/includes/restler/Data/ApiMethodInfo.php create mode 100644 htdocs/includes/restler/Data/Arr.php create mode 100644 htdocs/includes/restler/Data/Invalid.php create mode 100644 htdocs/includes/restler/Data/Object.php create mode 100644 htdocs/includes/restler/Data/String.php create mode 100644 htdocs/includes/restler/Data/ValidationInfo.php create mode 100644 htdocs/includes/restler/Data/Validator.php create mode 100644 htdocs/includes/restler/Data/ValueObject.php create mode 100644 htdocs/includes/restler/Data/iValidate.php create mode 100644 htdocs/includes/restler/Data/iValueObject.php create mode 100644 htdocs/includes/restler/Defaults.php create mode 100644 htdocs/includes/restler/EventDispatcher.php create mode 100644 htdocs/includes/restler/Filter/RateLimit.php create mode 100644 htdocs/includes/restler/Flash.php create mode 100644 htdocs/includes/restler/Format/AmfFormat.php create mode 100644 htdocs/includes/restler/Format/CsvFormat.php create mode 100644 htdocs/includes/restler/Format/Format.php create mode 100644 htdocs/includes/restler/Format/HtmlFormat.php create mode 100644 htdocs/includes/restler/Format/JsFormat.php create mode 100644 htdocs/includes/restler/Format/JsonFormat.php create mode 100644 htdocs/includes/restler/Format/MultiFormat.php create mode 100644 htdocs/includes/restler/Format/PlistFormat.php create mode 100644 htdocs/includes/restler/Format/TsvFormat.php create mode 100644 htdocs/includes/restler/Format/UploadFormat.php create mode 100644 htdocs/includes/restler/Format/UrlEncodedFormat.php create mode 100644 htdocs/includes/restler/Format/XmlFormat.php create mode 100644 htdocs/includes/restler/Format/YamlFormat.php create mode 100644 htdocs/includes/restler/Format/iDecodeStream.php create mode 100644 htdocs/includes/restler/Format/iFormat.php create mode 100644 htdocs/includes/restler/HumanReadableCache.php create mode 100644 htdocs/includes/restler/README.md create mode 100644 htdocs/includes/restler/Redirect.php create mode 100644 htdocs/includes/restler/Resources.php create mode 100644 htdocs/includes/restler/RestException.php create mode 100644 htdocs/includes/restler/Restler.php create mode 100644 htdocs/includes/restler/Routes.php create mode 100644 htdocs/includes/restler/Scope.php create mode 100644 htdocs/includes/restler/UI/Emmet.php create mode 100644 htdocs/includes/restler/UI/FormStyles.php create mode 100644 htdocs/includes/restler/UI/Forms.php create mode 100644 htdocs/includes/restler/UI/Nav.php create mode 100644 htdocs/includes/restler/UI/Tags.php create mode 100644 htdocs/includes/restler/User.php create mode 100644 htdocs/includes/restler/Util.php create mode 100644 htdocs/includes/restler/compatibility/iAuthenticate.php create mode 100644 htdocs/includes/restler/compatibility/restler1.php create mode 100644 htdocs/includes/restler/compatibility/restler2.php create mode 100644 htdocs/includes/restler/composer.json create mode 100644 htdocs/includes/restler/composer.lock create mode 100644 htdocs/includes/restler/iAuthenticate.php create mode 100755 htdocs/includes/restler/iCache.php create mode 100644 htdocs/includes/restler/iCompose.php create mode 100644 htdocs/includes/restler/iFilter.php create mode 100644 htdocs/includes/restler/iIdentifyUser.php create mode 100644 htdocs/includes/restler/iProvideMultiVersionApi.php create mode 100644 htdocs/includes/restler/iUseAuthentication.php create mode 100644 htdocs/includes/restler/vendor/autoload.php create mode 100644 htdocs/includes/restler/vendor/composer/ClassLoader.php create mode 100644 htdocs/includes/restler/vendor/composer/autoload_classmap.php create mode 100644 htdocs/includes/restler/vendor/composer/autoload_namespaces.php create mode 100644 htdocs/includes/restler/vendor/composer/autoload_psr4.php create mode 100644 htdocs/includes/restler/vendor/composer/autoload_real.php create mode 100644 htdocs/includes/restler/vendor/composer/installed.json create mode 100644 htdocs/includes/restler/views/debug.css create mode 100644 htdocs/includes/restler/views/debug.php diff --git a/htdocs/includes/restler/ApcCache.php b/htdocs/includes/restler/ApcCache.php new file mode 100644 index 00000000000..55b22107726 --- /dev/null +++ b/htdocs/includes/restler/ApcCache.php @@ -0,0 +1,114 @@ + + * @copyright 2013 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ApcCache implements iCache +{ + /** + * The namespace that all of the cached entries will be stored under. This allows multiple APIs to run concurrently. + * + * @var string + */ + static public $namespace = 'restler'; + + /** + * store data in the cache + * + * + * @param string $name + * @param mixed $data + * + * @return boolean true if successful + */ + public function set($name, $data) + { + function_exists('apc_store') || $this->apcNotAvailable(); + + try { + return apc_store(self::$namespace . "-" . $name, $data); + } catch + (\Exception $exception) { + return false; + } + } + + private function apcNotAvailable() + { + throw new \Exception('APC is not available for use as Restler Cache. Please make sure the module is installed. http://php.net/manual/en/apc.installation.php'); + } + + /** + * retrieve data from the cache + * + * + * @param string $name + * @param bool $ignoreErrors + * + * @throws \Exception + * @return mixed + */ + public function get($name, $ignoreErrors = false) + { + function_exists('apc_fetch') || $this->apcNotAvailable(); + + try { + return apc_fetch(self::$namespace . "-" . $name); + } catch (\Exception $exception) { + if (!$ignoreErrors) { + throw $exception; + } + return null; + } + } + + /** + * delete data from the cache + * + * + * @param string $name + * @param bool $ignoreErrors + * + * @throws \Exception + * @return boolean true if successful + */ + public function clear($name, $ignoreErrors = false) + { + function_exists('apc_delete') || $this->apcNotAvailable(); + + try { + apc_delete(self::$namespace . "-" . $name); + } catch (\Exception $exception) { + if (!$ignoreErrors) { + throw $exception; + } + } + } + + /** + * check if the given name is cached + * + * + * @param string $name + * + * @return boolean true if cached + */ + public function isCached($name) + { + function_exists('apc_exists') || $this->apcNotAvailable(); + return apc_exists(self::$namespace . "-" . $name); + } + +} \ No newline at end of file diff --git a/htdocs/includes/restler/AutoLoader.php b/htdocs/includes/restler/AutoLoader.php new file mode 100644 index 00000000000..9bb34bad9cf --- /dev/null +++ b/htdocs/includes/restler/AutoLoader.php @@ -0,0 +1,438 @@ + + * @copyright 2012 Luracast + * @version 3.0.0rc5 + */ +class AutoLoader +{ + protected static $instance, // the singleton instance reference + $perfectLoaders, // used to keep the ideal list of loaders + $rogueLoaders = array(), // other auto loaders now unregistered + $classMap = array(), // the class to include file mapping + $aliases = array( // aliases and prefixes instead of null list aliases + 'Luracast\\Restler' => null, + 'Luracast\\Restler\\Format' => null, + 'Luracast\\Restler\\Data' => null, + 'Luracast\\Restler\\Filter' => null, + ); + + /** + * Singleton instance facility. + * + * @static + * @return AutoLoader the current instance or new instance if none exists. + */ + public static function instance() + { + static::$instance = static::$instance ?: new static(); + return static::thereCanBeOnlyOne(); + } + + /** + * Helper function to add a path to the include path. + * AutoLoader uses the include path to discover classes. + * + * @static + * + * @param $path string absolute or relative path. + * + * @return bool false if the path cannot be resolved + * or the resolved absolute path. + */ + public static function addPath($path) { + if (false === $path = stream_resolve_include_path($path)) + return false; + else + set_include_path($path.PATH_SEPARATOR.get_include_path()); + return $path; + } + + /** + * Other autoLoaders interfere and cause duplicate class loading. + * AutoLoader is capable enough to handle all standards so no need + * for others stumbling about. + * + * @return callable the one true auto loader. + */ + public static function thereCanBeOnlyOne() { + if (static::$perfectLoaders === spl_autoload_functions()) + return static::$instance; + + if (false !== $loaders = spl_autoload_functions()) + if (0 < $count = count($loaders)) + for ($i = 0, static::$rogueLoaders += $loaders; + $i < $count && false != ($loader = $loaders[$i]); + $i++) + if ($loader !== static::$perfectLoaders[0]) + spl_autoload_unregister($loader); + + return static::$instance; + } + + /** + * Seen this before cache handler. + * Facilitates both lookup and persist operations as well as convenience, + * load complete map functionality. The key can only be given a non falsy + * value once, this will be truthy for life. + * + * @param $key mixed class name considered or a collection of + * classMap entries + * @param $value mixed optional not required when doing a query on + * key. Default is false we haven't seen this + * class. Most of the time it will be the filename + * for include and is set to true if we are unable + * to load this class iow true == it does not exist. + * value may also be a callable auto loader function. + * + * @return mixed The known value for the key or false if key has no value + */ + public static function seen($key, $value = false) + { + if (is_array($key)) { + static::$classMap = $key + static::$classMap; + return false; + } + + if (empty(static::$classMap[$key])) + static::$classMap[$key] = $value; + + if (is_string($alias = static::$classMap[$key])) + if (isset(static::$classMap[$alias])) + return static::$classMap[$alias]; + + return static::$classMap[$key]; + } + + /** + * Protected constructor to enforce singleton pattern. + * Populate a default include path. + * All possible includes cant possibly be catered for and if you + * require another path then simply add it calling set_include_path. + */ + protected function __construct() + { + static::$perfectLoaders = array($this); + + if (false === static::seen('__include_path')) { + + $paths = explode(PATH_SEPARATOR, get_include_path()); + $slash = DIRECTORY_SEPARATOR; + $dir = dirname(__DIR__); + $source_dir = dirname($dir); + $dir = dirname($source_dir); + + foreach ( + array( + array($source_dir), + array($dir, '..', '..', 'composer'), + array($dir, 'vendor', 'composer'), + array($dir, '..', '..', '..', 'php'), + array($dir, 'vendor', 'php')) + as $includePath) + if (false !== $path = stream_resolve_include_path( + implode($slash, $includePath) + )) + if ('composer' == end($includePath) && + false !== $classmapPath = stream_resolve_include_path( + "$path{$slash}autoload_classmap.php" + ) + ) { + static::seen(static::loadFile( + $classmapPath + )); + $paths = array_merge( + $paths, + array_values(static::loadFile( + "$path{$slash}autoload_namespaces.php" + )) + ); + } else + $paths[] = $path; + + $paths = array_filter(array_map( + function ($path) { + if (false == $realPath = @realpath($path)) + return null; + return $realPath . DIRECTORY_SEPARATOR; + }, + $paths + )); + natsort($paths); + static::seen( + '__include_path', + implode(PATH_SEPARATOR, array_unique($paths)) + ); + } + + set_include_path(static::seen('__include_path')); + } + + /** + * Attempt to include the path location. + * Called from a static context which will not expose the AutoLoader + * instance itself. + * + * @param $path string location of php file on the include path + * + * @return bool|mixed returns reference obtained from the include or false + */ + private static function loadFile($path) + { + return \Luracast_Restler_autoloaderInclude($path); + } + + /** + * Attempt to load class with namespace prefixes. + * + * @param $className string class name + * + * @return bool|mixed reference to discovered include or false + */ + private function loadPrefixes($className) + { + $currentClass = $className; + if (false !== $pos = strrpos($className, '\\')) + $className = substr($className, $pos); + else + $className = "\\$className"; + + for ( + $i = 0, + $file = false, + $count = count(static::$aliases), + $prefixes = array_keys(static::$aliases); + $i < $count + && false === $file + && false === $file = $this->discover( + $variant = $prefixes[$i++].$className, + $currentClass + ); + $file = $this->loadAliases($variant) + ); + + return $file; + } + + /** + * Attempt to load configured aliases based on namespace part of class name. + * + * @param $className string fully qualified class name. + * + * @return bool|mixed reference to discovered include or false + */ + private function loadAliases($className) + { + $file = false; + if (preg_match('/(.+)(\\\\\w+$)/U', $className, $parts)) + for ( + $i = 0, + $aliases = isset(static::$aliases[$parts[1]]) + ? static::$aliases[$parts[1]] : array(), + $count = count($aliases); + $i < $count && false === $file; + $file = $this->discover( + "{$aliases[$i++]}$parts[2]", + $className + ) + ) ; + + return $file; + } + + /** + * Load from rogueLoaders as last resort. + * It may happen that a custom auto loader may load classes in a unique way, + * these classes cannot be seen otherwise nor should we attempt to cover every + * possible deviation. If we still can't find a class, as a last resort, we will + * run through the list of rogue loaders and verify if we succeeded. + * + * @param $className string className that can't be found + * @param null $loader callable loader optional when the loader is known + * + * @return bool false unless className now exists + */ + private function loadLastResort($className, $loader = null) { + $loaders = array_unique(static::$rogueLoaders); + if (isset($loader)) { + if (false === array_search($loader, $loaders)) + static::$rogueLoaders[] = $loader; + return $this->loadThisLoader($className, $loader); + } + foreach ($loaders as $loader) + if (false !== $file = $this->loadThisLoader($className, $loader)) + return $file; + + return false; + } + + /** + * Helper for loadLastResort. + * Use loader with $className and see if className exists. + * + * @param $className string name of a class to load + * @param $loader callable autoLoader method + * + * @return bool false unless className exists + */ + private function loadThisLoader($className, $loader) { + if (is_callable($loader) + && false !== $file = $loader($className) + && $this->exists($className, $loader)) + return $file; + return false; + } + + /** + * Create an alias for class. + * + * @param $className string the name of the alias class + * @param $currentClass string the current class this alias references + */ + private function alias($className, $currentClass) + { + if ($className != $currentClass + && false !== strpos($className, $currentClass)) + if (!class_exists($currentClass, false) + && class_alias($className, $currentClass)) + static::seen($currentClass, $className); + } + + /** + * Discovery process. + * + * @param $className string class name to discover + * @param $currentClass string optional name of current class when + * looking up an alias + * + * @return bool|mixed resolved include reference or false + */ + private function discover($className, $currentClass = null) + { + $currentClass = $currentClass ?: $className; + + /** The short version we've done this before and found it in cache */ + if (false !== $file = static::seen($className)) { + if (!$this->exists($className)) + if (is_callable($file)) + $file = $this->loadLastResort($className, $file); + elseif($file = stream_resolve_include_path($file)) + $file = static::loadFile($file); + + $this->alias($className, $currentClass); + return $file; + } + + /** We did not find it in cache, lets look for it shall we */ + + /** replace \ with / and _ in CLASS NAME with / = PSR-0 in 3 lines */ + $file = preg_replace("/\\\|_(?=\w+$)/", DIRECTORY_SEPARATOR, $className); + if (false === $file = stream_resolve_include_path("$file.php")) + return false; + + /** have we loaded this file before could this be an alias */ + if (in_array($file, get_included_files())) { + if (false !== $sameFile = array_search($file, static::$classMap)) + if (!$this->exists($className, $file)) + if (false !== strpos($sameFile, $className)) + $this->alias($sameFile, $className); + + return $file; + } + + $state = array_merge(get_declared_classes(), get_declared_interfaces()); + + if (false !== $result = static::loadFile($file)) { + + if ($this->exists($className, $file)) + $this->alias($className, $currentClass); + elseif (false != $diff = array_diff( + array_merge(get_declared_classes(), get_declared_interfaces()), $state)) + foreach ($diff as $autoLoaded) + if ($this->exists($autoLoaded, $file)) + if (false !== strpos($autoLoaded, $className)) + $this->alias($autoLoaded, $className); + + if (!$this->exists($currentClass)) + $result = false; + } + + return $result; + } + + /** + * Checks whether supplied string exists in a loaded class or interface. + * As a convenience the supplied $mapping can be the value for seen. + * + * @param $className string The class or interface to verify + * @param $mapping string (optional) value for map/seen if found to exist + * + * @return bool whether the class/interface exists without calling auto loader + */ + private function exists($className, $mapping = null) + { + if (class_exists($className, false) + || interface_exists($className, false)) + if (isset($mapping)) + return static::seen($className, $mapping); + else + return true; + return false; + } + + /** + * Auto loader callback through __invoke object as function. + * + * @param $className string class/interface name to auto load + * + * @return mixed|null the reference from the include or null + */ + public function __invoke($className) + { + if (empty($className)) + return false; + + if (false !== $includeReference = $this->discover($className)) + return $includeReference; + + static::thereCanBeOnlyOne(); + + if (false !== $includeReference = $this->loadAliases($className)) + return $includeReference; + + if (false !== $includeReference = $this->loadPrefixes($className)) + return $includeReference; + + if (false !== $includeReference = $this->loadLastResort($className)) + return $includeReference; + + static::seen($className, true); + return null; + } +} +} + +namespace { + /** + * Include function in the root namespace to include files optimized + * for the global context. + * + * @param $path string path of php file to include into the global context. + * + * @return mixed|bool false if the file could not be included. + */ + function Luracast_Restler_autoloaderInclude($path) { + return include $path; + } +} + diff --git a/htdocs/includes/restler/CommentParser.php b/htdocs/includes/restler/CommentParser.php new file mode 100644 index 00000000000..839983b73db --- /dev/null +++ b/htdocs/includes/restler/CommentParser.php @@ -0,0 +1,466 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class CommentParser +{ + /** + * name for the embedded data + * + * @var string + */ + public static $embeddedDataName = 'properties'; + /** + * Regular Expression pattern for finding the embedded data and extract + * the inner information. It is used with preg_match. + * + * @var string + */ + public static $embeddedDataPattern + = '/```(\w*)[\s]*(([^`]*`{0,2}[^`]+)*)```/ms'; + /** + * Pattern will have groups for the inner details of embedded data + * this index is used to locate the data portion. + * + * @var int + */ + public static $embeddedDataIndex = 2; + /** + * Delimiter used to split the array data. + * + * When the name portion is of the embedded data is blank auto detection + * will be used and if URLEncodedFormat is detected as the data format + * the character specified will be used as the delimiter to find split + * array data. + * + * @var string + */ + public static $arrayDelimiter = ','; + + /** + * character sequence used to escape \@ + */ + const escapedAtChar = '\\@'; + + /** + * character sequence used to escape end of comment + */ + const escapedCommendEnd = '{@*}'; + + /** + * Instance of Restler class injected at runtime. + * + * @var Restler + */ + public $restler; + /** + * Comment information is parsed and stored in to this array. + * + * @var array + */ + private $_data = array(); + + /** + * Parse the comment and extract the data. + * + * @static + * + * @param $comment + * @param bool $isPhpDoc + * + * @return array associative array with the extracted values + */ + public static function parse($comment, $isPhpDoc = true) + { + $p = new self(); + if (empty($comment)) { + return $p->_data; + } + + if ($isPhpDoc) { + $comment = self::removeCommentTags($comment); + } + + $p->extractData($comment); + return $p->_data; + + } + + /** + * Removes the comment tags from each line of the comment. + * + * @static + * + * @param string $comment PhpDoc style comment + * + * @return string comments with out the tags + */ + public static function removeCommentTags($comment) + { + $pattern = '/(^\/\*\*)|(^\s*\**[ \/]?)|\s(?=@)|\s\*\//m'; + return preg_replace($pattern, '', $comment); + } + + /** + * Extracts description and long description, uses other methods to get + * parameters. + * + * @param $comment + * + * @return array + */ + private function extractData($comment) + { + //to use @ as part of comment we need to + $comment = str_replace( + array(self::escapedCommendEnd, self::escapedAtChar), + array('*/', '@'), + $comment); + + $description = array(); + $longDescription = array(); + $params = array(); + + $mode = 0; // extract short description; + $comments = preg_split("/(\r?\n)/", $comment); + // remove first blank line; + array_shift($comments); + $addNewline = false; + foreach ($comments as $line) { + $line = trim($line); + $newParam = false; + if (empty ($line)) { + if ($mode == 0) { + $mode++; + } else { + $addNewline = true; + } + continue; + } elseif ($line{0} == '@') { + $mode = 2; + $newParam = true; + } + switch ($mode) { + case 0 : + $description[] = $line; + if (count($description) > 3) { + // if more than 3 lines take only first line + $longDescription = $description; + $description[] = array_shift($longDescription); + $mode = 1; + } elseif (substr($line, -1) == '.') { + $mode = 1; + } + break; + case 1 : + if ($addNewline) { + $line = ' ' . $line; + } + $longDescription[] = $line; + break; + case 2 : + $newParam + ? $params[] = $line + : $params[count($params) - 1] .= ' ' . $line; + } + $addNewline = false; + } + $description = implode(' ', $description); + $longDescription = implode(' ', $longDescription); + $description = preg_replace('/\s+/msu', ' ', $description); + $longDescription = preg_replace('/\s+/msu', ' ', $longDescription); + list($description, $d1) + = $this->parseEmbeddedData($description); + list($longDescription, $d2) + = $this->parseEmbeddedData($longDescription); + $this->_data = compact('description', 'longDescription'); + $d2 += $d1; + if (!empty($d2)) { + $this->_data[self::$embeddedDataName] = $d2; + } + foreach ($params as $key => $line) { + list(, $param, $value) = preg_split('/\@|\s/', $line, 3) + + array('', '', ''); + list($value, $embedded) = $this->parseEmbeddedData($value); + $value = array_filter(preg_split('/\s+/msu', $value)); + $this->parseParam($param, $value, $embedded); + } + return $this->_data; + } + + /** + * Parse parameters that begin with (at) + * + * @param $param + * @param array $value + * @param array $embedded + */ + private function parseParam($param, array $value, array $embedded) + { + $data = & $this->_data; + $allowMultiple = false; + switch ($param) { + case 'param' : + $value = $this->formatParam($value); + $allowMultiple = true; + break; + case 'var' : + $value = $this->formatVar($value); + break; + case 'return' : + $value = $this->formatReturn($value); + break; + case 'class' : + $data = & $data[$param]; + list ($param, $value) = $this->formatClass($value); + break; + case 'access' : + $value = reset($value); + break; + case 'expires' : + case 'status' : + $value = intval(reset($value)); + break; + case 'throws' : + $value = $this->formatThrows($value); + $allowMultiple = true; + break; + case 'author': + $value = $this->formatAuthor($value); + $allowMultiple = true; + break; + case 'header' : + case 'link': + case 'example': + case 'todo': + $allowMultiple = true; + //don't break, continue with code for default: + default : + $value = implode(' ', $value); + } + if (!empty($embedded)) { + if (is_string($value)) { + $value = array('description' => $value); + } + $value[self::$embeddedDataName] = $embedded; + } + if (empty ($data[$param])) { + if ($allowMultiple) { + $data[$param] = array( + $value + ); + } else { + $data[$param] = $value; + } + } elseif ($allowMultiple) { + $data[$param][] = $value; + } elseif ($param == 'param') { + $arr = array( + $data[$param], + $value + ); + $data[$param] = $arr; + } else { + if (!is_string($value) && isset($value[self::$embeddedDataName]) + && isset($data[$param][self::$embeddedDataName]) + ) { + $value[self::$embeddedDataName] + += $data[$param][self::$embeddedDataName]; + } + $data[$param] = $value + $data[$param]; + } + } + + /** + * Parses the inline php doc comments and embedded data. + * + * @param $subject + * + * @return array + * @throws Exception + */ + private function parseEmbeddedData($subject) + { + $data = array(); + + //parse {@pattern } tags specially + while (preg_match('|(?s-m)({@pattern (/.+/[imsxuADSUXJ]*)})|', $subject, $matches)) { + $subject = str_replace($matches[0], '', $subject); + $data['pattern'] = $matches[2]; + } + while (preg_match('/{@(\w+)\s?([^}]*)}/ms', $subject, $matches)) { + $subject = str_replace($matches[0], '', $subject); + if ($matches[2] == 'true' || $matches[2] == 'false') { + $matches[2] = $matches[2] == 'true'; + } elseif ($matches[2] == '') { + $matches[2] = true; + } + if ($matches[1] == 'pattern') { + throw new Exception('Inline pattern tag should follow {@pattern /REGEX_PATTERN_HERE/} format and can optionally include PCRE modifiers following the ending `/`'); + } elseif (false !== strpos($matches[2], static::$arrayDelimiter)) { + $matches[2] = explode(static::$arrayDelimiter, $matches[2]); + } + $data[$matches[1]] = $matches[2]; + } + + while (preg_match(self::$embeddedDataPattern, $subject, $matches)) { + $subject = str_replace($matches[0], '', $subject); + $str = $matches[self::$embeddedDataIndex]; + if (isset ($this->restler) + && self::$embeddedDataIndex > 1 + && !empty ($matches[1]) + ) { + $extension = $matches[1]; + $formatMap = $this->restler->getFormatMap(); + if (isset ($formatMap[$extension])) { + /** + * @var \Luracast\Restler\Format\iFormat + */ + $format = $formatMap[$extension]; + $format = new $format(); + $data = $format->decode($str); + } + } else { // auto detect + if ($str{0} == '{') { + $d = json_decode($str, true); + if (json_last_error() != JSON_ERROR_NONE) { + throw new Exception('Error parsing embedded JSON data' + . " $str"); + } + $data = $d + $data; + } else { + parse_str($str, $d); + //clean up + $d = array_filter($d); + foreach ($d as $key => $val) { + $kt = trim($key); + if ($kt != $key) { + unset($d[$key]); + $key = $kt; + $d[$key] = $val; + } + if (is_string($val)) { + if ($val == 'true' || $val == 'false') { + $d[$key] = $val == 'true' ? true : false; + } else { + $val = explode(self::$arrayDelimiter, $val); + if (count($val) > 1) { + $d[$key] = $val; + } else { + $d[$key] = + preg_replace('/\s+/msu', ' ', + $d[$key]); + } + } + } + } + $data = $d + $data; + } + } + } + return array($subject, $data); + } + + private function formatThrows(array $value) + { + $r = array(); + $r['code'] = count($value) && is_numeric($value[0]) + ? intval(array_shift($value)) : 500; + $reason = implode(' ', $value); + $r['reason'] = empty($reason) ? '' : $reason; + return $r; + } + + private function formatClass(array $value) + { + $param = array_shift($value); + + if (empty($param)) { + $param = 'Unknown'; + } + $value = implode(' ', $value); + return array( + ltrim($param, '\\'), + array('description' => $value) + ); + } + + private function formatAuthor(array $value) + { + $r = array(); + $email = end($value); + if ($email{0} == '<') { + $email = substr($email, 1, -1); + array_pop($value); + $r['email'] = $email; + } + $r['name'] = implode(' ', $value); + return $r; + } + + private function formatReturn(array $value) + { + $data = explode('|', array_shift($value)); + $r = array( + 'type' => count($data) == 1 ? $data[0] : $data + ); + $r['description'] = implode(' ', $value); + return $r; + } + + private function formatParam(array $value) + { + $r = array(); + $data = array_shift($value); + if (empty($data)) { + $r['type'] = 'mixed'; + } elseif ($data{0} == '$') { + $r['name'] = substr($data, 1); + $r['type'] = 'mixed'; + } else { + $data = explode('|', $data); + $r['type'] = count($data) == 1 ? $data[0] : $data; + + $data = array_shift($value); + if (!empty($data) && $data{0} == '$') { + $r['name'] = substr($data, 1); + } + } + if ($value) { + $r['description'] = implode(' ', $value); + } + return $r; + } + + private function formatVar(array $value) + { + $r = array(); + $data = array_shift($value); + if (empty($data)) { + $r['type'] = 'mixed'; + } elseif ($data{0} == '$') { + $r['name'] = substr($data, 1); + $r['type'] = 'mixed'; + } else { + $data = explode('|', $data); + $r['type'] = count($data) == 1 ? $data[0] : $data; + } + if ($value) { + $r['description'] = implode(' ', $value); + } + return $r; + } +} diff --git a/htdocs/includes/restler/Compose.php b/htdocs/includes/restler/Compose.php new file mode 100644 index 00000000000..c4a6f03409c --- /dev/null +++ b/htdocs/includes/restler/Compose.php @@ -0,0 +1,71 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ +class Compose implements iCompose +{ + /** + * @var bool When restler is not running in production mode, this value will + * be checked to include the debug information on error response + */ + public static $includeDebugInfo = true; + /** + * Current Restler instance + * Injected at runtime + * + * @var Restler + */ + public $restler; + + /** + * Result of an api call is passed to this method + * to create a standard structure for the data + * + * @param mixed $result can be a primitive or array or object + * + * @return mixed + */ + public function response($result) + { + //TODO: check Defaults::language and change result accordingly + return $result; + } + + /** + * When the api call results in RestException this method + * will be called to return the error message + * + * @param RestException $exception exception that has reasons for failure + * + * @return array + */ + public function message(RestException $exception) + { + //TODO: check Defaults::language and change result accordingly + $r = array( + 'error' => array( + 'code' => $exception->getCode(), + 'message' => $exception->getErrorMessage(), + ) + $exception->getDetails() + ); + if (!Scope::get('Restler')->getProductionMode() && self::$includeDebugInfo) { + $r += array( + 'debug' => array( + 'source' => $exception->getSource(), + 'stages' => $exception->getStages(), + ) + ); + } + return $r; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/ApiMethodInfo.php b/htdocs/includes/restler/Data/ApiMethodInfo.php new file mode 100644 index 00000000000..c97c27098a9 --- /dev/null +++ b/htdocs/includes/restler/Data/ApiMethodInfo.php @@ -0,0 +1,55 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ApiMethodInfo extends ValueObject +{ + /** + * @var string target url + */ + public $url; + /** + * @var string + */ + public $className; + /** + * @var string + */ + public $methodName; + /** + * @var array parameters to be passed to the api method + */ + public $parameters = array(); + /** + * @var array information on parameters in the form of array(name => index) + */ + public $arguments = array(); + /** + * @var array default values for parameters if any + * in the form of array(index => value) + */ + public $defaults = array(); + /** + * @var array key => value pair of method meta information + */ + public $metadata = array(); + /** + * @var int access level + * 0 - @public - available for all + * 1 - @hybrid - both public and protected (enhanced info for authorized) + * 2 - @protected comment - only for authenticated users + * 3 - protected method - only for authenticated users + */ + public $accessLevel = 0; +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/Arr.php b/htdocs/includes/restler/Data/Arr.php new file mode 100644 index 00000000000..727f14d8080 --- /dev/null +++ b/htdocs/includes/restler/Data/Arr.php @@ -0,0 +1,33 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ +class Arr +{ + /** + * Deep copy given array + * + * @param array $arr + * + * @return array + */ + public static function copy(array $arr) + { + $copy = array(); + foreach ($arr as $key => $value) { + if (is_array($value)) $copy[$key] = static::copy($value); + else if (is_object($value)) $copy[$key] = clone $value; + else $copy[$key] = $value; + } + return $copy; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/Invalid.php b/htdocs/includes/restler/Data/Invalid.php new file mode 100644 index 00000000000..3eaec18017d --- /dev/null +++ b/htdocs/includes/restler/Data/Invalid.php @@ -0,0 +1,20 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Invalid extends Exception +{ + +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/Object.php b/htdocs/includes/restler/Data/Object.php new file mode 100644 index 00000000000..b34f82f0b79 --- /dev/null +++ b/htdocs/includes/restler/Data/Object.php @@ -0,0 +1,157 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Object +{ + /** + * @var bool|string|callable + */ + public static $stringEncoderFunction = false; + /** + * @var bool|string|callable + */ + public static $numberEncoderFunction = false; + /** + * @var array key value pairs for fixing value types using functions. + * For example + * + * 'id'=>'intval' will make sure all values of the id properties + * will be converted to integers intval function + * 'password'=> null will remove all the password entries + */ + public static $fix = array(); + /** + * @var string character that is used to identify sub objects + * + * For example + * + * when Object::$separatorChar = '.'; + * + * array('my.object'=>true) will result in + * + * array( + * 'my'=>array('object'=>true) + * ); + */ + public static $separatorChar = null; + /** + * @var bool set it to true when empty arrays, blank strings, null values + * to be automatically removed from response + */ + public static $removeEmpty = false; + /** + * @var bool set it to true to remove all null values from the result + */ + public static $removeNull = false; + + /** + * Convenience function that converts the given object + * in to associative array + * + * @static + * + * @param mixed $object that needs to be converted + * + * @param bool $forceObjectTypeWhenEmpty when set to true outputs + * actual type (array or + * object) rather than + * always an array when the + * array/object is empty + * + * @return array + */ + public static function toArray($object, + $forceObjectTypeWhenEmpty = false) + { + //if ($object instanceof JsonSerializable) { //wont work on PHP < 5.4 + if (is_object($object)) { + if (method_exists($object, 'jsonSerialize')) { + $object = $object->jsonSerialize(); + } elseif (method_exists($object, '__sleep')) { + $properties = $object->__sleep(); + $array = array(); + foreach ($properties as $key) { + $value = self::toArray($object->{$key}, + $forceObjectTypeWhenEmpty); + if (self::$stringEncoderFunction && is_string($value)) { + $value = self::$stringEncoderFunction ($value); + } elseif (self::$numberEncoderFunction && is_numeric($value)) { + $value = self::$numberEncoderFunction ($value); + } + $array [$key] = $value; + } + return $array; + } + } + if (is_array($object) || is_object($object)) { + $count = 0; + $array = array(); + foreach ($object as $key => $value) { + if ( + is_string(self::$separatorChar) && + false !== strpos($key, self::$separatorChar) + ) { + list($key, $obj) = explode(self::$separatorChar, $key, 2); + $object[$key][$obj] = $value; + $value = $object[$key]; + } + if (self::$removeEmpty && empty($value) && !is_numeric($value) && !is_bool($value)) { + continue; + } elseif (self::$removeNull && is_null($value)) { + continue; + } + if (array_key_exists($key, self::$fix)) { + if (isset(self::$fix[$key])) { + $value = call_user_func(self::$fix[$key], $value); + } else { + continue; + } + } + $value = self::toArray($value, $forceObjectTypeWhenEmpty); + if (self::$stringEncoderFunction && is_string($value)) { + $value = self::$encoderFunctionName ($value); + } elseif (self::$numberEncoderFunction && is_numeric($value)) { + $value = self::$numberEncoderFunction ($value); + } + $array [$key] = $value; + $count++; + } + return $forceObjectTypeWhenEmpty && $count == 0 ? $object : $array; + } + + return $object; + } + + public function __get($name) + { + isset(self::$fix[$name]) ? self::$fix[$name] : null; + } + + public function __set($name, $function) + { + self::$fix[$name] = $function; + } + + public function __isset($name) + { + return isset(self::$fix[$name]); + } + + public function __unset($name) + { + unset(self::$fix[$name]); + } +} + diff --git a/htdocs/includes/restler/Data/String.php b/htdocs/includes/restler/Data/String.php new file mode 100644 index 00000000000..268ca40c115 --- /dev/null +++ b/htdocs/includes/restler/Data/String.php @@ -0,0 +1,84 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ +class String +{ + /** + * Given haystack contains the needle or not? + * + * @param string $haystack + * @param string $needle + * @param bool $caseSensitive + * + * @return bool + */ + public static function contains($haystack, $needle, $caseSensitive = true) + { + if (empty($needle)) + return true; + return $caseSensitive + ? strpos($haystack, $needle) !== false + : stripos($haystack, $needle) !== false; + } + + /** + * Given haystack begins with the needle or not? + * + * @param string $haystack + * @param string $needle + * + * @return bool + */ + public static function beginsWith($haystack, $needle) + { + $length = strlen($needle); + return (substr($haystack, 0, $length) === $needle); + } + + /** + * Given haystack ends with the needle or not? + * + * @param string $haystack + * @param string $needle + * + * @return bool + */ + public static function endsWith($haystack, $needle) + { + $length = strlen($needle); + if ($length == 0) { + return true; + } + return (substr($haystack, -$length) === $needle); + } + + + /** + * Convert camelCased or underscored string in to a title + * + * @param string $name + * + * @return string + */ + public static function title($name) + { + return + ucwords( + preg_replace( + array('/(?<=[^A-Z])([A-Z])/', '/(?<=[^0-9])([0-9])/', '/(_)/'), + array(' $0', ' $0', ' '), + $name + ) + ); + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/ValidationInfo.php b/htdocs/includes/restler/Data/ValidationInfo.php new file mode 100644 index 00000000000..aacc3398c9a --- /dev/null +++ b/htdocs/includes/restler/Data/ValidationInfo.php @@ -0,0 +1,273 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ValidationInfo implements iValueObject +{ + /** + * @var mixed given value for the parameter + */ + public $value; + /** + * @var string proper name for given parameter + */ + public $label; + /** + * @var string html element that can be used to represent the parameter for + * input + */ + public $field; + /** + * @var mixed default value for the parameter + */ + public $default; + /** + * Name of the variable being validated + * + * @var string variable name + */ + public $name; + + /** + * @var bool is it required or not + */ + public $required; + + /** + * @var string body or header or query where this parameter is coming from + * in the http request + */ + public $from; + + /** + * Data type of the variable being validated. + * It will be mostly string + * + * @var string|array multiple types are specified it will be of + * type array otherwise it will be a string + */ + public $type; + + /** + * When the type is array, this field is used to define the type of the + * contents of the array + * + * @var string|null when all the items in an array are of certain type, we + * can set this property. It will be null if the items can be of any type + */ + public $contentType; + + /** + * Should we attempt to fix the value? + * When set to false validation class should throw + * an exception or return false for the validate call. + * When set to true it will attempt to fix the value if possible + * or throw an exception or return false when it cant be fixed. + * + * @var boolean true or false + */ + public $fix = false; + + /** + * @var array of children to be validated + */ + public $children = null; + + // ================================================================== + // + // VALUE RANGE + // + // ------------------------------------------------------------------ + /** + * Given value should match one of the values in the array + * + * @var array of choices to match to + */ + public $choice; + /** + * If the type is string it will set the lower limit for length + * else will specify the lower limit for the value + * + * @var number minimum value + */ + public $min; + /** + * If the type is string it will set the upper limit limit for length + * else will specify the upper limit for the value + * + * @var number maximum value + */ + public $max; + + // ================================================================== + // + // REGEX VALIDATION + // + // ------------------------------------------------------------------ + /** + * RegEx pattern to match the value + * + * @var string regular expression + */ + public $pattern; + + // ================================================================== + // + // CUSTOM VALIDATION + // + // ------------------------------------------------------------------ + /** + * Rules specified for the parameter in the php doc comment. + * It is passed to the validation method as the second parameter + * + * @var array custom rule set + */ + public $rules; + + /** + * Specifying a custom error message will override the standard error + * message return by the validator class + * + * @var string custom error response + */ + public $message; + + // ================================================================== + // + // METHODS + // + // ------------------------------------------------------------------ + + /** + * Name of the method to be used for validation. + * It will be receiving two parameters $input, $rules (array) + * + * @var string validation method name + */ + public $method; + + /** + * Instance of the API class currently being called. It will be null most of + * the time. Only when method is defined it will contain an instance. + * This behavior is for lazy loading of the API class + * + * @var null|object will be null or api class instance + */ + public $apiClassInstance = null; + + public static function numericValue($value) + { + return ( int )$value == $value + ? ( int )$value + : floatval($value); + } + + public static function arrayValue($value) + { + return is_array($value) ? $value : array( + $value + ); + } + + public static function stringValue($value, $glue = ',') + { + return is_array($value) + ? implode($glue, $value) + : ( string )$value; + } + + public static function booleanValue($value) + { + return is_bool($value) + ? $value + : $value !== 'false'; + } + + public static function filterArray(array $data, $keepNumericKeys) + { + $r = array(); + foreach ($data as $key => $value) { + if (is_numeric($key)) { + if ($keepNumericKeys) { + $r[$key] = $value; + } + } elseif (!$keepNumericKeys) { + $r[$key] = $value; + } + } + return $r; + } + + public function __toString() + { + return ' new ValidationInfo() '; + } + + private function getProperty(array &$from, $property) + { + $p = Util::nestedValue($from, $property); + unset($from[$property]); + $p2 = Util::nestedValue( + $from, CommentParser::$embeddedDataName, $property + ); + unset($from[CommentParser::$embeddedDataName][$property]); + + if ($property == 'type' && $p == 'array' && $p2) { + $this->contentType = $p2; + return $p; + } + $r = is_null($p2) ? (is_null($p) ? null : $p) : $p2; + if (!is_null($r)) { + if ($property == 'min' || $property == 'max') { + return static::numericValue($r); + } elseif ($property == 'required' || $property == 'fix') { + return static::booleanValue($r); + } elseif ($property == 'choice') { + return static::arrayValue($r); + } elseif ($property == 'pattern') { + return static::stringValue($r); + } + } + return $r; + } + + public function __construct(array $info) + { + $properties = get_object_vars($this); + unset($properties['contentType']); + foreach ($properties as $property => $value) { + $this->{$property} = $this->getProperty($info, $property); + } + $inner = Util::nestedValue($info, 'properties'); + $this->rules = !empty($inner) ? $inner + $info : $info; + unset($this->rules['properties']); + if (is_string($this->type) && $this->type == 'integer') { + $this->type = 'int'; + } + } + + /** + * Magic Method used for creating instance at run time + */ + public static function __set_state(array $info) + { + $o = new self ($info); + return $o; + } +} + diff --git a/htdocs/includes/restler/Data/Validator.php b/htdocs/includes/restler/Data/Validator.php new file mode 100644 index 00000000000..5e1940ba1b6 --- /dev/null +++ b/htdocs/includes/restler/Data/Validator.php @@ -0,0 +1,626 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Validator implements iValidate +{ + public static $holdException = false; + public static $exceptions = array(); + + /** + * Validate alphabetic characters. + * + * Check that given value contains only alphabetic characters. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function alpha($input, ValidationInfo $info = null) + { + if (ctype_alpha($input)) { + return $input; + } + if ($info && $info->fix) { + //remove non alpha characters + return preg_replace("/[^a-z]/i", "", $input); + } + throw new Invalid('Expecting only alphabetic characters.'); + } + + /** + * Validate alpha numeric characters. + * + * Check that given value contains only alpha numeric characters. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function alphanumeric($input, ValidationInfo $info = null) + { + if (ctype_alnum($input)) { + return $input; + } + if ($info && $info->fix) { + //remove non alpha numeric and space characters + return preg_replace("/[^a-z0-9 ]/i", "", $input); + } + throw new Invalid('Expecting only alpha numeric characters.'); + } + + /** + * Validate printable characters. + * + * Check that given value contains only printable characters. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function printable($input, ValidationInfo $info = null) + { + if (ctype_print($input)) { + return $input; + } + if ($info && $info->fix) { + //remove non printable characters + return preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $input); + } + throw new Invalid('Expecting only printable characters.'); + } + + /** + * Validate hexadecimal digits. + * + * Check that given value contains only hexadecimal digits. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function hex($input, ValidationInfo $info = null) + { + if (ctype_xdigit($input)) { + return $input; + } + throw new Invalid('Expecting only hexadecimal digits.'); + } + + /** + * Validate Telephone number + * + * Check if the given value is numeric with or without a `+` prefix + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function tel($input, ValidationInfo $info = null) + { + if (is_numeric($input) && '-' != substr($input, 0, 1)) { + return $input; + } + throw new Invalid('Expecting phone number, a numeric value ' . + 'with optional `+` prefix'); + } + + /** + * Validate Email + * + * Check if the given string is a valid email + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function email($input, ValidationInfo $info = null) + { + $r = filter_var($input, FILTER_VALIDATE_EMAIL); + if ($r) { + return $r; + } elseif ($info && $info->fix) { + $r = filter_var($input, FILTER_SANITIZE_EMAIL); + return static::email($r); + } + throw new Invalid('Expecting email in `name@example.com` format'); + } + + /** + * Validate IP Address + * + * Check if the given string is a valid ip address + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function ip($input, ValidationInfo $info = null) + { + $r = filter_var($input, FILTER_VALIDATE_IP); + if ($r) + return $r; + + throw new Invalid('Expecting IP address in IPV6 or IPV4 format'); + } + + /** + * Validate Url + * + * Check if the given string is a valid url + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function url($input, ValidationInfo $info = null) + { + $r = filter_var($input, FILTER_VALIDATE_URL); + if ($r) { + return $r; + } elseif ($info && $info->fix) { + $r = filter_var($input, FILTER_SANITIZE_URL); + return static::url($r); + } + throw new Invalid('Expecting url in `http://example.com` format'); + } + + /** + * MySQL Date + * + * Check if the given string is a valid date in YYYY-MM-DD format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function date($input, ValidationInfo $info = null) + { + if ( + preg_match( + '#^(?P\d{2}|\d{4})-(?P\d{1,2})-(?P\d{1,2})$#', + $input, + $date + ) + && checkdate($date['month'], $date['day'], $date['year']) + ) { + return $input; + } + throw new Invalid( + 'Expecting date in `YYYY-MM-DD` format, such as `' + . date("Y-m-d") . '`' + ); + } + + /** + * MySQL DateTime + * + * Check if the given string is a valid date and time in YYY-MM-DD HH:MM:SS format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function datetime($input, ValidationInfo $info = null) + { + if ( + preg_match('/^(?P19\d\d|20\d\d)\-(?P0[1-9]|1[0-2])\-' . + '(?P0\d|[1-2]\d|3[0-1]) (?P0\d|1\d|2[0-3]' . + ')\:(?P[0-5][0-9])\:(?P[0-5][0-9])$/', + $input, $date) + && checkdate($date['month'], $date['day'], $date['year']) + ) { + return $input; + } + throw new Invalid( + 'Expecting date and time in `YYYY-MM-DD HH:MM:SS` format, such as `' + . date("Y-m-d H:i:s") . '`' + ); + } + + /** + * Alias for Time + * + * Check if the given string is a valid time in HH:MM:SS format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function time24($input, ValidationInfo $info = null) + { + return static::time($input, $info); + } + + /** + * Time + * + * Check if the given string is a valid time in HH:MM:SS format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function time($input, ValidationInfo $info = null) + { + if (preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/', $input)) { + return $input; + } + throw new Invalid( + 'Expecting time in `HH:MM:SS` format, such as `' + . date("H:i:s") . '`' + ); + } + + /** + * Time in 12 hour format + * + * Check if the given string is a valid time 12 hour format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function time12($input, ValidationInfo $info = null) + { + if (preg_match( + '/^([1-9]|1[0-2]|0[1-9]){1}(:[0-5][0-9])?\s?([aApP][mM]{1})?$/', + $input) + ) { + return $input; + } + throw new Invalid( + 'Expecting time in 12 hour format, such as `08:00AM` and `10:05:11`' + ); + } + + /** + * Unix Timestamp + * + * Check if the given value is a valid timestamp + * + * @param String $input + * @param ValidationInfo $info + * + * @return int + * @throws Invalid + */ + public static function timestamp($input, ValidationInfo $info = null) + { + if ((string)(int)$input == $input + && ($input <= PHP_INT_MAX) + && ($input >= ~PHP_INT_MAX) + ) { + return (int)$input; + } + throw new Invalid('Expecting unix timestamp, such as ' . time()); + } + + /** + * Validate the given input + * + * Validates the input and attempts to fix it when fix is requested + * + * @param mixed $input + * @param ValidationInfo $info + * @param null $full + * + * @throws \Exception + * @return array|bool|float|int|mixed|null|number|string + */ + public static function validate($input, ValidationInfo $info, $full = null) + { + $html = Scope::get('Restler')->responseFormat instanceof HtmlFormat; + $name = $html ? "$info->label" : "`$info->name`"; + try { + if (is_null($input)) { + if ($info->required) { + throw new RestException (400, + "$name is required."); + } + return null; + } + $error = isset ($info->message) + ? $info->message + : "Invalid value specified for $name"; + + //if a validation method is specified + if (!empty($info->method)) { + $method = $info->method; + $info->method = ''; + $r = self::validate($input, $info); + return $info->apiClassInstance->{$method} ($r); + } + + // when type is an array check if it passes for any type + if (is_array($info->type)) { + //trace("types are ".print_r($info->type, true)); + $types = $info->type; + foreach ($types as $type) { + $info->type = $type; + try { + $r = self::validate($input, $info); + if ($r !== false) { + return $r; + } + } catch (RestException $e) { + // just continue + } + } + throw new RestException (400, $error); + } + + //patterns are supported only for non numeric types + if (isset ($info->pattern) + && $info->type != 'int' + && $info->type != 'float' + && $info->type != 'number' + ) { + if (!preg_match($info->pattern, $input)) { + throw new RestException (400, $error); + } + } + + if (isset ($info->choice)) { + if (is_array($input)) { + foreach ($input as $i) { + if (!in_array($i, $info->choice)) { + $error .= ". Expected one of (" . implode(',', $info->choice) . ")."; + throw new RestException (400, $error); + } + } + } elseif (!in_array($input, $info->choice)) { + $error .= ". Expected one of (" . implode(',', $info->choice) . ")."; + throw new RestException (400, $error); + } + } + + if (method_exists($class = get_called_class(), $info->type) && $info->type != 'validate') { + try { + return call_user_func("$class::$info->type", $input, $info); + } catch (Invalid $e) { + throw new RestException(400, $error . '. ' . $e->getMessage()); + } + } + + switch ($info->type) { + case 'int' : + case 'float' : + case 'number' : + if (!is_numeric($input)) { + $error .= '. Expecting ' + . ($info->type == 'int' ? 'integer' : 'numeric') + . ' value'; + break; + } + if ($info->type == 'int' && (int)$input != $input) { + if ($info->fix) { + $r = (int)$input; + } else { + $error .= '. Expecting integer value'; + break; + } + } else { + $r = $info->numericValue($input); + } + if (isset ($info->min) && $r < $info->min) { + if ($info->fix) { + $r = $info->min; + } else { + $error .= ". Minimum required value is $info->min."; + break; + } + } + if (isset ($info->max) && $r > $info->max) { + if ($info->fix) { + $r = $info->max; + } else { + $error .= ". Maximum allowed value is $info->max."; + break; + } + } + return $r; + + case 'string' : + if (!is_string($input)) { + $error .= '. Expecting alpha numeric value'; + break; + } + if ($info->required && $input === '') { + $error = "$name is required."; + break; + } + $r = strlen($input); + if (isset ($info->min) && $r < $info->min) { + if ($info->fix) { + $input = str_pad($input, $info->min, $input); + } else { + $char = $info->min > 1 ? 'characters' : 'character'; + $error .= ". Minimum $info->min $char required."; + break; + } + } + if (isset ($info->max) && $r > $info->max) { + if ($info->fix) { + $input = substr($input, 0, $info->max); + } else { + $char = $info->max > 1 ? 'characters' : 'character'; + $error .= ". Maximum $info->max $char allowed."; + break; + } + } + return $input; + + case 'bool': + case 'boolean': + if ($input === 'true' || $input === true) return true; + if (is_numeric($input)) return $input > 0; + return false; + + case 'array': + if ($info->fix && is_string($input)) { + $input = explode(CommentParser::$arrayDelimiter, $input); + } + if (is_array($input)) { + $contentType = + Util::nestedValue($info, 'contentType') ? : null; + if ($info->fix) { + if ($contentType == 'indexed') { + $input = $info->filterArray($input, true); + } elseif ($contentType == 'associative') { + $input = $info->filterArray($input, true); + } + } elseif ( + $contentType == 'indexed' && + array_values($input) != $input + ) { + $error .= '. Expecting a list of items but an item is given'; + break; + } elseif ( + $contentType == 'associative' && + array_values($input) == $input && + count($input) + ) { + $error .= '. Expecting an item but a list is given'; + break; + } + $r = count($input); + if (isset ($info->min) && $r < $info->min) { + $item = $info->max > 1 ? 'items' : 'item'; + $error .= ". Minimum $info->min $item required."; + break; + } + if (isset ($info->max) && $r > $info->max) { + if ($info->fix) { + $input = array_slice($input, 0, $info->max); + } else { + $item = $info->max > 1 ? 'items' : 'item'; + $error .= ". Maximum $info->max $item allowed."; + break; + } + } + if ( + isset($contentType) && + $contentType != 'associative' && + $contentType != 'indexed' + ) { + $name = $info->name; + $info->type = $contentType; + unset($info->contentType); + foreach ($input as $key => $chinput) { + $info->name = "{$name}[$key]"; + $input[$key] = static::validate($chinput, $info); + } + } + return $input; + } elseif (isset($contentType)) { + $error .= '. Expecting items of type ' . + ($html ? "$contentType" : "`$contentType`"); + break; + } + break; + case 'mixed': + case 'unknown_type': + case 'unknown': + case null: //treat as unknown + return $input; + default : + if (!is_array($input)) { + break; + } + //do type conversion + if (class_exists($info->type)) { + $input = $info->filterArray($input, false); + $implements = class_implements($info->type); + if ( + is_array($implements) && + in_array('Luracast\\Restler\\Data\\iValueObject', $implements) + ) { + return call_user_func( + "{$info->type}::__set_state", $input + ); + } + $class = $info->type; + $instance = new $class(); + if (is_array($info->children)) { + if ( + empty($input) || + !is_array($input) || + $input === array_values($input) + ) { + $error .= '. Expecting an item of type ' . + ($html ? "$info->type" : "`$info->type`"); + break; + } + foreach ($info->children as $key => $value) { + $cv = new ValidationInfo($value); + if (array_key_exists($key, $input) || $cv->required) { + $instance->{$key} = static::validate( + Util::nestedValue($input, $key), + $cv + ); + } + + } + } + return $instance; + } + } + throw new RestException (400, $error); + } catch (\Exception $e) { + static::$exceptions[] = $e; + if (static::$holdException) { + return null; + } + throw $e; + } + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/ValueObject.php b/htdocs/includes/restler/Data/ValueObject.php new file mode 100644 index 00000000000..811c170acbd --- /dev/null +++ b/htdocs/includes/restler/Data/ValueObject.php @@ -0,0 +1,61 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ValueObject implements iValueObject +{ + + public function __toString() + { + return ' new ' . get_called_class() . '() '; + } + + public static function __set_state(array $properties) + { + $class = get_called_class(); + $instance = new $class (); + $vars = get_object_vars($instance); + foreach ($properties as $property => $value) { + if (property_exists($instance, $property)) { + // see if the property is accessible + if (array_key_exists($property, $vars)) { + $instance->{$property} = $value; + } else { + $method = 'set' . ucfirst($property); + if (method_exists($instance, $method)) { + call_user_func(array( + $instance, + $method + ), $value); + } + } + } + } + return $instance; + } + + public function __toArray() + { + $r = get_object_vars($this); + $methods = get_class_methods($this); + foreach ($methods as $m) { + if (substr($m, 0, 3) == 'get') { + $r [lcfirst(substr($m, 3))] = @$this->{$m} (); + } + } + return $r; + } + +} + diff --git a/htdocs/includes/restler/Data/iValidate.php b/htdocs/includes/restler/Data/iValidate.php new file mode 100644 index 00000000000..8ed28b27b1b --- /dev/null +++ b/htdocs/includes/restler/Data/iValidate.php @@ -0,0 +1,32 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iValidate { + + /** + * method used for validation. + * + * @param mixed $input + * data that needs to be validated + * @param ValidationInfo $info + * information to be used for validation + * @return boolean false in case of failure or fixed value in the expected + * type + * @throws \Luracast\Restler\RestException 400 with information about the + * failed + * validation + */ + public static function validate($input, ValidationInfo $info); +} + diff --git a/htdocs/includes/restler/Data/iValueObject.php b/htdocs/includes/restler/Data/iValueObject.php new file mode 100644 index 00000000000..4597cdac825 --- /dev/null +++ b/htdocs/includes/restler/Data/iValueObject.php @@ -0,0 +1,39 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iValueObject +{ + + /** + * This static method is called for creating an instance of the class by + * passing the initiation values as an array. + * + * @static + * @abstract + * + * @param array $properties + * + * @return iValueObject + */ + public static function __set_state(array $properties); + + /** + * This method provides a string representation for the instance + * + * @return string + */ + public function __toString(); +} + diff --git a/htdocs/includes/restler/Defaults.php b/htdocs/includes/restler/Defaults.php new file mode 100644 index 00000000000..98b97202495 --- /dev/null +++ b/htdocs/includes/restler/Defaults.php @@ -0,0 +1,360 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Defaults +{ + // ================================================================== + // + // Class Mappings + // + // ------------------------------------------------------------------ + + /** + * @var string of name of the class that implements + * \Luracast\Restler\iCache the cache class to be used + */ + public static $cacheClass = 'Luracast\\Restler\\HumanReadableCache'; + + /** + * @var string full path of the directory where all the generated files will + * be kept. When set to null (default) it will use the cache folder that is + * in the same folder as index.php (gateway) + */ + public static $cacheDirectory; + + /** + * @var string of name of the class that implements + * \Luracast\Restler\Data\iValidate the validator class to be used + */ + public static $validatorClass = 'Luracast\\Restler\\Data\\Validator'; + + /** + * @var string name of the class that implements \Luracast\Restler\iCompose + * the class to be used to compose the response + */ + public static $composeClass = 'Luracast\\Restler\\Compose'; + + // ================================================================== + // + // Routing + // + // ------------------------------------------------------------------ + + /** + * @var bool should auto routing for public and protected api methods + * should be enabled by default or not. Set this to false to get + * Restler 1.0 style behavior + */ + public static $autoRoutingEnabled = true; + + /** + * @var boolean avoids creating multiple routes that can increase the + * ambiguity when set to true. when a method parameter is optional it is + * not mapped to the url and should only be used in request body or as + * query string `/resource?id=value`. When a parameter is required and is + * scalar, it will be mapped as part of the url `/resource/{id}` + */ + public static $smartAutoRouting = true; + + /** + * @var boolean enables more ways of finding the parameter data in the request. + * If you need backward compatibility with Restler 2 or below turn this off + */ + public static $smartParameterParsing = true; + + // ================================================================== + // + // API Version Management + // + // ------------------------------------------------------------------ + + /** + * @var null|string name that is used for vendor specific media type and + * api version using the Accept Header for example + * application/vnd.{vendor}-v1+json + * + * Keep this null if you do not want to use vendor MIME for specifying api version + */ + public static $apiVendor = null; + + /** + * @var bool set it to true to force vendor specific MIME for versioning. + * It will be automatically set to true when Defaults::$vendor is not + * null and client is requesting for the custom MIME type + */ + public static $useVendorMIMEVersioning = false; + + /** + * @var bool set it to true to use enableUrl based versioning + */ + public static $useUrlBasedVersioning = false; + + + // ================================================================== + // + // Request + // + // ------------------------------------------------------------------ + + /** + * @var string name to be used for the method parameter to capture the + * entire request data + */ + public static $fullRequestDataName = 'request_data'; + + /** + * @var string name of the property that can sent through $_GET or $_POST to + * override the http method of the request. Set it to null or + * blank string to disable http method override through request + * parameters. + */ + public static $httpMethodOverrideProperty = 'http_method'; + + /** + * @var bool should auto validating api parameters should be enabled by + * default or not. Set this to false to avoid validation. + */ + public static $autoValidationEnabled = true; + + /** + * @var string name of the class that implements iUser interface to identify + * the user for caching purposes + */ + public static $userIdentifierClass = 'Luracast\\Restler\\User'; + + // ================================================================== + // + // Response + // + // ------------------------------------------------------------------ + + /** + * @var bool HTTP status codes are set on all responses by default. + * Some clients (like flash, mobile) have trouble dealing with non-200 + * status codes on error responses. + * + * You can set it to true to force a HTTP 200 status code on all responses, + * even when errors occur. If you suppress status codes, look for an error + * response to determine if an error occurred. + */ + public static $suppressResponseCode = false; + + public static $supportedCharsets = array('utf-8', 'iso-8859-1'); + public static $supportedLanguages = array('en', 'en-US'); + + public static $charset = 'utf-8'; + public static $language = 'en'; + + /** + * @var bool when set to true, it will exclude the response body + */ + public static $emptyBodyForNullResponse = true; + + /** + * @var bool enables CORS support + */ + public static $crossOriginResourceSharing = false; + public static $accessControlAllowOrigin = '*'; + public static $accessControlAllowMethods = + 'GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD'; + + // ================================================================== + // + // Header + // + // ------------------------------------------------------------------ + + /** + * @var array default Cache-Control template that used to set the + * Cache-Control header and has two values, first one is used when + * Defaults::$headerExpires is 0 and second one when it has some time + * value specified. When only one value is specified it will be used for + * both cases + */ + public static $headerCacheControl = array( + 'no-cache, must-revalidate', + + /* "public, " or "private, " will be prepended based on api method + * called (public or protected) + */ + 'max-age={expires}, must-revalidate', + + ); + + + /** + * @var int sets the content to expire immediately when set to zero + * alternatively you can specify the number of seconds the content will + * expire. This setting can be altered at api level using php doc comment + * with @expires numOfSeconds + */ + public static $headerExpires = 0; + + // ================================================================== + // + // Access Control + // + // ------------------------------------------------------------------ + + /** + * @var null|callable if the api methods are under access control mechanism + * you can attach a function here that returns true or false to determine + * visibility of a protected api method. this function will receive method + * info as the only parameter. + */ + public static $accessControlFunction = null; + + /** + * @var int set the default api access mode + * value of 0 = public api + * value of 1 = hybrid api using `@access hybrid` comment + * value of 2 = protected api using `@access protected` comment + * value of 3 = protected api using `protected function` method + */ + public static $apiAccessLevel = 0; + + /** + * @var string authentication method to be called in iAuthenticate + * Interface + */ + public static $authenticationMethod = '__isAllowed'; + + /** + * @var int time in milliseconds for bandwidth throttling, + * which is the minimum response time for each api request. You can + * change it per api method by setting `@throttle 3000` in php doc + * comment either at the method level or class level + */ + public static $throttle = 0; + + // ================================================================== + // + // Overrides for API User + // + // ------------------------------------------------------------------ + + /** + * @var array use 'alternativeName'=> 'actualName' to set alternative + * names that can be used to represent the api method parameters and/or + * static properties of Defaults + */ + public static $aliases = array( + /** + * suppress_response_codes=true as an URL parameter to force + * a HTTP 200 status code on all responses + */ + 'suppress_response_codes' => 'suppressResponseCode', + ); + + /** + * @var array determines the defaults that can be overridden by the api + * user by passing them as URL parameters + */ + public static $overridables = array( + 'suppressResponseCode', + ); + + /** + * @var array contains validation details for defaults to be used when + * set through URL parameters + */ + public static $validation = array( + 'suppressResponseCode' => array('type' => 'bool'), + 'headerExpires' => array('type' => 'int', 'min' => 0), + ); + + // ================================================================== + // + // Overrides API Developer + // + // ------------------------------------------------------------------ + + /** + * @var array determines what are the phpdoc comment tags that will + * override the Defaults here with their values + */ + public static $fromComments = array( + + /** + * use PHPDoc comments such as the following + * ` + * + * @cache no-cache, must-revalidate` to set the Cache-Control header + * for a specific api method + */ + 'cache' => 'headerCacheControl', + + /** + * use PHPDoc comments such as the following + * ` + * + * @expires 50` to set the Expires header + * for a specific api method + */ + 'expires' => 'headerExpires', + + /** + * use PHPDoc comments such as the following + * ` + * + * @throttle 300` + * to set the bandwidth throttling for 300 milliseconds + * for a specific api method + */ + 'throttle' => 'throttle', + + /** + * enable or disable smart auto routing from method comments + * this one is hardwired so cant be turned off + * it is placed here just for documentation purpose + */ + 'smart-auto-routing' => 'smartAutoRouting', + ); + + // ================================================================== + // + // Util + // + // ------------------------------------------------------------------ + + /** + * Use this method to set value to a static properly of Defaults when + * you want to make sure only proper values are taken in with the help of + * validation + * + * @static + * + * @param string $name name of the static property + * @param mixed $value value to set the property to + * + * @return bool + */ + public static function setProperty($name, $value) + { + if (!property_exists(__CLASS__, $name)) return false; + if (@is_array(Defaults::$validation[$name])) { + $info = new ValidationInfo(Defaults::$validation[$name]); + $value = Validator::validate($value, $info); + } + Defaults::$$name = $value; + return true; + } + +} + diff --git a/htdocs/includes/restler/EventDispatcher.php b/htdocs/includes/restler/EventDispatcher.php new file mode 100644 index 00000000000..8e913bc563d --- /dev/null +++ b/htdocs/includes/restler/EventDispatcher.php @@ -0,0 +1,98 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +use Closure; + +class EventDispatcher +{ + private $listeners = array(); + protected static $_waitList = array(); + + public static $self; + protected $events = array(); + + public function __construct() { + static::$self = $this; + if (!empty(static::$_waitList)) { + foreach (static::$_waitList as $param) { + call_user_func_array(array($this,$param[0]), $param[1]); + } + } + } + + public static function __callStatic($eventName, $params) + { + if (0 === strpos($eventName, 'on')) { + if(static::$self){ + return call_user_func_array(array(static::$self, $eventName), $params); + } + static::$_waitList[] = func_get_args(); + return false; + } + } + + public function __call($eventName, $params) + { + if (0 === strpos($eventName, 'on')) { + if (!@is_array($this->listeners[$eventName])) + $this->listeners[$eventName] = array(); + $this->listeners[$eventName][] = $params[0]; + } + return $this; + } + + public static function addListener($eventName, Closure $callback) + { + return static::$eventName($callback); + } + + public function on(array $eventHandlers) + { + for ( + $count = count($eventHandlers), + $events = array_map( + 'ucfirst', + $keys = array_keys( + $eventHandlers = array_change_key_case( + $eventHandlers, + CASE_LOWER + ) + ) + ), + $i = 0; + $i < $count; + call_user_func( + array($this, "on{$events[$i]}"), + $eventHandlers[$keys[$i++]] + ) + ); + } + + /** + * Fire an event to notify all listeners + * + * @param string $eventName name of the event + * @param array $params event related data + */ + protected function dispatch($eventName, array $params = array()) + { + $this->events[] = $eventName; + $params = func_get_args(); + $eventName = 'on'.ucfirst(array_shift($params)); + if (isset($this->listeners[$eventName])) + foreach ($this->listeners[$eventName] as $callback) + call_user_func_array($callback, $params); + } + +} + diff --git a/htdocs/includes/restler/Filter/RateLimit.php b/htdocs/includes/restler/Filter/RateLimit.php new file mode 100644 index 00000000000..99539f70eff --- /dev/null +++ b/htdocs/includes/restler/Filter/RateLimit.php @@ -0,0 +1,178 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class RateLimit implements iFilter, iUseAuthentication +{ + /** + * @var \Luracast\Restler\Restler; + */ + public $restler; + /** + * @var int + */ + public static $usagePerUnit = 1200; + /** + * @var int + */ + public static $authenticatedUsagePerUnit = 5000; + /** + * @var string + */ + public static $unit = 'hour'; + /** + * @var string group the current api belongs to + */ + public static $group = 'common'; + + protected static $units = array( + 'second' => 1, + 'minute' => 60, + 'hour' => 3600, // 60*60 seconds + 'day' => 86400, // 60*60*24 seconds + 'week' => 604800, // 60*60*24*7 seconds + 'month' => 2592000, // 60*60*24*30 seconds + ); + + /** + * @var array all paths beginning with any of the following will be excluded + * from documentation + */ + public static $excludedPaths = array('resources'); + + + /** + * @param string $unit + * @param int $usagePerUnit + * @param int $authenticatedUsagePerUnit set it to false to give unlimited access + * + * @throws \InvalidArgumentException + * @return void + */ + public static function setLimit( + $unit, $usagePerUnit, $authenticatedUsagePerUnit = null + ) + { + static::$unit = $unit; + static::$usagePerUnit = $usagePerUnit; + static::$authenticatedUsagePerUnit = + is_null($authenticatedUsagePerUnit) ? $usagePerUnit : $authenticatedUsagePerUnit; + } + + public function __isAllowed() + { + if (static::$authenticatedUsagePerUnit + == static::$usagePerUnit + ) return $this->check(); + return null; + } + + public function __setAuthenticationStatus($isAuthenticated = false) + { + header('X-Auth-Status: ' . ($isAuthenticated ? 'true' : 'false')); + $this->check($isAuthenticated); + } + + private static function validate($unit) + { + if (!isset(static::$units[$unit])) + throw new \InvalidArgumentException( + 'Rate Limit time unit should be ' + . implode('|', array_keys(static::$units)) . '.' + ); + } + + private function check($isAuthenticated = false) + { + $path = $this->restler->url; + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude) && empty($path)) { + return true; + } elseif (0 === strpos($path, $exclude)) { + return true; + } + } + static::validate(static::$unit); + $timeUnit = static::$units[static::$unit]; + $maxPerUnit = $isAuthenticated + ? static::$authenticatedUsagePerUnit + : static::$usagePerUnit; + if ($maxPerUnit) { + $user = Defaults::$userIdentifierClass; + if (!method_exists($user, 'getUniqueIdentifier')) { + throw new \UnexpectedValueException('`Defaults::$userIdentifierClass` must implement `iIdentifyUser` interface'); + } + $id = "RateLimit_" . $maxPerUnit . '_per_' . static::$unit + . '_for_' . static::$group + . '_' . $user::getUniqueIdentifier(); + $lastRequest = $this->restler->cache->get($id, true) + ? : array('time' => 0, 'used' => 0); + $time = $lastRequest['time']; + $diff = time() - $time; # in seconds + $used = $lastRequest['used']; + + header("X-RateLimit-Limit: $maxPerUnit per " . static::$unit); + if ($diff >= $timeUnit) { + $used = 1; + $time = time(); + } elseif ($used >= $maxPerUnit) { + header("X-RateLimit-Remaining: 0"); + $wait = $timeUnit - $diff; + sleep(1); + throw new RestException(429, + 'Rate limit of ' . $maxPerUnit . ' request' . + ($maxPerUnit > 1 ? 's' : '') . ' per ' + . static::$unit . ' exceeded. Please wait for ' + . static::duration($wait) . '.' + ); + } else { + $used++; + } + $remainingPerUnit = $maxPerUnit - $used; + header("X-RateLimit-Remaining: $remainingPerUnit"); + $this->restler->cache->set($id, + array('time' => $time, 'used' => $used)); + } + return true; + } + + private function duration($secs) + { + $units = array( + 'week' => (int)($secs / 86400 / 7), + 'day' => $secs / 86400 % 7, + 'hour' => $secs / 3600 % 24, + 'minute' => $secs / 60 % 60, + 'second' => $secs % 60); + + $ret = array(); + + //$unit = 'days'; + foreach ($units as $k => $v) { + if ($v > 0) { + $ret[] = $v > 1 ? "$v {$k}s" : "$v $k"; + //$unit = $k; + } + } + $i = count($ret) - 1; + if ($i) { + $ret[$i] = 'and ' . $ret[$i]; + } + return implode(' ', $ret); //." $unit."; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Flash.php b/htdocs/includes/restler/Flash.php new file mode 100644 index 00000000000..f18768e64ff --- /dev/null +++ b/htdocs/includes/restler/Flash.php @@ -0,0 +1,146 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Flash //implements \JsonSerializable +{ + const SUCCESS = 'success'; + const INFO = 'info'; + const WARNING = 'warning'; + const DANGER = 'danger'; + + /** + * @var Flash + */ + private static $instance; + private $usedOnce = false; + + /** + * Flash a success message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function success($message, $header = '') + { + return static::message($message, $header, Flash::SUCCESS); + } + + /** + * Flash a info message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function info($message, $header = '') + { + return static::message($message, $header, Flash::INFO); + } + + /** + * Flash a warning message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function warning($message, $header = '') + { + return static::message($message, $header, Flash::WARNING); + } + + /** + * Flash a error message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function danger($message, $header = '') + { + return static::message($message, $header, Flash::DANGER); + } + + /** + * Flash a message to user + * + * @param string $text message text + * @param string $header + * @param string $type + * + * @return Flash + */ + public static function message($text, $header = '', $type = Flash::WARNING) + { + return static::set(array('message' => $text, 'header' => $header, 'type' => $type)); + } + + /** + * Set some data for one time use + * + * @param array $data array of key value pairs {@type associative} + * + * @return Flash + */ + public static function set(array $data) + { + if (!static::$instance) + static::$instance = new Flash(); + if (!isset($_SESSION['flash'])) + $_SESSION['flash'] = array(); + $_SESSION['flash'] += $data; + HtmlFormat::$data['flash'] = static::$instance; + return static::$instance; + } + + public function __get($name) + { + $this->usedOnce = true; + return Util::nestedValue($_SESSION, 'flash', $name); + } + + public function __isset($name) + { + return !is_null(Util::nestedValue($_SESSION, 'flash', $name)); + } + + public function __destruct() + { + if ($this->usedOnce) + unset($_SESSION['flash']); + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + */ + public function jsonSerialize() + { + $this->usedOnce = true; + return isset($_SESSION['flash']) + ? $_SESSION['flash'] + : array(); + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/AmfFormat.php b/htdocs/includes/restler/Format/AmfFormat.php new file mode 100644 index 00000000000..e477badb01c --- /dev/null +++ b/htdocs/includes/restler/Format/AmfFormat.php @@ -0,0 +1,45 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class AmfFormat extends Format +{ + const MIME = 'application/x-amf'; + const EXTENSION = 'amf'; + + public function encode($data, $humanReadable = false) + { + + $stream = new OutputStream(); + $serializer = new Serializer($stream); + $serializer->writeTypeMarker($data); + + return $stream->getStream(); + } + + public function decode($data) + { + $stream = new InputStream(substr($data, 1)); + $deserializer = new Deserializer($stream); + + return $deserializer->readTypeMarker(); + } +} + diff --git a/htdocs/includes/restler/Format/CsvFormat.php b/htdocs/includes/restler/Format/CsvFormat.php new file mode 100644 index 00000000000..c0dedf1b515 --- /dev/null +++ b/htdocs/includes/restler/Format/CsvFormat.php @@ -0,0 +1,181 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class CsvFormat extends Format implements iDecodeStream +{ + + const MIME = 'text/csv'; + const EXTENSION = 'csv'; + public static $delimiter = ','; + public static $enclosure = '"'; + public static $escape = '\\'; + public static $haveHeaders = null; + + /** + * Encode the given data in the csv format + * + * @param array $data + * resulting data that needs to + * be encoded in the given format + * @param boolean $humanReadable + * set to TRUE when restler + * is not running in production mode. Formatter has to + * make the encoded output more human readable + * + * @return string encoded string + * + * @throws RestException 500 on unsupported data + */ + public function encode($data, $humanReadable = false) + { + $char = Object::$separatorChar; + Object::$separatorChar = false; + $data = Object::toArray($data); + Object::$separatorChar = $char; + if (is_array($data) && array_values($data) == $data) { + //if indexed array + $lines = array(); + $row = array_shift($data); + if (array_values($row) != $row) { + $lines[] = static::putRow(array_keys($row)); + } + $lines[] = static::putRow(array_values($row)); + foreach ($data as $row) { + $lines[] = static::putRow(array_values($row)); + } + return implode(PHP_EOL, $lines) . PHP_EOL; + } + throw new RestException( + 500, + 'Unsupported data for ' . strtoupper(static::EXTENSION) . ' format' + ); + } + + protected static function putRow($data) + { + $fp = fopen('php://temp', 'r+'); + fputcsv($fp, $data, static::$delimiter, static::$enclosure); + rewind($fp); + $data = fread($fp, 1048576); + fclose($fp); + return rtrim($data, PHP_EOL); + } + + /** + * Decode the given data from the csv format + * + * @param string $data + * data sent from client to + * the api in the given format. + * + * @return array associative array of the parsed data + */ + public function decode($data) + { + $decoded = array(); + + if (empty($data)) { + return $decoded; + } + + $lines = array_filter(explode(PHP_EOL, $data)); + + $keys = false; + $row = static::getRow(array_shift($lines)); + + if (is_null(static::$haveHeaders)) { + //try to guess with the given data + static::$haveHeaders = !count(array_filter($row, 'is_numeric')); + } + + static::$haveHeaders ? $keys = $row : $decoded[] = $row; + + while (($row = static::getRow(array_shift($lines), $keys)) !== FALSE) + $decoded [] = $row; + + $char = Object::$separatorChar; + Object::$separatorChar = false; + $decoded = Object::toArray($decoded); + Object::$separatorChar = $char; + return $decoded; + } + + protected static function getRow($data, $keys = false) + { + if (empty($data)) { + return false; + } + $line = str_getcsv( + $data, + static::$delimiter, + static::$enclosure, + static::$escape + ); + + $row = array(); + foreach ($line as $key => $value) { + if (is_numeric($value)) + $value = floatval($value); + if ($keys) { + if (isset($keys [$key])) + $row [$keys [$key]] = $value; + } else { + $row [$key] = $value; + } + } + if ($keys) { + for ($i = count($row); $i < count($keys); $i++) { + $row[$keys[$i]] = null; + } + } + return $row; + } + + /** + * Decode the given data stream + * + * @param string $stream A stream resource with data + * sent from client to the api + * in the given format. + * + * @return array associative array of the parsed data + */ + public function decodeStream($stream) + { + $decoded = array(); + + $keys = false; + $row = static::getRow(stream_get_line($stream, 0, PHP_EOL)); + if (is_null(static::$haveHeaders)) { + //try to guess with the given data + static::$haveHeaders = !count(array_filter($row, 'is_numeric')); + } + + static::$haveHeaders ? $keys = $row : $decoded[] = $row; + + while (($row = static::getRow(stream_get_line($stream, 0, PHP_EOL), $keys)) !== FALSE) + $decoded [] = $row; + + $char = Object::$separatorChar; + Object::$separatorChar = false; + $decoded = Object::toArray($decoded); + Object::$separatorChar = $char; + return $decoded; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/Format.php b/htdocs/includes/restler/Format/Format.php new file mode 100644 index 00000000000..d04f74cf4ac --- /dev/null +++ b/htdocs/includes/restler/Format/Format.php @@ -0,0 +1,140 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +abstract class Format implements iFormat +{ + /** + * override in the extending class + */ + const MIME = 'text/plain'; + /** + * override in the extending class + */ + const EXTENSION = 'txt'; + + /** + * @var string charset encoding defaults to UTF8 + */ + protected $charset='utf-8'; + + /** + * Injected at runtime + * + * @var \Luracast\Restler\Restler + */ + public $restler; + + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap() + { + return array( + static::MIME => static::EXTENSION + ); + } + + /** + * Set the selected MIME type + * + * @param string $mime + * MIME type + */ + public function setMIME($mime) + { + //do nothing + } + + /** + * Content-Type field of the HTTP header can send a charset + * parameter in the HTTP header to specify the character + * encoding of the document. + * This information is passed + * here so that Format class can encode data accordingly + * Format class may choose to ignore this and use its + * default character set. + * + * @param string $charset + * Example utf-8 + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Content-Type accepted by the Format class + * + * @return string $charset Example utf-8 + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Get selected MIME type + */ + public function getMIME() + { + return static::MIME; + } + + /** + * Set the selected file extension + * + * @param string $extension + * file extension + */ + public function setExtension($extension) + { + //do nothing; + } + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension() + { + return static::EXTENSION; + } + + /** + * @return boolean is parsing the request supported? + */ + public function isReadable() + { + return true; + } + + /** + * @return boolean is composing response supported? + */ + public function isWritable() + { + return true; + } + + public function __toString() + { + return $this->getExtension(); + } + +} + diff --git a/htdocs/includes/restler/Format/HtmlFormat.php b/htdocs/includes/restler/Format/HtmlFormat.php new file mode 100644 index 00000000000..65d2bb6279b --- /dev/null +++ b/htdocs/includes/restler/Format/HtmlFormat.php @@ -0,0 +1,485 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class HtmlFormat extends Format +{ + public static $mime = 'text/html'; + public static $extension = 'html'; + public static $view; + public static $errorView = 'debug.php'; + public static $template = 'php'; + public static $handleSession = true; + + public static $useSmartViews = true; + /** + * @var null|string defaults to template named folder in Defaults::$cacheDirectory + */ + public static $cacheDirectory = null; + /** + * @var array global key value pair to be supplied to the templates. All + * keys added here will be available as a variable inside the template + */ + public static $data = array(); + /** + * @var string set it to the location of your the view files. Defaults to + * views folder which is same level as vendor directory. + */ + public static $viewPath; + /** + * @var array template and its custom extension key value pair + */ + public static $customTemplateExtensions = array('blade' => 'blade.php'); + /** + * @var bool used internally for error handling + */ + protected static $parseViewMetadata = true; + /** + * @var Restler; + */ + public $restler; + + public function __construct() + { + //============ SESSION MANAGEMENT =============// + if (static::$handleSession) { + if (session_start() && isset($_SESSION['flash'])) { + static::$data['flash'] = $_SESSION['flash']; + unset($_SESSION['flash']); + } + } + if (!static::$viewPath) { + $array = explode('vendor', __DIR__, 2); + static::$viewPath = $array[0] . 'views'; + } + } + + public static function blade(array $data, $debug = true) + { + if (!class_exists('\Illuminate\View\View', true)) + throw new RestException(500, + 'Blade templates require laravel view classes to be installed using `composer install`'); + $resolver = new EngineResolver(); + $files = new Filesystem(); + $compiler = new BladeCompiler($files, static::$cacheDirectory); + $engine = new CompilerEngine($compiler); + $resolver->register('blade', function () use ($engine) { + return $engine; + }); + + /** @var Restler $restler */ + $restler = Scope::get('Restler'); + + //Lets expose shortcuts for our classes + spl_autoload_register(function ($className) use ($restler) { + if (isset($restler->apiMethodInfo->metadata['scope'][$className])) { + return class_alias($restler->apiMethodInfo->metadata['scope'][$className], $className); + } + if (isset(Scope::$classAliases[$className])) { + return class_alias(Scope::$classAliases[$className], $className); + } + return false; + }, true, true); + + $viewFinder = new FileViewFinder($files, array(static::$viewPath)); + $factory = new Factory($resolver, $viewFinder, new Dispatcher()); + $path = $viewFinder->find(self::$view); + $view = new View($factory, $engine, self::$view, $path, $data); + $factory->callCreator($view); + return $view->render(); + } + + public static function twig(array $data, $debug = true) + { + if (!class_exists('\Twig_Environment', true)) + throw new RestException(500, + 'Twig templates require twig classes to be installed using `composer install`'); + $loader = new \Twig_Loader_Filesystem(static::$viewPath); + $twig = new \Twig_Environment($loader, array( + 'cache' => static::$cacheDirectory, + 'debug' => $debug, + 'use_strict_variables' => $debug, + )); + if ($debug) + $twig->addExtension(new \Twig_Extension_Debug()); + + $twig->addFunction( + new \Twig_SimpleFunction( + 'form', + 'Luracast\Restler\UI\Forms::get', + array('is_safe' => array('html')) + ) + ); + $twig->addFunction( + new \Twig_SimpleFunction( + 'form_key', + 'Luracast\Restler\UI\Forms::key' + ) + ); + $twig->addFunction( + new \Twig_SimpleFunction( + 'nav', + 'Luracast\Restler\UI\Nav::get' + ) + ); + + $twig->registerUndefinedFunctionCallback(function ($name) { + if ( + isset(HtmlFormat::$data[$name]) && + is_callable(HtmlFormat::$data[$name]) + ) { + return new \Twig_SimpleFunction( + $name, + HtmlFormat::$data[$name] + ); + } + return false; + }); + + $template = $twig->loadTemplate(static::getViewFile()); + return $template->render($data); + } + + public static function handlebar(array $data, $debug = true) + { + return static::mustache($data, $debug); + } + + public static function mustache(array $data, $debug = true) + { + if (!class_exists('\Mustache_Engine', true)) + throw new RestException( + 500, + 'Mustache/Handlebar templates require mustache classes ' . + 'to be installed using `composer install`' + ); + if (!isset($data['nav'])) + $data['nav'] = array_values(Nav::get()); + $options = array( + 'loader' => new \Mustache_Loader_FilesystemLoader( + static::$viewPath, + array('extension' => static::getViewExtension()) + ), + 'helpers' => array( + 'form' => function ($text, \Mustache_LambdaHelper $m) { + $params = explode(',', $m->render($text)); + return call_user_func_array( + 'Luracast\Restler\UI\Forms::get', + $params + ); + }, + ) + ); + if (!$debug) + $options['cache'] = static::$cacheDirectory; + $m = new \Mustache_Engine($options); + return $m->render(static::getViewFile(), $data); + } + + public static function php(array $data, $debug = true) + { + if (static::$view == 'debug') + static::$viewPath = dirname(__DIR__) . '/views'; + $view = static::getViewFile(true); + + if (!is_readable($view)) { + throw new RestException( + 500, + "view file `$view` is not readable. " . + 'Check for file presence and file permissions' + ); + } + + $path = static::$viewPath . DIRECTORY_SEPARATOR; + $template = function ($view) use ($data, $path) { + $form = function () { + return call_user_func_array( + 'Luracast\Restler\UI\Forms::get', + func_get_args() + ); + }; + if (!isset($data['form'])) + $data['form'] = $form; + $nav = function () { + return call_user_func_array( + 'Luracast\Restler\UI\Nav::get', + func_get_args() + ); + }; + if (!isset($data['nav'])) + $data['nav'] = $nav; + + $_ = function () use ($data, $path) { + extract($data); + $args = func_get_args(); + $task = array_shift($args); + switch ($task) { + case 'require': + case 'include': + $file = $path . $args[0]; + if (is_readable($file)) { + if ( + isset($args[1]) && + ($arrays = Util::nestedValue($data, $args[1])) + ) { + $str = ''; + foreach ($arrays as $arr) { + extract($arr); + $str .= include $file; + } + return $str; + } else { + return include $file; + } + } + break; + case 'if': + if (count($args) < 2) + $args[1] = ''; + if (count($args) < 3) + $args[2] = ''; + return $args[0] ? $args[1] : $args[2]; + break; + default: + if (isset($data[$task]) && is_callable($data[$task])) + return call_user_func_array($data[$task], $args); + } + return ''; + }; + extract($data); + return @include $view; + }; + $value = $template($view); + if (is_string($value)) + return $value; + } + + /** + * Encode the given data in the format + * + * @param array $data resulting data that needs to + * be encoded in the given format + * @param boolean $humanReadable set to TRUE when restler + * is not running in production mode. + * Formatter has to make the encoded + * output more human readable + * + * @throws \Exception + * @return string encoded string + */ + public function encode($data, $humanReadable = false) + { + if (!is_readable(static::$viewPath)) { + throw new \Exception( + 'The views directory `' + . self::$viewPath . '` should exist with read permission.' + ); + } + static::$data['basePath'] = dirname($_SERVER['SCRIPT_NAME']); + static::$data['baseUrl'] = $this->restler->getBaseUrl(); + static::$data['currentPath'] = $this->restler->url; + + try { + $exception = $this->restler->exception; + $success = is_null($exception); + $error = $success ? null : $exception->getMessage(); + $data = array( + 'response' => Object::toArray($data), + 'stages' => $this->restler->getEvents(), + 'success' => $success, + 'error' => $error + ); + $info = $data['api'] = $this->restler->apiMethodInfo; + $metadata = Util::nestedValue( + $this->restler, 'apiMethodInfo', 'metadata' + ); + $view = $success ? 'view' : 'errorView'; + $value = false; + if (static::$parseViewMetadata && isset($metadata[$view])) { + if (is_array($metadata[$view])) { + self::$view = $metadata[$view]['description']; + $value = Util::nestedValue( + $metadata[$view], 'properties', 'value' + ); + } else { + self::$view = $metadata[$view]; + } + } elseif (!self::$view) { + $file = static::$viewPath . '/' . $this->restler->url . '.' . static::getViewExtension(); + self::$view = static::$useSmartViews && is_readable($file) + ? $this->restler->url + : static::$errorView; + } + if ( + isset($metadata['param']) + && (!$value || 0 === strpos($value, 'request')) + ) { + $params = $metadata['param']; + foreach ($params as $index => &$param) { + $index = intval($index); + if (is_numeric($index)) { + $param['value'] = $this + ->restler + ->apiMethodInfo + ->parameters[$index]; + } + } + $data['request']['parameters'] = $params; + } + if ($value) { + $data = Util::nestedValue($data, explode('.', $value)); + } + $data += static::$data; + if (false === ($i = strrpos(self::$view, '.'))) { + $template = self::$template; + } else { + self::$template = $template = substr(self::$view, $i + 1); + self::$view = substr(self::$view, 0, $i); + } + if (!static::$cacheDirectory) { + static::$cacheDirectory = Defaults::$cacheDirectory . DIRECTORY_SEPARATOR . $template; + if (!file_exists(static::$cacheDirectory)) { + if (!mkdir(static::$cacheDirectory)) { + throw new RestException(500, 'Unable to create cache directory `' . static::$cacheDirectory . '`'); + } + } + } + if (method_exists($class = get_called_class(), $template)) { + return call_user_func("$class::$template", $data, $humanReadable); + } + throw new RestException(500, "Unsupported template system `$template`"); + } catch (Exception $e) { + static::$parseViewMetadata = false; + $this->reset(); + throw $e; + } + } + + public static function getViewExtension() + { + return isset(static::$customTemplateExtensions[static::$template]) + ? static::$customTemplateExtensions[static::$template] + : static::$template; + } + + public static function getViewFile($fullPath = false, $includeExtension = true) + { + $v = $fullPath ? static::$viewPath . '/' : ''; + $v .= static::$view; + if ($includeExtension) + $v .= '.' . static::getViewExtension(); + return $v; + } + + private function reset() + { + static::$mime = 'text/html'; + static::$extension = 'html'; + static::$view = 'debug'; + static::$template = 'php'; + } + + /** + * Decode the given data from the format + * + * @param string $data + * data sent from client to + * the api in the given format. + * + * @return array associative array of the parsed data + * + * @throws RestException + */ + public function decode($data) + { + throw new RestException(500, 'HtmlFormat is write only'); + } + + /** + * @return bool false as HTML format is write only + */ + public function isReadable() + { + return false; + } + + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap() + { + return array( + static::$mime => static::$extension + ); + } + + /** + * Set the selected MIME type + * + * @param string $mime MIME type + */ + public function setMIME($mime) + { + static::$mime = $mime; + } + + /** + * Get selected MIME type + */ + public function getMIME() + { + return static::$mime; + } + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension() + { + return static::$extension; + } + + /** + * Set the selected file extension + * + * @param string $extension file extension + */ + public function setExtension($extension) + { + static::$extension = $extension; + } +} diff --git a/htdocs/includes/restler/Format/JsFormat.php b/htdocs/includes/restler/Format/JsFormat.php new file mode 100644 index 00000000000..067fd1aaa37 --- /dev/null +++ b/htdocs/includes/restler/Format/JsFormat.php @@ -0,0 +1,48 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class JsFormat extends JsonFormat +{ + const MIME = 'text/javascript'; + const EXTENSION = 'js'; + + public static $callbackMethodName = 'parseResponse'; + public static $callbackOverrideQueryString = 'callback'; + public static $includeHeaders = true; + + public function encode($data, $human_readable = false) + { + $r = array(); + if (static::$includeHeaders) { + $r['meta'] = array(); + foreach (headers_list() as $header) { + list($h, $v) = explode(': ', $header, 2); + $r['meta'][$h] = $v; + } + } + $r['data'] = $data; + if (isset($_GET[static::$callbackOverrideQueryString])) { + static::$callbackMethodName + = (string) $_GET[static::$callbackOverrideQueryString]; + } + return static::$callbackMethodName . '(' + . parent::encode($r, $human_readable) . ');'; + } + + public function isReadable() + { + return false; + } +} diff --git a/htdocs/includes/restler/Format/JsonFormat.php b/htdocs/includes/restler/Format/JsonFormat.php new file mode 100644 index 00000000000..1b7fd938c3c --- /dev/null +++ b/htdocs/includes/restler/Format/JsonFormat.php @@ -0,0 +1,210 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class JsonFormat extends Format +{ + /** + * @var boolean|null shim for json_encode option JSON_PRETTY_PRINT set + * it to null to use smart defaults + */ + public static $prettyPrint = null; + + /** + * @var boolean|null shim for json_encode option JSON_UNESCAPED_SLASHES + * set it to null to use smart defaults + */ + public static $unEscapedSlashes = null; + + /** + * @var boolean|null shim for json_encode JSON_UNESCAPED_UNICODE set it + * to null to use smart defaults + */ + public static $unEscapedUnicode = null; + + /** + * @var boolean|null shim for json_decode JSON_BIGINT_AS_STRING set it to + * null to + * use smart defaults + */ + public static $bigIntAsString = null; + + const MIME = 'application/json'; + const EXTENSION = 'json'; + + public function encode($data, $humanReadable = false) + { + if (!is_null(self::$prettyPrint)) { + $humanReadable = self::$prettyPrint; + } + if (is_null(self::$unEscapedSlashes)) { + self::$unEscapedSlashes = $humanReadable; + } + if (is_null(self::$unEscapedUnicode)) { + self::$unEscapedUnicode = $this->charset == 'utf-8'; + } + $options = 0; + if ((PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION >= 4) // PHP >= 5.4 + || PHP_MAJOR_VERSION > 5 // PHP >= 6.0 + ) { + if ($humanReadable) $options |= JSON_PRETTY_PRINT; + if (self::$unEscapedSlashes) $options |= JSON_UNESCAPED_SLASHES; + if (self::$bigIntAsString) $options |= JSON_BIGINT_AS_STRING; + if (self::$unEscapedUnicode) $options |= JSON_UNESCAPED_UNICODE; + return json_encode( + Object::toArray($data, true), $options + ); + } + + $result = json_encode(Object::toArray($data, true)); + if ($humanReadable) $result = $this->formatJson($result); + if (self::$unEscapedUnicode) { + $result = preg_replace_callback('/\\\u(\w\w\w\w)/', + function($matches) + { + if (function_exists('mb_convert_encoding')) + { + return mb_convert_encoding(pack('H*', $matches[1]), 'UTF-8', 'UTF-16BE'); + } + else + { + return iconv('UTF-16BE','UTF-8',pack('H*', $matches[1])); + } + } + , $result); + } + if (self::$unEscapedSlashes) $result = str_replace('\/', '/', $result); + return $result; + } + + public function decode($data) + { + $options = 0; + if (self::$bigIntAsString) { + if ((PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION >= 4) // PHP >= 5.4 + || PHP_MAJOR_VERSION > 5 // PHP >= 6.0 + ) { + $options |= JSON_BIGINT_AS_STRING; + } else { + $data = preg_replace( + '/:\s*(\-?\d+(\.\d+)?([e|E][\-|\+]\d+)?)/', + ': "$1"', $data + ); + } + } + $decoded = json_decode($data, $options); + if (function_exists('json_last_error')) { + switch (json_last_error()) { + case JSON_ERROR_NONE : + return Object::toArray($decoded); + break; + case JSON_ERROR_DEPTH : + $message = 'maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH : + $message = 'underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR : + $message = 'unexpected control character found'; + break; + case JSON_ERROR_SYNTAX : + $message = 'malformed JSON'; + break; + case JSON_ERROR_UTF8 : + $message = 'malformed UTF-8 characters, possibly ' . + 'incorrectly encoded'; + break; + default : + $message = 'unknown error'; + break; + } + throw new RestException (400, 'Error parsing JSON, ' . $message); + } elseif (strlen($data) && $decoded === null || $decoded === $data) { + throw new RestException (400, 'Error parsing JSON'); + } + + return Object::toArray($decoded); + } + + /** + * Pretty print JSON string + * + * @param string $json + * + * @return string formatted json + */ + private function formatJson($json) + { + $tab = ' '; + $newJson = ''; + $indentLevel = 0; + $inString = false; + $len = strlen($json); + for ($c = 0; $c < $len; $c++) { + $char = $json [$c]; + switch ($char) { + case '{' : + case '[' : + if (!$inString) { + $newJson .= $char . "\n" . + str_repeat($tab, $indentLevel + 1); + $indentLevel++; + } else { + $newJson .= $char; + } + break; + case '}' : + case ']' : + if (!$inString) { + $indentLevel--; + $newJson .= "\n" . + str_repeat($tab, $indentLevel) . $char; + } else { + $newJson .= $char; + } + break; + case ',' : + if (!$inString) { + $newJson .= ",\n" . + str_repeat($tab, $indentLevel); + } else { + $newJson .= $char; + } + break; + case ':' : + if (!$inString) { + $newJson .= ': '; + } else { + $newJson .= $char; + } + break; + case '"' : + if ($c == 0) { + $inString = true; + } elseif ($c > 0 && $json [$c - 1] != '\\') { + $inString = !$inString; + } + default : + $newJson .= $char; + break; + } + } + + return $newJson; + } +} + diff --git a/htdocs/includes/restler/Format/MultiFormat.php b/htdocs/includes/restler/Format/MultiFormat.php new file mode 100644 index 00000000000..2c93cd04fe9 --- /dev/null +++ b/htdocs/includes/restler/Format/MultiFormat.php @@ -0,0 +1,144 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +abstract class MultiFormat implements iFormat +{ + /** + * override in the extending class + */ + const MIME = 'text/plain,text/html'; + /** + * override in the extending class + */ + const EXTENSION = 'txt,html'; + + /** + * @var string charset encoding defaults to UTF8 + */ + protected $charset='utf-8'; + + public static $mime; + public static $extension; + + /** + * Injected at runtime + * + * @var \Luracast\Restler\Restler + */ + public $restler; + + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap() + { + $extensions = explode(',',static::EXTENSION); + $mimes = explode(',',static::MIME); + $count = max(count($extensions), count($mimes)); + $extensions += array_fill(0, $count, end($extensions)); + $mimes += array_fill(0, $count, end($mimes)); + return array_combine($mimes,$extensions); + } + + /** + * Set the selected MIME type + * + * @param string $mime + * MIME type + */ + public function setMIME($mime) + { + static::$mime = $mime; + } + + /** + * Content-Type field of the HTTP header can send a charset + * parameter in the HTTP header to specify the character + * encoding of the document. + * This information is passed + * here so that Format class can encode data accordingly + * Format class may choose to ignore this and use its + * default character set. + * + * @param string $charset + * Example utf-8 + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Content-Type accepted by the Format class + * + * @return string $charset Example utf-8 + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Get selected MIME type + */ + public function getMIME() + { + return static::$mime; + } + + /** + * Set the selected file extension + * + * @param string $extension + * file extension + */ + public function setExtension($extension) + { + static::$extension = $extension; + } + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension() + { + return static::$extension; + } + + /** + * @return boolean is parsing the request supported? + */ + public function isReadable() + { + return true; + } + + /** + * @return boolean is composing response supported? + */ + public function isWritable() + { + return true; + } + + public function __toString() + { + return $this->getExtension(); + } +} + diff --git a/htdocs/includes/restler/Format/PlistFormat.php b/htdocs/includes/restler/Format/PlistFormat.php new file mode 100644 index 00000000000..2c645eb0dfe --- /dev/null +++ b/htdocs/includes/restler/Format/PlistFormat.php @@ -0,0 +1,91 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class PlistFormat extends MultiFormat +{ + /** + * @var boolean set it to true binary plist is preferred + */ + public static $compact = null; + const MIME = 'application/xml,application/x-plist'; + const EXTENSION = 'plist'; + + public function setMIME($mime) + { + static::$mime = $mime; + static::$compact = $mime == 'application/x-plist'; + } + + /** + * Encode the given data in plist format + * + * @param array $data + * resulting data that needs to + * be encoded in plist format + * @param boolean $humanReadable + * set to true when restler + * is not running in production mode. Formatter has to + * make the encoded output more human readable + * + * @return string encoded string + */ + public function encode($data, $humanReadable = false) + { + //require_once 'CFPropertyList.php'; + if (!isset(self::$compact)) { + self::$compact = !$humanReadable; + } + /** + * + * @var CFPropertyList + */ + $plist = new CFPropertyList (); + $td = new CFTypeDetector (); + $guessedStructure = $td->toCFType( + Object::toArray($data) + ); + $plist->add($guessedStructure); + + return self::$compact + ? $plist->toBinary() + : $plist->toXML(true); + } + + /** + * Decode the given data from plist format + * + * @param string $data + * data sent from client to + * the api in the given format. + * + * @return array associative array of the parsed data + */ + public function decode($data) + { + //require_once 'CFPropertyList.php'; + $plist = new CFPropertyList (); + $plist->parse($data); + + return $plist->toArray(); + } +} + diff --git a/htdocs/includes/restler/Format/TsvFormat.php b/htdocs/includes/restler/Format/TsvFormat.php new file mode 100644 index 00000000000..f67ebfcd677 --- /dev/null +++ b/htdocs/includes/restler/Format/TsvFormat.php @@ -0,0 +1,24 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class TsvFormat extends CsvFormat +{ + const MIME = 'text/csv'; + const EXTENSION = 'csv'; + public static $delimiter = "\t"; + public static $enclosure = '"'; + public static $escape = '\\'; + public static $haveHeaders = null; +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/UploadFormat.php b/htdocs/includes/restler/Format/UploadFormat.php new file mode 100644 index 00000000000..aaebf6b5695 --- /dev/null +++ b/htdocs/includes/restler/Format/UploadFormat.php @@ -0,0 +1,145 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class UploadFormat extends Format +{ + const MIME = 'multipart/form-data'; + const EXTENSION = 'post'; + public static $errors = array( + 0 => false, + 1 => "The uploaded file exceeds the maximum allowed size", + 2 => "The uploaded file exceeds the maximum allowed size", + 3 => "The uploaded file was only partially uploaded", + 4 => "No file was uploaded", + 6 => "Missing a temporary folder" + ); + /** + * use it if you need to restrict uploads based on file type + * setting it as an empty array allows all file types + * default is to allow only png and jpeg images + * + * @var array + */ + public static $allowedMimeTypes = array('image/jpeg', 'image/png'); + /** + * use it to restrict uploads based on file size + * set it to 0 to allow all sizes + * please note that it upload restrictions in the server + * takes precedence so it has to be lower than or equal to that + * default value is 1MB (1024x1024)bytes + * usual value for the server is 8388608 + * + * @var int + */ + public static $maximumFileSize = 1048576; + /** + * Your own validation function for validating each uploaded file + * it can return false or throw an exception for invalid file + * use anonymous function / closure in PHP 5.3 and above + * use function name in other cases + * + * @var Callable + */ + public static $customValidationFunction; + /** + * Since exceptions are triggered way before at the `get` stage + * + * @var bool + */ + public static $suppressExceptionsAsError = false; + + protected static function checkFile(& $file, $doMimeCheck = false, $doSizeCheck = false) + { + try { + if ($file['error']) { + //server is throwing an error + //assume that the error is due to maximum size limit + throw new RestException($file['error'] > 5 ? 500 : 413, static::$errors[$file['error']]); + } + $typeElements = explode('/', $file['type']); + $genericType = $typeElements[0].'/*'; + if ( + $doMimeCheck + && !( + in_array($file['type'], self::$allowedMimeTypes) + || in_array($genericType, self::$allowedMimeTypes) + ) + ) { + throw new RestException(403, "File type ({$file['type']}) is not supported."); + } + if ($doSizeCheck && $file['size'] > self::$maximumFileSize) { + throw new RestException(413, "Uploaded file ({$file['name']}) is too big."); + } + if (self::$customValidationFunction) { + if (!call_user_func(self::$customValidationFunction, $file)) { + throw new RestException(403, "File ({$file['name']}) is not supported."); + } + } + } catch (RestException $e) { + if (static::$suppressExceptionsAsError) { + $file['error'] = $e->getCode() == 413 ? 1 : 6; + $file['exception'] = $e; + } else { + throw $e; + } + } + } + + public function encode($data, $humanReadable = false) + { + throw new RestException(500, 'UploadFormat is read only'); + } + + public function decode($data) + { + $doMimeCheck = !empty(self::$allowedMimeTypes); + $doSizeCheck = self::$maximumFileSize ? TRUE : FALSE; + //validate + foreach ($_FILES as & $file) { + if (is_array($file['error'])) { + foreach ($file['error'] as $i => $error) { + $innerFile = array(); + foreach ($file as $property => $value) { + $innerFile[$property] = $value[$i]; + } + if ($innerFile['name']) + static::checkFile($innerFile, $doMimeCheck, $doSizeCheck); + + if (isset($innerFile['exception'])) { + $file['error'][$i] = $innerFile['error']; + $file['exception'] = $innerFile['exception']; + break; + } + } + } else { + if ($file['name']) + static::checkFile($file, $doMimeCheck, $doSizeCheck); + if (isset($innerFile['exception'])) { + break; + } + } + } + //sort file order if needed; + return UrlEncodedFormat::decoderTypeFix($_FILES + $_POST); + } + + function isWritable() + { + return false; + } + +} diff --git a/htdocs/includes/restler/Format/UrlEncodedFormat.php b/htdocs/includes/restler/Format/UrlEncodedFormat.php new file mode 100644 index 00000000000..a37d5d6c5cf --- /dev/null +++ b/htdocs/includes/restler/Format/UrlEncodedFormat.php @@ -0,0 +1,58 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class UrlEncodedFormat extends Format +{ + const MIME = 'application/x-www-form-urlencoded'; + const EXTENSION = 'post'; + + public function encode($data, $humanReadable = false) + { + return http_build_query(static::encoderTypeFix($data)); + } + + public function decode($data) + { + parse_str($data, $r); + return self::decoderTypeFix($r); + } + + public static function encoderTypeFix(array $data) + { + foreach ($data as $k => $v) { + if (is_bool($v)) { + $data[$k] = $v = $v ? 'true' : 'false'; + } elseif (is_array($v)) { + $data[$k] = $v = static::decoderTypeFix($v); + } + } + return $data; + } + + public static function decoderTypeFix(array $data) + { + foreach ($data as $k => $v) { + if ($v === 'true' || $v === 'false') { + $data[$k] = $v = $v === 'true'; + } elseif (is_array($v)) { + $data[$k] = $v = static::decoderTypeFix($v); + } elseif (empty($v) && $v != 0) { + unset($data[$k]); + } + } + return $data; + } +} + diff --git a/htdocs/includes/restler/Format/XmlFormat.php b/htdocs/includes/restler/Format/XmlFormat.php new file mode 100644 index 00000000000..52fe50c6755 --- /dev/null +++ b/htdocs/includes/restler/Format/XmlFormat.php @@ -0,0 +1,348 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class XmlFormat extends Format +{ + const MIME = 'application/xml'; + const EXTENSION = 'xml'; + + // ================================================================== + // + // Properties related to reading/parsing/decoding xml + // + // ------------------------------------------------------------------ + public static $importSettingsFromXml = false; + public static $parseAttributes = true; + public static $parseNamespaces = true; + public static $parseTextNodeAsProperty = true; + + // ================================================================== + // + // Properties related to writing/encoding xml + // + // ------------------------------------------------------------------ + public static $useTextNodeProperty = true; + public static $useNamespaces = true; + public static $cdataNames = array(); + + // ================================================================== + // + // Common Properties + // + // ------------------------------------------------------------------ + public static $attributeNames = array(); + public static $textNodeName = 'text'; + public static $namespaces = array(); + public static $namespacedProperties = array(); + /** + * Default name for the root node. + * + * @var string $rootNodeName + */ + public static $rootName = 'response'; + public static $defaultTagName = 'item'; + + /** + * When you decode an XML its structure is copied to the static vars + * we can use this function to echo them out and then copy paste inside + * our service methods + * + * @return string PHP source code to reproduce the configuration + */ + public static function exportCurrentSettings() + { + $s = 'XmlFormat::$rootName = "' . (self::$rootName) . "\";\n"; + $s .= 'XmlFormat::$attributeNames = ' . + (var_export(self::$attributeNames, true)) . ";\n"; + $s .= 'XmlFormat::$defaultTagName = "' . + self::$defaultTagName . "\";\n"; + $s .= 'XmlFormat::$parseAttributes = ' . + (self::$parseAttributes ? 'true' : 'false') . ";\n"; + $s .= 'XmlFormat::$parseNamespaces = ' . + (self::$parseNamespaces ? 'true' : 'false') . ";\n"; + if (self::$parseNamespaces) { + $s .= 'XmlFormat::$namespaces = ' . + (var_export(self::$namespaces, true)) . ";\n"; + $s .= 'XmlFormat::$namespacedProperties = ' . + (var_export(self::$namespacedProperties, true)) . ";\n"; + } + + return $s; + } + + public function encode($data, $humanReadable = false) + { + $data = Object::toArray($data); + $xml = new XMLWriter(); + $xml->openMemory(); + $xml->startDocument('1.0', $this->charset); + if ($humanReadable) { + $xml->setIndent(true); + $xml->setIndentString(' '); + } + static::$useNamespaces && isset(static::$namespacedProperties[static::$rootName]) + ? + $xml->startElementNs( + static::$namespacedProperties[static::$rootName], + static::$rootName, + static::$namespaces[static::$namespacedProperties[static::$rootName]] + ) + : + $xml->startElement(static::$rootName); + if (static::$useNamespaces) { + foreach (static::$namespaces as $prefix => $ns) { + if (isset(static::$namespacedProperties[static::$rootName]) + && static::$namespacedProperties[static::$rootName] == $prefix + ) + continue; + $prefix = 'xmlns' . (empty($prefix) ? '' : ':' . $prefix); + $xml->writeAttribute($prefix, $ns); + } + } + $this->write($xml, $data, static::$rootName); + $xml->endElement(); + return $xml->outputMemory(); + } + + public function write(XMLWriter $xml, $data, $parent) + { + $text = array(); + if (is_array($data)) { + if (static::$useTextNodeProperty && isset($data[static::$textNodeName])) { + $text [] = $data[static::$textNodeName]; + unset($data[static::$textNodeName]); + } + $attributes = array_flip(static::$attributeNames); + //make sure we deal with attributes first + $temp = array(); + foreach ($data as $key => $value) { + if (isset($attributes[$key])) { + $temp[$key] = $data[$key]; + unset($data[$key]); + } + } + $data = array_merge($temp, $data); + foreach ($data as $key => $value) { + if (is_numeric($key)) { + if (!is_array($value)) { + $text [] = $value; + continue; + } + $key = static::$defaultTagName; + } + $useNS = static::$useNamespaces + && !empty(static::$namespacedProperties[$key]) + && false === strpos($key, ':'); + if (is_array($value)) { + if ($value == array_values($value)) { + //numeric array, create siblings + foreach ($value as $v) { + $useNS + ? $xml->startElementNs( + static::$namespacedProperties[$key], + $key, + null + ) + : $xml->startElement($key); + $this->write($xml, $v, $key); + $xml->endElement(); + } + } else { + $useNS + ? $xml->startElementNs( + static::$namespacedProperties[$key], + $key, + null + ) + : $xml->startElement($key); + $this->write($xml, $value, $key); + $xml->endElement(); + } + continue; + } elseif (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + if (isset($attributes[$key])) { + $xml->writeAttribute($useNS ? static::$namespacedProperties[$key] . ':' . $key : $key, $value); + } else { + $useNS + ? + $xml->startElementNs( + static::$namespacedProperties[$key], + $key, + null + ) + : $xml->startElement($key); + $this->write($xml, $value, $key); + $xml->endElement(); + } + } + } else { + $text [] = (string)$data; + } + if (!empty($text)) { + if (count($text) == 1) { + in_array($parent, static::$cdataNames) + ? $xml->writeCdata(implode('', $text)) + : $xml->text(implode('', $text)); + } else { + foreach ($text as $t) { + $xml->writeElement(static::$textNodeName, $t); + } + } + } + } + + public function decode($data) + { + try { + if ($data == '') { + return array(); + } + libxml_use_internal_errors(true); + $xml = simplexml_load_string($data, + "SimpleXMLElement", LIBXML_NOBLANKS | LIBXML_NOCDATA | LIBXML_COMPACT); + if (false === $xml) { + $error = libxml_get_last_error(); + throw new RestException(400, 'Malformed XML. ' + . trim($error->message, "\r\n") . ' at line ' . $error->line); + } + libxml_clear_errors(); + if (static::$importSettingsFromXml) { + static::$attributeNames = array(); + static::$namespacedProperties = array(); + static::$namespaces = array(); + static::$rootName = $xml->getName(); + $namespaces = $xml->getNamespaces(); + if (count($namespaces)) { + $p = strpos($data, $xml->getName()); + if ($p && $data{$p - 1} == ':') { + $s = strpos($data, '<') + 1; + $prefix = substr($data, $s, $p - $s - 1); + static::$namespacedProperties[static::$rootName] = $prefix; + } + } + } + $data = $this->read($xml); + if (count($data) == 1 && isset($data[static::$textNodeName])) + $data = $data[static::$textNodeName]; + return $data; + } catch (\RuntimeException $e) { + throw new RestException(400, + "Error decoding request. " . $e->getMessage()); + } + } + + public function read(SimpleXMLElement $xml, $namespaces = null) + { + $r = array(); + $text = (string)$xml; + + if (static::$parseAttributes) { + $attributes = $xml->attributes(); + foreach ($attributes as $key => $value) { + if (static::$importSettingsFromXml + && !in_array($key, static::$attributeNames) + ) { + static::$attributeNames[] = $key; + } + $r[$key] = static::setType((string)$value); + } + } + $children = $xml->children(); + foreach ($children as $key => $value) { + if (isset($r[$key])) { + if (is_array($r[$key])) { + if ($r[$key] != array_values($r[$key])) + $r[$key] = array($r[$key]); + } else { + $r[$key] = array($r[$key]); + } + $r[$key][] = $this->read($value, $namespaces); + } else { + $r[$key] = $this->read($value); + } + } + + if (static::$parseNamespaces) { + if (is_null($namespaces)) + $namespaces = $xml->getDocNamespaces(true); + foreach ($namespaces as $prefix => $ns) { + static::$namespaces[$prefix] = $ns; + if (static::$parseAttributes) { + $attributes = $xml->attributes($ns); + foreach ($attributes as $key => $value) { + if (isset($r[$key])) { + $key = "{$prefix}:$key"; + } + if (static::$importSettingsFromXml + && !in_array($key, static::$attributeNames) + ) { + static::$namespacedProperties[$key] = $prefix; + static::$attributeNames[] = $key; + } + $r[$key] = static::setType((string)$value); + } + } + $children = $xml->children($ns); + foreach ($children as $key => $value) { + if (static::$importSettingsFromXml) + static::$namespacedProperties[$key] = $prefix; + if (isset($r[$key])) { + if (is_array($r[$key])) { + if ($r[$key] != array_values($r[$key])) + $r[$key] = array($r[$key]); + } else { + $r[$key] = array($r[$key]); + } + $r[$key][] = $this->read($value, $namespaces); + } else { + $r[$key] = $this->read($value, $namespaces); + } + } + } + } + + if (empty($text) && $text !== '0') { + if (empty($r)) return null; + } else { + empty($r) + ? $r = static::setType($text) + : ( + static::$parseTextNodeAsProperty + ? $r[static::$textNodeName] = static::setType($text) + : $r[] = static::setType($text) + ); + } + return $r; + } + + public static function setType($value) + { + if (empty($value) && $value !== '0') + return null; + if ($value == 'true') + return true; + if ($value == 'false') + return true; + return $value; + } +} + diff --git a/htdocs/includes/restler/Format/YamlFormat.php b/htdocs/includes/restler/Format/YamlFormat.php new file mode 100644 index 00000000000..d87ad7bbc99 --- /dev/null +++ b/htdocs/includes/restler/Format/YamlFormat.php @@ -0,0 +1,36 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class YamlFormat extends Format +{ + const MIME = 'text/plain'; + const EXTENSION = 'yaml'; + + public function encode($data, $humanReadable = false) + { +// require_once 'sfyaml.php'; + return @Yaml::dump(Object::toArray($data)); + } + + public function decode($data) + { +// require_once 'sfyaml.php'; + return Yaml::parse($data); + } +} + diff --git a/htdocs/includes/restler/Format/iDecodeStream.php b/htdocs/includes/restler/Format/iDecodeStream.php new file mode 100644 index 00000000000..8f2765fcfd8 --- /dev/null +++ b/htdocs/includes/restler/Format/iDecodeStream.php @@ -0,0 +1,30 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iDecodeStream +{ + + /** + * Decode the given data stream + * + * @param string $stream A stream resource with data + * sent from client to the api + * in the given format. + * + * @return array associative array of the parsed data + */ + public function decodeStream($stream); + +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/iFormat.php b/htdocs/includes/restler/Format/iFormat.php new file mode 100644 index 00000000000..60c6f0a7ede --- /dev/null +++ b/htdocs/includes/restler/Format/iFormat.php @@ -0,0 +1,109 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iFormat +{ + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap(); + + /** + * Set the selected MIME type + * + * @param string $mime + * MIME type + */ + public function setMIME($mime); + + /** + * Content-Type field of the HTTP header can send a charset + * parameter in the HTTP header to specify the character + * encoding of the document. + * This information is passed + * here so that Format class can encode data accordingly + * Format class may choose to ignore this and use its + * default character set. + * + * @param string $charset + * Example utf-8 + */ + public function setCharset($charset); + + /** + * Content-Type accepted by the Format class + * + * @return string $charset Example utf-8 + */ + public function getCharset(); + + /** + * Get selected MIME type + */ + public function getMIME(); + + /** + * Set the selected file extension + * + * @param string $extension + * file extension + */ + public function setExtension($extension); + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension(); + + /** + * Encode the given data in the format + * + * @param array $data + * resulting data that needs to + * be encoded in the given format + * @param boolean $humanReadable + * set to TRUE when restler + * is not running in production mode. Formatter has to + * make the encoded output more human readable + * @return string encoded string + */ + public function encode($data, $humanReadable = false); + + /** + * Decode the given data from the format + * + * @param string $data + * data sent from client to + * the api in the given format. + * @return array associative array of the parsed data + */ + public function decode($data); + + /** + * @return boolean is parsing the request supported? + */ + public function isReadable(); + + /** + * @return boolean is composing response supported? + */ + public function isWritable(); +} + diff --git a/htdocs/includes/restler/HumanReadableCache.php b/htdocs/includes/restler/HumanReadableCache.php new file mode 100644 index 00000000000..f134605e4d1 --- /dev/null +++ b/htdocs/includes/restler/HumanReadableCache.php @@ -0,0 +1,129 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class HumanReadableCache implements iCache +{ + /** + * @var string path of the folder to hold cache files + */ + public static $cacheDir; + + public function __construct() + { + if (is_null(self::$cacheDir)) { + self::$cacheDir = Defaults::$cacheDirectory; + } + } + + /** + * store data in the cache + * + * @param string $name + * @param mixed $data + * + * @throws \Exception + * @return boolean true if successful + */ + public function set($name, $data) + { + if (is_array($data)) { + $s = '$o = array();' . PHP_EOL . PHP_EOL; + $s .= '// ** THIS IS AN AUTO GENERATED FILE.' + . ' DO NOT EDIT MANUALLY ** '; + foreach ($data as $key => $value) { + $s .= PHP_EOL . PHP_EOL . + "//==================== $key ====================" + . PHP_EOL . PHP_EOL; + if (is_array($value)) { + $s .= '$o[\'' . $key . '\'] = array();'; + foreach ($value as $ke => $va) { + $s .= PHP_EOL . PHP_EOL . "//==== $key $ke ====" + . PHP_EOL . PHP_EOL; + $s .= '$o[\'' . $key . '\'][\'' . $ke . '\'] = ' . + str_replace(' ', ' ', + var_export($va, true)) . ';'; + } + } else { + $s .= '$o[\'' . $key . '\'] = ' + . var_export($value, true) . ';'; + } + } + $s .= PHP_EOL . 'return $o;'; + } else { + $s = 'return ' . var_export($data, true) . ';'; + } + $file = $this->_file($name); + $r = @file_put_contents($file, "throwException(); + } + return $r; + } + + /** + * retrieve data from the cache + * + * @param string $name + * @param bool $ignoreErrors + * + * @return mixed + */ + public function get($name, $ignoreErrors = false) + { + $file = $this->_file($name); + if (file_exists($file)) { + return include($file); + } + } + + /** + * delete data from the cache + * + * @param string $name + * @param bool $ignoreErrors + * + * @return boolean true if successful + */ + public function clear($name, $ignoreErrors = false) + { + return @unlink($this->_file($name)); + } + + /** + * check if the given name is cached + * + * @param string $name + * + * @return boolean true if cached + */ + public function isCached($name) + { + return file_exists($this->_file($name)); + } + + private function _file($name) + { + return self::$cacheDir . '/' . $name . '.php'; + } + + private function throwException() + { + throw new \Exception( + 'The cache directory `' + . self::$cacheDir . '` should exist with write permission.' + ); + } +} + diff --git a/htdocs/includes/restler/README.md b/htdocs/includes/restler/README.md new file mode 100644 index 00000000000..26d47f174bc --- /dev/null +++ b/htdocs/includes/restler/README.md @@ -0,0 +1,8 @@ +Luracast Restler Framework +========================== + +Restler is a simple and effective multi-format Web API Server written in PHP. + +This repository contains just the framework files for installing the framework core using composer require statements. + +For more information, usage examples, pull requests, and issues go to the [Main Repository](https://github.com/Luracast/Restler) \ No newline at end of file diff --git a/htdocs/includes/restler/Redirect.php b/htdocs/includes/restler/Redirect.php new file mode 100644 index 00000000000..7c494bc3c19 --- /dev/null +++ b/htdocs/includes/restler/Redirect.php @@ -0,0 +1,53 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Redirect +{ + /** + * Redirect to given url + * + * @param string $url relative path or full url + * @param array $params associative array of query parameters + * @param array $flashData associative array of properties to be set in $_SESSION for one time use + * @param int $status http status code to send the response with ideally 301 or 302 + * + * @return array + */ + public static function to($url, array $params = array(), array $flashData = array(), $status = 302) + { + $url = ltrim($url, '/'); + /** @var $r Restler */ + $r = Scope::get('Restler'); + $base = $r->getBaseUrl() . '/'; + if (0 !== strpos($url, 'http')) + $url = $base . $url; + if (!empty($flashData) || $base . $r->url !== $url || Util::getRequestMethod() != 'GET') { + if ($r->responseFormat instanceof JsonFormat) + return array('redirect' => $url); + if (!empty($params)) { + $url .= '?' . http_build_query($params); + } + Flash::set($flashData); + header( + "{$_SERVER['SERVER_PROTOCOL']} $status " . + (isset(RestException::$codes[$status]) ? RestException::$codes[$status] : '') + ); + header("Location: $url"); + die(''); + } + return array(); + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Resources.php b/htdocs/includes/restler/Resources.php new file mode 100644 index 00000000000..23d2407c486 --- /dev/null +++ b/htdocs/includes/restler/Resources.php @@ -0,0 +1,1006 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Resources implements iUseAuthentication, iProvideMultiVersionApi +{ + /** + * @var bool should protected resources be shown to unauthenticated users? + */ + public static $hideProtected = true; + /** + * @var bool should we use format as extension? + */ + public static $useFormatAsExtension = true; + /** + * @var bool should we include newer apis in the list? works only when + * Defaults::$useUrlBasedVersioning is set to true; + */ + public static $listHigherVersions = true; + /** + * @var array all http methods specified here will be excluded from + * documentation + */ + public static $excludedHttpMethods = array('OPTIONS'); + /** + * @var array all paths beginning with any of the following will be excluded + * from documentation + */ + public static $excludedPaths = array(); + /** + * @var bool + */ + public static $placeFormatExtensionBeforeDynamicParts = true; + /** + * @var bool should we group all the operations with the same url or not + */ + public static $groupOperations = false; + /** + * @var null|callable if the api methods are under access control mechanism + * you can attach a function here that returns true or false to determine + * visibility of a protected api method. this function will receive method + * info as the only parameter. + */ + public static $accessControlFunction = null; + /** + * @var array type mapping for converting data types to javascript / swagger + */ + public static $dataTypeAlias = array( + 'string' => 'string', + 'int' => 'int', + 'number' => 'float', + 'float' => 'float', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'NULL' => 'null', + 'array' => 'Array', + 'object' => 'Object', + 'stdClass' => 'Object', + 'mixed' => 'string', + 'DateTime' => 'Date' + ); + /** + * @var array configurable symbols to differentiate public, hybrid and + * protected api + */ + public static $apiDescriptionSuffixSymbols = array( + 0 => '  ', //public api + 1 => '  ', //hybrid api + 2 => '  ', //protected api + ); + + /** + * Injected at runtime + * + * @var Restler instance of restler + */ + public $restler; + /** + * @var string when format is not used as the extension this property is + * used to set the extension manually + */ + public $formatString = ''; + protected $_models; + protected $_bodyParam; + /** + * @var bool|stdClass + */ + protected $_fullDataRequested = false; + protected $crud = array( + 'POST' => 'create', + 'GET' => 'retrieve', + 'PUT' => 'update', + 'DELETE' => 'delete', + 'PATCH' => 'partial update' + ); + protected static $prefixes = array( + 'get' => 'retrieve', + 'index' => 'list', + 'post' => 'create', + 'put' => 'update', + 'patch' => 'modify', + 'delete' => 'remove', + ); + protected $_authenticated = false; + protected $cacheName = ''; + + public function __construct() + { + if (static::$useFormatAsExtension) { + $this->formatString = '.{format}'; + } + } + + /** + * This method will be called first for filter classes and api classes so + * that they can respond accordingly for filer method call and api method + * calls + * + * + * @param bool $isAuthenticated passes true when the authentication is + * done, false otherwise + * + * @return mixed + */ + public function __setAuthenticationStatus($isAuthenticated = false) + { + $this->_authenticated = $isAuthenticated; + } + + /** + * pre call for get($id) + * + * if cache is present, use cache + */ + public function _pre_get_json($id) + { + $userClass = Defaults::$userIdentifierClass; + $this->cacheName = $userClass::getCacheIdentifier() . '_resources_' . $id; + if ($this->restler->getProductionMode() + && !$this->restler->refreshCache + && $this->restler->cache->isCached($this->cacheName) + ) { + //by pass call, compose, postCall stages and directly send response + $this->restler->composeHeaders(); + die($this->restler->cache->get($this->cacheName)); + } + } + + /** + * post call for get($id) + * + * create cache if in production mode + * + * @param $responseData + * + * @internal param string $data composed json output + * + * @return string + */ + public function _post_get_json($responseData) + { + if ($this->restler->getProductionMode()) { + $this->restler->cache->set($this->cacheName, $responseData); + } + return $responseData; + } + + /** + * @access hybrid + * + * @param string $id + * + * @throws RestException + * @return null|stdClass + * + * @url GET {id} + */ + public function get($id = '') + { + $version = $this->restler->getRequestedApiVersion(); + if (empty($id)) { + //do nothing + } elseif (false !== ($pos = strpos($id, '-v'))) { + //$version = intval(substr($id, $pos + 2)); + $id = substr($id, 0, $pos); + } elseif ($id{0} == 'v' && is_numeric($v = substr($id, 1))) { + $id = ''; + //$version = $v; + } elseif ($id == 'root' || $id == 'index') { + $id = ''; + } + $this->_models = new stdClass(); + $r = null; + $count = 0; + + $tSlash = !empty($id); + $target = empty($id) ? '' : $id; + $tLen = strlen($target); + + $filter = array(); + + $routes + = Util::nestedValue(Routes::toArray(), "v$version") + ? : array(); + + $prefix = Defaults::$useUrlBasedVersioning ? "/v$version" : ''; + + foreach ($routes as $value) { + foreach ($value as $httpMethod => $route) { + if (in_array($httpMethod, static::$excludedHttpMethods)) { + continue; + } + $fullPath = $route['url']; + if ($fullPath !== $target && !String::beginsWith($fullPath, $target)) { + continue; + } + $fLen = strlen($fullPath); + if ($tSlash) { + if ($fLen != $tLen && !String::beginsWith($fullPath, $target . '/')) + continue; + } elseif ($fLen > $tLen + 1 && $fullPath{$tLen + 1} != '{' && !String::beginsWith($fullPath, '{')) { + //when mapped to root exclude paths that have static parts + //they are listed else where under that static part name + continue; + } + + if (!static::verifyAccess($route)) { + continue; + } + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if ($fullPath == $exclude) + continue 2; + } elseif (String::beginsWith($fullPath, $exclude)) { + continue 2; + } + } + $m = $route['metadata']; + if ($id == '' && $m['resourcePath'] != '') { + continue; + } + if (isset($filter[$httpMethod][$fullPath])) { + continue; + } + $filter[$httpMethod][$fullPath] = true; + // reset body params + $this->_bodyParam = array( + 'required' => false, + 'description' => array() + ); + $count++; + $className = $this->_noNamespace($route['className']); + if (!$r) { + $resourcePath = '/' + . trim($m['resourcePath'], '/'); + $r = $this->_operationListing($resourcePath); + } + $parts = explode('/', $fullPath); + $pos = count($parts) - 1; + if (count($parts) == 1 && $httpMethod == 'GET') { + } else { + for ($i = 0; $i < count($parts); $i++) { + if (strlen($parts[$i]) && $parts[$i]{0} == '{') { + $pos = $i - 1; + break; + } + } + } + $nickname = $this->_nickname($route); + $index = static::$placeFormatExtensionBeforeDynamicParts && $pos > 0 ? $pos : 0; + if (!empty($parts[$index])) + $parts[$index] .= $this->formatString; + + $fullPath = implode('/', $parts); + $description = isset( + $m['classDescription']) + ? $m['classDescription'] + : $className . ' API'; + if (empty($m['description'])) { + $m['description'] = $this->restler->getProductionMode() + ? '' + : 'routes to ' + . $route['className'] + . '::' + . $route['methodName'] . '();'; + } + if (empty($m['longDescription'])) { + $m['longDescription'] = $this->restler->getProductionMode() + ? '' + : 'Add PHPDoc long description to ' + . "$className::" + . $route['methodName'] . '();' + . ' (the api method) to write here'; + } + $operation = $this->_operation( + $route, + $nickname, + $httpMethod, + $m['description'], + $m['longDescription'] + ); + if (isset($m['throws'])) { + foreach ($m['throws'] as $exception) { + $operation->errorResponses[] = array( + 'reason' => $exception['reason'], + 'code' => $exception['code']); + } + } + if (isset($m['param'])) { + foreach ($m['param'] as $param) { + //combine body params as one + $p = $this->_parameter($param); + if ($p->paramType == 'body') { + $this->_appendToBody($p); + } else { + $operation->parameters[] = $p; + } + } + } + if ( + count($this->_bodyParam['description']) || + ( + $this->_fullDataRequested && + $httpMethod != 'GET' && + $httpMethod != 'DELETE' + ) + ) { + $operation->parameters[] = $this->_getBody(); + } + if (isset($m['return']['type'])) { + $responseClass = $m['return']['type']; + if (is_string($responseClass)) { + if (class_exists($responseClass)) { + $this->_model($responseClass); + $operation->responseClass + = $this->_noNamespace($responseClass); + } elseif (strtolower($responseClass) == 'array') { + $operation->responseClass = 'Array'; + $rt = $m['return']; + if (isset( + $rt[CommentParser::$embeddedDataName]['type']) + ) { + $rt = $rt[CommentParser::$embeddedDataName] + ['type']; + if (class_exists($rt)) { + $this->_model($rt); + $operation->responseClass .= '[' . + $this->_noNamespace($rt) . ']'; + } + } + } + } + } + $api = false; + + if (static::$groupOperations) { + foreach ($r->apis as $a) { + if ($a->path == "/$fullPath") { + $api = $a; + break; + } + } + } + + if (!$api) { + $api = $this->_api("$prefix/$fullPath", $description); + $r->apis[] = $api; + } + + $api->operations[] = $operation; + } + } + if (!$count) { + throw new RestException(404); + } + if (!is_null($r)) + $r->models = $this->_models; + usort( + $r->apis, + function ($a, $b) { + $order = array( + 'GET' => 1, + 'POST' => 2, + 'PUT' => 3, + 'PATCH' => 4, + 'DELETE' => 5 + ); + return + $a->operations[0]->httpMethod == + $b->operations[0]->httpMethod + ? $a->path > $b->path + : $order[$a->operations[0]->httpMethod] > + $order[$b->operations[0]->httpMethod]; + + } + ); + return $r; + } + + protected function _nickname(array $route) + { + static $hash = array(); + $method = $route['methodName']; + if (isset(static::$prefixes[$method])) { + $method = static::$prefixes[$method]; + } else { + $method = str_replace( + array_keys(static::$prefixes), + array_values(static::$prefixes), + $method + ); + } + while (isset($hash[$method]) && $route['url'] != $hash[$method]) { + //create another one + $method .= '_'; + } + $hash[$method] = $route['url']; + return $method; + } + + protected function _noNamespace($className) + { + $className = explode('\\', $className); + return end($className); + } + + protected function _operationListing($resourcePath = '/') + { + $r = $this->_resourceListing(); + $r->resourcePath = $resourcePath; + $r->models = new stdClass(); + return $r; + } + + protected function _resourceListing() + { + $r = new stdClass(); + $r->apiVersion = (string)$this->restler->_requestedApiVersion; + $r->swaggerVersion = "1.1"; + $r->basePath = $this->restler->getBaseUrl(); + $r->produces = $this->restler->getWritableMimeTypes(); + $r->consumes = $this->restler->getReadableMimeTypes(); + $r->apis = array(); + return $r; + } + + protected function _api($path, $description = '') + { + $r = new stdClass(); + $r->path = $path; + $r->description = + empty($description) && $this->restler->getProductionMode() + ? 'Use PHPDoc comment to describe here' + : $description; + $r->operations = array(); + return $r; + } + + protected function _operation( + $route, + $nickname, + $httpMethod = 'GET', + $summary = 'description', + $notes = 'long description', + $responseClass = 'void' + ) + { + //reset body params + $this->_bodyParam = array( + 'required' => false, + 'description' => array() + ); + + $r = new stdClass(); + $r->httpMethod = $httpMethod; + $r->nickname = $nickname; + $r->responseClass = $responseClass; + + $r->parameters = array(); + + $r->summary = $summary . ($route['accessLevel'] > 2 + ? static::$apiDescriptionSuffixSymbols[2] + : static::$apiDescriptionSuffixSymbols[$route['accessLevel']] + ); + $r->notes = $notes; + + $r->errorResponses = array(); + return $r; + } + + protected function _parameter($param) + { + $r = new stdClass(); + $r->name = $param['name']; + $r->description = !empty($param['description']) + ? $param['description'] . '.' + : ($this->restler->getProductionMode() + ? '' + : 'add @param {type} $' . $r->name + . ' {comment} to describe here'); + //paramType can be path or query or body or header + $r->paramType = Util::nestedValue($param, CommentParser::$embeddedDataName, 'from') ? : 'query'; + $r->required = isset($param['required']) && $param['required']; + if (isset($param['default'])) { + $r->defaultValue = $param['default']; + } elseif (isset($param[CommentParser::$embeddedDataName]['example'])) { + $r->defaultValue + = $param[CommentParser::$embeddedDataName]['example']; + } + $r->allowMultiple = false; + $type = 'string'; + if (isset($param['type'])) { + $type = $param['type']; + if (is_array($type)) { + $type = array_shift($type); + } + if ($type == 'array') { + $contentType = Util::nestedValue( + $param, + CommentParser::$embeddedDataName, + 'type' + ); + if ($contentType) { + if ($contentType == 'indexed') { + $type = 'Array'; + } elseif ($contentType == 'associative') { + $type = 'Object'; + } else { + $type = "Array[$contentType]"; + } + if (Util::isObjectOrArray($contentType)) { + $this->_model($contentType); + } + } elseif (isset(static::$dataTypeAlias[$type])) { + $type = static::$dataTypeAlias[$type]; + } + } elseif (Util::isObjectOrArray($type)) { + $this->_model($type); + } elseif (isset(static::$dataTypeAlias[$type])) { + $type = static::$dataTypeAlias[$type]; + } + } + $r->dataType = $type; + if (isset($param[CommentParser::$embeddedDataName])) { + $p = $param[CommentParser::$embeddedDataName]; + if (isset($p['min']) && isset($p['max'])) { + $r->allowableValues = array( + 'valueType' => 'RANGE', + 'min' => $p['min'], + 'max' => $p['max'], + ); + } elseif (isset($p['choice'])) { + $r->allowableValues = array( + 'valueType' => 'LIST', + 'values' => $p['choice'] + ); + } + } + return $r; + } + + protected function _appendToBody($p) + { + if ($p->name === Defaults::$fullRequestDataName) { + $this->_fullDataRequested = $p; + unset($this->_bodyParam['names'][Defaults::$fullRequestDataName]); + return; + } + $this->_bodyParam['description'][$p->name] + = "$p->name" + . ' : ' . $p->dataType . ' ' + . ($p->required ? ' (required) - ' : ' - ') + . $p->description; + $this->_bodyParam['required'] = $p->required + || $this->_bodyParam['required']; + $this->_bodyParam['names'][$p->name] = $p; + } + + protected function _getBody() + { + $r = new stdClass(); + $n = isset($this->_bodyParam['names']) + ? array_values($this->_bodyParam['names']) + : array(); + if (count($n) == 1) { + if (isset($this->_models->{$n[0]->dataType})) { + // ============ custom class =================== + $r = $n[0]; + $c = $this->_models->{$r->dataType}; + $a = $c->properties; + $r->description = "Paste JSON data here"; + if (count($a)) { + $r->description .= " with the following" + . (count($a) > 1 ? ' properties.' : ' property.'); + foreach ($a as $k => $v) { + $r->description .= "
$k : " + . $v['type'] . ' ' + . (isset($v['required']) ? '(required)' : '') + . ' - ' . $v['description']; + } + } + $r->defaultValue = "{\n \"" + . implode("\": \"\",\n \"", + array_keys($c->properties)) + . "\": \"\"\n}"; + return $r; + } elseif (false !== ($p = strpos($n[0]->dataType, '['))) { + // ============ array of custom class =============== + $r = $n[0]; + $t = substr($r->dataType, $p + 1, -1); + if ($c = Util::nestedValue($this->_models, $t)) { + $a = $c->properties; + $r->description = "Paste JSON data here"; + if (count($a)) { + $r->description .= " with an array of objects with the following" + . (count($a) > 1 ? ' properties.' : ' property.'); + foreach ($a as $k => $v) { + $r->description .= "
$k : " + . $v['type'] . ' ' + . (isset($v['required']) ? '(required)' : '') + . ' - ' . $v['description']; + } + } + $r->defaultValue = "[\n {\n \"" + . implode("\": \"\",\n \"", + array_keys($c->properties)) + . "\": \"\"\n }\n]"; + return $r; + } else { + $r->description = "Paste JSON data here with an array of $t values."; + $r->defaultValue = "[ ]"; + return $r; + } + } elseif ($n[0]->dataType == 'Array') { + // ============ array =============================== + $r = $n[0]; + $r->description = "Paste JSON array data here" + . ($r->required ? ' (required) . ' : '. ') + . "
$r->description"; + $r->defaultValue = "[\n {\n \"" + . "property\" : \"\"\n }\n]"; + return $r; + } elseif ($n[0]->dataType == 'Object') { + // ============ object ============================== + $r = $n[0]; + $r->description = "Paste JSON object data here" + . ($r->required ? ' (required) . ' : '. ') + . "
$r->description"; + $r->defaultValue = "{\n \"" + . "property\" : \"\"\n}"; + return $r; + } + } + $p = array_values($this->_bodyParam['description']); + $r->name = 'REQUEST_BODY'; + $r->description = "Paste JSON data here"; + if (count($p) == 0 && $this->_fullDataRequested) { + $r->required = $this->_fullDataRequested->required; + $r->defaultValue = "{\n \"property\" : \"\"\n}"; + } else { + $r->description .= " with the following" + . (count($p) > 1 ? ' properties.' : ' property.') + . '
' + . implode("
", $p); + $r->required = $this->_bodyParam['required']; + // Create default object that includes parameters to be submitted + $defaultObject = new \StdClass(); + foreach ($this->_bodyParam['names'] as $name => $values) { + if (!$values->required) + continue; + if (class_exists($values->dataType)) { + $myClassName = $values->dataType; + $defaultObject->$name = new $myClassName(); + } else { + $defaultObject->$name = ''; + } + } + $r->defaultValue = Scope::get('JsonFormat')->encode($defaultObject, true); + } + $r->paramType = 'body'; + $r->allowMultiple = false; + $r->dataType = 'Object'; + return $r; + } + + protected function _model($className, $instance = null) + { + $id = $this->_noNamespace($className); + if (isset($this->_models->{$id})) { + return; + } + $properties = array(); + if (!$instance) { + if (!class_exists($className)) + return; + $instance = new $className(); + } + $data = get_object_vars($instance); + $reflectionClass = new \ReflectionClass($className); + foreach ($data as $key => $value) { + + $propertyMetaData = null; + + try { + $property = $reflectionClass->getProperty($key); + if ($c = $property->getDocComment()) { + $propertyMetaData = Util::nestedValue( + CommentParser::parse($c), + 'var' + ); + } + } catch (\ReflectionException $e) { + } + + if (is_null($propertyMetaData)) { + $type = $this->getType($value, true); + $description = ''; + } else { + $type = Util::nestedValue( + $propertyMetaData, + 'type' + ) ? : $this->getType($value, true); + $description = Util::nestedValue( + $propertyMetaData, + 'description' + ) ? : ''; + + if (class_exists($type)) { + $this->_model($type); + $type = $this->_noNamespace($type); + } + } + + if (isset(static::$dataTypeAlias[$type])) { + $type = static::$dataTypeAlias[$type]; + } + $properties[$key] = array( + 'type' => $type, + 'description' => $description + ); + if (Util::nestedValue( + $propertyMetaData, + CommentParser::$embeddedDataName, + 'required' + ) + ) { + $properties[$key]['required'] = true; + } + if ($type == 'Array') { + $itemType = Util::nestedValue( + $propertyMetaData, + CommentParser::$embeddedDataName, + 'type' + ) ? : + (count($value) + ? $this->getType(end($value), true) + : 'string'); + if (class_exists($itemType)) { + $this->_model($itemType); + $itemType = $this->_noNamespace($itemType); + } + $properties[$key]['item'] = array( + 'type' => $itemType, + /*'description' => '' */ //TODO: add description + ); + } else if (preg_match('/^Array\[(.+)\]$/', $type, $matches)) { + $itemType = $matches[1]; + $properties[$key]['type'] = 'Array'; + $properties[$key]['item']['type'] = $itemType; + + if (class_exists($itemType)) { + $this->_model($itemType); + } + } + } + if (!empty($properties)) { + $model = new stdClass(); + $model->id = $id; + $model->properties = $properties; + $this->_models->{$id} = $model; + } + } + + /** + * Find the data type of the given value. + * + * + * @param mixed $o given value for finding type + * + * @param bool $appendToModels if an object is found should we append to + * our models list? + * + * @return string + * + * @access private + */ + public function getType($o, $appendToModels = false) + { + if (is_object($o)) { + $oc = get_class($o); + if ($appendToModels) { + $this->_model($oc, $o); + } + return $this->_noNamespace($oc); + } + if (is_array($o)) { + if (count($o)) { + $child = end($o); + if (Util::isObjectOrArray($child)) { + $childType = $this->getType($child, $appendToModels); + return "Array[$childType]"; + } + } + return 'array'; + } + if (is_bool($o)) return 'boolean'; + if (is_numeric($o)) return is_float($o) ? 'float' : 'int'; + return 'string'; + } + + /** + * pre call for index() + * + * if cache is present, use cache + */ + public function _pre_index_json() + { + $userClass = Defaults::$userIdentifierClass; + $this->cacheName = $userClass::getCacheIdentifier() + . '_resources-v' + . $this->restler->_requestedApiVersion; + if ($this->restler->getProductionMode() + && !$this->restler->refreshCache + && $this->restler->cache->isCached($this->cacheName) + ) { + //by pass call, compose, postCall stages and directly send response + $this->restler->composeHeaders(); + die($this->restler->cache->get($this->cacheName)); + } + } + + /** + * post call for index() + * + * create cache if in production mode + * + * @param $responseData + * + * @internal param string $data composed json output + * + * @return string + */ + public function _post_index_json($responseData) + { + if ($this->restler->getProductionMode()) { + $this->restler->cache->set($this->cacheName, $responseData); + } + return $responseData; + } + + /** + * @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; + } + + protected function _loadResource($url) + { + $ch = curl_init($this->restler->getBaseUrl() . $url + . (empty($_GET) ? '' : '?' . http_build_query($_GET))); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Accept:application/json', + )); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + $result = json_decode(curl_exec($ch)); + $http_status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + return array($http_status, $result); + } + + protected function _mapResources(array $allRoutes, array &$map, $version = 1) + { + foreach ($allRoutes as $fullPath => $routes) { + $path = explode('/', $fullPath); + $resource = isset($path[0]) ? $path[0] : ''; + if ($resource == 'resources' || String::endsWith($resource, 'index')) + continue; + foreach ($routes as $httpMethod => $route) { + if (in_array($httpMethod, static::$excludedHttpMethods)) { + continue; + } + if (!static::verifyAccess($route)) { + continue; + } + + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if ($fullPath == $exclude) + continue 2; + } elseif (String::beginsWith($fullPath, $exclude)) { + continue 2; + } + } + + $res = $resource + ? ($version == 1 ? "/resources/$resource" : "/v$version/resources/$resource-v$version") + : ($version == 1 ? "/resources/root" : "/v$version/resources/root-v$version"); + + if (empty($map[$res])) { + $map[$res] = isset( + $route['metadata']['classDescription']) + ? $route['metadata']['classDescription'] : ''; + } + } + } + } + + /** + * Maximum api version supported by the api class + * @return int + */ + public static function __getMaximumSupportedVersion() + { + return Scope::get('Restler')->getApiVersion(); + } + + /** + * Verifies that the requesting user is allowed to view the docs for this API + * + * @param $route + * + * @return boolean True if the user should be able to view this API's docs + */ + protected function verifyAccess($route) + { + if ($route['accessLevel'] < 2) { + return true; + } + if ( + static::$hideProtected + && !$this->_authenticated + && $route['accessLevel'] > 1 + ) { + return false; + } + if ($this->_authenticated + && static::$accessControlFunction + && (!call_user_func( + static::$accessControlFunction, $route['metadata'])) + ) { + return false; + } + return true; + } +} diff --git a/htdocs/includes/restler/RestException.php b/htdocs/includes/restler/RestException.php new file mode 100644 index 00000000000..763ec8361f1 --- /dev/null +++ b/htdocs/includes/restler/RestException.php @@ -0,0 +1,138 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ + +class RestException extends Exception +{ + /** + * HTTP status codes + * + * @var array + */ + public static $codes = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => '(Unused)', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 429 => 'Too Many Requests', //still in draft but used for rate limiting + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported' + ); + private $details; + private $stage; + + /** + * @param string $httpStatusCode http status code + * @param string|null $errorMessage error message + * @param array $details any extra detail about the exception + * @param Exception $previous previous exception if any + */ + public function __construct($httpStatusCode, $errorMessage = null, array $details = array(), Exception $previous = null) + { + $events = Scope::get('Restler')->getEvents(); + if(count($events)<= 1){ + $this->stage = 'setup'; + } else { + $this->stage = $previous ? $events[count($events)-2] : end($events); + } + $this->details = $details; + parent::__construct($errorMessage, $httpStatusCode, $previous); + } + + /** + * Get extra details about the exception + * + * @return array details array + */ + public function getDetails() + { + return $this->details; + } + + public function getStage() + { + return $this->stage; + } + + public function getStages() + { + $e = Scope::get('Restler')->getEvents(); + $i = array_search($this->stage, $e); + return array( + 'success' => array_slice($e, 0, $i), + 'failure' => array_slice($e, $i), + ); + } + + public function getErrorMessage() + { + $statusCode = $this->getCode(); + $message = $this->getMessage(); + if (isset(RestException::$codes[$statusCode])) { + $message = RestException::$codes[$statusCode] . + (empty($message) ? '' : ': ' . $message); + } + return $message; + } + + public function getSource() + { + $e = $this; + while ($e->getPrevious()) { + $e = $e->getPrevious(); + } + return basename($e->getFile()) . ':' + . $e->getLine() . ' at ' + . $this->getStage() . ' stage'; + } +} + diff --git a/htdocs/includes/restler/Restler.php b/htdocs/includes/restler/Restler.php new file mode 100644 index 00000000000..99c00a89ed3 --- /dev/null +++ b/htdocs/includes/restler/Restler.php @@ -0,0 +1,1451 @@ + + * + * @category Framework + * @package Restler + * @author R.Arul Kumaran + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Restler extends EventDispatcher +{ + const VERSION = '3.0.0rc5'; + + // ================================================================== + // + // Public variables + // + // ------------------------------------------------------------------ + /** + * Reference to the last exception thrown + * @var RestException + */ + public $exception = null; + /** + * Used in production mode to store the routes and more + * + * @var iCache + */ + public $cache; + /** + * URL of the currently mapped service + * + * @var string + */ + public $url; + /** + * Http request method of the current request. + * Any value between [GET, PUT, POST, DELETE] + * + * @var string + */ + public $requestMethod; + /** + * Requested data format. + * Instance of the current format class + * which implements the iFormat interface + * + * @var iFormat + * @example jsonFormat, xmlFormat, yamlFormat etc + */ + public $requestFormat; + /** + * Response data format. + * + * Instance of the current format class + * which implements the iFormat interface + * + * @var iFormat + * @example jsonFormat, xmlFormat, yamlFormat etc + */ + public $responseFormat; + /** + * Http status code + * + * @var int + */ + public $responseCode=200; + /** + * @var string base url of the api service + */ + protected $baseUrl; + /** + * @var bool Used for waiting till verifying @format + * before throwing content negotiation failed + */ + protected $requestFormatDiffered = false; + /** + * method information including metadata + * + * @var ApiMethodInfo + */ + public $apiMethodInfo; + /** + * @var int for calculating execution time + */ + protected $startTime; + /** + * When set to false, it will run in debug mode and parse the + * class files every time to map it to the URL + * + * @var boolean + */ + protected $productionMode = false; + public $refreshCache = false; + /** + * Caching of url map is enabled or not + * + * @var boolean + */ + protected $cached; + /** + * @var int + */ + protected $apiVersion = 1; + /** + * @var int + */ + protected $requestedApiVersion = 1; + /** + * @var int + */ + protected $apiMinimumVersion = 1; + /** + * @var array + */ + protected $apiVersionMap = array(); + /** + * Associated array that maps formats to their respective format class name + * + * @var array + */ + protected $formatMap = array(); + /** + * List of the Mime Types that can be produced as a response by this API + * + * @var array + */ + protected $writableMimeTypes = array(); + /** + * List of the Mime Types that are supported for incoming requests by this API + * + * @var array + */ + protected $readableMimeTypes = array(); + /** + * Associated array that maps formats to their respective format class name + * + * @var array + */ + protected $formatOverridesMap = array('extensions' => array()); + /** + * list of filter classes + * + * @var array + */ + protected $filterClasses = array(); + /** + * instances of filter classes that are executed after authentication + * + * @var array + */ + protected $postAuthFilterClasses = array(); + + + // ================================================================== + // + // Protected variables + // + // ------------------------------------------------------------------ + + /** + * Data sent to the service + * + * @var array + */ + protected $requestData = array(); + /** + * list of authentication classes + * + * @var array + */ + protected $authClasses = array(); + /** + * list of error handling classes + * + * @var array + */ + protected $errorClasses = array(); + protected $authenticated = false; + protected $authVerified = false; + /** + * @var mixed + */ + protected $responseData; + + /** + * Constructor + * + * @param boolean $productionMode When set to false, it will run in + * debug mode and parse the class files + * every time to map it to the URL + * + * @param bool $refreshCache will update the cache when set to true + */ + public function __construct($productionMode = false, $refreshCache = false) + { + parent::__construct(); + $this->startTime = time(); + Util::$restler = $this; + Scope::set('Restler', $this); + $this->productionMode = $productionMode; + if (is_null(Defaults::$cacheDirectory)) { + Defaults::$cacheDirectory = dirname($_SERVER['SCRIPT_FILENAME']) . + DIRECTORY_SEPARATOR . 'cache'; + } + $this->cache = new Defaults::$cacheClass(); + $this->refreshCache = $refreshCache; + // use this to rebuild cache every time in production mode + if ($productionMode && $refreshCache) { + $this->cached = false; + } + } + + /** + * Main function for processing the api request + * and return the response + * + * @throws Exception when the api service class is missing + * @throws RestException to send error response + */ + public function handle() + { + try { + try { + try { + $this->get(); + } catch (Exception $e) { + $this->requestData + = array(Defaults::$fullRequestDataName => array()); + if (!$e instanceof RestException) { + $e = new RestException( + 500, + $this->productionMode ? null : $e->getMessage(), + array(), + $e + ); + } + $this->route(); + throw $e; + } + if (Defaults::$useVendorMIMEVersioning) + $this->responseFormat = $this->negotiateResponseFormat(); + $this->route(); + } catch (Exception $e) { + $this->negotiate(); + if (!$e instanceof RestException) { + $e = new RestException( + 500, + $this->productionMode ? null : $e->getMessage(), + array(), + $e + ); + } + throw $e; + } + $this->negotiate(); + $this->preAuthFilter(); + $this->authenticate(); + $this->postAuthFilter(); + $this->validate(); + $this->preCall(); + $this->call(); + $this->compose(); + $this->postCall(); + $this->respond(); + } catch (Exception $e) { + try{ + $this->message($e); + } catch (Exception $e2) { + $this->message($e2); + } + } + } + + /** + * read the request details + * + * Find out the following + * - baseUrl + * - url requested + * - version requested (if url based versioning) + * - http verb/method + * - negotiate content type + * - request data + * - set defaults + */ + protected function get() + { + $this->dispatch('get'); + if (empty($this->formatMap)) { + $this->setSupportedFormats('JsonFormat'); + } + $this->url = $this->getPath(); + $this->requestMethod = Util::getRequestMethod(); + $this->requestFormat = $this->getRequestFormat(); + $this->requestData = $this->getRequestData(false); + + //parse defaults + foreach ($_GET as $key => $value) { + if (isset(Defaults::$aliases[$key])) { + $_GET[Defaults::$aliases[$key]] = $value; + unset($_GET[$key]); + $key = Defaults::$aliases[$key]; + } + if (in_array($key, Defaults::$overridables)) { + Defaults::setProperty($key, $value); + } + } + } + + /** + * Returns a list of the mime types (e.g. ["application/json","application/xml"]) that the API can respond with + * @return array + */ + public function getWritableMimeTypes() + { + return $this->writableMimeTypes; + } + + /** + * Returns the list of Mime Types for the request that the API can understand + * @return array + */ + public function getReadableMimeTypes() + { + return $this->readableMimeTypes; + } + + /** + * Call this method and pass all the formats that should be supported by + * the API Server. Accepts multiple parameters + * + * @param string ,... $formatName class name of the format class that + * implements iFormat + * + * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...); + * @throws Exception + */ + public function setSupportedFormats($format = null /*[, $format2...$farmatN]*/) + { + $args = func_get_args(); + $extensions = array(); + $throwException = $this->requestFormatDiffered; + $this->writableMimeTypes = $this->readableMimeTypes = array(); + foreach ($args as $className) { + + $obj = Scope::get($className); + + if (!$obj instanceof iFormat) + throw new Exception('Invalid format class; must implement ' . + 'iFormat interface'); + if ($throwException && get_class($obj) == get_class($this->requestFormat)) { + $throwException = false; + } + + foreach ($obj->getMIMEMap() as $mime => $extension) { + if($obj->isWritable()){ + $this->writableMimeTypes[]=$mime; + $extensions[".$extension"] = true; + } + if($obj->isReadable()) + $this->readableMimeTypes[]=$mime; + if (!isset($this->formatMap[$extension])) + $this->formatMap[$extension] = $className; + if (!isset($this->formatMap[$mime])) + $this->formatMap[$mime] = $className; + } + } + if ($throwException) { + throw new RestException( + 403, + 'Content type `' . $this->requestFormat->getMIME() . '` is not supported.' + ); + } + $this->formatMap['default'] = $args[0]; + $this->formatMap['extensions'] = array_keys($extensions); + } + + /** + * Call this method and pass all the formats that can be used to override + * the supported formats using `@format` comment. Accepts multiple parameters + * + * @param string ,... $formatName class name of the format class that + * implements iFormat + * + * @example $restler->setOverridingFormats('JsonFormat', 'XmlFormat'...); + * @throws Exception + */ + public function setOverridingFormats($format = null /*[, $format2...$farmatN]*/) + { + $args = func_get_args(); + $extensions = array(); + foreach ($args as $className) { + + $obj = Scope::get($className); + + if (!$obj instanceof iFormat) + throw new Exception('Invalid format class; must implement ' . + 'iFormat interface'); + + foreach ($obj->getMIMEMap() as $mime => $extension) { + if (!isset($this->formatOverridesMap[$extension])) + $this->formatOverridesMap[$extension] = $className; + if (!isset($this->formatOverridesMap[$mime])) + $this->formatOverridesMap[$mime] = $className; + if($obj->isWritable()) + $extensions[".$extension"] = true; + } + } + $this->formatOverridesMap['extensions'] = array_keys($extensions); + } + + /** + * Parses the request url and get the api path + * + * @return string api path + */ + protected function getPath() + { + // fix SCRIPT_NAME for PHP 5.4 built-in web server + if (false === strpos($_SERVER['SCRIPT_NAME'], '.php')) + $_SERVER['SCRIPT_NAME'] + = '/' . Util::removeCommonPath($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']); + + $fullPath = urldecode($_SERVER['REQUEST_URI']); + $path = Util::removeCommonPath( + $fullPath, + $_SERVER['SCRIPT_NAME'] + ); + $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80'; + $https = $port == '443' || + (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || // Amazon ELB + (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'); + + $baseUrl = ($https ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']; + + if (!$https && $port != '80' || $https && $port != '443') + $baseUrl .= ':' . $port; + + $this->baseUrl = rtrim($baseUrl + . substr($fullPath, 0, strlen($fullPath) - strlen($path)), '/'); + + $path = rtrim(strtok($path, '?'), '/'); //remove query string and trailing slash if found any + $path = str_replace( + array_merge( + $this->formatMap['extensions'], + $this->formatOverridesMap['extensions'] + ), + '', + $path + ); + if (Defaults::$useUrlBasedVersioning && strlen($path) && $path{0} == 'v') { + $version = intval(substr($path, 1)); + if ($version && $version <= $this->apiVersion) { + $this->requestedApiVersion = $version; + $path = explode('/', $path, 2); + $path = $path[1]; + } + } else { + $this->requestedApiVersion = $this->apiMinimumVersion; + } + return $path; + } + + /** + * Parses the request to figure out format of the request data + * + * @throws RestException + * @return iFormat any class that implements iFormat + * @example JsonFormat + */ + protected function getRequestFormat() + { + $format = null ; + // check if client has sent any information on request format + if ( + !empty($_SERVER['CONTENT_TYPE']) || + ( + !empty($_SERVER['HTTP_CONTENT_TYPE']) && + $_SERVER['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE'] + ) + ) { + $mime = $_SERVER['CONTENT_TYPE']; + if (false !== $pos = strpos($mime, ';')) { + $mime = substr($mime, 0, $pos); + } + if ($mime == UrlEncodedFormat::MIME) + $format = Scope::get('UrlEncodedFormat'); + elseif (isset($this->formatMap[$mime])) { + $format = Scope::get($this->formatMap[$mime]); + $format->setMIME($mime); + } elseif (!$this->requestFormatDiffered && isset($this->formatOverridesMap[$mime])) { + //if our api method is not using an @format comment + //to point to this $mime, we need to throw 403 as in below + //but since we don't know that yet, we need to defer that here + $format = Scope::get($this->formatOverridesMap[$mime]); + $format->setMIME($mime); + $this->requestFormatDiffered = true; + } else { + throw new RestException( + 403, + "Content type `$mime` is not supported." + ); + } + } + if(!$format){ + $format = Scope::get($this->formatMap['default']); + } + return $format; + } + + public function getRequestStream() + { + static $tempStream = false; + if (!$tempStream) { + $tempStream = fopen('php://temp', 'r+'); + $rawInput = fopen('php://input', 'r'); + stream_copy_to_stream($rawInput, $tempStream); + } + rewind($tempStream); + return $tempStream; + } + + /** + * Parses the request data and returns it + * + * @param bool $includeQueryParameters + * + * @return array php data + */ + public function getRequestData($includeQueryParameters = true) + { + $get = UrlEncodedFormat::decoderTypeFix($_GET); + if ($this->requestMethod == 'PUT' + || $this->requestMethod == 'PATCH' + || $this->requestMethod == 'POST' + ) { + if (!empty($this->requestData)) { + return $includeQueryParameters + ? $this->requestData + $get + : $this->requestData; + } + + $stream = $this->getRequestStream(); + if($stream === FALSE) + return array(); + $r = $this->requestFormat instanceof iDecodeStream + ? $this->requestFormat->decodeStream($stream) + : $this->requestFormat->decode(stream_get_contents($stream)); + + $r = is_array($r) + ? array_merge($r, array(Defaults::$fullRequestDataName => $r)) + : array(Defaults::$fullRequestDataName => $r); + return $includeQueryParameters + ? $r + $get + : $r; + } + return $includeQueryParameters ? $get : array(); //no body + } + + /** + * Find the api method to execute for the requested Url + */ + protected function route() + { + $this->dispatch('route'); + + $params = $this->getRequestData(); + + //backward compatibility for restler 2 and below + if (!Defaults::$smartParameterParsing) { + $params = $params + array(Defaults::$fullRequestDataName => $params); + } + + $this->apiMethodInfo = $o = Routes::find( + $this->url, $this->requestMethod, + $this->requestedApiVersion, $params + ); + //set defaults based on api method comments + if (isset($o->metadata)) { + foreach (Defaults::$fromComments as $key => $defaultsKey) { + if (array_key_exists($key, $o->metadata)) { + $value = $o->metadata[$key]; + Defaults::setProperty($defaultsKey, $value); + } + } + } + if (!isset($o->className)) + throw new RestException(404); + + if(isset($this->apiVersionMap[$o->className])){ + Scope::$classAliases[Util::getShortName($o->className)] + = $this->apiVersionMap[$o->className][$this->requestedApiVersion]; + } + + foreach ($this->authClasses as $auth) { + if (isset($this->apiVersionMap[$auth])) { + Scope::$classAliases[$auth] = $this->apiVersionMap[$auth][$this->requestedApiVersion]; + } elseif (isset($this->apiVersionMap[Scope::$classAliases[$auth]])) { + Scope::$classAliases[$auth] + = $this->apiVersionMap[Scope::$classAliases[$auth]][$this->requestedApiVersion]; + } + } + } + + /** + * Negotiate the response details such as + * - cross origin resource sharing + * - media type + * - charset + * - language + */ + protected function negotiate() + { + $this->dispatch('negotiate'); + $this->negotiateCORS(); + $this->responseFormat = $this->negotiateResponseFormat(); + $this->negotiateCharset(); + $this->negotiateLanguage(); + } + + protected function negotiateCORS() + { + if ( + $this->requestMethod == 'OPTIONS' + && Defaults::$crossOriginResourceSharing + ) { + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) + header('Access-Control-Allow-Methods: ' + . Defaults::$accessControlAllowMethods); + + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) + header('Access-Control-Allow-Headers: ' + . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']); + + header('Access-Control-Allow-Origin: ' . + (Defaults::$accessControlAllowOrigin == '*' ? $_SERVER['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin)); + header('Access-Control-Allow-Credentials: true'); + + exit(0); + } + } + + // ================================================================== + // + // Protected functions + // + // ------------------------------------------------------------------ + + /** + * Parses the request to figure out the best format for response. + * Extension, if present, overrides the Accept header + * + * @throws RestException + * @return iFormat + * @example JsonFormat + */ + protected function negotiateResponseFormat() + { + $metadata = Util::nestedValue($this, 'apiMethodInfo', 'metadata'); + //check if the api method insists on response format using @format comment + + if ($metadata && isset($metadata['format'])) { + $formats = explode(',', (string)$metadata['format']); + foreach ($formats as $i => $f) { + $f = trim($f); + if (!in_array($f, $this->formatOverridesMap)) + throw new RestException( + 500, + "Given @format is not present in overriding formats. Please call `\$r->setOverridingFormats('$f');` first." + ); + $formats[$i] = $f; + } + call_user_func_array(array($this, 'setSupportedFormats'), $formats); + } + + // check if client has specified an extension + /** @var $format iFormat*/ + $format = null; + $extensions = explode( + '.', + parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) + ); + while ($extensions) { + $extension = array_pop($extensions); + $extension = explode('/', $extension); + $extension = array_shift($extension); + if ($extension && isset($this->formatMap[$extension])) { + $format = Scope::get($this->formatMap[$extension]); + $format->setExtension($extension); + // echo "Extension $extension"; + return $format; + } + } + // check if client has sent list of accepted data formats + if (isset($_SERVER['HTTP_ACCEPT'])) { + $acceptList = Util::sortByPriority($_SERVER['HTTP_ACCEPT']); + foreach ($acceptList as $accept => $quality) { + if (isset($this->formatMap[$accept])) { + $format = Scope::get($this->formatMap[$accept]); + $format->setMIME($accept); + //echo "MIME $accept"; + // Tell cache content is based on Accept header + @header('Vary: Accept'); + + return $format; + } elseif (false !== ($index = strrpos($accept, '+'))) { + $mime = substr($accept, 0, $index); + if (is_string(Defaults::$apiVendor) + && 0 === stripos($mime, + 'application/vnd.' + . Defaults::$apiVendor . '-v') + ) { + $extension = substr($accept, $index + 1); + if (isset($this->formatMap[$extension])) { + //check the MIME and extract version + $version = intval(substr($mime, + 18 + strlen(Defaults::$apiVendor))); + if ($version > 0 && $version <= $this->apiVersion) { + $this->requestedApiVersion = $version; + $format = Scope::get($this->formatMap[$extension]); + $format->setExtension($extension); + // echo "Extension $extension"; + Defaults::$useVendorMIMEVersioning = true; + @header('Vary: Accept'); + + return $format; + } + } + } + + } + } + } else { + // RFC 2616: If no Accept header field is + // present, then it is assumed that the + // client accepts all media types. + $_SERVER['HTTP_ACCEPT'] = '*/*'; + } + if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) { + if (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/*')) { + $format = Scope::get('JsonFormat'); + } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], 'text/*')) { + $format = Scope::get('XmlFormat'); + } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], '*/*')) { + $format = Scope::get($this->formatMap['default']); + } + } + if (empty($format)) { + // RFC 2616: If an Accept header field is present, and if the + // server cannot send a response which is acceptable according to + // the combined Accept field value, then the server SHOULD send + // a 406 (not acceptable) response. + $format = Scope::get($this->formatMap['default']); + $this->responseFormat = $format; + throw new RestException( + 406, + 'Content negotiation failed. ' . + 'Try `' . $format->getMIME() . '` instead.' + ); + } else { + // Tell cache content is based at Accept header + @header("Vary: Accept"); + return $format; + } + } + + protected function negotiateCharset() + { + if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) { + $found = false; + $charList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_CHARSET']); + foreach ($charList as $charset => $quality) { + if (in_array($charset, Defaults::$supportedCharsets)) { + $found = true; + Defaults::$charset = $charset; + break; + } + } + if (!$found) { + if (strpos($_SERVER['HTTP_ACCEPT_CHARSET'], '*') !== false) { + //use default charset + } else { + throw new RestException( + 406, + 'Content negotiation failed. ' . + 'Requested charset is not supported' + ); + } + } + } + } + + protected function negotiateLanguage() + { + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $found = false; + $langList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_LANGUAGE']); + foreach ($langList as $lang => $quality) { + foreach (Defaults::$supportedLanguages as $supported) { + if (strcasecmp($supported, $lang) == 0) { + $found = true; + Defaults::$language = $supported; + break 2; + } + } + } + if (!$found) { + if (strpos($_SERVER['HTTP_ACCEPT_LANGUAGE'], '*') !== false) { + //use default language + } else { + //ignore + } + } + } + } + + /** + * Filer api calls before authentication + */ + protected function preAuthFilter() + { + if (empty($this->filterClasses)) { + return; + } + $this->dispatch('preAuthFilter'); + foreach ($this->filterClasses as $filterClass) { + /** + * @var iFilter + */ + $filterObj = Scope::get($filterClass); + + if (!$filterObj instanceof iFilter) { + throw new RestException ( + 500, 'Filter Class ' . + 'should implement iFilter'); + } else if (!($ok = $filterObj->__isAllowed())) { + if (is_null($ok) + && $filterObj instanceof iUseAuthentication + ) { + //handle at authentication stage + $this->postAuthFilterClasses[] = $filterClass; + continue; + } + throw new RestException(403); //Forbidden + } + } + } + + protected function authenticate() + { + $o = & $this->apiMethodInfo; + $accessLevel = max(Defaults::$apiAccessLevel, + $o->accessLevel); + try { + if ($accessLevel || count($this->postAuthFilterClasses)) { + $this->dispatch('authenticate'); + if (!count($this->authClasses)) { + throw new RestException( + 403, + 'at least one Authentication Class is required' + ); + } + foreach ($this->authClasses as $authClass) { + $authObj = Scope::get($authClass); + if (!method_exists($authObj, + Defaults::$authenticationMethod) + ) { + throw new RestException ( + 500, 'Authentication Class ' . + 'should implement iAuthenticate'); + } elseif ( + !$authObj->{Defaults::$authenticationMethod}() + ) { + throw new RestException(401); + } + } + $this->authenticated = true; + } + $this->authVerified = true; + } catch (RestException $e) { + $this->authVerified = true; + if ($accessLevel > 1) { //when it is not a hybrid api + throw ($e); + } else { + $this->authenticated = false; + } + } + } + + /** + * Filer api calls after authentication + */ + protected function postAuthFilter() + { + if(empty($this->postAuthFilterClasses)) { + return; + } + $this->dispatch('postAuthFilter'); + foreach ($this->postAuthFilterClasses as $filterClass) { + Scope::get($filterClass); + } + } + + protected function validate() + { + if (!Defaults::$autoValidationEnabled) { + return; + } + $this->dispatch('validate'); + + $o = & $this->apiMethodInfo; + foreach ($o->metadata['param'] as $index => $param) { + $info = & $param [CommentParser::$embeddedDataName]; + if (!isset ($info['validate']) + || $info['validate'] != false + ) { + if (isset($info['method'])) { + $info ['apiClassInstance'] = Scope::get($o->className); + } + //convert to instance of ValidationInfo + $info = new ValidationInfo($param); + $validator = Defaults::$validatorClass; + //if(!is_subclass_of($validator, 'Luracast\\Restler\\Data\\iValidate')) { + //changed the above test to below for addressing this php bug + //https://bugs.php.net/bug.php?id=53727 + if (function_exists("$validator::validate")) { + throw new \UnexpectedValueException( + '`Defaults::$validatorClass` must implement `iValidate` interface' + ); + } + $valid = $o->parameters[$index]; + $o->parameters[$index] = null; + if (empty(Validator::$exceptions)) + $o->metadata['param'][$index]['autofocus'] = true; + $valid = $validator::validate( + $valid, $info + ); + $o->parameters[$index] = $valid; + unset($o->metadata['param'][$index]['autofocus']); + } + } + } + + protected function call() + { + $this->dispatch('call'); + $o = & $this->apiMethodInfo; + $accessLevel = max(Defaults::$apiAccessLevel, + $o->accessLevel); + $object = Scope::get($o->className); + switch ($accessLevel) { + case 3 : //protected method + $reflectionMethod = new \ReflectionMethod( + $object, + $o->methodName + ); + $reflectionMethod->setAccessible(true); + $result = $reflectionMethod->invokeArgs( + $object, + $o->parameters + ); + break; + default : + $result = call_user_func_array(array( + $object, + $o->methodName + ), $o->parameters); + } + $this->responseData = $result; + } + + protected function compose() + { + $this->dispatch('compose'); + $this->composeHeaders(); + /** + * @var iCompose Default Composer + */ + $compose = Scope::get(Defaults::$composeClass); + $this->responseData = is_null($this->responseData) && + Defaults::$emptyBodyForNullResponse + ? '' + : $this->responseFormat->encode( + $compose->response($this->responseData), + !$this->productionMode + ); + } + + public function composeHeaders(RestException $e = null) + { + //only GET method should be cached if allowed by API developer + $expires = $this->requestMethod == 'GET' ? Defaults::$headerExpires : 0; + if(!is_array(Defaults::$headerCacheControl)) + Defaults::$headerCacheControl = array(Defaults::$headerCacheControl); + $cacheControl = Defaults::$headerCacheControl[0]; + if ($expires > 0) { + $cacheControl = $this->apiMethodInfo->accessLevel + ? 'private, ' : 'public, '; + $cacheControl .= end(Defaults::$headerCacheControl); + $cacheControl = str_replace('{expires}', $expires, $cacheControl); + $expires = gmdate('D, d M Y H:i:s \G\M\T', time() + $expires); + } + @header('Cache-Control: ' . $cacheControl); + @header('Expires: ' . $expires); + @header('X-Powered-By: Luracast Restler v' . Restler::VERSION); + + if (Defaults::$crossOriginResourceSharing + && isset($_SERVER['HTTP_ORIGIN']) + ) { + header('Access-Control-Allow-Origin: ' . + (Defaults::$accessControlAllowOrigin == '*' + ? $_SERVER['HTTP_ORIGIN'] + : Defaults::$accessControlAllowOrigin) + ); + header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Max-Age: 86400'); + } + + $this->responseFormat->setCharset(Defaults::$charset); + $charset = $this->responseFormat->getCharset() + ? : Defaults::$charset; + + @header('Content-Type: ' . ( + Defaults::$useVendorMIMEVersioning + ? 'application/vnd.' + . Defaults::$apiVendor + . "-v{$this->requestedApiVersion}" + . '+' . $this->responseFormat->getExtension() + : $this->responseFormat->getMIME()) + . '; charset=' . $charset + ); + + @header('Content-Language: ' . Defaults::$language); + + if (isset($this->apiMethodInfo->metadata['header'])) { + foreach ($this->apiMethodInfo->metadata['header'] as $header) + @header($header, true); + } + $code = 200; + if (!Defaults::$suppressResponseCode) { + if ($e) { + $code = $e->getCode(); + } elseif (isset($this->apiMethodInfo->metadata['status'])) { + $code = $this->apiMethodInfo->metadata['status']; + } + } + $this->responseCode = $code; + @header( + "{$_SERVER['SERVER_PROTOCOL']} $code " . + (isset(RestException::$codes[$code]) ? RestException::$codes[$code] : '') + ); + } + + protected function respond() + { + $this->dispatch('respond'); + //handle throttling + if (Defaults::$throttle) { + $elapsed = time() - $this->startTime; + if (Defaults::$throttle / 1e3 > $elapsed) { + usleep(1e6 * (Defaults::$throttle / 1e3 - $elapsed)); + } + } + if ($this->responseCode == 401) { + $authString = count($this->authClasses) + ? Scope::get($this->authClasses[0])->__getWWWAuthenticateString() + : 'Unknown'; + @header('WWW-Authenticate: ' . $authString, false); + } + echo $this->responseData; + $this->dispatch('complete'); + exit; + } + + protected function message(Exception $exception) + { + $this->dispatch('message'); + + if (!$exception instanceof RestException) { + $exception = new RestException( + 500, + $this->productionMode ? null : $exception->getMessage(), + array(), + $exception + ); + } + + $this->exception = $exception; + + $method = 'handle' . $exception->getCode(); + $handled = false; + foreach ($this->errorClasses as $className) { + if (method_exists($className, $method)) { + $obj = Scope::get($className); + if ($obj->$method()) + $handled = true; + } + } + if ($handled) { + return; + } + if (!isset($this->responseFormat)) { + $this->responseFormat = Scope::get('JsonFormat'); + } + $this->composeHeaders($exception); + /** + * @var iCompose Default Composer + */ + $compose = Scope::get(Defaults::$composeClass); + $this->responseData = $this->responseFormat->encode( + $compose->message($exception), + !$this->productionMode + ); + $this->respond(); + } + + /** + * Provides backward compatibility with older versions of Restler + * + * @param int $version restler version + * + * @throws \OutOfRangeException + */ + public function setCompatibilityMode($version = 2) + { + if ($version <= intval(self::VERSION) && $version > 0) { + require __DIR__."/compatibility/restler{$version}.php"; + return; + } + throw new \OutOfRangeException(); + } + + /** + * @param int $version maximum version number supported + * by the api + * @param int $minimum minimum version number supported + * (optional) + * + * @throws InvalidArgumentException + * @return void + */ + public function setAPIVersion($version = 1, $minimum = 1) + { + if (!is_int($version) && $version < 1) { + throw new InvalidArgumentException + ('version should be an integer greater than 0'); + } + $this->apiVersion = $version; + if (is_int($minimum)) { + $this->apiMinimumVersion = $minimum; + } + } + + /** + * Classes implementing iFilter interface can be added for filtering out + * the api consumers. + * + * It can be used for rate limiting based on usage from a specific ip + * address or filter by country, device etc. + * + * @param $className + */ + public function addFilterClass($className) + { + $this->filterClasses[] = $className; + } + + /** + * protected methods will need at least one authentication class to be set + * in order to allow that method to be executed + * + * @param string $className of the authentication class + * @param string $resourcePath optional url prefix for mapping + */ + public function addAuthenticationClass($className, $resourcePath = null) + { + $this->authClasses[] = $className; + $this->addAPIClass($className, $resourcePath); + } + + /** + * Add api classes through this method. + * + * All the public methods that do not start with _ (underscore) + * will be will be exposed as the public api by default. + * + * All the protected methods that do not start with _ (underscore) + * will exposed as protected api which will require authentication + * + * @param string $className name of the service class + * @param string $resourcePath optional url prefix for mapping, uses + * lowercase version of the class name when + * not specified + * + * @return null + * + * @throws Exception when supplied with invalid class name + */ + public function addAPIClass($className, $resourcePath = null) + { + try{ + if ($this->productionMode && is_null($this->cached)) { + $routes = $this->cache->get('routes'); + if (isset($routes) && is_array($routes)) { + $this->apiVersionMap = $routes['apiVersionMap']; + unset($routes['apiVersionMap']); + Routes::fromArray($routes); + $this->cached = true; + } else { + $this->cached = false; + } + } + if (isset(Scope::$classAliases[$className])) { + $className = Scope::$classAliases[$className]; + } + if (!$this->cached) { + $maxVersionMethod = '__getMaximumSupportedVersion'; + if (class_exists($className)) { + if (method_exists($className, $maxVersionMethod)) { + $max = $className::$maxVersionMethod(); + for ($i = 1; $i <= $max; $i++) { + $this->apiVersionMap[$className][$i] = $className; + } + } else { + $this->apiVersionMap[$className][1] = $className; + } + } + //versioned api + if (false !== ($index = strrpos($className, '\\'))) { + $name = substr($className, 0, $index) + . '\\v{$version}' . substr($className, $index); + } else if (false !== ($index = strrpos($className, '_'))) { + $name = substr($className, 0, $index) + . '_v{$version}' . substr($className, $index); + } else { + $name = 'v{$version}\\' . $className; + } + + for ($version = $this->apiMinimumVersion; + $version <= $this->apiVersion; + $version++) { + + $versionedClassName = str_replace('{$version}', $version, + $name); + if (class_exists($versionedClassName)) { + Routes::addAPIClass($versionedClassName, + Util::getResourcePath( + $className, + $resourcePath + ), + $version + ); + if (method_exists($versionedClassName, $maxVersionMethod)) { + $max = $versionedClassName::$maxVersionMethod(); + for ($i = $version; $i <= $max; $i++) { + $this->apiVersionMap[$className][$i] = $versionedClassName; + } + } else { + $this->apiVersionMap[$className][$version] = $versionedClassName; + } + } elseif (isset($this->apiVersionMap[$className][$version])) { + Routes::addAPIClass($this->apiVersionMap[$className][$version], + Util::getResourcePath( + $className, + $resourcePath + ), + $version + ); + } + } + + } + } catch (Exception $e) { + $e = new Exception( + "addAPIClass('$className') failed. ".$e->getMessage(), + $e->getCode(), + $e + ); + $this->setSupportedFormats('JsonFormat'); + $this->message($e); + } + } + + /** + * Add class for custom error handling + * + * @param string $className of the error handling class + */ + public function addErrorClass($className) + { + $this->errorClasses[] = $className; + } + + /** + * Associated array that maps formats to their respective format class name + * + * @return array + */ + public function getFormatMap() + { + return $this->formatMap; + } + + /** + * API version requested by the client + * @return int + */ + public function getRequestedApiVersion() + { + return $this->requestedApiVersion; + } + + /** + * When false, restler will run in debug mode and parse the class files + * every time to map it to the URL + * + * @return bool + */ + public function getProductionMode() + { + return $this->productionMode; + } + + /** + * Chosen API version + * + * @return int + */ + public function getApiVersion() + { + return $this->apiVersion; + } + + /** + * Base Url of the API Service + * + * @return string + * + * @example http://localhost/restler3 + * @example http://restler3.com + */ + public function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * List of events that fired already + * + * @return array + */ + public function getEvents() + { + return $this->events; + } + + /** + * Magic method to expose some protected variables + * + * @param string $name name of the hidden property + * + * @return null|mixed + */ + public function __get($name) + { + if ($name{0} == '_') { + $hiddenProperty = substr($name, 1); + if (isset($this->$hiddenProperty)) { + return $this->$hiddenProperty; + } + } + return null; + } + + /** + * 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) + ); + } + } + + /** + * pre call + * + * call _pre_{methodName)_{extension} if exists with the same parameters as + * the api method + * + * @example _pre_get_json + * + */ + protected function preCall() + { + $o = & $this->apiMethodInfo; + $preCall = '_pre_' . $o->methodName . '_' + . $this->requestFormat->getExtension(); + + if (method_exists($o->className, $preCall)) { + $this->dispatch('preCall'); + call_user_func_array(array( + Scope::get($o->className), + $preCall + ), $o->parameters); + } + } + + /** + * post call + * + * call _post_{methodName}_{extension} if exists with the composed and + * serialized (applying the repose format) response data + * + * @example _post_get_json + */ + protected function postCall() + { + $o = & $this->apiMethodInfo; + $postCall = '_post_' . $o->methodName . '_' . + $this->responseFormat->getExtension(); + if (method_exists($o->className, $postCall)) { + $this->dispatch('postCall'); + $this->responseData = call_user_func(array( + Scope::get($o->className), + $postCall + ), $this->responseData); + } + } +} diff --git a/htdocs/includes/restler/Routes.php b/htdocs/includes/restler/Routes.php new file mode 100644 index 00000000000..74a02eba935 --- /dev/null +++ b/htdocs/includes/restler/Routes.php @@ -0,0 +1,696 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Routes +{ + public static $prefixingParameterNames = array( + 'id' + ); + protected static $routes = array(); + + protected static $models = array(); + + /** + * 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); + 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'] = $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 (empty($m['label'])) + $m['label'] = static::label($m['name']); + if (is_null($type) && isset($m['type'])) { + $type = $m['type']; + } + if ($m['name'] == 'email' && empty($m[CommentParser::$embeddedDataName]['type']) && $type == 'string') + $m[CommentParser::$embeddedDataName]['type'] = 'email'; + $m ['default'] = $defaults [$position]; + $m ['required'] = !$param->isOptional(); + $contentType = Util::nestedValue( + $m, + CommentParser::$embeddedDataName, + 'type' + ); + if ($contentType && $qualified = Scope::resolve($contentType, $scope)) { + list($m[CommentParser::$embeddedDataName]['type'], $children) = static::getTypeAndModel( + new ReflectionClass($qualified), $scope + ); + } + if ($type instanceof ReflectionClass) { + list($type, $children) = static::getTypeAndModel($type, $scope); + } elseif ($type && is_string($type) && $qualified = Scope::resolve($type, $scope)) { + list($type, $children) + = static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } + if (isset($type)) { + $m['type'] = $type; + } + $m['children'] = $children; + + if ($m['name'] == Defaults::$fullRequestDataName) { + $from = 'body'; + if (!isset($m['type'])) { + $type = $m['type'] = 'array'; + } + + } elseif (isset($m[CommentParser::$embeddedDataName]['from'])) { + $from = $m[CommentParser::$embeddedDataName]['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'; + } + } + $m[CommentParser::$embeddedDataName]['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][CommentParser::$embeddedDataName]['from'] = 'path'; + } elseif ($httpMethod == 'GET' || $httpMethod == 'DELETE') { + $copy['metadata']['param'][$i][CommentParser::$embeddedDataName]['from'] = 'query'; + } elseif ($p[CommentParser::$embeddedDataName]['from'] == 'path') { + $copy['metadata']['param'][$i][CommentParser::$embeddedDataName]['from'] = 'body'; + } + } + $url = preg_replace_callback('/{[^}]+}|:[^\/]+/', + function ($matches) use ($call) { + $match = trim($matches[0], '{}:'); + $index = $call['arguments'][$match]; + return '{' . + Routes::typeChar(isset( + $call['metadata']['param'][$index]['type']) + ? $call['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][CommentParser::$embeddedDataName]['from']; + if ($from == 'body' && ($httpMethod == 'GET' || + $httpMethod == 'DELETE') + ) { + $call['metadata']['param'][$position][CommentParser::$embeddedDataName]['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); + } + } + } + } + } + + /** + * @access private + */ + public static function typeChar($type = null) + { + if (!$type) { + return 's'; + } + switch ($type{0}) { + case 'i': + case 'f': + return 'n'; + } + return 's'; + } + + protected static function addPath($path, array $call, + $httpMethod = 'GET', $version = 1) + { + $call['url'] = preg_replace_callback( + "/\{\S(\d+)\}/", + function ($matches) use ($call) { + return '{' . + $call['metadata']['param'][$matches[1]]['name'] . '}'; + }, + $path + ); + //check for wildcard routes + if (substr($path, -1, 1) == '*') { + $path = rtrim($path, '/*'); + static::$routes["v$version"]['*'][$path][$httpMethod] = $call; + } else { + static::$routes["v$version"][$path][$httpMethod] = $call; + //create an alias with index if the method name is index + if ($call['methodName'] == 'index') + static::$routes["v$version"][ltrim("$path/index", '/')][$httpMethod] = $call; + } + } + + /** + * Find the api method for the given url and http method + * + * @param string $path Requested url path + * @param string $httpMethod GET|POST|PUT|PATCH|DELETE etc + * @param int $version Api Version number + * @param array $data Data collected from the request + * + * @throws RestException + * @return ApiMethodInfo + */ + public static function find($path, $httpMethod, + $version = 1, array $data = array()) + { + $p = Util::nestedValue(static::$routes, "v$version"); + if (!$p) { + throw new RestException( + 404, + $version == 1 ? '' : "Version $version is not supported" + ); + } + $status = 404; + $message = null; + $methods = array(); + if (isset($p[$path][$httpMethod])) { + //================== static routes ========================== + return static::populate($p[$path][$httpMethod], $data); + } elseif (isset($p['*'])) { + //================== wildcard routes ======================== + uksort($p['*'], function ($a, $b) { + return strlen($b) - strlen($a); + }); + foreach ($p['*'] as $key => $value) { + if (strpos($path, $key) === 0 && isset($value[$httpMethod])) { + //path found, convert rest of the path to parameters + $path = substr($path, strlen($key) + 1); + $call = ApiMethodInfo::__set_state($value[$httpMethod]); + $call->parameters = empty($path) + ? array() + : explode('/', $path); + return $call; + } + } + } + //================== dynamic routes ============================= + //add newline char if trailing slash is found + if (substr($path, -1) == '/') + $path .= PHP_EOL; + //if double slash is found fill in newline char; + $path = str_replace('//', '/' . PHP_EOL . '/', $path); + ksort($p); + foreach ($p as $key => $value) { + if (!isset($value[$httpMethod])) { + continue; + } + $regex = str_replace(array('{', '}'), + array('(?P<', '>[^/]+)'), $key); + if (preg_match_all(":^$regex$:i", $path, $matches, PREG_SET_ORDER)) { + $matches = $matches[0]; + $found = true; + foreach ($matches as $k => $v) { + if (is_numeric($k)) { + unset($matches[$k]); + continue; + } + $index = intval(substr($k, 1)); + $details = $value[$httpMethod]['metadata']['param'][$index]; + if ($k{0} == 's' || strpos($k, static::pathVarTypeOf($v)) === 0) { + //remove the newlines + $data[$details['name']] = trim($v, PHP_EOL); + } else { + $status = 400; + $message = 'invalid value specified for `' + . $details['name'] . '`'; + $found = false; + break; + } + } + if ($found) { + return static::populate($value[$httpMethod], $data); + } + } + } + if ($status == 404) { + //check if other methods are allowed + if (isset($p[$path])) { + $status = 405; + $methods = array_keys($p[$path]); + } + } + if ($status == 405) { + header('Allow: ' . implode(', ', $methods)); + } + throw new RestException($status, $message); + } + + /** + * Populates the parameter values + * + * @param array $call + * @param $data + * + * @return ApiMethodInfo + * + * @access private + */ + protected static function populate(array $call, $data) + { + $call['parameters'] = $call['defaults']; + $p = & $call['parameters']; + foreach ($data as $key => $value) { + if (isset($call['arguments'][$key])) { + $p[$call['arguments'][$key]] = $value; + } + } + if (Defaults::$smartParameterParsing && 'post' != (string)Util::$restler->requestFormat) { + if ( + count($p) == 1 && + ($m = Util::nestedValue($call, 'metadata', 'param', 0)) && + !array_key_exists($m['name'], $data) && + array_key_exists(Defaults::$fullRequestDataName, $data) && + !is_null($d = $data[Defaults::$fullRequestDataName]) && + isset($m['type']) && + static::typeMatch($m['type'], $d) + ) { + $p[0] = $d; + } else { + $bodyParamCount = 0; + $lastBodyParamIndex = -1; + $lastM = null; + foreach ($call['metadata']['param'] as $k => $m) { + if ($m[CommentParser::$embeddedDataName]['from'] == 'body') { + $bodyParamCount++; + $lastBodyParamIndex = $k; + $lastM = $m; + } + } + if ( + $bodyParamCount == 1 && + !array_key_exists($lastM['name'], $data) && + array_key_exists(Defaults::$fullRequestDataName, $data) && + !is_null($d = $data[Defaults::$fullRequestDataName]) + ) { + $p[$lastBodyParamIndex] = $d; + } + } + } + $r = ApiMethodInfo::__set_state($call); + $modifier = "_modify_{$r->methodName}_api"; + if (method_exists($r->className, $modifier)) { + $stage = end(Scope::get('Restler')->getEvents()); + if (empty($stage)) + $stage = 'setup'; + $r = Scope::get($r->className)->$modifier($r, $stage) ? : $r; + } + return $r; + } + + /** + * @access private + */ + protected static function pathVarTypeOf($var) + { + if (is_numeric($var)) { + return 'n'; + } + if ($var === 'true' || $var === 'false') { + return 'b'; + } + return 's'; + } + + protected static function typeMatch($type, $var) + { + switch ($type) { + case 'boolean': + case 'bool': + return is_bool($var); + case 'array': + case 'object': + return is_array($var); + case 'string': + case 'int': + case 'integer': + case 'float': + case 'number': + return is_scalar($var); + } + return true; + } + + /** + * Get the type and associated model + * + * @param ReflectionClass $class + * @param array $scope + * + * @throws RestException + * @throws \Exception + * @return array + * + * @access protected + */ + protected static function getTypeAndModel(ReflectionClass $class, array $scope) + { + $className = $class->getName(); + if (isset(static::$models[$className])) { + return static::$models[$className]; + } + $children = array(); + try { + $props = $class->getProperties(ReflectionProperty::IS_PUBLIC); + foreach ($props as $prop) { + $name = $prop->getName(); + $child = array('name' => $name); + if ($c = $prop->getDocComment()) { + $child += Util::nestedValue(CommentParser::parse($c), 'var'); + } else { + $o = $class->newInstance(); + $p = $prop->getValue($o); + if (is_object($p)) { + $child['type'] = get_class($p); + } elseif (is_array($p)) { + $child['type'] = 'array'; + if (count($p)) { + $pc = reset($p); + if (is_object($pc)) { + $child['contentType'] = get_class($pc); + } + } + } + } + $child += array( + 'type' => $child['name'] == 'email' ? 'email' : 'string', + 'label' => static::label($child['name']) + ); + isset($child[CommentParser::$embeddedDataName]) + ? $child[CommentParser::$embeddedDataName] += array('required' => true) + : $child[CommentParser::$embeddedDataName]['required'] = true; + if ($qualified = Scope::resolve($child['type'], $scope)) { + list($child['type'], $child['children']) + = static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } elseif ( + ($contentType = Util::nestedValue($child, CommentParser::$embeddedDataName, 'type')) && + ($qualified = Scope::resolve($contentType, $scope)) + ) { + list($child['contentType'], $child['children']) + = static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } + $children[$name] = $child; + } + } catch (Exception $e) { + if (String::endsWith($e->getFile(), 'CommentParser.php')) { + throw new RestException(500, "Error while parsing comments of `$className` class. " . $e->getMessage()); + } + throw $e; + } + static::$models[$className] = array($className, $children); + return static::$models[$className]; + } + + /** + * Import previously created routes from cache + * + * @param array $routes + */ + public static function fromArray(array $routes) + { + static::$routes = $routes; + } + + /** + * Export current routes for cache + * + * @return array + */ + public static function toArray() + { + return static::$routes; + } + + public static function type($var) + { + if (is_object($var)) return get_class($var); + if (is_array($var)) return 'array'; + if (is_bool($var)) return 'boolean'; + if (is_numeric($var)) return is_float($var) ? 'float' : 'int'; + return 'string'; + } + + /** + * Create a label from name of the parameter or property + * + * Convert `camelCase` style names into proper `Title Case` names + * + * @param string $name + * + * @return string + */ + public static function label($name) + { + return ucfirst(preg_replace(array('/(?<=[^A-Z])([A-Z])/', '/(?<=[^0-9])([0-9])/'), ' $0', $name)); + } + + public static function scope(ReflectionClass $class) + { + $namespace = $class->getNamespaceName(); + $imports = array( + '*' => empty($namespace) ? '' : $namespace . '\\' + ); + $file = file_get_contents($class->getFileName()); + $tokens = token_get_all($file); + $namespace = ''; + $alias = ''; + $reading = false; + $last = 0; + foreach ($tokens as $token) { + if (is_string($token)) { + if ($reading && ',' == $token) { + //===== STOP =====// + $reading = false; + if (!empty($namespace)) + $imports[$alias] = trim($namespace, '\\'); + //===== START =====// + $reading = true; + $namespace = ''; + $alias = ''; + } else { + //===== STOP =====// + $reading = false; + if (!empty($namespace)) + $imports[$alias] = trim($namespace, '\\'); + } + } elseif (T_USE == $token[0]) { + //===== START =====// + $reading = true; + $namespace = ''; + $alias = ''; + } elseif ($reading) { + //echo token_name($token[0]) . ' ' . $token[1] . PHP_EOL; + switch ($token[0]) { + case T_WHITESPACE: + continue 2; + case T_STRING: + $alias = $token[1]; + if (T_AS == $last) { + break; + } + //don't break; + case T_NS_SEPARATOR: + $namespace .= $token[1]; + break; + } + $last = $token[0]; + } + } + return $imports; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Scope.php b/htdocs/includes/restler/Scope.php new file mode 100644 index 00000000000..a6b1baae57d --- /dev/null +++ b/htdocs/includes/restler/Scope.php @@ -0,0 +1,190 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Scope +{ + public static $classAliases = array( + + //Core + 'Restler' => 'Luracast\Restler\Restler', + + //Format classes + 'AmfFormat' => 'Luracast\Restler\Format\AmfFormat', + 'JsFormat' => 'Luracast\Restler\Format\JsFormat', + 'JsonFormat' => 'Luracast\Restler\Format\JsonFormat', + 'HtmlFormat' => 'Luracast\Restler\Format\HtmlFormat', + 'PlistFormat' => 'Luracast\Restler\Format\PlistFormat', + 'UploadFormat' => 'Luracast\Restler\Format\UploadFormat', + 'UrlEncodedFormat' => 'Luracast\Restler\Format\UrlEncodedFormat', + 'XmlFormat' => 'Luracast\Restler\Format\XmlFormat', + 'YamlFormat' => 'Luracast\Restler\Format\YamlFormat', + 'CsvFormat' => 'Luracast\Restler\Format\CsvFormat', + 'TsvFormat' => 'Luracast\Restler\Format\TsvFormat', + + //Filter classes + 'RateLimit' => 'Luracast\Restler\Filter\RateLimit', + + //UI classes + 'Forms' => 'Luracast\Restler\UI\Forms', + 'Nav' => 'Luracast\Restler\UI\Nav', + 'Emmet' => 'Luracast\Restler\UI\Emmet', + 'T' => 'Luracast\Restler\UI\Tags', + + //API classes + 'Resources' => 'Luracast\Restler\Resources', + + //Cache classes + 'HumanReadableCache' => 'Luracast\Restler\HumanReadableCache', + 'ApcCache' => 'Luracast\Restler\ApcCache', + + //Utility classes + 'Object' => 'Luracast\Restler\Data\Object', + 'String' => 'Luracast\Restler\Data\String', + 'Arr' => 'Luracast\Restler\Data\Arr', + + //Exception + 'RestException' => 'Luracast\Restler\RestException' + ); + public static $properties = array(); + protected static $instances = array(); + protected static $registry = array(); + + public static function register($name, Callable $function, $singleton = true) + { + static::$registry[$name] = (object)compact('function', 'singleton'); + } + + public static function set($name, $instance) + { + static::$instances[$name] = (object)array('instance' => $instance); + } + + public static function get($name) + { + $r = null; + $initialized = false; + $properties = array(); + if (array_key_exists($name, static::$instances)) { + $initialized = true; + $r = static::$instances[$name]->instance; + } elseif (!empty(static::$registry[$name])) { + $function = static::$registry[$name]->function; + $r = $function(); + if (static::$registry[$name]->singleton) + static::$instances[$name] = (object)array('instance' => $r); + } else { + $fullName = $name; + if (isset(static::$classAliases[$name])) { + $fullName = static::$classAliases[$name]; + } + if (class_exists($fullName)) { + $shortName = Util::getShortName($name); + $r = new $fullName(); + static::$instances[$name] = (object)array('instance' => $r); + if ($name != 'Restler') { + $r->restler = static::get('Restler'); + $m = Util::nestedValue($r->restler, 'apiMethodInfo', 'metadata'); + if ($m) { + $properties = Util::nestedValue( + $m, 'class', $fullName, + CommentParser::$embeddedDataName + ) ? : (Util::nestedValue( + $m, 'class', $shortName, + CommentParser::$embeddedDataName + ) ? : array()); + } else { + static::$instances[$name]->initPending = true; + } + } + } + } + if ( + $r instanceof iUseAuthentication && + static::get('Restler')->_authVerified && + !isset(static::$instances[$name]->authVerified) + ) { + static::$instances[$name]->authVerified = true; + $r->__setAuthenticationStatus + (static::get('Restler')->_authenticated); + } + if (isset(static::$instances[$name]->initPending)) { + $m = Util::nestedValue(static::get('Restler'), 'apiMethodInfo', 'metadata'); + $fullName = $name; + if (class_exists($name)) { + $shortName = Util::getShortName($name); + } else { + $shortName = $name; + if (isset(static::$classAliases[$name])) + $fullName = static::$classAliases[$name]; + } + if ($m) { + $properties = Util::nestedValue( + $m, 'class', $fullName, + CommentParser::$embeddedDataName + ) ? : (Util::nestedValue( + $m, 'class', $shortName, + CommentParser::$embeddedDataName + ) ? : array()); + unset(static::$instances[$name]->initPending); + $initialized = false; + } + } + if (!$initialized && is_object($r)) { + $properties += static::$properties; + $objectVars = get_object_vars($r); + $className = get_class($r); + foreach ($properties as $property => $value) { + if (property_exists($className, $property)) { + //if not a static property + array_key_exists($property, $objectVars) + ? $r->{$property} = $value + : $r::$$property = $value; + } + } + } + return $r; + } + + /** + * Get fully qualified class name for the given scope + * + * @param string $className + * @param array $scope local scope + * + * @return string|boolean returns the class name or false + */ + public static function resolve($className, array $scope) + { + if (empty($className) || !is_string($className)) + return false; + $divider = '\\'; + $qualified = false; + if ($className{0} == $divider) { + $qualified = trim($className, $divider); + } elseif (array_key_exists($className, $scope)) { + $qualified = $scope[$className]; + } else { + $qualified = $scope['*'] . $className; + } + if (class_exists($qualified)) + return $qualified; + if (isset(static::$classAliases[$className])) { + $qualified = static::$classAliases[$className]; + if (class_exists($qualified)) + return $qualified; + } + return false; + } +} diff --git a/htdocs/includes/restler/UI/Emmet.php b/htdocs/includes/restler/UI/Emmet.php new file mode 100644 index 00000000000..658cdea91bd --- /dev/null +++ b/htdocs/includes/restler/UI/Emmet.php @@ -0,0 +1,383 @@ ++^[=" ]{$@-#}'; + + /** + * Create the needed tag hierarchy from emmet string + * + * @param string $string + * + * @param array|string $data + * + * @return array|T + */ + public static function make($string, $data = null) + { + if (!strlen($string)) + return array(); + + $implicitTag = + function () use (& $tag) { + if (empty($tag->tag)) { + switch ($tag->parent->tag) { + case 'ul': + case 'ol': + $tag->tag = 'li'; + break; + case 'em': + $tag->tag = 'span'; + break; + case 'table': + case 'tbody': + case 'thead': + case 'tfoot': + $tag->tag = 'tr'; + break; + case 'tr': + $tag->tag = 'td'; + break; + case 'select': + case 'optgroup': + $tag->tag = 'option'; + break; + default: + $tag->tag = 'div'; + } + } + }; + + $parseText = + function ( + $text, $round, $total, $data, $delimiter = null + ) + use ( + & $tokens, & $tag + ) { + $digits = 0; + if ($delimiter == null) + $delimiter = array( + '.' => true, + '#' => true, + '*' => true, + '>' => true, + '+' => true, + '^' => true, + '[' => true, + ']' => true, + '=' => true, + ); + while (!empty($tokens) && + !isset($delimiter[$t = array_shift($tokens)])) { + while ('$' === $t) { + $digits++; + $t = array_shift($tokens); + } + if ($digits) { + $negative = false; + $offset = 0; + if ('@' == $t) { + if ('-' == ($t = array_shift($tokens))) { + $negative = true; + if (is_numeric(reset($tokens))) { + $offset = array_shift($tokens); + } + } elseif (is_numeric($t)) { + $offset = $t; + } else { + array_unshift($tokens, $t); + } + } elseif ('#' == ($h = array_shift($tokens))) { + if (!empty($t)) { + $data = Util::nestedValue($data, $t); + if (is_null($data)) { + return null; + } + } + if (is_numeric($data)) { + $text .= sprintf("%0{$digits}d", (int)$data); + } elseif (is_string($data)) { + $text .= $data; + } + $digits = 0; + continue; + } else { + array_unshift($tokens, $t, $h); + } + if ($negative) { + $n = $total + 1 - $round + $offset; + } else { + $n = $round + $offset; + } + $text .= sprintf("%0{$digits}d", $n); + $digits = 0; + } else { + $text .= $t; + } + } + if (isset($t)) + array_unshift($tokens, $t); + return $text; + }; + + $parseAttributes = + function (Callable $self, $round, $total, $data) + use (& $tokens, & $tag, $parseText) { + $a = $parseText( + '', $round, $total, $data + ); + if (is_null($a)) + return; + if ('=' == ($v = array_shift($tokens))) { + //value + if ('"' == ($v = array_shift($tokens))) { + $text = ''; + $tag->$a($parseText( + $text, $round, $total, $data, + array('"' => true) + )); + } else { + array_unshift($tokens, $v); + $text = ''; + $tag->$a($parseText( + $text, $round, $total, $data, + array(' ' => true, ']' => true) + )); + } + if (' ' == ($v = array_shift($tokens))) { + $self($self, $round, $total, $data); + } + } elseif (']' == $v) { + //end + $tag->$a(''); + return; + } elseif (' ' == $v) { + $tag->$a(''); + $self($self, $round, $total, $data); + } + }; + + $tokens = static::tokenize($string); + $tag = new T(array_shift($tokens)); + $parent = $root = new T; + + $parse = + function ( + Callable $self, $round = 1, $total = 1 + ) + use ( + & $tokens, & $parent, & $tag, & $data, + $parseAttributes, $implicitTag, $parseText + ) { + $offsetTokens = null; + $parent[] = $tag; + $isInChild = false; + while ($tokens) { + switch (array_shift($tokens)) { + //class + case '.': + $offsetTokens = array_values($tokens); + array_unshift($offsetTokens, '.'); + $implicitTag(); + $e = array_filter(explode(' ', $tag->class)); + $e[] = $parseText('', $round, $total, $data); + $tag->class(implode(' ', array_unique($e))); + break; + //id + case '#': + $offsetTokens = array_values($tokens); + array_unshift($offsetTokens, '#'); + $implicitTag(); + $tag->id( + $parseText( + array_shift($tokens), $round, $total, $data + ) + ); + break; + //attributes + case '[': + $offsetTokens = array_values($tokens); + array_unshift($offsetTokens, '['); + $implicitTag(); + $parseAttributes( + $parseAttributes, $round, $total, $data + ); + break; + //child + case '{': + $text = ''; + $tag[] = $parseText( + $text, $round, $total, $data, array('}' => true) + ); + break; + case '>': + $isInChild = true; + $offsetTokens = null; + if ('{' == ($t = array_shift($tokens))) { + array_unshift($tokens, $t); + $child = new T(); + $tag[] = $child; + $parent = $tag; + $tag = $child; + } elseif ('[' == $t) { + array_unshift($tokens, $t); + } else { + $child = new T($t); + $tag[] = $child; + $parent = $tag; + $tag = $child; + } + break; + //sibling + case '+': + $offsetTokens = null; + if (!$isInChild && $round != $total) { + $tokens = array(); + break; + } + if ('{' == ($t = array_shift($tokens))) { + $tag = $tag->parent; + array_unshift($tokens, $t); + break; + } elseif ('[' == $t) { + array_unshift($tokens, $t); + } else { + $child = new T($t); + $tag = $tag->parent; + $tag[] = $child; + $tag = $child; + } + break; + //sibling of parent + case '^': + if ($round != $total) { + $tokens = array(); + break; + } + $tag = $tag->parent; + if ($tag->parent) + $tag = $tag->parent; + while ('^' == ($t = array_shift($tokens))) { + if ($tag->parent) + $tag = $tag->parent; + } + $child = new T($t); + $tag[] = $child; + $tag = $child; + break; + //clone + case '*': + $times = array_shift($tokens); + $removeCount = 2; + $delimiter = array( + '.' => true, + '#' => true, + '*' => true, + '>' => true, + '+' => true, + '^' => true, + '[' => true, + ']' => true, + '=' => true, + ); + if (!is_numeric($times)) { + if (is_string($times)) { + if (!isset($delimiter[$times])) { + $data = Util::nestedValue($data, $times) + ? : $data; + } else { + array_unshift($tokens, $times); + $removeCount = 1; + } + } + $indexed = array_values($data); + $times = is_array($data) && $indexed == $data + ? count($data) : 0; + } + $source = $tag; + if (!empty($offsetTokens)) { + if (false !== strpos($source->class, ' ')) { + $class = explode(' ', $source->class); + array_pop($class); + $class = implode(' ', $class); + } else { + $class = null; + } + $tag->class($class); + $star = array_search('*', $offsetTokens); + array_splice($offsetTokens, $star, $removeCount); + $remainingTokens = $offsetTokens; + } else { + $remainingTokens = $tokens; + } + $source->parent = null; + $sourceData = $data; + $currentParent = $parent; + for ($i = 1; $i <= $times; $i++) { + $tag = clone $source; + $parent = $currentParent; + $data = is_array($sourceData) + && isset($sourceData[$i - 1]) + ? $sourceData[$i - 1] + : @(string)$sourceData; + $tokens = array_values($remainingTokens); + $self($self, $i, $times); + } + $round = 1; + $offsetTokens = null; + $tag = $source; + $tokens = array(); //$remainingTokens; + break; + } + } + }; + $parse($parse); + return count($root) == 1 ? $root[0] : $root; + } + + public static function tokenize($string) + { + $r = array(); + $f = strtok($string, static::DELIMITERS); + $pos = 0; + do { + $start = $pos; + $pos = strpos($string, $f, $start); + $tokens = array(); + for ($i = $start; $i < $pos; $i++) { + $token = $string{$i}; + if (('#' == $token || '.' == $token) && + (!empty($tokens) || $i == 0) + ) { + $r[] = ''; + } + $r[] = $tokens[] = $token; + } + $pos += strlen($f); + $r[] = $f; + } while (false != ($f = strtok(static::DELIMITERS))); + for ($i = $pos; $i < strlen($string); $i++) { + $token = $string{$i}; + $r[] = $tokens[] = $token; + } + return $r; + /* sample output produced by ".row*3>.col*3" + [0] => div + [1] => . + [2] => row + [3] => * + [4] => 3 + [5] => > + [6] => div + [7] => . + [8] => col + [9] => * + [10] => 4 + */ + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/FormStyles.php b/htdocs/includes/restler/UI/FormStyles.php new file mode 100644 index 00000000000..486cb972e4f --- /dev/null +++ b/htdocs/includes/restler/UI/FormStyles.php @@ -0,0 +1,59 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class FormStyles +{ + public static $html = array( + 'form' => 'form[role=form id=$id# name=$name# method=$method# action=$action# enctype=$enctype#]', + 'input' => '.row>section>label{$label#}^input[name=$name# value=$value# type=$type# required=$required# autofocus=$autofocus# placeholder=$default# accept=$accept#]', + 'textarea' => '.row>label{$label#}^textarea[name=$name# required=$required# autofocus=$autofocus# placeholder=$default# rows=3]{$value#}', + 'radio' => '.row>section>label{$label#}^span>label*options>input[name=$name# value=$value# type=radio checked=$selected# required=$required#]+{ $text#}', + 'select' => '.row>label{$label#}^select[name=$name# required=$required#]>option[value]+option[value=$value# selected=$selected#]{$text#}*options', + 'submit' => '.row>label{   }^button[type=submit]{$label#}', + 'fieldset' => 'fieldset>legend{$label#}', + 'checkbox' => '.row>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{$label#}', + //------------- TYPE BASED STYLES ---------------------// + 'checkbox-array' => 'fieldset>legend{$label#}+section*options>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{ $text#}', + 'select-array' => 'label{$label#}+select[name=$name# required=$required# multiple style="height: auto;background-image: none; outline: inherit;"]>option[value=$value# selected=$selected#]{$text#}*options', + ); + public static $bootstrap3 = array( + 'form' => 'form[role=form id=$id# name=$name# method=$method# action=$action# enctype=$enctype#]', + 'input' => '.form-group>label{$label#}+input.form-control[name=$name# value=$value# type=$type# required=$required# autofocus=$autofocus# placeholder=$default# accept=$accept#]', + 'textarea' => '.form-group>label{$label#}+textarea.form-control[name=$name# required=$required# autofocus=$autofocus# placeholder=$default# rows=3]{$value#}', + 'radio' => 'fieldset>legend{$label#}>.radio*options>label>input.radio[name=$name# value=$value# type=radio checked=$selected# required=$required#]{$text#}', + 'select' => '.form-group>label{$label#}+select.form-control[name=$name# multiple=$multiple# required=$required#]>option[value]+option[value=$value# selected=$selected#]{$text#}*options', + 'submit' => 'button.btn.btn-primary[type=submit]{$label#}', + 'fieldset' => 'fieldset>legend{$label#}', + 'checkbox' => '.checkbox>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{$label#}', + //------------- TYPE BASED STYLES ---------------------// + 'checkbox-array' => 'fieldset>legend{$label#}>.checkbox*options>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required#]{$text#}', + 'select-array' => '.form-group>label{$label#}+select.form-control[name=$name# multiple=$multiple# required=$required#] size=$options#>option[value=$value# selected=$selected#]{$text#}*options', + //------------- CUSTOM STYLES ---------------------// + 'radio-inline' => '.form-group>label{$label# :  }+label.radio-inline*options>input.radio[name=$name# value=$value# type=radio checked=$selected# required=$required#]+{$text#}', + ); + public static $foundation5 = array( + 'form' => 'form[id=$id# name=$name# method=$method# action=$action# enctype=$enctype#]', + 'input' => 'label{$label#}+input[name=$name# value=$value# type=$type# required=$required# autofocus=$autofocus# placeholder=$default# accept=$accept#]', + 'textarea' => 'label{$label#}+textarea[name=$name# required=$required# autofocus=$autofocus# placeholder=$default# rows=3]{$value#}', + 'radio' => 'label{$label# :  }+label.radio-inline*options>input.radio[name=$name# value=$value# type=radio checked=$selected# required=$required#]+{$text#}', + 'select' => 'label{$label#}+select[name=$name# required=$required#]>option[value]+option[value=$value# selected=$selected#]{$text#}*options', + 'submit' => 'button.button[type=submit]{$label#}', + 'fieldset' => 'fieldset>legend{$label#}', + 'checkbox' => 'label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{ $label#}', + //------------- TYPE BASED STYLES ---------------------// + 'checkbox-array' => 'fieldset>legend{$label#}+label*options>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{ $text#}', + 'select-array' => 'label{$label#}+select[name=$name# required=$required# multiple style="height: auto;background-image: none; outline: inherit;"]>option[value=$value# selected=$selected#]{$text#}*options', + //------------- CUSTOM STYLES ---------------------// + ); +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/Forms.php b/htdocs/includes/restler/UI/Forms.php new file mode 100644 index 00000000000..289f40b6f0f --- /dev/null +++ b/htdocs/includes/restler/UI/Forms.php @@ -0,0 +1,434 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Forms implements iFilter +{ + const FORM_KEY = 'form_key'; + public static $filterFormRequestsOnly = false; + + public static $excludedPaths = array(); + + public static $style; + /** + * @var bool should we fill up the form using given data? + */ + public static $preFill = true; + /** + * @var ValidationInfo + */ + public static $validationInfo = null; + protected static $inputTypes = array( + 'hidden', + 'password', + 'button', + 'image', + 'file', + 'reset', + 'submit', + 'search', + 'checkbox', + 'radio', + 'email', + 'text', + 'color', + 'date', + 'datetime', + 'datetime-local', + 'email', + 'month', + 'number', + 'range', + 'search', + 'tel', + 'time', + 'url', + 'week', + ); + protected static $fileUpload = false; + private static $key = array(); + /** + * @var ApiMethodInfo; + */ + private static $info; + + /** + * 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; + } + + public static function style($name, array $metadata, $type = '') + { + return isset($metadata[CommentParser::$embeddedDataName][$name]) + ? $metadata[CommentParser::$embeddedDataName][$name] + : (!empty($type) && isset(static::$style["$name-$type"]) + ? static::$style["$name-$type"] + : (isset(static::$style[$name]) + ? static::$style[$name] + : null + ) + ); + } + + public static function fields($dataOnly = false) + { + $m = static::$info->metadata; + $params = $m['param']; + $values = static::$info->parameters; + $r = array(); + foreach ($params as $k => $p) { + $value = Util::nestedValue($values, $k); + if ( + is_scalar($value) || + ($p['type'] == 'array' && is_array($value) && $value == array_values($value)) || + is_object($value) && $p['type'] == get_class($value) + ) + $p['value'] = $value; + static::$validationInfo = $v = new ValidationInfo($p); + if ($v->from == 'path') + continue; + if (!empty($v->children)) { + $t = Emmet::make(static::style('fieldset', $m), array('label' => $v->label)); + foreach ($v->children as $n => $c) { + $value = Util::nestedValue($v->value, $n); + if ( + is_scalar($value) || + ($c['type'] == 'array' && is_array($value) && $value == array_values($value)) || + is_object($value) && $c['type'] == get_class($value) + ) + $c['value'] = $value; + static::$validationInfo = $vc = new ValidationInfo($c); + if ($vc->from == 'path') + continue; + $vc->name = $v->name . '[' . $vc->name . ']'; + $t [] = static::field($vc, $dataOnly); + } + $r[] = $t; + static::$validationInfo = null; + } else { + $f = static::field($v, $dataOnly); + $r [] = $f; + } + static::$validationInfo = null; + } + return $r; + } + + /** + * @param ValidationInfo $p + * + * @param bool $dataOnly + * + * @return array|T + */ + public static function field(ValidationInfo $p, $dataOnly = false) + { + if (is_string($p->value)) { + //prevent XSS attacks + $p->value = htmlspecialchars($p->value, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + } + $type = $p->field ? : static::guessFieldType($p); + $tag = in_array($type, static::$inputTypes) + ? 'input' : $type; + $options = array(); + $name = $p->name; + $multiple = null; + if ($p->type == 'array' && $p->contentType != 'associative') { + $name .= '[]'; + $multiple = true; + } + if ($p->choice) { + foreach ($p->choice as $i => $choice) { + $option = array('name' => $name, 'value' => $choice); + $option['text'] = isset($p->rules['select'][$i]) + ? $p->rules['select'][$i] + : $choice; + if ($choice == $p->value) + $option['selected'] = true; + $options[] = $option; + } + } elseif ($p->type == 'boolean' || $p->type == 'bool') { + if (String::beginsWith($type, 'radio')) { + $options[] = array('name' => $p->name, 'text' => ' Yes ', + 'value' => 'true'); + $options[] = array('name' => $p->name, 'text' => ' No ', + 'value' => 'false'); + if ($p->value || $p->default) + $options[0]['selected'] = true; + } else { + $r = array( + 'tag' => $tag, + 'name' => $name, + 'type' => $type, + 'label' => $p->label, + 'value' => 'true', + 'default' => $p->default, + ); + $r['text'] = 'Yes'; + if ($p->default) { + $r['selected'] = true; + } + } + } + if (empty($r)) { + $r = array( + 'tag' => $tag, + 'name' => $name, + 'type' => $type, + 'label' => $p->label, + 'value' => $p->value, + 'default' => $p->default, + 'options' => & $options, + 'multiple' => $multiple, + ); + } + if ($type == 'file') { + static::$fileUpload = true; + $r['accept'] = implode(', ', UploadFormat::$allowedMimeTypes); + } + + if (true === $p->required) + $r['required'] = true; + if (isset($p->rules['autofocus'])) + $r['autofocus'] = true; + /* + echo "
";
+        print_r($r);
+        echo "
"; + */ + if ($dataOnly) + return $r; + if (isset($p->rules['form'])) + return Emmet::make($p->rules['form'], $r); + $m = static::$info->metadata; + $t = Emmet::make(static::style($type, $m, $p->type) ? : static::style($tag, $m, $p->type), $r); + return $t; + } + + protected static function guessFieldType(ValidationInfo $p, $type = 'type') + { + if (in_array($p->$type, static::$inputTypes)) + return $p->$type; + if ($p->choice) + return $p->type == 'array' ? 'checkbox' : 'select'; + switch ($p->$type) { + case 'boolean': + return 'radio'; + case 'int': + case 'number': + case 'float': + return 'number'; + case 'array': + return static::guessFieldType($p, 'contentType'); + } + if ($p->name == 'password') + return 'password'; + return 'text'; + } + + /** + * Get the form key + * + * @param string $method http method for form key + * @param string $action relative path from the web root. When set to null + * it uses the current api method's path + * + * @return string generated form key + */ + public static function key($method = 'POST', $action = null) + { + if (is_null($action)) + $action = Scope::get('Restler')->url; + $target = "$method $action"; + if (empty(static::$key[$target])) + static::$key[$target] = md5($target . User::getIpAddress() . uniqid(mt_rand())); + $_SESSION[static::FORM_KEY] = static::$key; + return static::$key[$target]; + } + + /** + * Access verification method. + * + * API access will be denied when this method returns false + * + * @return boolean true when api access is allowed false otherwise + * + * @throws RestException 403 security violation + */ + public function __isAllowed() + { + if (session_id() == '') { + session_start(); + } + /** @var Restler $restler */ + $restler = $this->restler; + $url = $restler->url; + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if ($url == $exclude) + return true; + } elseif (String::beginsWith($url, $exclude)) { + return true; + } + } + $check = static::$filterFormRequestsOnly + ? $restler->requestFormat instanceof UrlEncodedFormat || $restler->requestFormat instanceof UploadFormat + : true; + if (!empty($_POST) && $check) { + if ( + isset($_POST[static::FORM_KEY]) && + ($target = Util::getRequestMethod() . ' ' . $restler->url) && + isset($_SESSION[static::FORM_KEY][$target]) && + $_POST[static::FORM_KEY] == $_SESSION[static::FORM_KEY][$target] + ) { + return true; + } + throw new RestException(403, 'Insecure form submission'); + } + return true; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/Nav.php b/htdocs/includes/restler/UI/Nav.php new file mode 100644 index 00000000000..7245dbddf8d --- /dev/null +++ b/htdocs/includes/restler/UI/Nav.php @@ -0,0 +1,208 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Nav +{ + public static $root = 'home'; + /** + * @var null|callable if the api methods are under access control mechanism + * you can attach a function here that returns true or false to determine + * visibility of a protected api method. this function will receive method + * info as the only parameter. + */ + public static $accessControlFunction = null; + /** + * @var array all paths beginning with any of the following will be excluded + * from documentation. if an empty string is given it will exclude the root + */ + public static $excludedPaths = array(''); + /** + * @var array prefix additional menu items with one of the following syntax + * [$path => $text] + * [$path] + * [$path => ['text' => $text, 'url' => $url]] + */ + public static $prepends = array(); + /** + * @var array suffix additional menu items with one of the following syntax + * [$path => $text] + * [$path] + * [$path => ['text' => $text, 'url' => $url]] + */ + public static $appends = array(); + + public static $addExtension = true; + + protected static $extension = ''; + + 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; + } + + protected static function build(&$tree, $path, + $url = null, $text = null, $activeUrl = null) + { + $parts = explode('/', $path); + if (count($parts) == 1 && empty($parts[0])) + $parts = array(static::$root); + $p = & $tree; + $end = end($parts); + foreach ($parts as $part) { + if (!isset($p[$part])) { + $p[$part] = array( + 'href' => '#', + 'text' => static::title($part) + ); + if ($part == $end) { + $p[$part]['class'] = $part; + if ($text) + $p[$part]['text'] = $text; + if (is_null($url)) { + if (empty($path) && !empty(static::$extension)) + $path = 'index'; + $p[$part]['href'] = Util::$restler->getBaseUrl() + . '/' . $path . static::$extension; + } else { + if (empty($url) && !empty(static::$extension)) + $url = 'index'; + $p[$part]['href'] = $url . static::$extension; + } + if ($path == $activeUrl) { + $p[$part]['active'] = true; + } + } + $p[$part]['children'] = array(); + + } + $p = & $p[$part]['children']; + } + + } + + protected static function title($name) + { + if (empty($name)) { + $name = static::$root; + } else { + $name = ltrim($name, '#'); + } + return ucfirst(preg_replace(array('/(?<=[^A-Z])([A-Z])/', '/(?<=[^0-9])([0-9])/'), ' $0', $name)); + } + +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/Tags.php b/htdocs/includes/restler/UI/Tags.php new file mode 100644 index 00000000000..b6302a28236 --- /dev/null +++ b/htdocs/includes/restler/UI/Tags.php @@ -0,0 +1,282 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + * + * ============================ magic properties ============================== + * @property Tags parent parent tag + * ============================== magic methods =============================== + * @method Tags name(string $value) name attribute + * @method Tags action(string $value) action attribute + * @method Tags placeholder(string $value) placeholder attribute + * @method Tags value(string $value) value attribute + * @method Tags required(boolean $value) required attribute + * @method Tags class(string $value) required attribute + * + * =========================== static magic methods ============================ + * @method static Tags form() creates a html form + * @method static Tags input() creates a html input element + * @method static Tags button() creates a html button element + * + */ +class Tags implements ArrayAccess, Countable +{ + public static $humanReadable = true; + public static $initializer = null; + protected static $instances = array(); + public $prefix = ''; + public $indent = ' '; + public $tag; + protected $attributes = array(); + protected $children = array(); + protected $_parent; + + public function __construct($name = null, array $children = array()) + { + $this->tag = $name; + $c = array(); + foreach ($children as $child) { + is_array($child) + ? $c = array_merge($c, $child) + : $c [] = $child; + } + $this->markAsChildren($c); + $this->children = $c; + if (static::$initializer) + call_user_func_array(static::$initializer, array(& $this)); + } + + /** + * Get Tag by id + * + * Retrieve a tag by its id attribute + * + * @param string $id + * + * @return Tags|null + */ + public static function byId($id) + { + return Util::nestedValue(static::$instances, $id); + } + + /** + * @param $name + * @param array $children + * + * @return Tags + */ + public static function __callStatic($name, array $children) + { + return new static($name, $children); + } + + public function toString($prefix = '', $indent = ' ') + { + $this->prefix = $prefix; + $this->indent = $indent; + return $this->__toString(); + } + + public function __toString() + { + $children = ''; + if (static::$humanReadable) { + $lineBreak = false; + foreach ($this->children as $key => $child) { + $prefix = $this->prefix; + if (!is_null($this->tag)) + $prefix .= $this->indent; + if ($child instanceof $this) { + $child->prefix = $prefix; + $child->indent = $this->indent; + $children .= PHP_EOL . $child; + $lineBreak = true; + } else { + $children .= $child; + } + } + if ($lineBreak) + $children .= PHP_EOL . $this->prefix; + } else { + $children = implode('', $this->children); + } + if (is_null($this->tag)) + return $children; + $attributes = ''; + foreach ($this->attributes as $attribute => &$value) + $attributes .= " $attribute=\"$value\""; + + if (count($this->children)) + return static::$humanReadable + ? "$this->prefix<{$this->tag}{$attributes}>" + . "$children" + . "tag}>" + : "<{$this->tag}{$attributes}>$childrentag}>"; + + return "$this->prefix<{$this->tag}{$attributes}/>"; + } + + public function toArray() + { + $r = array(); + $r['attributes'] = $this->attributes; + $r['tag'] = $this->tag; + $children = array(); + foreach ($this->children as $key => $child) { + $children[$key] = $child instanceof $this + ? $child->toArray() + : $child; + } + $r['children'] = $children; + return $r; + } + + /** + * Set the id attribute of the current tag + * + * @param string $value + * + * @return string + */ + public function id($value) + { + $this->attributes['id'] = isset($value) + ? (string)$value + : Util::nestedValue($this->attributes, 'name'); + static::$instances[$value] = $this; + return $this; + } + + public function __get($name) + { + if ('parent' == $name) + return $this->_parent; + if (isset($this->attributes[$name])) + return $this->attributes[$name]; + return; + } + + public function __set($name, $value) + { + if ('parent' == $name) { + if ($this->_parent) { + unset($this->_parent[array_search($this, $this->_parent->children)]); + } + if (!empty($value)) { + $value[] = $this; + } + } + } + + public function __isset($name) + { + return isset($this->attributes[$name]); + } + + /** + * @param $attribute + * @param $value + * + * @return Tags + */ + public function __call($attribute, $value) + { + if (is_null($value)) { + return isset($this->attributes[$attribute]) + ? $this->attributes[$attribute] + : null; + } + $value = $value[0]; + if (is_null($value)) { + unset($this->attributes[$attribute]); + return $this; + } + $this->attributes[$attribute] = is_bool($value) + ? ($value ? 'true' : 'false') + : @(string)$value; + return $this; + } + + public function offsetGet($index) + { + if ($this->offsetExists($index)) { + return $this->children[$index]; + } + return false; + } + + public function offsetExists($index) + { + return isset($this->children[$index]); + } + + public function offsetSet($index, $value) + { + if ($index) { + $this->children[$index] = $value; + } elseif (is_array($value)) { + $c = array(); + foreach ($value as $child) { + is_array($child) + ? $c = array_merge($c, $child) + : $c [] = $child; + } + $this->markAsChildren($c); + $this->children += $c; + } else { + $c = array($value); + $this->markAsChildren($c); + $this->children[] = $value; + } + return true; + } + + public function offsetUnset($index) + { + $this->children[$index]->_parent = null; + unset($this->children[$index]); + return true; + } + + public function getContents() + { + return $this->children; + } + + public function count() + { + return count($this->children); + } + + private function markAsChildren(& $children) + { + foreach ($children as $i => $child) { + if (is_string($child)) + continue; + if (!is_object($child)) { + unset($children[$i]); + continue; + } + //echo $child; + if (isset($child->_parent) && $child->_parent != $this) { + //remove from current parent + unset($child->_parent[array_search($child, $child->_parent->children)]); + } + $child->_parent = $this; + } + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/User.php b/htdocs/includes/restler/User.php new file mode 100644 index 00000000000..ea7ff79d603 --- /dev/null +++ b/htdocs/includes/restler/User.php @@ -0,0 +1,100 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class User implements iIdentifyUser +{ + private static $initialized = false; + public static $id = null; + public static $cacheId = null; + public static $ip; + public static $browser = ''; + public static $platform = ''; + + public static function init() + { + static::$initialized = true; + static::$ip = static::getIpAddress(); + } + + public static function getUniqueIdentifier($includePlatform = false) + { + if (!static::$initialized) static::init(); + return static::$id ? : base64_encode('ip:' . ($includePlatform + ? static::$ip . '-' . static::$platform + : static::$ip + )); + } + + public static function getIpAddress($ignoreProxies = false) + { + foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', + 'REMOTE_ADDR') as $key) { + if (array_key_exists($key, $_SERVER) === true) { + foreach (explode(',', $_SERVER[$key]) as $ip) { + $ip = trim($ip); // just to be safe + + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 + | FILTER_FLAG_NO_PRIV_RANGE + | FILTER_FLAG_NO_RES_RANGE) !== false + ) { + return $ip; + } + } + } + } + } + + /** + * Authentication classes should call this method + * + * @param string $id user id as identified by the authentication classes + * + * @return void + */ + public static function setUniqueIdentifier($id) + { + static::$id = $id; + } + + /** + * User identity to be used for caching purpose + * + * When the dynamic cache service places an object in the cache, it needs to + * label it with a unique identifying string known as a cache ID. This + * method gives that identifier + * + * @return string + */ + public static function getCacheIdentifier() + { + return static::$cacheId ?: static::$id; + } + + /** + * User identity for caching purpose + * + * In a role based access control system this will be based on role + * + * @param $id + * + * @return void + */ + public static function setCacheIdentifier($id) + { + static::$cacheId = $id; + } +} diff --git a/htdocs/includes/restler/Util.php b/htdocs/includes/restler/Util.php new file mode 100644 index 00000000000..0a1fc8a40ea --- /dev/null +++ b/htdocs/includes/restler/Util.php @@ -0,0 +1,201 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Util +{ + /** + * @var Restler instance injected at runtime + */ + public static $restler; + + /** + * verify if the given data type string is scalar or not + * + * @static + * + * @param string $type data type as string + * + * @return bool true or false + */ + public static function isObjectOrArray($type) + { + if (is_array($type)) { + foreach ($type as $t) { + if (static::isObjectOrArray($t)) { + return true; + } + } + return false; + } + return !(boolean)strpos('|bool|boolean|int|float|string|', $type); + } + + /** + * Get the value deeply nested inside an array / object + * + * Using isset() to test the presence of nested value can give a false positive + * + * This method serves that need + * + * When the deeply nested property is found its value is returned, otherwise + * false is returned. + * + * @param array $from array to extract the value from + * @param string|array $key ... pass more to go deeply inside the array + * alternatively you can pass a single array + * + * @return null|mixed null when not found, value otherwise + */ + public static function nestedValue($from, $key/**, $key2 ... $key`n` */) + { + if (is_array($key)) { + $keys = $key; + } else { + $keys = func_get_args(); + array_shift($keys); + } + foreach ($keys as $key) { + if (is_array($from) && isset($from[$key])) { + $from = $from[$key]; + continue; + } elseif (is_object($from) && isset($from->{$key})) { + $from = $from->{$key}; + continue; + } + return null; + } + return $from; + } + + public static function getResourcePath($className, + $resourcePath = null, + $prefix = '') + { + if (is_null($resourcePath)) { + if (Defaults::$autoRoutingEnabled) { + $resourcePath = strtolower($className); + if (false !== ($index = strrpos($className, '\\'))) + $resourcePath = substr($resourcePath, $index + 1); + if (false !== ($index = strrpos($resourcePath, '_'))) + $resourcePath = substr($resourcePath, $index + 1); + } else { + $resourcePath = ''; + } + } else + $resourcePath = trim($resourcePath, '/'); + if (strlen($resourcePath) > 0) + $resourcePath .= '/'; + return $prefix . $resourcePath; + } + + /** + * Compare two strings and remove the common + * sub string from the first string and return it + * + * @static + * + * @param string $fromPath + * @param string $usingPath + * @param string $char + * optional, set it as + * blank string for char by char comparison + * + * @return string + */ + public static function removeCommonPath($fromPath, $usingPath, $char = '/') + { + if (empty($fromPath)) + return ''; + $fromPath = explode($char, $fromPath); + $usingPath = explode($char, $usingPath); + while (count($usingPath)) { + if (count($fromPath) && $fromPath[0] == $usingPath[0]) { + array_shift($fromPath); + } else { + break; + } + array_shift($usingPath); + } + return implode($char, $fromPath); + } + + /** + * Parses the request to figure out the http request type + * + * @static + * + * @return string which will be one of the following + * [GET, POST, PUT, PATCH, DELETE] + * @example GET + */ + public static function getRequestMethod() + { + $method = $_SERVER['REQUEST_METHOD']; + if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } elseif ( + !empty(Defaults::$httpMethodOverrideProperty) + && isset($_REQUEST[Defaults::$httpMethodOverrideProperty]) + ) { + // support for exceptional clients who can't set the header + $m = strtoupper($_REQUEST[Defaults::$httpMethodOverrideProperty]); + if ($m == 'PUT' || $m == 'DELETE' || + $m == 'POST' || $m == 'PATCH' + ) { + $method = $m; + } + } + // support for HEAD request + if ($method == 'HEAD') { + $method = 'GET'; + } + return $method; + } + + /** + * Pass any content negotiation header such as Accept, + * Accept-Language to break it up and sort the resulting array by + * the order of negotiation. + * + * @static + * + * @param string $accept header value + * + * @return array sorted by the priority + */ + public static function sortByPriority($accept) + { + $acceptList = array(); + $accepts = explode(',', strtolower($accept)); + if (!is_array($accepts)) { + $accepts = array($accepts); + } + foreach ($accepts as $pos => $accept) { + $parts = explode(';q=', trim($accept)); + $type = array_shift($parts); + $quality = count($parts) ? + floatval(array_shift($parts)) : + (1000 - $pos) / 1000; + $acceptList[$type] = $quality; + } + arsort($acceptList); + return $acceptList; + } + + public static function getShortName($className) + { + $className = explode('\\', $className); + return end($className); + } +} + diff --git a/htdocs/includes/restler/compatibility/iAuthenticate.php b/htdocs/includes/restler/compatibility/iAuthenticate.php new file mode 100644 index 00000000000..0463df948f1 --- /dev/null +++ b/htdocs/includes/restler/compatibility/iAuthenticate.php @@ -0,0 +1,10 @@ +isFile() + && 'php' === $fileInfo->getExtension() + && ctype_lower($fileInfo->getBasename('.php')) + && preg_match( + '/^ *(class|interface|abstract +class)' + . ' +([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/m', + file_get_contents($fileInfo->getPathname()), + $matches + ) + ) + $classMap[$matches[2]] = $fileInfo->getPathname(); + +AutoLoader::seen($classMap); + +//changes in iAuthenticate +Defaults::$authenticationMethod = '__isAuthenticated'; + +include __DIR__ . '/iAuthenticate.php'; + +//changes in auto routing +Defaults::$smartAutoRouting = false; +Defaults::$smartParameterParsing = false; +Defaults::$autoValidationEnabled = false; + +//changes in parsing embedded data in comments +CommentParser::$embeddedDataPattern = '/\((\S+)\)/ms'; +CommentParser::$embeddedDataIndex = 1; \ No newline at end of file diff --git a/htdocs/includes/restler/composer.json b/htdocs/includes/restler/composer.json new file mode 100644 index 00000000000..1e096989b01 --- /dev/null +++ b/htdocs/includes/restler/composer.json @@ -0,0 +1,70 @@ +{ + "name":"restler/framework", + "description":"Just the Restler Framework without the tests and examples", + "type":"library", + "keywords":["server","api","framework","REST"], + "homepage":"http://luracast.com/products/restler/", + "license":"LGPL-2.1", + "authors":[ + { + "name":"Luracast", + "email":"arul@luracast.com" + }, + { + "name":"Nick nickl- Lombard", + "email":"github@jigsoft.co.za" + } + ], + "extra":{ + "branch-alias":{ + "master":"v3.0.x-dev" + } + }, + "suggest":{ + "luracast/explorer":"Restler's very own api explorer (see require-dev for details)", + "rodneyrehm/plist":"Restler supports tho Apple plist xml format (see require-dev for details)", + "zendframework/zendamf":"Support for the amf document format (see require-dev for details)", + "symfony/yaml":"Restler can produce content in yaml format as well (see require-dev for details)", + "twig/twig":"Restler can render HtmlView using twig templates (see require-dev for details)", + "mustache/mustache":"Restler can render HtmlView using mustache/handlebar templates (see require-dev for details)", + "bshaffer/oauth2-server-php":"Restler can provide OAuth2 authentication using this library (see require-dev for details)" + }, + "require":{ + "php":">=5.3.0" + }, + "require-dev":{ + "luracast/explorer":"*", + "rodneyrehm/plist":"dev-master", + "zendframework/zendamf":"dev-master", + "symfony/yaml":"*", + "mustache/mustache": "dev-master", + "twig/twig": "v1.13.0", + "bshaffer/oauth2-server-php":"v1.0" + }, + "repositories":[ + { + "type":"vcs", + "url":"https://github.com/zendframework/ZendAmf.git" + }, + { + "type":"package", + "package":{ + "name":"luracast/explorer", + "version":"v3.0.0", + "dist":{ + "type":"zip", + "url":"https://github.com/Luracast/Restler-API-Explorer/zipball/v3.0.0" + } + } + } + ], + "autoload":{ + "psr-0":{ + "Luracast\\Restler":"" + } + }, + "target-dir": "Luracast/Restler", + "replace": { + "luracast/restler":"3.*" + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/composer.lock b/htdocs/includes/restler/composer.lock new file mode 100644 index 00000000000..914abc5e16d --- /dev/null +++ b/htdocs/includes/restler/composer.lock @@ -0,0 +1,23 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "ee84444dcf34101555d20a813d528c44", + "packages": [], + "packages-dev": null, + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "rodneyrehm/plist": 20, + "zendframework/zendamf": 20, + "mustache/mustache": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.0" + }, + "platform-dev": [] +} diff --git a/htdocs/includes/restler/iAuthenticate.php b/htdocs/includes/restler/iAuthenticate.php new file mode 100644 index 00000000000..6e71f0fc291 --- /dev/null +++ b/htdocs/includes/restler/iAuthenticate.php @@ -0,0 +1,25 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iAuthenticate extends iFilter +{ + /** + * @return string string to be used with WWW-Authenticate header + * @example Basic + * @example Digest + * @example OAuth + */ + public function __getWWWAuthenticateString(); +} diff --git a/htdocs/includes/restler/iCache.php b/htdocs/includes/restler/iCache.php new file mode 100755 index 00000000000..53a7a9da6a7 --- /dev/null +++ b/htdocs/includes/restler/iCache.php @@ -0,0 +1,63 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iCache +{ + /** + * store data in the cache + * + * @abstract + * + * @param string $name + * @param mixed $data + * + * @return boolean true if successful + */ + public function set($name, $data); + + /** + * retrieve data from the cache + * + * @abstract + * + * @param string $name + * @param bool $ignoreErrors + * + * @return mixed + */ + public function get($name, $ignoreErrors = false); + + /** + * delete data from the cache + * + * @abstract + * + * @param string $name + * @param bool $ignoreErrors + * + * @return boolean true if successful + */ + public function clear($name, $ignoreErrors = false); + + /** + * check if the given name is cached + * + * @abstract + * + * @param string $name + * + * @return boolean true if cached + */ + public function isCached($name); +} + diff --git a/htdocs/includes/restler/iCompose.php b/htdocs/includes/restler/iCompose.php new file mode 100644 index 00000000000..a37d24d867d --- /dev/null +++ b/htdocs/includes/restler/iCompose.php @@ -0,0 +1,36 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iCompose { + /** + * Result of an api call is passed to this method + * to create a standard structure for the data + * + * @param mixed $result can be a primitive or array or object + */ + public function response($result); + + /** + * When the api call results in RestException this method + * will be called to return the error message + * + * @param RestException $exception exception that has reasons for failure + * + * @return + */ + public function message(RestException $exception); +} \ No newline at end of file diff --git a/htdocs/includes/restler/iFilter.php b/htdocs/includes/restler/iFilter.php new file mode 100644 index 00000000000..40205e5def8 --- /dev/null +++ b/htdocs/includes/restler/iFilter.php @@ -0,0 +1,30 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iFilter +{ + /** + * Access verification method. + * + * API access will be denied when this method returns false + * + * @abstract + * @return boolean true when api access is allowed false otherwise + */ + public function __isAllowed(); + +} + diff --git a/htdocs/includes/restler/iIdentifyUser.php b/htdocs/includes/restler/iIdentifyUser.php new file mode 100644 index 00000000000..9ba061d3327 --- /dev/null +++ b/htdocs/includes/restler/iIdentifyUser.php @@ -0,0 +1,63 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iIdentifyUser +{ + /** + * A way to uniquely identify the current api consumer + * + * When his user id is known it should be used otherwise ip address + * can be used + * + * @param bool $includePlatform Should we consider user alone or should + * consider the application/platform/device + * as well for generating unique id + * + * @return string + */ + public static function getUniqueIdentifier($includePlatform = false); + + /** + * User identity to be used for caching purpose + * + * When the dynamic cache service places an object in the cache, it needs to + * label it with a unique identifying string known as a cache ID. This + * method gives that identifier + * + * @return string + */ + public static function getCacheIdentifier(); + + /** + * Authentication classes should call this method + * + * @param string $id user id as identified by the authentication classes + * + * @return void + */ + public static function setUniqueIdentifier($id); + + /** + * User identity for caching purpose + * + * In a role based access control system this will be based on role + * + * @param $id + * + * @return void + */ + public static function setCacheIdentifier($id); +} \ No newline at end of file diff --git a/htdocs/includes/restler/iProvideMultiVersionApi.php b/htdocs/includes/restler/iProvideMultiVersionApi.php new file mode 100644 index 00000000000..ed74dd1b9e6 --- /dev/null +++ b/htdocs/includes/restler/iProvideMultiVersionApi.php @@ -0,0 +1,11 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iUseAuthentication +{ + /** + * This method will be called first for filter classes and api classes so + * that they can respond accordingly for filer method call and api method + * calls + * + * @abstract + * + * @param bool $isAuthenticated passes true when the authentication is + * done false otherwise + * + * @return mixed + */ + public function __setAuthenticationStatus($isAuthenticated=false); +} + diff --git a/htdocs/includes/restler/vendor/autoload.php b/htdocs/includes/restler/vendor/autoload.php new file mode 100644 index 00000000000..bf60b992d40 --- /dev/null +++ b/htdocs/includes/restler/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/htdocs/includes/restler/vendor/composer/autoload_classmap.php b/htdocs/includes/restler/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000000..7a91153b0d8 --- /dev/null +++ b/htdocs/includes/restler/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($baseDir . '/'), +); diff --git a/htdocs/includes/restler/vendor/composer/autoload_psr4.php b/htdocs/includes/restler/vendor/composer/autoload_psr4.php new file mode 100644 index 00000000000..b265c64a22f --- /dev/null +++ b/htdocs/includes/restler/vendor/composer/autoload_psr4.php @@ -0,0 +1,9 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + spl_autoload_register(array('ComposerAutoloaderInite65e15efc7e9ea1f2b7ba7fa697ba485', 'autoload'), true, true); + + $loader->register(true); + + return $loader; + } + + public static function autoload($class) + { + $dir = dirname(dirname(__DIR__)) . '/'; + $prefixes = array('Luracast\\Restler'); + foreach ($prefixes as $prefix) { + if (0 !== strpos($class, $prefix)) { + continue; + } + $path = $dir . implode('/', array_slice(explode('\\', $class), 2)).'.php'; + if (!$path = stream_resolve_include_path($path)) { + return false; + } + require $path; + + return true; + } + } +} + +function composerRequiree65e15efc7e9ea1f2b7ba7fa697ba485($file) +{ + require $file; +} diff --git a/htdocs/includes/restler/vendor/composer/installed.json b/htdocs/includes/restler/vendor/composer/installed.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/htdocs/includes/restler/vendor/composer/installed.json @@ -0,0 +1 @@ +[] diff --git a/htdocs/includes/restler/views/debug.css b/htdocs/includes/restler/views/debug.css new file mode 100644 index 00000000000..130fdb5acc0 --- /dev/null +++ b/htdocs/includes/restler/views/debug.css @@ -0,0 +1,441 @@ +@CHARSET "UTF-8"; + +h2, h3 { + color: #ffffff; + background-color: #363636; + margin-bottom: 0; + padding: 10px; + -webkit-box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); + box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); +} + +body { + margin: 0; + padding: 0; + font-family: Helvetica, Arial, sans-serif; + font-size: 14px; + color: #000; + background-image: url(data:image/png;base64,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); +} + +article { + left: 20px; + right: 20px; + position: absolute; + overflow: auto; +} + +header { + color: #ffffff; + display: block; + background-color: #000000; + margin: 0; + padding: 10px; +} + +ul { + padding: 20px; + list-style-type: none; + border: 1px solid #BBBBBB; + background-color: #F9F9F9; + margin-top: 0; +} + +ul ul { + border: none; + border-top: 1px solid #BBBBBB; + border-left: 2px dotted #CCCCCC; + background-color: #EFEFEF; +} + +ul ul ul { + background-color: #E0E0E0; +} + +ul ul ul ul { + background-color: #CFCFCF; +} + +ul ul ul ul ul { + background-color: #E0E0E0; +} + +ul ul ul ul ul ul { + background-color: #EFEFEF; +} + +ul ul ul ul ul ul ul { + background-color: #FFFFFF; +} + +li { + padding: 4px +} + +a { + color: #2368AF; +} + +pre { + padding: 20px; + background-color: #EEEEEE; + border: 1px solid #BBBBBB; + overflow: auto; + margin-top: 0; + -webkit-box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); + box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); +} + +.shadow { + -webkit-box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); + box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); +} + +pre.header { + padding: 10px; + margin-bottom: 0; + background: rgb(250,251,252); /* Old browsers */ + background: -moz-linear-gradient(top, rgba(250,251,252,1) 0%, rgba(240,244,246,1) 50%, rgba(226,232,236,1) 51%, rgba(243,249,253,1) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(250,251,252,1)), color-stop(50%,rgba(240,244,246,1)), color-stop(51%,rgba(226,232,236,1)), color-stop(100%,rgba(243,249,253,1))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fafbfc', endColorstr='#f3f9fd',GradientType=0 ); /* IE6-9 */ + +} + +/* +.info,.success,.warning,.error { + display: block; + border: 1px solid; + margin: 6px 0px; + padding: 8px 4px 8px 24px; + background-repeat: no-repeat; + background-position: 5px center; +} + +.info { + color: #00529B; + background-color: #BDE5F8; + background-image: url('information.png'); +} + +.success { + color: #4F8A10; + background-color: #DFF2BF; + background-image: url('success.png'); +} + +.warning { + color: #9F6000; + background-color: #FEEFB3; + background-image: url('warning.png'); +} + +.error { + color: #D8000C; + background-color: #FFBABA; + background-image: url('error.png'); +} +*/ +right { + position: relative; + right: -6px; + top: -4px; + float: right; +} +icon { + position: relative; + padding: 0; + margin: 0; + left:0; + padding-left:28px; + top: 15px; +} + + +icon:before { + left:0; + display: inline-block; + content: ""; + position: absolute; +} + +icon:after { + left:0; + display: inline-block; + content: ""; + position: absolute; +} + +/* SUCCESS +------------------------------------------------------------------------------------------------------------------------------- */ + +.success { + color:#7F913C; +} + +.success:before { + left:3px; + width:16px; + height:16px; + margin-top:-8px; + /* css3 */ + -webkit-border-radius:16px; + -moz-border-radius:16px; + border-radius:16px; + background:#7F913C; +} + +.success:after { + left:8px; + width:3px; + height:8px; + border-width:0 2px 2px 0; + border-style:solid; + border-color:#fff; + margin-top:-6px; + -webkit-transform:rotate(45deg); + -moz-transform:rotate(45deg); + -ms-transform:rotate(45deg); + -o-transform:rotate(45deg); + transform:rotate(45deg); +} + +/* Alternative style */ + +.success-alt { + color:#7F913C; +} + +.success-alt:before { + left:6px; + width:5px; + height:12px; + border-width:0 5px 5px 0; + border-style:solid; + border-color:#7F913C; + margin-top:-11px; + -webkit-transform:rotate(45deg); + -moz-transform:rotate(45deg); + -ms-transform:rotate(45deg); + -o-transform:rotate(45deg); + transform:rotate(45deg); +} + +/* COMMENT +------------------------------------------------------------------------------------------------------------------------------- */ + +.comment a:before { + width:16px; + height:10px; + margin-top:-8px; + /* css3 */ + -webkit-border-radius:2px; + -moz-border-radius:2px; + border-radius:2px; +} + +.comment a:after { + left:8px; + border:2px solid transparent; + border-top-color:#c55500; + border-left-color:#c55500; + margin-top:2px; + background:transparent; +} + +.comment a:hover:after, +.comment a:focus:after, +.comment a:active:after { + border-top-color:#730800; + border-left-color:#730800; +} + + +/* WARNING +------------------------------------------------------------------------------------------------------------------------------- */ + +.warning:before { + content:"!"; + z-index:2; + left:8px; + margin-top:-8px; + font-size:14px; + font-weight:bold; + color:#000; +} + +.warning:after { + z-index:1; + border-width:0 11px 18px; + border-style:solid; + border-color:#F8D201 transparent; + margin-top:-10px; + background:transparent; +} + +/* DENIED +------------------------------------------------------------------------------------------------------------------------------- */ + +.denied { + color:#C00000; +} + +.denied:before { + left:3px; + width:10px; + height:10px; + border:3px solid #C00000; + margin-top:-8px; + background:transparent; + /* css3 */ + -webkit-border-radius:16px; + -moz-border-radius:16px; + border-radius:16px; +} + +.denied:after { + left:6px; + width:11px; + height:3px; + margin-top:-2px; + background:#C00000; + /* css3 */ + -webkit-transform:rotate(-45deg); + -moz-transform:rotate(-45deg); + -ms-transform:rotate(-45deg); + -o-transform:rotate(-45deg); + transform:rotate(-45deg); +} + +#breadcrumbs-one{ + background: #eee; + border-width: 2px; + border-style: solid; + border-color: #f5f5f5 #e5e5e5 #ccc; + box-shadow: 0 0 2px rgba(0,0,0,.2); + overflow: hidden; +} + +#breadcrumbs-one li{ + float: left; +} + +#breadcrumbs-one a{ + padding: .3em 1em .3em 2em; + float: left; + text-decoration: none; + color: #444; + position: relative; + text-shadow: 0 1px 0 rgba(255,255,255,.5); + background-color: #ddd; + background-image: linear-gradient(to right, #f5f5f5, #ddd); + background-image: linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -o-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -moz-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -webkit-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -ms-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + + background-image: -webkit-gradient( + linear, + right top, + left top, + color-stop(0.1, rgb(221,221,221)), + color-stop(0.9, rgb(245,245,245)) + ); +} + +#breadcrumbs-one li:first-child a{ + padding-left: 1em; + border-radius: 5px 0 0 5px; +} + +#breadcrumbs-one a.failure:hover, +#breadcrumbs-one a:hover{ + background: #fff; +} + +#breadcrumbs-one a.failure{ + background: #fff5f5; + background-image: linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -o-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -moz-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -webkit-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -ms-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + + background-image: -webkit-gradient( + linear, + right top, + left top, + color-stop(0.1, rgb(242,228,228)), + color-stop(0.9, rgb(255,245,245)) + ); +} +#breadcrumbs-one a.failure::after{ + border-left-color: #f2e4e4; +} + +#breadcrumbs-one a::after, +#breadcrumbs-one a::before{ + content: ""; + position: absolute; + top: 50%; + margin-top: -1.5em; + border-top: 1.5em solid transparent; + border-bottom: 1.5em solid transparent; + border-left: 1em solid; + right: -1em; +} + +#breadcrumbs-one a::after{ + z-index: 2; + border-left-color: #ddd; +} + +#breadcrumbs-one a::before{ + border-left-color: #ccc; + right: -1.1em; + z-index: 1; +} + +#breadcrumbs-one a:hover::after{ + border-left-color: #fff; +} + +#breadcrumbs-one .current, +#breadcrumbs-one .current:hover{ + font-weight: bold; + background: none; +} + +#breadcrumbs-one .current::after, +#breadcrumbs-one .current::before{ + content: normal; +} + +.state { + background: #f00; + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + border: #000000 solid 3px; + position: relative; + z-index: 10; +} + +.state:after { + content: ""; + border-right: #000000 solid 4px;; + position: relative; + top: 8px; + left: 2px; + +} + +li > strong { + min-width: 80px; + display: inline-block; +} \ No newline at end of file diff --git a/htdocs/includes/restler/views/debug.php b/htdocs/includes/restler/views/debug.php new file mode 100644 index 00000000000..346c229c5d5 --- /dev/null +++ b/htdocs/includes/restler/views/debug.php @@ -0,0 +1,169 @@ +_exceptions; + if (count($source)) { + $source = end($source); + $traces = array(); + do { + $traces += $source->getTrace(); + } while ($source = $source->getPrevious()); + $traces += debug_backtrace(); + $call_trace + = parse_backtrace($traces, 0); + } else { + $call_trace + = parse_backtrace(debug_backtrace()); + } + +} +exceptions(); + +function parse_backtrace($raw, $skip = 1) +{ + $output = ""; + foreach ($raw as $entry) { + if ($skip-- > 0) { + continue; + } + //$output .= print_r($entry, true) . "\n"; + $output .= "\nFile: " . $entry['file'] . " (Line: " . $entry['line'] . ")\n"; + if (isset($entry['class'])) + $output .= $entry['class'] . "::"; + $output .= $entry['function'] + . "( " . json_encode($entry['args']) . " )\n"; + } + return $output; +} + + +//print_r(get_defined_vars()); +//print_r($response); +$icon; +if ($success && isset($api)) { + $arguments = implode(', ', $api->parameters); + $icon = ""; + $title = "{$api->className}::" + . "{$api->methodName}({$arguments})"; +} else { + if (isset($response['error']['message'])) { + $icon = ''; + $title = end(explode(':',$response['error']['message'])); + } else { + $icon = ''; + $title = 'No Matching Resource'; + } +} +function render($data, $shadow=true) +{ + $r = ''; + if (empty($data)) + return $r; + $r .= $shadow ? "
    \n": "
      \n"; + if (is_array($data)) { + // field name + foreach ($data as $key => $value) { + $r .= '
    • '; + $r .= is_numeric($key) + ? "[$key] " + : "$key: "; + $r .= ''; + if (is_array($value)) { + // recursive + $r .= render($value,false); + } else { + // value, with hyperlinked hyperlinks + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + $value = htmlentities($value, ENT_COMPAT, 'UTF-8'); + if (strpos($value, 'http://') === 0) { + $r .= '' . $value . ''; + } else { + $r .= $value; + } + } + $r .= "
    • \n"; + } + } elseif (is_bool($data)) { + $r .= '
    • ' . ($data ? 'true' : 'false') . '
    • '; + } else { + $r .= "
    • $data
    • "; + } + $r .= "
    \n"; + return $r; +} +$reqHeadersArr = array(); +$requestHeaders = $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . ' ' . $_SERVER['SERVER_PROTOCOL'] . PHP_EOL; +foreach ($reqHeadersArr as $key => $value) { + if ($key == 'Host') + continue; + $requestHeaders .= "$key: $value" . PHP_EOL; +} +// $requestHeaders = $this->encode(apache_request_headers(), FALSE, +// FALSE); +$responseHeaders = implode(PHP_EOL, headers_list()).PHP_EOL.'Status: HTTP/1.1 '; +$responseHeaders .= Util::$restler->responseCode.' '.\Luracast\Restler\RestException::$codes[Util::$restler->responseCode]; + +?> + + + + <?php echo $title?> + + + + + +
    +

    +
    +
    + +

    Response:

    +
    + +

    Additional Template Data:

    + +

    Restler v

    +
    + + \ No newline at end of file From 30c901c2661879f1b0ad4c037732a9e65d915b66 Mon Sep 17 00:00:00 2001 From: jfefe Date: Fri, 1 May 2015 16:12:30 +0200 Subject: [PATCH 03/66] Begin REST API implementation with thirdparty classes and methods needed. Work in progress ! --- htdocs/api/class/api.class.php | 90 +++++++++++ htdocs/api/index.php | 8 + htdocs/public/api/index.php | 95 ++++++++++++ htdocs/societe/class/api_thirdparty.class.php | 143 ++++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 htdocs/api/class/api.class.php create mode 100644 htdocs/api/index.php create mode 100644 htdocs/public/api/index.php create mode 100644 htdocs/societe/class/api_thirdparty.class.php diff --git a/htdocs/api/class/api.class.php b/htdocs/api/class/api.class.php new file mode 100644 index 00000000000..298beb685d6 --- /dev/null +++ b/htdocs/api/class/api.class.php @@ -0,0 +1,90 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use Luracast\Restler\Restler; + +/** + * Class for API + */ +class DolibarrApi { + + /** + * @var DoliDb $db Database object + */ + static protected $db; + + /** + * @var Restler $r Restler object + */ + var $r; + + /** + * Constructor + * + * @var DoliDB $db Database handler + */ + function __construct($db) { + $this->db = $db; + $this->r = new Restler(); + } + + /** + * Executed method when API is called without parameter + * + * Display a short message an return a http code 200 + * @return array + */ + function index() + { + return array( + 'success' => array( + 'code' => 200, + 'message' => __class__.' is up and running!' + ) + ); + } + + + /** + * Clean sensible object datas + * @var object $object Object to clean + * @return array Array of cleaned object properties + * + * @todo use an array for properties to clean + * + */ + protected function cleanObjectDatas($object){ + + unset($object->db); + + return array($object); + } + +} + +/** + * API init + * This class exists to show 200 code when request url root /api/ + * + */ +class DolibarrApiInit extends DolibarrApi { + + function __construct() { + + } + +} \ No newline at end of file diff --git a/htdocs/api/index.php b/htdocs/api/index.php new file mode 100644 index 00000000000..3ac00faa65f --- /dev/null +++ b/htdocs/api/index.php @@ -0,0 +1,8 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \defgroup api Module DolibarrApi + * \brief API loader + * Search files htdocs//class/api_.class.php + * \file htdocs/api/indexphp + * + * @todo User authentication with api_key + * + * + */ +if (! defined("NOLOGIN")) define("NOLOGIN",'1'); + +$res=0; +if (! $res && file_exists("../../main.inc.php")) $res=@include '../../main.inc.php'; +if (! $res && file_exists("../../../dolibarr/htdocs/main.inc.php")) $res=@include '../../../dolibarr/htdocs/main.inc.php'; // For custom directory +if (! $res) die("Include of main fails"); + +require_once DOL_DOCUMENT_ROOT.'/includes/restler/vendor/autoload.php'; +require_once DOL_DOCUMENT_ROOT.'/api/class/api.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; + +$api = new DolibarrApi($db); + +$api->r->setSupportedFormats('JsonFormat', 'XmlFormat'); +$api->r->addAPIClass('Luracast\\Restler\\Resources'); //this creates resources.json at API Root +$api->r->addAPIClass('DolibarrApiInit',''); // Just for url root page + +$modulesdir = dolGetModulesDirs(); +foreach ($modulesdir as $dir) +{ + /* + * Search available module + */ + dol_syslog("Scan directory ".$dir." for API modules"); + + $handle=@opendir(dol_osencode($dir)); + if (is_resource($handle)) + { + while (($file = readdir($handle))!==false) + { + if (is_readable($dir.$file) && preg_match("/^(mod.*)\.class\.php$/i",$file,$reg)) + { + $modulename=$reg[1]; + + // Defined if module is enabled + $enabled=true; + $part=$obj=strtolower(preg_replace('/^mod/i','',$modulename)); + //if ($part == 'propale') $part='propal'; + if ($part == 'societe') { + $obj = 'thirdparty'; + } + if (empty($conf->$part->enabled)) $enabled=false; + + if ($enabled) + { + /* + * If exists, load the API class for enable module + * + * Search a file api_.class.php into /htdocs//class directory + * + * @todo : take care of externals module! + * @todo : use getElementProperties() function + */ + $file = DOL_DOCUMENT_ROOT.'/'.$part."/class/api_".$obj.".class.php"; + + $classname = ucwords($obj).'Api'; + if (file_exists($file)) + { + require_once $file; + $api->r->addAPIClass($classname,''); + } + } + } + } + } +} + +$api->r->handle(); //serve the response \ No newline at end of file diff --git a/htdocs/societe/class/api_thirdparty.class.php b/htdocs/societe/class/api_thirdparty.class.php new file mode 100644 index 00000000000..bd5581ef6a5 --- /dev/null +++ b/htdocs/societe/class/api_thirdparty.class.php @@ -0,0 +1,143 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + use Luracast\Restler\RestException; + + +/** + * + * API class for thirdparty object + * + * @smart-auto-routing false + * + */ +class ThirdpartyApi extends DolibarrApi { + + static $FIELDS = array( + 'name', + 'email' + ); + + /** + * @var Societe $company {@type Societe} + */ + public $company; + + /** + * Constructor + * + * @url thirdparty/ + * + */ + function __construct() + { + global $db; + $this->db = $db; + $this->company = new Societe($this->db); + } + + /** + * Get properties of a thirdparty object + * + * Return an array with thirdparty informations + * + * @url GET thirdparty/{id} + * @param int $id ID of thirdparty + * @return array|mixed data without useless information + * @throws RestException + */ + function get($id) + { + $result = $this->company->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Thirdparty not found'); + } + + return $this->cleanObjectDatas($this->company); + } + + /** + * Fetch a list of thirdparties + * + * @url GET /thirdparties/list + * + * @return array Array of thirdparty objects + */ + function getList() { + + $result = $this->company->fetch_all($id); + if( ! $result ) { + throw new RestException(404, 'Thirdparties not found'); + } + + return $this->cleanObjectDatas($this->company->lines); + + } + /** + * Create thirdparty object + * + * @url POST thirdparty/ + * @param type $request_data + * @return type + */ + function post($request_data = NULL) + { + return $this->company->create($this->_validate($request_data)); + } + + /** + * Update thirdparty + * + * @url PUT thirdparty/{id} + * @param type $id + * @param type $request_data + * @return type$this->company + */ + function put($id, $request_data = NULL) + { + return $this->company->update($id, $this->_validate($request_data)); + } + + /** + * Delete thirdparty + * + * @url DELETE thirdparty/{id} + * @param type $id + * @return type + */ + function delete($id) + { + return $this->company->delete($id); + } + + /** + * Validate fields before create or update object + * @param type $data + * @return array + * @throws RestException + */ + private function _validate($data) + { + $thirdparty = array(); + foreach (ThirdpartyApi::$FIELDS as $field) { + if (!isset($data[$field])) + throw new RestException(400, "$field field missing"); + $thirdparty[$field] = $data[$field]; + } + return $thirdparty; + } +} From d406830e06900daba4506f932f1643b2408cec64 Mon Sep 17 00:00:00 2001 From: jfefe Date: Fri, 1 May 2015 16:13:58 +0200 Subject: [PATCH 04/66] NEW : add an explorer for REST API consultation & documentation --- htdocs/public/api/explorer/css/screen.css | 1538 +++++++++++++ .../api/explorer/images/pet_store_api.png | Bin 0 -> 824 bytes .../public/api/explorer/images/throbber.gif | Bin 0 -> 9257 bytes htdocs/public/api/explorer/index.html | 95 + .../public/api/explorer/lib/backbone-min.js | 38 + .../api/explorer/lib/handlebars-1.0.rc.1.js | 1920 +++++++++++++++++ .../lib/handlebars.runtime-1.0.0.beta.6.js | 223 ++ .../api/explorer/lib/jquery-1.8.0.min.js | 2 + .../api/explorer/lib/jquery.ba-bbq.min.js | 18 + .../api/explorer/lib/jquery.slideto.min.js | 1 + .../api/explorer/lib/jquery.wiggle.min.js | 8 + htdocs/public/api/explorer/lib/swagger.js | 694 ++++++ .../public/api/explorer/lib/underscore-min.js | 32 + htdocs/public/api/explorer/swagger-ui.js | 1238 +++++++++++ htdocs/public/api/explorer/swagger-ui.min.js | 1 + 15 files changed, 5808 insertions(+) create mode 100644 htdocs/public/api/explorer/css/screen.css create mode 100644 htdocs/public/api/explorer/images/pet_store_api.png create mode 100644 htdocs/public/api/explorer/images/throbber.gif create mode 100644 htdocs/public/api/explorer/index.html create mode 100644 htdocs/public/api/explorer/lib/backbone-min.js create mode 100644 htdocs/public/api/explorer/lib/handlebars-1.0.rc.1.js create mode 100644 htdocs/public/api/explorer/lib/handlebars.runtime-1.0.0.beta.6.js create mode 100644 htdocs/public/api/explorer/lib/jquery-1.8.0.min.js create mode 100644 htdocs/public/api/explorer/lib/jquery.ba-bbq.min.js create mode 100644 htdocs/public/api/explorer/lib/jquery.slideto.min.js create mode 100644 htdocs/public/api/explorer/lib/jquery.wiggle.min.js create mode 100644 htdocs/public/api/explorer/lib/swagger.js create mode 100644 htdocs/public/api/explorer/lib/underscore-min.js create mode 100644 htdocs/public/api/explorer/swagger-ui.js create mode 100644 htdocs/public/api/explorer/swagger-ui.min.js diff --git a/htdocs/public/api/explorer/css/screen.css b/htdocs/public/api/explorer/css/screen.css new file mode 100644 index 00000000000..e16ad7b6a73 --- /dev/null +++ b/htdocs/public/api/explorer/css/screen.css @@ -0,0 +1,1538 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +caption, th, td { + text-align: left; + font-weight: normal; + vertical-align: middle; +} + +q, blockquote { + quotes: none; +} + +q:before, q:after, blockquote:before, blockquote:after { + content: ""; + content: none; +} + +a img { + border: none; +} + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { + display: block; +} + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + text-decoration: none; +} + +h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover { + text-decoration: underline; +} + +h1 span.divider, h2 span.divider, h3 span.divider, h4 span.divider, h5 span.divider, h6 span.divider { + color: #aaaaaa; +} + +h1 { + color: #547f00; + color: black; + font-size: 1.5em; + line-height: 1.3em; + padding: 10px 0 10px 0; + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} + +h2 { + color: #89bf04; + color: black; + font-size: 1.3em; + padding: 10px 0 10px 0; +} + +h2 a { + color: black; +} + +h2 span.sub { + font-size: 0.7em; + color: #999999; + font-style: italic; +} + +h2 span.sub a { + color: #777777; +} + +h3 { + color: black; + font-size: 1.1em; + padding: 10px 0 10px 0; +} + +div.heading_with_menu { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +div.heading_with_menu h1, div.heading_with_menu h2, div.heading_with_menu h3, div.heading_with_menu h4, div.heading_with_menu h5, div.heading_with_menu h6 { + display: block; + clear: none; + float: left; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + width: 60%; +} + +div.heading_with_menu ul { + display: block; + clear: none; + float: right; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + margin-top: 10px; +} + +.body-textarea { + width: 300px; + height: 100px; +} + +p { + line-height: 1.4em; + padding: 0 0 10px 0; + color: #333333; +} + +ol { + margin: 0px 0 10px 0; + padding: 0 0 0 18px; + list-style-type: decimal; +} + +ol li { + padding: 5px 0px; + font-size: 0.9em; + color: #333333; +} + +mark { + padding: 2px; + color: #7e7b6d; + font-weight: bold; + background: rgba(255, 255, 255, 0.8); +} + +tag { + font-family: Verdana,Arial,Helvetica,sans-serif; + background-color: #999999; + padding: 1px 3px 2px; + font-size: xx-small; + font-weight: bold; + color: #ffffff; + letter-spacing: 1px; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.markdown h3 { + color: #547f00; +} + +.markdown h4 { + color: #666666; +} + +.markdown pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; + padding: 10px; + margin: 0 0 10px 0; +} + +.markdown pre code { + line-height: 1.6em; +} + +.markdown p code, .markdown li code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #f0f0f0; + color: black; + padding: 1px 3px; +} + +.markdown ol, .markdown ul { + font-family: "Droid Sans", sans-serif; + margin: 5px 0 10px 0; + padding: 0 0 0 18px; + list-style-type: disc; +} + +.markdown ol li, .markdown ul li { + padding: 3px 0px; + line-height: 1.4em; + color: #333333; +} + +div.gist { + margin: 20px 0 25px 0 !important; +} + +p.big, div.big p { + font-size: 1em; + margin-bottom: 10px; +} + +span.weak { + color: #666666; +} + +span.blank, span.empty { + color: #888888; + font-style: italic; +} + +a { + color: #547f00; +} + +strong { + font-family: "Droid Sans", sans-serif; + font-weight: bold; + font-weight: bold; +} + +.code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; +} + +pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; + padding: 10px; + /* white-space: pre-line */ +} + +pre code { + line-height: 1.6em; +} + +.required { + font-weight: bold; +} + +table.fullwidth { + width: 100%; +} + +table thead tr th { + padding: 5px; + font-size: 0.9em; + color: #666666; + border-bottom: 1px solid #999999; +} + +table tbody tr.offset { + background-color: #f5f5f5; +} + +table tbody tr td { + padding: 6px; + font-size: 0.9em; + border-bottom: 1px solid #cccccc; + vertical-align: top; + line-height: 1.3em; +} + +table tbody tr:last-child td { + border-bottom: none; +} + +table tbody tr.offset { + background-color: #f0f0f0; +} + +form.form_box { + background-color: #ebf3f9; + border: 1px solid black; + border-color: #c3d9ec; + padding: 10px; +} + +form.form_box label { + color: #0f6ab4 !important; +} + +form.form_box input[type=submit] { + display: block; + padding: 10px; +} + +form.form_box p { + font-size: 0.9em; + padding: 0 0 15px 0; + color: #7e7b6d; +} + +form.form_box p a { + color: #646257; +} + +form.form_box p strong { + color: black; +} + +form.form_box p.weak { + font-size: 0.8em; +} + +form.formtastic fieldset.inputs ol li p.inline-hints { + margin-left: 0; + font-style: italic; + font-size: 0.9em; + margin: 0; +} + +form.formtastic fieldset.inputs ol li label { + display: block; + clear: both; + width: auto; + padding: 0 0 3px 0; + color: #666666; +} + +form.formtastic fieldset.inputs ol li label abbr { + padding-left: 3px; + color: #888888; +} + +form.formtastic fieldset.inputs ol li.required label { + color: black; +} + +form.formtastic fieldset.inputs ol li.string input, form.formtastic fieldset.inputs ol li.url input, form.formtastic fieldset.inputs ol li.numeric input { + display: block; + padding: 4px; + width: auto; + clear: both; +} + +form.formtastic fieldset.inputs ol li.string input.title, form.formtastic fieldset.inputs ol li.url input.title, form.formtastic fieldset.inputs ol li.numeric input.title { + font-size: 1.3em; +} + +form.formtastic fieldset.inputs ol li.text textarea { + font-family: "Droid Sans", sans-serif; + height: 250px; + padding: 4px; + display: block; + clear: both; +} + +form.formtastic fieldset.inputs ol li.select select { + display: block; + clear: both; +} + +form.formtastic fieldset.inputs ol li.boolean { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +form.formtastic fieldset.inputs ol li.boolean input { + display: block; + float: left; + clear: none; + margin: 0 5px 0 0; +} + +form.formtastic fieldset.inputs ol li.boolean label { + display: block; + float: left; + clear: none; + margin: 0; + padding: 0; +} + +form.formtastic fieldset.buttons { + margin: 0; + padding: 0; +} + +form.fullwidth ol li.string input, form.fullwidth ol li.url input, form.fullwidth ol li.text textarea, form.fullwidth ol li.numeric input { + width: 500px !important; +} + +body { + font-family: "Droid Sans", sans-serif; +} + +body #content_message { + margin: 10px 15px; + font-style: italic; + color: #999999; +} + +body #header { + background-color: #646257; + padding: 14px; +} + +body #header a#logo { + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + background: transparent url(http://luracast.com/images/restler3-icon.png) no-repeat left center; + padding: 20px 0 20px 40px; + color: white; +} + +body #header form#api_selector { + display: block; + clear: none; + float: right; +} + +body #header form#api_selector .input { + display: block; + clear: none; + float: left; + margin: 0 10px 0 0; +} + +body #header form#api_selector .input input { + font-size: 0.9em; + padding: 3px; + margin: 0; +} + +body #header form#api_selector .input input#input_baseUrl { + width: 400px; +} + +body #header form#api_selector .input input#input_apiKey { + width: 200px; +} + +body #header form#api_selector .input a#explore { + display: block; + text-decoration: none; + font-weight: bold; + padding: 6px 8px; + font-size: 0.9em; + color: white; + background-color: #000000; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; + -ms-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; +} + +body #header form#api_selector .input a#explore:hover { + background-color: #a41e22; +} + +body p#colophon { + margin: 0 15px 40px 15px; + padding: 10px 0; + font-size: 0.8em; + border-top: 1px solid #dddddd; + font-family: "Droid Sans", sans-serif; + color: #999999; + font-style: italic; +} + +body p#colophon a { + text-decoration: none; + color: #547f00; +} + +body ul#resources { + font-family: "Droid Sans", sans-serif; + font-size: 0.9em; +} + +body ul#resources li.resource { + border-bottom: 1px solid #dddddd; +} + +body ul#resources li.resource:last-child { + border-bottom: none; +} + +body ul#resources li.resource div.heading { + border: 1px solid transparent; + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource div.heading h2 { + color: #999999; + padding-left: 0px; + display: block; + clear: none; + float: left; + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} + +body ul#resources li.resource div.heading h2 a { + color: #999999; +} + +body ul#resources li.resource div.heading h2 a:hover { + color: black; +} + +body ul#resources li.resource div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 14px 10px 0 0; +} + +body ul#resources li.resource div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource div.heading ul.options li:first-child, body ul#resources li.resource div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource div.heading ul.options li:last-child, body ul#resources li.resource div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource div.heading ul.options li { + color: #666666; + font-size: 0.9em; +} + +body ul#resources li.resource div.heading ul.options li a { + color: #aaaaaa; + text-decoration: none; +} + +body ul#resources li.resource div.heading ul.options li a:hover { + text-decoration: underline; + color: black; +} + +body ul#resources li.resource:hover div.heading h2 a, body ul#resources li.resource.active div.heading h2 a { + color: black; +} + +body ul#resources li.resource:hover div.heading ul.options li a, body ul#resources li.resource.active div.heading ul.options li a { + color: #555555; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #e7f0f7; + border: 1px solid black; + border-color: #c3d9ec; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #0f6ab4; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + border-right-color: #c3d9ec; + color: #0f6ab4; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a { + color: #0f6ab4; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content { + background-color: #ebf3f9; + border: 1px solid black; + border-color: #c3d9ec; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4 { + color: #0f6ab4; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #6fa5d2; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #e7f6ec; + border: 1px solid black; + border-color: #c3e8d1; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #10a54a; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + border-right-color: #c3e8d1; + color: #10a54a; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a { + color: #10a54a; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content { + background-color: #ebf7f0; + border: 1px solid black; + border-color: #c3e8d1; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4 { + color: #10a54a; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #6fc992; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #f9f2e9; + border: 1px solid black; + border-color: #f0e0ca; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #c5862b; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + border-right-color: #f0e0ca; + color: #c5862b; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a { + color: #c5862b; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content { + background-color: #faf5ee; + border: 1px solid black; + border-color: #f0e0ca; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4 { + color: #c5862b; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #dcb67f; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #FCE9E3; + border: 1px solid black; + border-color: #F5D5C3; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #D38042; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li { + border-right-color: #f0cecb; + color: #D38042; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a { + color: #D38042; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content { + background-color: #faf0ef; + border: 1px solid black; + border-color: #f0cecb; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content h4 { + color: #D38042; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #F5D5C3; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #dcb67f; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #f5e8e8; + border: 1px solid black; + border-color: #e8c6c7; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #a41e22; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + border-right-color: #e8c6c7; + color: #a41e22; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a { + color: #a41e22; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content { + background-color: #f7eded; + border: 1px solid black; + border-color: #e8c6c7; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4 { + color: #a41e22; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #c8787a; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + + +.model-signature { + font-family: "Droid Sans", sans-serif; + font-size: 1em; + line-height: 1.5em; +} +.model-signature span { + font-size: 0.9em; + line-height: 1.5em; +} +.model-signature span:nth-child(odd) { color:#333; } +.model-signature span:nth-child(even) { color:#C5862B; } diff --git a/htdocs/public/api/explorer/images/pet_store_api.png b/htdocs/public/api/explorer/images/pet_store_api.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f9cd4aeb35a108c4b2f1dddb59977d56c595d8 GIT binary patch literal 824 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 z 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 + + Restler API Explorer + + + + + + + + + + + + + + + + + + + + + +
    +   +
    + +
    + +
    + + + + \ No newline at end of file diff --git a/htdocs/public/api/explorer/lib/backbone-min.js b/htdocs/public/api/explorer/lib/backbone-min.js new file mode 100644 index 00000000000..c1c0d4fff28 --- /dev/null +++ b/htdocs/public/api/explorer/lib/backbone-min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('